[CISCN2025]初赛 Re
[CISCN2025]初赛 ReWP
1. wasm-login
下载附件之后google打开分析一下,查看一下html文件,发现逻辑都在release.js当中:
并且在最后一行提示<!-- 测试账号 admin 测试密码 admin-->接着分析release.js:

会使用authenticate执行wasm当中的逻辑,然后返回。看到build里面还有一个wasm.map,这个文件可以直接查看源码,notepad++打开,发现非常混乱,直接正则表达式将\\n换成\r\n,然后就能看到正常的格式了,接着把authenticate有关的提取出来

就是先将密码base64,然后获取时间戳,构建json,对json进行hmac-sha256,再用签名的值构建一个json,最后返回。要注意的是,base64并不是标准的base64,在map当中进行了表的变换:

变成了这个表:NhR4UJ+z5qFGiTCaAIDYwZ0dLl6PEXKgostxuMv8rHBp3n9emjQf1cWb2/VkS7yO。还有就是sha256的ipad和opad都魔改过:

最后再结合题目信息给到:

已经有时间范围了,大致就是21日到22日的范围,就剩下一个时间不确定,需要爆破,在html找到md5之后的前缀

接下来就能写爆破脚本了:
import hashlib
import base64
import datetime
md5_pre = "ccaf33e3512e31f3"
old_table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
new_table = "NhR4UJ+z5qFGiTCaAIDYwZ0dLl6PEXKgostxuMv8rHBp3n9emjQf1cWb2/VkS7yO"
tran_table = str.maketrans(old_table, new_table)
def mybase64(data: bytes) -> str:
std_b64_bytes = base64.b64encode(data)
std_b64_str = std_b64_bytes.decode('ascii')
return std_b64_str.translate(tran_table)
def sha256(data: bytes) -> bytes:
return hashlib.sha256(data).digest()
def hmac_sha256(key: bytes, message: bytes) -> bytes:
block = 64
if len(key) > block:
key_h = sha256(key)
padded = key_h + b"\x00" * (block - len(key_h))
else:
padded = key + b"\x00" * (block - len(key))
ipad = bytes([b ^ 0x76 for b in padded])
opad = bytes([b ^ 0x3C for b in padded])
inner_hash = sha256(ipad + message)
outer_hash = sha256(inner_hash + opad)
return outer_hash
def main() -> None:
beijing_tz = datetime.timezone(datetime.timedelta(hours=8))
start_dt = datetime.datetime(2025, 12, 22, 0, 0, 0, tzinfo=beijing_tz)
end_dt = datetime.datetime(2025, 12, 22, 2, 00, 0, tzinfo=beijing_tz)
start_sec = int(start_dt.timestamp())
end_sec = int(end_dt.timestamp())
username = "admin"
password = "admin"
encoded_password = mybase64(password.encode("utf-8"))
message_bytes = f'{{"username":"{username}","password":"{encoded_password}"}}'.encode("utf-8")
prefix_bytes = f'{{"username":"{username}","password":"{encoded_password}","signature":"'.encode("utf-8")
suffix_bytes = b'"}'
base_md5 = hashlib.md5(prefix_bytes)
start_ms = start_sec * 1000
end_ms = end_sec * 1000
print(f"Start Time (Beijing): {start_dt}")
print(f"End Time (Beijing): {end_dt}")
for ts in range(start_ms, end_ms + 1):
sig = hmac_sha256(str(ts).encode("utf-8"), message_bytes)
sig_b64 = mybase64(sig).encode("utf-8")
h = base_md5.copy()
h.update(sig_b64)
h.update(suffix_bytes)
check = h.hexdigest()
if check.startswith(md5_pre):
print(f"flag{{{check}}}")
return
raise RuntimeError("not found in given time window")
main()
# Start Time (Beijing): 2025-12-22 00:00:00+08:00
# End Time (Beijing): 2025-12-22 02:00:00+08:00
# flag{ccaf33e3512e31f36228f0b97ccbc8f1}
2. baby-game
打开游戏发现图标是:

这是用 Godot Engine (Godot 游戏引擎) 编写的游戏,直接使用gdreTools拆包,运行游戏的时候,发现需要收集完所有金币才能校验flag,先看看coin的文件:

发现用到了game_manager这个东西,检查看看:

score加一就将flag.key当中的A替换成B。然后看看flag.gdc文件:

