中级ROP

中级ROP

参考:https://wiki.x10sec.org/pwn/linux/user-mode/stackoverflow/x86/medium-rop/

参考:https://blog.csdn.net/qq_41202237/article/details/105913597?spm=1001.2014.3001.5501

参考:https://blog.csdn.net/qq_41202237/article/details/105913705?spm=1001.2014.3001.5501

1.原理

中级ROP主要使用了比较巧妙的gadgets

1.1 ret2csu

在64位的程序下面,如果调用了libc函数,就会存在对于libc初始化的函数 _ _ l i b c _ c s u _ i n i t \_\_libc\_csu\_init __libc_csu_init。而我们知道,64程序的前6个参数是通过寄存器传递的,可以使用 _ _ l i b c _ c s u _ i n i t \_\_libc\_csu\_init __libc_csu_init 为参数进行赋值。

1.1.1 libc_csu_init展示

下面看看 _ _ l i b c _ c s u _ i n i t \_\_libc\_csu\_init __libc_csu_init 函数:

.text:00000000004005C0 ; void _libc_csu_init(void)
.text:00000000004005C0                 public __libc_csu_init
.text:00000000004005C0 __libc_csu_init proc near               ; DATA XREF: _start+16↑o
.text:00000000004005C0 ; __unwind {
.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+54↓j
.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+34↑j
.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 ; } // starts at 4005C0
.text:0000000000400624 __libc_csu_init endp
1.1.2 可用gadget介绍

以上的gadget存在几个可利用的点:

如果不理解就拿笔画下流程,画个998244353遍大概就能记住了!

  • 从0x000000000040061A一直到结尾,可以利用栈溢出控制rbx, rbp, r12, r13, r14, r15。我把这块记为csu_gadget1
.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
  • 从0x0000000000400600到0x0000000000400609,我们将r13赋值给rdx, 将r14赋值给rsi,将r15d赋值给edi(这里赋值的是rdi的低32位,此时rdi的高32位寄存器值为0),所以其实我们可以控制rdi,只不过只能控制低32位。这三个寄存器,是x64传参的前三个参数。此外,我们可以通过控制r12和rbx来指定我们想要调用的函数,比如说我们让rbx为0,让r12为要调用的函数的地址。我把这块记为csu_gadget2
.text:0000000000400600                 mov     rdx, r13
.text:0000000000400603                 mov     rsi, r14
.text:0000000000400606                 mov     edi, r15d
.text:0000000000400609                 call    qword ptr [r12+rbx*8]
  • 从0x000000000040060D到0x0000000000400614,可以控制rbx和rbp的关系为rbx + 1 == rbp,那么就不会执行loc_40060,进而可以继续执行下面的汇编程序,比如:rbx = 0, rbp = 1。我把这块记为csu_gadget3
.text:000000000040060D                 add     rbx, 1
.text:0000000000400611                 cmp     rbx, rbp
.text:0000000000400614                 jnz     short loc_40060
1.1.3 组合使用gadget思路

给出一个将三个gadget组合使用(其实只使用了csu_gadget1和csu_gadget2)的思路(只要知道x64下任意函数和参数的真实地址,通过以下思路可以调用x64下任意的函数):

  1. 通过栈溢出执行csu_gadget1,将需要的参数放入寄存器rbx、rbp、r12、r13、r14、r15;

  2. 然后csu_gadget1的retn执行跳转到cus_gadget2;

  3. 执行csu_gadget2,对x64的前三个参数rdi、rsi、rdx进行赋值,程序跳转到需要执行的函数处:call qword ptr [r12+rbx*8];

  4. 当要执行的函数执行完,汇编指令返回000000000040060D,继续往下执行,通过前面控制rbx和rbp,使得程序不进入loc_400600;

  5. 程序继续往下走,直到进入0000000000400624的retn,这个时候可以控制程序返回任意地址,比如说main()函数。

1.1.4 类似于libc_csu_init,其他可用的gadget

