[CISCN2025]初赛 Re

[CISCN2025]初赛 ReWP

1. wasm-login

下载附件之后google打开分析一下,查看一下html文件,发现逻辑都在release.js当中:

并且在最后一行提示<!-- 测试账号 admin 测试密码 admin-->接着分析release.js
img

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

​ 就是先将密码base64,然后获取时间戳,构建json,对json进行hmac-sha256,再用签名的值构建一个json,最后返回。要注意的是,base64并不是标准的base64,在map当中进行了表的变换:
img
变成了这个表:NhR4UJ+z5qFGiTCaAIDYwZ0dLl6PEXKgostxuMv8rHBp3n9emjQf1cWb2/VkS7yO。还有就是sha256的ipad和opad都魔改过:
img
最后再结合题目信息给到:
img
已经有时间范围了,大致就是21日到22日的范围,就剩下一个时间不确定,需要爆破,在html找到md5之后的前缀
img
接下来就能写爆破脚本了:

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

打开游戏发现图标是:
img

这是用 Godot Engine (Godot 游戏引擎) 编写的游戏,直接使用gdreTools拆包,运行游戏的时候,发现需要收集完所有金币才能校验flag,先看看coin的文件:
img
发现用到了game_manager这个东西,检查看看:
img
score加一就将flag.key当中的A替换成B。然后看看flag.gdc文件:
img
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

​ 这是一题协议逆向的题目。下载题目,解压后有三个文件,一个runkworkertcp.pcap。首先看看这个run:

kworker 192.168.8.160:13337

直接启动了kworker,并且传入了ip。猜测是个后门,C2是192.168.8.160:13337。接着看看数据包,追踪一下tcp流:
img

发现数据包的data部分都有一个ET3RNUMX的魔数,估计就是协议头了。接着分析一下hexdump,发现魔数后面的四字节就是真正数据的长度:
img
但是发现数据是乱码,猜测是被加密了。那么接下来就得分析一下kworker这个后门程序了。首先die查壳发现加了UPX,直接upx脱一下壳,接着ida打开:
img

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

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

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

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

​ 接着就是数据格式,AES-GCM的Go 标准库的封装习惯是这样的:

\[\text{FinalPayload} = \underbrace{\text{Nonce}}_{\text{12 bytes}} + \underbrace{\text{Ciphertext}}_{\text{N bytes}} + \underbrace{\text{Tag}}_{\text{16 bytes}} \]

最后推测出来数据包当中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:
img
解密后分别是:

# 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开头才是正确的密文:
img

# flag{b7c58700-2b01-4dd4-8526-a4a47a65a1a9}
本题遇到的问题:

​ 流量分析这里后面解密出来的数据为什么有这么多奇怪的数据,这个go的后门分析的也不是很清楚,如果有懂的师傅请私信一下,万分感谢orz。

4. vvvmmm

​ 拿到题目丢虚拟机里面运行一下看看有没有什么信息:
img
发现了大标题和input的提示,接着丢到ida,发现有upx,脱壳之后丢到IDA当中,从start很容易能找到main函数:
img
只不过这个流程图长得太抽象了,反编译之后发现有很长一段异或的东西:
img
猜测一下,估计就是输出大标题的,因为Strings当中没找到相关的字符串。接着通过Strings看到了有Unicorn的字样:
img
猜测使用了unicorn engine。但是符号都被去掉了,先尝试还原一下符号,还原符号后main函数拉到最下面就是主要逻辑了:
img

使用的就是uc来模拟执行一段shellcode。用了三个uc_mem_write,将数据写到模拟器地址当中,接着看到这个uc_mem_open,该函数声明如下:

uc_open(arch, mode, &handle)

第一个函数传入的是8,那么在unicorn.h当中代表的就是UC_ARCH_RISCV,所以这里模拟的是riscv架构下的执行。接着就是动调了,需要将相关的shellcode给dump下来:
img

这个就是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框架,然后反编译就能看到加密代码了:
img
前面的一部分是将key进行哈希摘要,用作后续的加密运算。这个hash方式如下:

\[newHash = 32 \times hash + char \]

加密当中最主要的就是最后的部分,就是个异或:
img

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

0x45034F63, 0x534762D2, 0x44B36D04, 0x44C3ED6A,
0x79BB60B0, 0x42A1E767, 0x3EDB7E6C, 0x30E1551D,
0x4D3ABAA4, 0x6AA29948, 0x51CE8847, 0x51623FAF

接着就是找key了。这个模拟的函数会传入两个参数,根据riscv架构的传参规则如下:参数1使用reg11,参数2使用reg12,再回到vm当中看到有uc_reg_write写入模拟寄存器:
img

写入的是两个地址,也就是指针了。那么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"
posted @ 2026-01-04 19:31  x0rrrr  阅读(2)  评论(0)    收藏  举报