flag里面就是一个AES,然后使用变换之后的key进行ECB加密,最后和d458af702a680ae4d089ce32fc39945d进行比较,直接exp:
from Crypto.Cipher import AES
key = b'FanBglFanBglOoO!'
cipher = AES.new(key, AES.MODE_ECB)
data = bytes.fromhex('d458af702a680ae4d089ce32fc39945d')
print(cipher.decrypt(data))
# wOW~youAregrEaT!
3. Eternum
这是一题协议逆向的题目。下载题目,解压后有三个文件,一个run,kworker,tcp.pcap。首先看看这个run:
kworker 192.168.8.160:13337
直接启动了kworker,并且传入了ip。猜测是个后门,C2是192.168.8.160:13337。接着看看数据包,追踪一下tcp流:

发现数据包的data部分都有一个ET3RNUMX的魔数,估计就是协议头了。接着分析一下hexdump,发现魔数后面的四字节就是真正数据的长度:

但是发现数据是乱码,猜测是被加密了。那么接下来就得分析一下kworker这个后门程序了。首先die查壳发现加了UPX,直接upx脱一下壳,接着ida打开:

发现被去符号了,main都找不到。不过runtime猜测应该是Go写的后门,这里用GoReSym修复一下符号:

不过修复之后貌似还是混淆的函数名,不过可以找到main函数了,接着往下找发现了一个mian_main_func1的函数,被runtime_newproc这个放到任务队列,然后异步执行的

接下来就得分析一下mian_main_func1这个函数了:

这个函数就是创建一个GCM模式的加密对象,这里的HpkfE6vaP2b_e1JiVk9Nmh作用是GCM的构造器,接着传入的里面可以看到有AES的字样,那么这里推测就是使用的AES-GCM加密,这里的16和12分别就是认证签名的长度,和nonce也就是iv的长度。通过上层的这个函数可以获取到密钥

密钥为:xfqGcVjrOWp5tUGCPFQq448nPDjILTe7。
接着就是数据格式,AES-GCM的Go 标准库的封装习惯是这样的:
最后推测出来数据包当中Data的格式如下:
|<-- 8bytes -->|<--4 bytes -->|<------body-------------------->|
magic number body_len noce(12bytes)+cipher+tag(16bytes)
那么接下来写个脚本解密一下数据包:
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
key = b"xfqGcVjrOWp5tUGCPFQq448nPDjILTe7"
aesgcm = AESGCM(key)
data = bytes.fromhex("[放入data]")
flag = data[:8]
bodylen = int.from_bytes(data[8:12], byteorder="big")
tag = data[-16:]
iv = data[12:24]
cipher = data[24:24+bodylen]
plain = aesgcm.decrypt(iv,cipher,None)
plainText = ""
for i in plain:
if i >= 32 and i <=126:
plainText += str(chr(i))
elif chr(i) == '\n':
plainText += '\n'
print(plainText)
print(plain)
print("hex : " , plain.hex())
最后在这两条数据包找到flag:

