回顾

第二次参加校赛,仍然是一个人。不过2024校赛的时候啥都不会,python都没怎么学,基本就是几个方向签了下到就结束了。
去年秋季参加了 NewStarCTF 2024 和 hackergame 2024,对Web和Misc方向有了一点点了解。这次校赛凭着兴趣打算试一试RE, 也是我第一次做CTF的RE题目。 由于之前只会一点点PE结构和x32dbg的调试,ida用的也还不算熟练,所以做题进度比较缓慢。做出来modern-lang(现代的语言)的时候真的很激动,但让我比较难受的是Hello Obfuscator没解出来,那道迷宫题reMazeR则没什么头绪。
img1
img2

Misc(1题)

签到问卷

F12打开开发者工具,查看network,flag就在这个meta开头的请求的响应体中
签到问卷

Reverse(4题)

网站管理员的登录密码

首先使用Wireshark分析backup.pcapng流量包,追踪HTTP请求,找到登录成功的请求和响应(状态码为200)
网站管理员的登录密码1

提取出加密后的密码
网站管理员的登录密码2

浏览器搜索了一下形式,可能是AES加密
接下来寻找加密逻辑,查看请求网页调用堆栈看见了onClick函数,点进去之后发现调用了x函数,追踪一下
网站管理员的登录密码3
网站管理员的登录密码4
网站管理员的登录密码5

确定就是是AES-CBC加密,parse的第一段是密钥,第二段是偏移,都是Hex格式,和加密后的密码一起放进解密工具得到flag
网站管理员的登录密码6

现代的语言

