TypechoJoeTheme

霍雅的博客

登录
用户名
密码
/
注册
用户名
邮箱

霍雅

追求源于热爱,极致源于梦想!
网站页面

2026长城杯决赛RE wp

2026-05-20
/
1 评论
/
119 阅读
/
正在检测是否收录...
05/20

长城杯决赛RE wp

attachment WP_new

attachment

解压文件,发现有个jar包,一个pcap,先来看看这个pcap。可以看到存在大量http请求,主要通信目标为192.168.117.136

结合另一个movie-review-system-1.0.0.jar文件猜测这个ip是这个服务提供的接口,而且在这个http流量当中发现下面这些接口被大量调用:

·/api/reviews/add

·/api/user/avatar

在reviews这个评论接口,出现了大量特殊格式的内容,例如:

{"content": "data[/payload.enc]"}
{"content": "data[0]"}
{"content": "data[102]"}
{"content": "data[/tmp/payload_run]"}
{"content": "data[chmod +x /tmp/payload_run]"}

而头像avatar接口则不断出现类似的内容:

{"emojiAvatarId": 16}
{"emojiAvatarId": 25}
{"emojiAvatarId": 23}

这时候就推测这个请求不是正常的用户行为,而是在通过这两个接口传递某种特殊的数据,这里面的data[]猜测是在传递emojiAvatarId。这时候需要对jar包进行分析

使用jadx反编译,翻了一下文件发现有一个playload.enc

还发现了几个比较可疑的类名:

那么首先来看看这个UserBehaviorAnalyticsAspect

这里使用了Spring AOP,将请求当中的数据写入到VM上下文当中,这些数据来自emjiAvatarId,以及评论内容[]当中的数据,就比如说{"content": "data[/payload.enc]"}会向VMContext当中传入/payload.enc这个字符串

接下来看看这个VMContext类:

非常简单啊,这个ctx当中,就是一个list类型的buffer,结合上上面的User类分析,这个就是虚拟机类似opcode集合的东西了,然后虚拟机执行就是靠读取这个opcode流来一步步执行的。

接下来就是这个AnalyticsReportGenerator 类

可以看到这里就是一个vm的结构了,然后根据这个结构恢复一下对应的opcode的作用:

opcode作用
10读取资源文件
11读取byte数组指定下标
12修改byte数组指定下标
13异或运算
15写文件
16执行系统命令
17获取byte数组长度
18字符串转整数
20交换栈顶元素
21Jmp
22Jcc
23Sub
24Add
25从栈中取出指定位置的元素
26Pop

大概就是这样的

这里先将https的追踪流从wireshark当中保存下来,然后写个python脚本将vm有关的给单独拎出来:

import re
​
INPUT_FILE = "https.txt"
OUTPUT_FILE = "vm_stream.txt"
​
​
def main():
    with open(INPUT_FILE, "r", encoding="utf-8", errors="ignore") as f:
        text = f.read()
​
    events = []
​
    # 按 HTTP 请求切开,只处理 POST 请求块
    blocks = re.split(r"(?=POST\s+/api/)", text)
​
    for block in blocks:
        # 1. 评论接口:提取 data[...] 里面的内容
        if block.startswith("POST /api/reviews/add"):
            m = re.search(r'"content"\s*:\s*"data\[(.*?)\]"', block, re.S)
            if m:
                events.append(m.group(1))
​
        # 2. 头像接口:提取 emojiAvatarId,作为 opcode
        elif block.startswith("POST /api/user/avatar"):
            m = re.search(r'"emojiAvatarId"\s*:\s*(\d+)', block)
            if m:
                events.append(m.group(1))
​
    with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
        for e in events:
            f.write(str(e) + "\n")
​
    print(f"[+] 提取到 {len(events)} 条 VM 事件")
    print(f"[+] 已保存到 {OUTPUT_FILE}")
​
​
if __name__ == "__main__":
    main()

结果如下:

根据dump下来的指令流,根据vm规则恢复一下大概大概是这样的:

key = 102;
​
for (i = 0; i < len(payload_enc); i++) {
    p = (payload_enc[i] ^ key) - i;
    payload_enc[i] = p & 0xff;
    key = p ^ 55;
}

那么此时可以直接根据这个逻辑来解密这个payload_enc了:

enc = open("payload.enc", "rb").read()
​
key = 102
out = bytearray()
​
for i, c in enumerate(enc):
    p = ((c ^ key) - i) & 0xff
    out.append(p)
    key = p ^ 55
​
open("payload_run", "wb").write(out)

然后根据vm指令流当中的字符串,大致可以判断这个文件是个elf文件,直接丢ida看看:

分析了一下结构发现这个就是一个aes cbc的结构,然后可以从xmmword_5280,xmmword_5270,byte_6970当中分别存放的是key,iv以及sbox

从内存当中获取一下:

key = 4a7f2c91b35ed816fa4309cc7be5283d
iv  = 8e1af65530c974bb2d974e1160daa73c

sbox

发现这个sbox并不是标准的sbox,还需要做一定的修改。

再分析后发现这个aes的输入在哪里:

在开头可以发现这里读取了文件,这个文件路径是:
/home/ccb/flag.txt,此外还发现了一些有趣的字符串:

那么猜测就是通过读取flag.txt然后aescbc加密,然后就传到了/api/telementry这个接口当中,那么就通过wireshark查查这个ip和接口

可以发现密文长度是80字节,那么直接扒出来写个解密脚本

sbox = [
0xC5,0xEA,0xB8,0x6C,0x91,0xA2,0x11,0x44,0x05,0xBA,0x76,0x99,0x45,0x53,0xEF,0x54,
0xA5,0xF9,0x90,0x06,0xF6,0x28,0xEB,0x48,0x85,0x66,0x64,0x5C,0x3A,0x0E,0xE7,0x1B,
0xF5,0x70,0xDB,0xA1,0x6F,0xE4,0xCE,0xCF,0xB6,0xE2,0xD9,0xA4,0xD2,0xB2,0xE9,0xC7,
0xE5,0x9D,0xFE,0x2E,0xFF,0x84,0x09,0x50,0xD0,0x41,0x20,0x5F,0xD4,0x4D,0xAA,0x61,
0xDD,0x15,0x1F,0x26,0xCA,0xFD,0x1D,0xBD,0x7A,0x57,0xBF,0x46,0x40,0xB3,0x2A,0x93,
0x96,0x39,0x56,0xBE,0xCB,0x9C,0x9F,0xF1,0x4E,0x49,0x7E,0x8E,0xD3,0xB9,0xC4,0xFA,
0xD5,0x67,0x03,0x1A,0x58,0x55,0x30,0x7F,0x32,0xC3,0x8F,0xDF,0xA3,0xD1,0x0F,0xDA,
0x4F,0x88,0x6D,0xC1,0x37,0xD6,0x62,0x17,0xA7,0x19,0x6B,0x27,0x98,0xA9,0x7D,0x0C,
0x23,0x82,0xAD,0x52,0x42,0x68,0xDE,0x1E,0xA8,0x3B,0x33,0x3D,0x43,0x9B,0x13,0x0B,
0xF0,0xCC,0x8C,0x01,0x12,0x75,0xEE,0x47,0x07,0x8B,0x14,0x2B,0xD8,0xAE,0x04,0x87,
0x86,0xDC,0xBB,0xE8,0xE6,0x3C,0x78,0x77,0xC2,0xE0,0x69,0x29,0x02,0xB1,0x35,0xB5,
0x00,0x7C,0x83,0xC8,0x18,0x8A,0x60,0x36,0x24,0xC9,0xFB,0x38,0xAF,0x80,0xB0,0x31,
0xCD,0x59,0x94,0xC6,0x4C,0xD7,0xC0,0x71,0xE1,0xED,0x8D,0x79,0x3F,0x4B,0x72,0x9E,
0x3E,0x08,0x2C,0x9A,0x0A,0x63,0x22,0x5B,0x1C,0x5A,0x25,0xF8,0x4A,0xF7,0xA0,0xE3,
0x6A,0x2F,0x89,0x74,0x7B,0x5E,0x2D,0x5D,0xBC,0x95,0xA6,0x0D,0x16,0xFC,0xAC,0x34,
0xF4,0x51,0xAB,0x6E,0x92,0xEC,0xB4,0x97,0x81,0x65,0xF3,0xB7,0x73,0x10,0x21,0xF2
]
​
inv_sbox = [0] * 256
for i, x in enumerate(sbox):
    inv_sbox[x] = i