解密后分别是:
# 31
(/
base32 /var/opt/s*/
b'(\xb5/\xfd\x04\x00\xb1\x00\x00\x12\x14\n\x12base32 /var/opt/s*/\xf2\xa9\xf1'
# 32
(/yKIMZWGCZ33MI3WGNJYG4YDALJSMIYDCLJUMRSDILJYGUZDMLLBGRQTIN3BGY2WCMLBHF6QU===
o
b'(\xb5/\xfd\x04\x00y\x02\x00\x08\x01\x12K\x12IMZWGCZ33MI3WGNJYG4YDALJSMIYDCLJUMRSDILJYGUZDMLLBGRQTIN3BGY2WCMLBHF6QU===\n\xa1o\xf2\x12'
得知是一个base32的加密,但是最后解密出来的密文有点问题,从MZ开头才是正确的密文:

# flag{b7c58700-2b01-4dd4-8526-a4a47a65a1a9}
本题遇到的问题:
流量分析这里后面解密出来的数据为什么有这么多奇怪的数据,这个go的后门分析的也不是很清楚,如果有懂的师傅请私信一下,万分感谢orz。
4. vvvmmm
拿到题目丢虚拟机里面运行一下看看有没有什么信息:

发现了大标题和input的提示,接着丢到ida,发现有upx,脱壳之后丢到IDA当中,从start很容易能找到main函数:

只不过这个流程图长得太抽象了,反编译之后发现有很长一段异或的东西:

猜测一下,估计就是输出大标题的,因为Strings当中没找到相关的字符串。接着通过Strings看到了有Unicorn的字样:

猜测使用了unicorn engine。但是符号都被去掉了,先尝试还原一下符号,还原符号后main函数拉到最下面就是主要逻辑了:

使用的就是uc来模拟执行一段shellcode。用了三个uc_mem_write,将数据写到模拟器地址当中,接着看到这个uc_mem_open,该函数声明如下:
uc_open(arch, mode, &handle)
第一个函数传入的是8,那么在unicorn.h当中代表的就是UC_ARCH_RISCV,所以这里模拟的是riscv架构下的执行。接着就是动调了,需要将相关的shellcode给dump下来:

这个就是shellCode,将shellcode写到模拟器的0地址。接下来用idapython给dump下来:
import ida_bytes
start_ea = 0x64C3F0
size = 662
output_file = "D:\\浏览器下载\\test\\vm\\test.bin"
data = ida_bytes.get_bytes(start_ea, size)
if data:
with open(output_file, "wb") as f:
f.write(data)
print(f"[+] Success: Dumped {len(data)} bytes from {hex(start_ea)} to {output_file}")
else:
print("[-] Error: Failed to get bytes. Check address.")
dump下来之后使用ida打开,并选择riscv框架,然后反编译就能看到加密代码了:

前面的一部分是将key进行哈希摘要,用作后续的加密运算。这个hash方式如下:
加密当中最主要的就是最后的部分,就是个异或:

v8[i]与运算后的v4异或存放在v7[i+1],v8[i+1]存放在v7[i+1]。而这里的v4和v6简单推导一下就是不断的模0x13579BDF再相乘。密文嵌在汇编代码当中,经过简单计算之后抽出来:

0x45034F63, 0x534762D2, 0x44B36D04, 0x44C3ED6A,
0x79BB60B0, 0x42A1E767, 0x3EDB7E6C, 0x30E1551D,
0x4D3ABAA4, 0x6AA29948, 0x51CE8847, 0x51623FAF
接着就是找key了。这个模拟的函数会传入两个参数,根据riscv架构的传参规则如下:参数1使用reg11,参数2使用reg12,再回到vm当中看到有uc_reg_write写入模拟寄存器:

写入的是两个地址,也就是指针了。那么0x10000000是input的话,那么0x10001000就是key了,动调到适当位置,就能将64C6C0中的key摘出来来了:
"e4Y8YRXVzg2HRrCUy35CM0Txq91HzMGZ"
最后写出解密脚本:
#include <stdio.h>
#include <stdint.h>
#include <string.h>
uint64_t power(uint64_t base, uint64_t exp, uint64_t mod) {
uint64_t res = 1;
base %= mod;
while (exp > 0) {
if (exp % 2 == 1) res = (res * base) % mod;
base = (base * base) % mod;
exp /= 2;
}
return res;
}
int main() {
uint32_t vals[] = {
0x45034F63, 0x534762D2, 0x44B36D04, 0x44C3ED6A,
0x79BB60B0, 0x42A1E767, 0x3EDB7E6C, 0x30E1551D,
0x4D3ABAA4, 0x6AA29948, 0x51CE8847, 0x51623FAF
};
uint64_t m = 324508639;
char key_str[] = "e4Y8YRXVzg2HRrCUy35CM0Txq91HzMGZ";
uint64_t h = 1;
size_t len = strlen(key_str);
for (size_t i = 0; i < len; i++) {
h = (key_str[i] + 31 * h) & 0xfffffffffffffff;
}
uint64_t s1 = h;
uint64_t s2 = h >> 16;
for (int i = 0; i < 12; i += 2) {
s2 = power(s2 & 0xffffffff, 13, m);
s1 = power(s1 & 0xffffffff, 13, m);
uint32_t t1 = vals[i] ^ (uint32_t)s2;
uint32_t t2 = vals[i+1] ^ (uint32_t)s1;
uint8_t *p = (uint8_t *)&t1;
for (int j = 0; j < 4; j++) printf("%c", p[j]);
p = (uint8_t *)&t2;
for (int j = 0; j < 4; j++) printf("%c", p[j]);
}
return 0;
}
执行解出flag:
"fANUES0XtUXBDEbOXs4xFcXDb3Q5kMU87bZLMZJfuRnCvfwX"

浙公网安备 33010602011771号