除了上面介绍的libc_csu_init,gcc编译时,还会有其他的gadget:

_init
_start
call_gmon_start
deregister_tm_clones
register_tm_clones
__do_global_dtors_aux
frame_dummy
__libc_csu_init
__libc_csu_fini
_fini

这里面也有些gadget,挖掘他们的性质可用进行其他方式的漏洞利用。

1.1.5 利用libc_csu_init尾部偏移,控制其他寄存器

我们知道libc_csu_init中的csu_gadget1(000000000040061A),控制了:rbx,rbp,r12,r13,r14,r15,csu_gadget2(0000000000400600)控制了: rdx,rsi,edi。

libc_csu_init尾部的偏移还可以控制其他的寄存器,比如说:

  1. 0x000000000040061d (csu_gadget1 + 3):就可以控制rsp
  2. 0x000000000040061f (csu_gadget1 + 5): 就可以控制rbp
  3. 0x0000000000400621 (csu_gadget1 + 7):就可以控制rsi
  4. 0x0000000000400623 (csu_gadget1 + 9):就可以控制rdi

通过gdb查看:

pwndbg> 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
pwndbg> 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    
pwndbg> x/5i 0x000000000040061A + 5
   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
pwndbg> x/5i 0x000000000040061A + 7
   0x400621 <__libc_csu_init+97>:       pop    rsi
   0x400622 <__libc_csu_init+98>:       pop    r15
   0x400624 <__libc_csu_init+100>:      ret    
   0x400625:    nop
   0x400626:    nop    WORD PTR cs:[rax+rax*1+0x0]
pwndbg> 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

1.2 ret2reg

1.2.1 原理
  1. 查看栈溢出返回时哪个寄存器(比如:eax)恰好缓冲区空间。
  2. 查找对应的call 寄存器(比如:call eax)或者jmp 寄存器(比如:jmp eax)指令作为gadget,将EIP设置为该指令地址,即栈溢出时用这个gadget覆盖ret。
  3. 将寄存器所指向的空间(即缓冲区)上注入shellcode(确保该空间是可以执行的,通常是栈上的)
1.2.2 防御方法

在函数ret之前,把所有的寄存器都清空

1.3 BROP

1.3.1 概念

BROP(Blind ROP)盲rop,用在看不到程序C代码和二进制文件的情况下,对程序进行攻击,劫持程序的执行流。因为看不到程序C代码和二进制文件,因此栈溢出的长度、栈上的canary、都需要暴力枚举。

1.3.2 攻击条件
  • 程序必须存在栈溢出
  • 服务器端的进程在崩溃之后会重新启动,并且重新启动的进程的地址与先前的地址一样(这也就是说即使程序有 ASLR 保护,但是其只是在程序最初启动的时候有效果)。目前 nginx, MySQL, Apache, OpenSSH 等服务器应用都是符合这种特性的。
1.3.3 攻击原理

目前,大部分应用都会开启 ASLR、NX、Canary 保护。这里我们分别讲解在 BROP 中如何绕过这些保护,以及如何进行攻击。

  • 判断栈溢出的长度:暴力枚举

  • Stack Reading:获取栈上的数据来泄露canary以及ebp和返回地址

  • Blind ROP:找到足够多的gadget来控制输出函数的参数,并且对其进行调用,比如write函数或者puts函数

  • 写EXP:利用输出函数来dump出程序以便于找到更多的gadget,然后写gadget

1.3.3.1 栈溢出长度

从长度为1开始暴力枚举长度,直到程序产生崩溃。

比如说:

len为枚举的长度,我们第一次发生len长度个’a’,如果程序没有崩溃,说明还没有溢出,下一次就可以发出len + 1 个’a’;如果当长度为len时产生崩溃,那么栈溢出长度就是len - 1。

1.3.3.2 Stack Reading

一般栈上canary都是以如下的方式布置的:

buffer | canary | ebp | ret