这道题是Rust反编译,但是我的做的时候没有使用插件。
第一次做逆向见到这种题目有点懵,直接运行了一下exe,控制台里面看起来每一位的有效输入和前面的输入有点关系,还挺好玩的(。猜测这道题肯定对输入进行了特殊处理,直接在内存区域检索“input”“scanf”之类的字样,检索到了check_the_whole_input这个函数,查看一下交叉引用,可以确定这就是校验函数,应该就是把整段输入做个加密和加密后的flag比对,这里发现“HackForFun”字样,可能是密钥什么的。
现代的语言1

接着就是F5大法了,读了一下反编译的代码并且问了一下AI之后发现是标准的RC4加密,那么“HackForFun”应该是密钥。发现while循环验证密码,而&xmmword_1400AEA92是加密flag数组存储空间的起始地址,由前置的if条件“v7 >= 0x46”或者直接查看十六进制数据发现密文长度为70字节,也就是说flag长度是70位
现代的语言2
现代的语言3

注意ida默认小端序存储,解密如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from Crypto.Cipher import ARC4

encrypted_data = bytes.fromhex(
"C529F00D83E3C5E55FDD1A734C5FABF7"
"D7D4096E11D98D8E2E8A5A3453FE8FB6"
"CD1FD874F9E1420A8B1DC89847DBA3A8"
"964DA981BE666C9E6D7D78417720A6B8"
"687FE674CEB9"
)

rc4 = ARC4.new(b"HackForFun")

rc4_bytes = rc4.encrypt(bytes(70))

flag = bytes([encrypted_data[i] ^ rc4_bytes[i] for i in range(70)])
print(encrypted_data)
print(flag.decode(errors="ignore"))
# W4terCTF{3MPOwErIN6_eVeRYOne_t0_BuiLD_R3II4B1E_4nD_EfficIEnT_SoF7waRE}

和谐小APP

题目附件entry-default.hap,看来是一道类似apk逆向的题目
HarmonyOS官方文档查阅.hap文件的定义:
HAP(Harmony Ability Package)是应用安装和运行的基本单元。HAP包是由代码、资源、第三方库、配置文件等打包生成的模块包,其主要分为两种类型:entry和feature。
+entry:应用的主模块,作为应用的入口,提供了应用的基础功能。
+feature:应用的动态特性模块,作为应用能力的扩展,可以根据用户的需求和设备类型进行选择性安装。

.hap文件结构如下:
和谐小APP1

使用鸿蒙hap应用反编译工具 abc-decompiler查看modules.abc.jadx,全局检索flag找到如下代码:
和谐小APP2

注意到20250428可能是密钥,猜测flag及其加密逻辑在libentry.so中,ida打开找到函数sub_1B80,可以确定v12就是密钥20250428,target数组存放密文,长度为128字节
和谐小APP3
和谐小APP4

解密如下:

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
target_data = bytes([
0x57, 0x34, 0x74, 0x65, 0x72, 0x6F, 0xA8, 0x4E, 0x7D, 0x57, 0x30, 0x9D, 0x7F, 0x53, 0x59, 0xEB,
0xA1, 0x68, 0x4D, 0x44, 0xC2, 0xC3, 0x42, 0x75, 0x73, 0x83, 0xAF, 0x43, 0x73, 0x33, 0x57, 0xA8,
0x04, 0x6D, 0x56, 0x07, 0xBA, 0x47, 0x65, 0x75, 0x47, 0x93, 0x02, 0x6F, 0x75, 0xC2, 0xAE, 0x04,
0x79, 0x50, 0xC8, 0xB8, 0x3B, 0x70, 0x45, 0x99, 0xD5, 0x62, 0x42, 0x00, 0x10, 0xD2, 0x6B, 0x48,
0x00, 0x3C, 0xCE, 0x74, 0x4E, 0x00, 0x68, 0xCA, 0x7D, 0x54, 0x00, 0x94, 0xC6, 0x86, 0x5A, 0x00,
0xC0, 0xC2, 0x8F, 0x60, 0x00, 0xEC, 0xBE, 0x98, 0x66, 0x00, 0x18, 0xBB, 0xA1, 0x6C, 0x00, 0x44,
0xB7, 0xAA, 0x72, 0x00, 0x70, 0xB3, 0xB3, 0x78, 0x00, 0x9C, 0xAF, 0xBC, 0x7E, 0x00, 0xC8, 0xAB,
0xC5, 0x84, 0x00, 0xF4, 0xA7, 0xCE, 0x8A, 0x00, 0x20, 0xA4, 0xD7, 0x90, 0x00, 0x00, 0x00, 0x01,
])

key = 20250428
v4 = 5 * key
v5 = 15 * key
v6 = 20 * key
v7 = 10 * key

buffer = bytearray(target_data)
v3 = 0

for i in range(0, len(buffer), 20):
chunk = buffer[i:i+20]

if len(chunk) >= 20:
dword_val = int.from_bytes(chunk[15:19], 'little')
chunk[15:19] = (dword_val ^ (v5 + v3)).to_bytes(4, 'little')

if len(chunk) >= 10:
dword_val = int.from_bytes(chunk[10:14], 'little')
chunk[10:14] = (dword_val ^ (v7 + v3)).to_bytes(4, 'little')

if len(chunk) >= 5:
dword_val = int.from_bytes(chunk[5:9], 'little')
chunk[5:9] = (dword_val ^ (v4 + v3)).to_bytes(4, 'little')

dword_val = int.from_bytes(chunk[0:4], 'little')
chunk[0:4] = (dword_val ^ v3).to_bytes(4, 'little')

buffer[i:i+len(chunk)] = chunk
v3 += v6

print("Decrypted input:", buffer.rstrip(b'\x00').decode('utf-8', errors='ignore'))
# W4terCTF{When_YoUr_Dr3ams_Com3_A1IV3_yOu'rE_uNsTOPpabLE}

Lite Obfuscator

拿到文件lite-obf,先使用DIE(Detect It Easy)查看格式,发现是ELF64格式,由C语言编写,直接使用ida打开:
LitOb1

进来看到main函数,看似简单直白,上来就是输入和加密处理比较flag,这里看到加密flag存储在命名为unk_30038的数组中,提取出来。接下来开开心心点开了sub_1465B函数,然后看到调用了一堆函数。
LitOb2
LitOb8
LitOb3

不用多说,接下来里面随便点进去一个都是继续调用一堆函数,看函数调用图即可(菜单栏View->Graphs->Function calls)。也许是某种不可名状的代码混淆技术,至少现在我还不知道这是什么混淆方式,反正一个个点进去完全不可能,但是选择一条点到底了之后发现是空函数体,多看了几条也是这样。也就是说运气好的话可能只有一层加密。
LitOb4
LitOb5

然后就开始费劲心思地去想怎么找到加密函数,想到一般的加密方法都有异或操作,直接全局搜索^,找到加密函数就是sub_F269,是标准TEA加密:
LitOb6
LitOb7

解密如下:

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
import struct

def decrypt(data):
# 将字节列表转换为bytes对象
data_bytes = bytes(data)
# # 将bytes转换为uint32列表(小端序)
data_uint32 = list(struct.unpack(f'<{len(data_bytes)//4}I', data_bytes))

for i in range(0, len(data_uint32), 2):
if i + 1 >= len(data_uint32):
break # 跳过不完整的最后一组

v5, v4 = data_uint32[i], data_uint32[i + 1]
v7 = -1640531527 * 32 # 32轮后的v7

for _ in range(32):
v4 = (v4 - ((v7 + v5) ^ ((v5 >> 5) + 939851419) ^ ((v5 << 4) + 1221578118))) & 0xFFFFFFFF
v5 = (v5 - ((v7 + v4) ^ ((v4 >> 5) - 1360295094) ^ ((v4 << 4) + 2112973469))) & 0xFFFFFFFF
v7 = (v7 + 1640531527) & 0xFFFFFFFF

data_uint32[i], data_uint32[i + 1] = v5, v4

# 将uint32列表转回bytes(小端序)
return struct.pack(f'<{len(data_uint32)}I', *data_uint32)

hex_data = [
0xBB, 0xB6, 0x26, 0x0C, 0xBA, 0xC2, 0xD5, 0x34,
0x7E, 0xD6, 0xE9, 0xB8, 0xE0, 0x42, 0x0B, 0xE3,
0xB4, 0x3D, 0x9D, 0xE6, 0x4A, 0x14, 0x97, 0x30,
0xD6, 0xB8, 0x7A, 0x8D, 0xF0, 0xDA, 0x87, 0xA2,
0x0D, 0x34, 0x95, 0xB2, 0xE2, 0x92, 0x41, 0xCF
]

decrypted_data = decrypt(hex_data)

print(decrypted_data)
# W4terCTF{FUnCtioN_CA1l1ng_coN5UmeS_7ime}