​
rcon = [0,1,2,4,8,16,32,64,128,27,54]
​
def xtime(x):
    return (((x << 1) ^ 0x1b) & 0xff) if x & 0x80 else ((x << 1) & 0xff)
​
def gmul(a, b):
    r = 0
    while b:
        if b & 1:
            r ^= a
        a = xtime(a)
        b >>= 1
    return r
​
def expand_key(key):
    w = list(key)
    for i in range(4, 44):
        t = w[4*(i-1):4*i]
        if i % 4 == 0:
            t = [sbox[t[1]], sbox[t[2]], sbox[t[3]], sbox[t[0]]]
            t[0] ^= rcon[i // 4]
        for j in range(4):
            w.append(w[4*(i-4)+j] ^ t[j])
    return [w[16*i:16*i+16] for i in range(11)]
​
def add_round_key(s, rk):
    for r in range(4):
        for c in range(4):
            s[4*r+c] ^= rk[4*c+r]
​
def inv_shift_rows(s):
    shifts = [0, 3, 1, 2]
    for r in range(1, 4):
        row = s[4*r:4*r+4]
        sh = shifts[r]
        for c in range(4):
            s[4*r+c] = row[(c - sh) & 3]
​
def inv_sub_bytes(s):
    for i in range(16):
        s[i] = inv_sbox[s[i]]
​
def inv_mix_columns(s):
    for c in range(4):
        a = [s[c], s[4+c], s[8+c], s[12+c]]
        s[c]    = gmul(a[0],14) ^ gmul(a[1],11) ^ gmul(a[2],13) ^ gmul(a[3],9)
        s[4+c]  = gmul(a[0],9)  ^ gmul(a[1],14) ^ gmul(a[2],11) ^ gmul(a[3],13)
        s[8+c]  = gmul(a[0],13) ^ gmul(a[1],9)  ^ gmul(a[2],14) ^ gmul(a[3],11)
        s[12+c] = gmul(a[0],11) ^ gmul(a[1],13) ^ gmul(a[2],9)  ^ gmul(a[3],14)
​
def decrypt_block(block, keys):
    s = [0] * 16
    for c in range(4):
        for r in range(4):
            s[4*r+c] = block[4*c+r]
​
    add_round_key(s, keys[10])
    for rnd in range(9, 0, -1):
        inv_shift_rows(s)
        inv_sub_bytes(s)
        add_round_key(s, keys[rnd])
        inv_mix_columns(s)
​
    inv_shift_rows(s)
    inv_sub_bytes(s)
    add_round_key(s, keys[0])
​
    out = [0] * 16
    for c in range(4):
        for r in range(4):
            out[4*c+r] = s[4*r+c]
    return bytes(out)
​
key = bytes.fromhex("4a7f2c91b35ed816fa4309cc7be5283d")
iv = bytes.fromhex("8e1af65530c974bb2d974e1160daa73c")
ct = bytes.fromhex(
    "08a76a304f8a7d64baace233c30d8e78"
    "9e27ec1ae589e7b36252ea00ecf2a9"
    "c274b18754f4758095956a08dc1c6793"
    "e07cf91658ae232ac3935aa17c03294a"
    "625c90ba2ef4b482ffb145388829ed5554"
)
​
keys = expand_key(key)
pt = b""
prev = iv
​
for i in range(0, len(ct), 16):
    dec = decrypt_block(ct[i:i+16], keys)
    pt += bytes(a ^ b for a, b in zip(dec, prev))
    prev = ct[i:i+16]
​
pad = pt[-1]
pt = pt[:-pad]
​
print(pt.decode())
# flag{F1n@1ly_Y0u_G0t_Th1s_f1ag_and_f1nd_7h3_TRUTH_D0_Y0u_L1k3_1t?}

dokigame

基于 Ren'Py 打包的小游戏。进入目录后可以看到 game/script.rpyc

使用unrpyc解包

打开解包后的文件

可以看到有个PE文件落地并执行了

提出来有个upx

Upx-d 直接拖

回到rpy文件

大概逻辑就是

输入一个值xor 35

和1.exe的输出比较

输出的值

Exp:

a=[0x45, 0x12, 0x14, 0x40, 0x16, 0x10, 0x40, 0x10, 0xe, 0x47, 0x40, 0x11, 0x15, 0xe, 0x17, 0x15, 0x41, 0x12, 0xe, 0x41, 0x10, 0x14, 0x10, 0xe, 0x11, 0x40, 0x42, 0x13, 0x13, 0x42, 0x15, 0x42, 0x15, 0x14, 0x11, 0x12]

flag = ""

for i in range(len(a)):

flag += chr(a[i]^35)

print(flag)

#f17c53c3-dc26-46b1-b373-2ca00a6a6721

dumbcpp

这题是一个 Unity IL2CPP 逆向题

我们先用Il2CppDumper

恢复符号和偏移

但是GameAssembly.dll有tmd壳

所以得先拖壳

先用x64dbg调试

然后用virtualprotect多运行几次

直到dll不再是tmd的那种壳结构

可以运行28次,29是别的dll

直接dump

然后恢复符号

查看dbspy

符号名是xyz987

恢复完就是这些了

字段初始化

check_input()

手写一下大概就是

input = input.Trim();
if (input.Length != 42)
    ShowWrong();

for (int i = 0; i < 100000; i++)
    rand.Next();

realKey = GetRealKey();
cipher = AESEncrypt(input, realKey);

if (cipher == target_cipher && anti_debug_flag == false)
    ShowRight();
else
    ShowWrong();

aes是cbc模式

然后key向右补0

把 key_part1 的整数逐个拼成字符串,再拼上 key_part2 从第 3 位开始的 5 个字符,生成 AES 的真实密钥

回到ctor

dump.cs 中,<PrivateImplementationDetails> 给出了静态数组的 Metadata offset。本题需要的两段数据分别是:

target_cipher:偏移 0x2B9890,长度 48

key_part1:偏移 0x2B98E8,长度 20

因此可以直接读取 global-metadata.dat

import pathlib
import struct

meta = pathlib.Path("dumbcpp_Data/il2cpp_data/Metadata/global-metadata.dat").read_bytes()

target_cipher = meta[0x2B9890:0x2B9890+48]
key_part1 = struct.unpack("<5I", meta[0x2B98E8:0x2B98E8+20])

print(target_cipher.hex())
print(key_part1)

key_part1 转成字符后得到:

asdfg

而:

key_part2 = "9876543210"
key_part2.Substring(3, 5) = "65432"

因此真实 key 明文为:

asdfg65432

然后像右补0得到asdfg654320000000000000000000000

Iv

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

ct = bytes.fromhex("c6788f4ebc791b7572eca04b2a9ccb23f9a1cee4a0dd3ec4e8d64d119fe3ac1b9cc08893bfba51ee7a3687207b7b3c68")
key = b"asdfg654320000000000000000000000"
iv = b"1234567890ABCDEF"

pt = unpad(AES.new(key, AES.MODE_CBC, iv).decrypt(ct), 16)
print(pt.decode())
朗读
赞(0)
版权属于:

霍雅的博客

本文链接:

https://www.huoya.work/bk/index.php/archives/571/(转载时请注明本文出处及文章链接)

评论 (1)
  1. 你爹 作者
    Windows 10 · Google Chrome

    你爹来了!

    2026-05-26 回复

人生倒计时

今日已经过去小时
这周已经过去
本月已经过去
今年已经过去个月
  • 你爹来了!