在32位程序里,canary长度为32位;在64位程序里,canary长度为64位。

canary本身可用通过爆破来获取,在1.3.2的攻击条件的第2点我们知道,canary其实是固定的,即使程序崩溃后重启canary也和原来一样。我们可用逐字节来进行爆破,每个字节最多有2 ^ 8 = 256种可能,因此32位程序只需要爆破256 * 4 = 1024次, 而64位程序需要爆破256 * 8 = 2048次。

如何进行逐字节爆破:

假设是32位程序:canary分为4个部分canary = c1 | c2 | c3 | c4,每个cx都是2字节

那么我第一次构造payload = buffer + c1,然后把payload打过去,看程序是否崩溃,如果崩溃,说明当前c1错误,如果没有崩溃,说明找到了c1。这样的过程需要进行256次。

现在我们找到了c1的值,第二次构造payload = buffer + c1 + c2,然后把payload打过去,看程序是否崩溃,如果崩溃,说明当前c2错误,如果没有崩溃,说明找到了c2。这样的过程需要进行256次。

总共256 * 4 = 1024 次即可找到c1、c2、c3、c4。(注意是逐字节爆破,不是一起爆破

1.3.3.3 Blind ROP

我们在确定了栈溢出长度和canary之后,如何获取shell呢?

最朴素的想法就是ret2syscall,但是没有二进制文件,且程序可能开启了aslr,我们没有足够的gadget来进行ret2syscall。

那么如果要使用ret2libc的思路,我们就需要获取libc基地址。

可以从以下的逻辑思考:

  1. 想要获取libc基地址,就需要知道某些函数比如puts的真实地址和在.so的地址,将其做差。
  2. 那么如何获取puts函数的真实地址呢?我们就要获取got[“puts”]。
  3. 如何获取got[“puts”]呢?我们可以使用plt[“puts”]把所有的程序打印出来,然后找到got[“puts”]即可。
  4. 那现在问题就转移到了寻找plt[“puts”]。我们知道64位的elf文件,在不开启aslr的情况下0x400000位置的前几个字符为”\x7fELF“。那么我们可以枚举地址addr,假设其为plt[“puts”]的地址,让它去打印0x400000前面几个字符,判断是否为”\x7fELF“。如果是”\x7fELF“,就说明当前addr即为plt[“puts”]。但是,想要去打印0x400000位置的内容,需要有个寄存器存放这个地址来作为plt[“puts”]的参数。
  5. 怎么获取寻找这样一个寄存器呢?换句话说,如果我们能够找到pop rdi, ret这样的gadget,就能完成传参。但是怎么找这样的gadget呢?可以使用libc_csu_init的结尾一段的gadgets来实现,即去寻找csu_gadget1。只要找到csu_gadget1,就可以利用偏移找到关于rdi和rsi的gadget:csu_gadget1 + 7位置为 pop rsi ; pop r15; ret,csu_gadget1 + 9 位置为 pop rdi; ret。那么就可以解决函数传参的问题,且可以找到前两个参数(当前的puts()只需要一个参数,但是如果我们要去寻找write(),它就要3个参数啦)。对于第三个参数rdx,在write()函数中,用来限制输出长度,一般不会为0,但是为了保险起见,还是需要设置一下,但是几乎没有pop rdx这样的指令。这里需要说明指向strcmp的时候,rdx会被设置为将要被比较的字符串的长度,因此我们可以找到strcmp()函数,从而控制rdx。(比如说,我们只要找到了strcmp函数,把它放在栈上,劫持程序执行strcmp,那么rdx就会被控制,从而控制了rdx的取值。怎么找strcmp呢?思路放在1.3.3.3.3一起说吧)
  6. 那么怎么找libc_csu_init结尾的csu_gadget1呢?观察csu_gadget1的特点,它是连着6个pop,然后一个ret。因此假设当前地址addr是csu_gadget1,那么如果payload的构造如下(64位下):payload = cyclic(len) + addr + cyclic(6 * 8) + stop_gadget。如果addr恰好是csu_gadget1的话,完成6个pop,程序就会去执行stop_gagdget。stop_gadget是一段让程序卡住的代码(先这样理解,下面1.3.3.3.1再具体展开解释),这个时候程序如果卡住,说明找到了csu_gadget1;如果程序没有卡住,产生了崩溃,那么说明不是csu_gadget1(下面1.3.3.3.1再具体展开解释)。

下面对于上述的思路展开具体解释:

1.3.3.3.1 找stop gadget

我们控制返回地址时,一般会出现三种情况

  1. 程序直接崩溃:ret地址指向的是一个程序内不存在的地址

  2. 程序运行一段时间后崩溃:比如运行自己构造的函数,该函数的返回地址指向不存在的地址

  3. 程序一直运行而不崩溃

上面说的第3种情况程序一直运行不崩溃的gadget就是stop gadget,当程序进入这段gadget时,程序无限循环,不断运行下去。有点像贪吃蛇,当程序进入ret,又被劫持到了stop gadget,然后回到程序开始的位置。

一般来说,stop gadget找到的都是main或者 _ s t a r t \_start _start

我们画个图来举例,比如说是找到的这个stop gadget是main():

由于brop是看不到二进制文件的,因此我们只能去猜测stop地址,假设程序是64位的,那么起始地址是0x400000,我们的addr从0x400000开始枚举,用addr覆盖ret,然后观察程序是否崩溃,如果崩溃,说明当前地址不是stop gadget,那么就把addr += 1;如果不崩溃,说明找到了stop gadget,将该地址打印出来即可。

1.3.3.3.2 找brop gadget(csu_gadget1)

当我们找到了stop_gadget,现在要去找csu_gadget1了。

再看一下csu_gadget1的结构:

.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

我们定义栈上三种地址:

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

因此我们的payload可以如下构造:payload = cyclic(len) + addr + Trap * 6 + stop_gadget + Trap * 2,如下图:

如果probe恰好是csu_gadget1,那么就会把6个traps给pop掉,然后ret执行stop gadget,程序卡住,这就表明找到了csu_gadget1。

假设找到的地址为addr,那么:

  1. addr+ 7位置为 pop rsi ; pop r15; ret
  2. addr+ 9 位置为 pop rdi; ret
1.3.3.3.3 泄露plt[“puts”]

64位的elf文件,在不开启aslr的情况下0x400000位置的前几个字符为”\x7fELF“。那么我们可以枚举地址addr,假设其为plt[“puts”]的地址,让它去打印0x400000前面几个字符,判断是否为”\x7fELF“。如果是”\x7fELF“,就说明当前addr即为plt[“puts”]。plt[“puts”]的参数我们可以使用csu_gadget1 + 9来传参。

payload如下:payload = padding + p64(csu_gadget1 + 9) + 0x400000 + addr + stop_gadget。

上面的addr,如果是plt[“puts”]的话,那么程序就会打印”\x7fELF“,然后程序卡住(因为返回到了stop_gadget)。如果程序崩溃,那么说明addr不是plt[“puts”],我们就addr += 1,然后继续做同样的流程。

这里讲一下获得第三个参数rdx的方法:

一般来说,rdx不会是0,所以比如我们调用write()函数时,rdx作为写入内容的长度,还是可以进行写入内容。

但是为了保险,我们最好是要把rdx进行赋值使用,然而 pop rdx; ret 这样的gadget基本找不到。在调用strcmp()函数时,rdx会被设置为要比较的字符串长度。因此,如果能够找到strcmp(),调用这个函数,那么rdx就会被赋值,从而实现对于rdx的控制。

那么怎么找到strcmp()函数呢?

我们知道strcmp(para1, para2),只有para1和para2都是有效地址时,strcmp()函数才能够正确执行。我们前面已经知道了两个可用的gadget,csu_gadget1 + 7 和csu_gadget1 + 9。因此我们可用将这两个地址分别作为para1和para2,然后执行strcmp(),判断是否成功执行,如果成功执行,说明找到了strcmp()。当然啦,也有可能找到strncmp()和strcasecmp()函数,但是它们和strcmp()有一样的效果。

我们也可以想找plt[“puts”]一样:

payload = cyclic(*) + p64(csu_gadet1+ 9) + p64(csu_gadget1 + 9) + p64(csu_gadget1 + 7) + p64(csu_gadget1 + 7) + p64(csu_gadget1 + 7) + addr + stop_gadget

注意:第二个p64(csu_gadget1 + 9)是用来放在rdi中的,第二个p64(csu_gadget1 + 7)是用来放在rsi中的,然后程序就会执行addr,如果addr恰好是plt[“strcmp”],那么程序就会进入stop_gadget,然后卡住,和寻找plt[“puts”]的思路类似。

1.3.3.3.4 泄露got[“puts”]

现在我们已经有了plt[“puts”],那么我们就可以把整个程序的二进制全部dump出来了。整个程序的二进制都被dump出来,我们就得到了二进制文件,然后拿IDA打开就知道got[“puts”]了。

payload = ‘a’ * length + p64(rdi_ret) + p64(leak_addr) + p64(puts_plt) + p64(stop_gadget),如下图所示:

整个程序都泄露出来后,我们将其组合为一个完整的二进制文件,然后拿IDA打开即可,即可找到got[“puts”]

1.3.3.3.5 泄露libc基地址

我们得到了got[“puts”],那么就得到了puts()的真实地址。我们利用它找到libc的版本,然后找到libc[“puts”],做个差值即可知道libc的基地址。

1.3.3.4 写EXP

当已经泄露了libc基地址,那么就可用获得system() 和 "/bin/sh"的真实地址了,然后按照ret2libc3的思路写exp即可。

1.4 JOP、COP

jop:Jump-oriented programming

cop:Call-oriented programming

和rop原理基本一样,不同的是原来利用ret指令,现在利用jump和call指令

2.例题

2.1 ret2csu

2.1.1 习题信息

习题路径:ctf-challenges\pwn\stackoverflow\ret2__libc_csu_init\hitcon-level5\level5

2.1.2 程序分析

首先看一下程序的保护机制:

$ checksec level5
[*] '/mnt/d/study/ctf/资料/ctf-challenges/pwn/stackoverflow/ret2__libc_csu_init/hitcon-level5/level5'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

发现程序是64位的,没有开PIE,但是NX开了

IDA查看程序,发现程序存在栈溢出:

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

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

观察程序,发现程序中不存在system()函数和"/bin/sh"字符串。程序中存在write()函数和read()函数。程序中存在 _ _ l i b c _ c s u _ i n i t \_\_libc\_csu\_init __libc_csu_init,那么我们

注意:本题system("/bin/sh")无法成功,需要使用execve("/bin/sh"),至于为什么,我不懂= =

本题的漏洞利用思路如下:

  1. 第一次打payload,利用栈溢出组合执行libc_csu_gadgets(组合思路见1.1.3)来获取write函数的真实地址,并使得程序重新执行main()函数
  2. 根据write()函数的真实地址获取对应的libc版本及其execve()函数地址
  3. 第二次打payload,再次利用栈溢出,执行libc_csu_gadgets,这一次执行read()函数,向bss段内写入execve()和"/bin/sh"的地址,控制程序重新执行main()函数
  4. 第三次打payload,再次利用栈溢出,执行libc_csu_gadgets,执行execve("/bin/sh")获取shell
2.1.3 exp编写
#coding=utf-8
from pwn import *
from LibcSearcher import LibcSearcher

#context.log_level = 'debug'

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_gadget2 = 0x0000000000400600
csu_gadget1 = 0x000000000040061A


def csu(rbx, rbp
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值