关于fini_array的巧妙利用

文件启动之ELF

经常调试的朋友都知道,main函数其实不是程序的起点。程序的启动流程依次为

_start --> __libc_start_main --> main

__libc_start_main分析

对应_start的代码,可以发现__libc_start_main函数的参数中,有3个是函数指针:

  • rdi <- main
  • rcx <- __libc_csu_init
  • r8 <- __libc_csu_fini

不难想到,除main以外的这两位兄弟,一位在main开始执行前执行,一位在main执行完毕后执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pwndbg> x/20i 0x402bd0
0x402bd0 <__libc_csu_fini>: push rbp
0x402bd1 <__libc_csu_fini+1>: lea rax,[rip+0xb24e8] # 0x4b50c0
0x402bd8 <__libc_csu_fini+8>: lea rbp,[rip+0xb24d1] # 0x4b50b0
0x402bdf <__libc_csu_fini+15>: push rbx
0x402be0 <__libc_csu_fini+16>: sub rax,rbp
0x402be3 <__libc_csu_fini+19>: sub rsp,0x8
0x402be7 <__libc_csu_fini+23>: sar rax,0x3
0x402beb <__libc_csu_fini+27>: je 0x402c06 <__libc_csu_fini+54>
0x402bed <__libc_csu_fini+29>: lea rbx,[rax-0x1]
0x402bf1 <__libc_csu_fini+33>: nop DWORD PTR [rax+0x0]
0x402bf8 <__libc_csu_fini+40>: call QWORD PTR [rbp+rbx*8+0x0]
0x402bfc <__libc_csu_fini+44>: sub rbx,0x1
0x402c00 <__libc_csu_fini+48>: cmp rbx,0xffffffffffffffff
0x402c04 <__libc_csu_fini+52>: jne 0x402bf8 <__libc_csu_fini+40>
0x402c06 <__libc_csu_fini+54>: add rsp,0x8
0x402c0a <__libc_csu_fini+58>: pop rbx
0x402c0b <__libc_csu_fini+59>: pop rbp
0x402c0c <__libc_csu_fini+60>: jmp 0x48f52c <_fini>

利用方式 - 栈迁移

首先,看下面这条指令:

1
2
0x402bd8: lea rbp,[rip+0xb24d1] # 0x4b50b0
1

rbp = 0x4b50b00x4b50b0fini_array的首地址

这条指令相当于lea rbp,[fini_array],因此,在这里配合gadget:

1
2
3
leave ; (mov rsp,ebp; pop rbp)
ret
12

便可以把__栈迁移__到fini_arrayfini_array存储的函数指针,可能有__写权限__)

利用方式 - 控制流劫持

下面还有一条call指令:

1
2
0x402bf8: call QWORD PTR [rbp+rbx*8]
1

rbp即为fini_array,因此这里将调用fini_array中的函数

只要修改fini_array中的值,就可以实现__控制流的转移__啦(传说中的fini_array劫持)

这里分析的64位的静态编译程序,可见其中的__libc_csu_fini函数简直好用的不得了鸭,既可以完成__栈迁移__,又能够劫持__控制流__

动态链接的程序__libc_csu_fini很短,并没有上述指令…但是也有类似fini_array的函数指针

fini_array分析

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
pwndbg> elfheader 
0x400200 - 0x400224 .note.gnu.build-id
0x400224 - 0x400244 .note.ABI-tag
0x400248 - 0x400470 .rela.plt
0x401000 - 0x401017 .init
0x401018 - 0x4010d0 .plt
0x4010d0 - 0x48d630 .text
0x48d630 - 0x48f52b __libc_freeres_fn
0x48f52c - 0x48f535 .fini
0x490000 - 0x4a95dc .rodata
0x4a95dc - 0x4a95dd .stapsdt.base
0x4a95e0 - 0x4b3d00 .eh_frame
0x4b3d00 - 0x4b3da9 .gcc_except_table
0x4b5080 - 0x4b50a0 .tdata
0x4b50a0 - 0x4b50b0 .init_array
0x4b50a0 - 0x4b50e0 .tbss
0x4b50b0 - 0x4b50c0 .fini_array
0x4b50c0 - 0x4b7ef4 .data.rel.ro
0x4b7ef8 - 0x4b7fe8 .got
0x4b8000 - 0x4b80d0 .got.plt
0x4b80e0 - 0x4b9bf0 .data
0x4b9bf0 - 0x4b9c38 __libc_subfreeres
0x4b9c40 - 0x4ba2e8 __libc_IO_vtables
0x4ba2e8 - 0x4ba2f0 __libc_atexit
0x4ba300 - 0x4bba78 .bss
0x4bba78 - 0x4bbaa0 __libc_freeres_ptrs

其中0x4b50b0 - 0x4b50c0.fini_array数组,其中存在两个函数指针:

1
2
3
4
5
6
7
pwndbg> x/2xg 0x4b50b0
0x4b50b0: 0x0000000000401b10 0x0000000000401580
pwndbg> x/i 0x0000000000401b10
0x401b10 <__do_global_dtors_aux>: cmp BYTE PTR [rip+0xb87e9],0x0
pwndbg> x/i 0x0000000000401580
0x401580 <fini>: mov rax,QWORD PTR [rip+0xb9b71]
123456
1
2
array[0]`->`__do_global_dtors_aux`
`array[1]`->`fini

这两个函数都会在main执行完毕后执行,因此只要__覆盖这两个函数指针,即可实现控制流的劫持__

此外,静态链接的程序也有PLT表和GOT表,也可以覆盖通过GOT中的函数指针实现控制流劫持

上述fini_array中的两个函数指针在__libc_csu_fini(上文说的那位兄弟)中被执行

执行的顺序是array[1]->array[0](后有详解)

一种好玩儿的利用方式

循环大法

一种比较好玩儿的操作:

  • array[0]的值覆盖为那位兄弟(__libc_csu_fini函数)的地址
  • array[1]的值覆盖为另一个函数地址,就叫他addrA

于是,main执行完毕后执行__libc_csu_fini,于是有意思的来了!

  • __libc_csu_fini先执行一遍array[1]:addrA,返回后再执行array[0]:__libc_csu_fini
  • __libc_csu_fini先执行一遍array[1]:addrA,返回后再执行array[0]:__libc_csu_fini
  • __libc_csu_fini先执行一遍array[1]:addrA,返回后再执行array[0]:__libc_csu_fini

看!连起来啦~ main->__libc_csu_fini->addrA->__libc_csu_fini->addrA-> ......