hgame2026
Hgame2026
PVZ
DIE

发现是一个封装在C++壳下的jar包
7zip打开gpvz.exe,解压

jadx分析一下gpvz文件夹,发现一个很扎眼的what_is_this.png

发现是“神秘仪式”的具体内容

运行游戏,用一点作弊技巧在结束前摆出神秘阵型,获得flag


看不懂的华容道
IDA分析
无符号表,一坨sub_xxxxx函数,没有头绪
先看看字符串

点进HuarongDao2026_Salt看看
跟踪到sub_1400266B0,发现是一个VM的depatcher
VM控制华容道运作
game.bin就是操作码表

opcode:
-
0x15:读用户输入(交互输入“编号+方向”) -
0x16:调用sub_140011212 -> sub_14001EC40,重建棋盘状态编码 -
0x18:对状态做哈希,写入寄存器 -
0x17:打印两个 64-bit 组成的节点值 -
0xE0/E1/E2/E3:跳转与条件判断 -
0xFF:结束
棋盘编码

分析game.bin恢复初始棋盘为

加密算法为md5,不过不重要,可以通过编写bfs脚本直接跑出符合题目要求的路径,直接运行exe输入,获得最终棋盘hash
特别要注意题目追加的条件:操作路径顺序按照棋子编号从小到大 操作顺序wasd
1 | from collections import deque |

hgame{c4a8ae149d34f8552875b87bb317ffa}
Signal Storm
DIE

IDA分析

在main中注册了三个信号处理函数,通过BUG(),raise(5)异常调用,控制执行流
分析信号处理函数,发现这其实是一个魔改 RC4 算法:
-
sub_1780(KSA):利用原始字符串对 S 盒 (byte_4100) 进行初始化打乱。 -
sub_1640(PRGA变形):这是 RC4 的 S 盒交换逻辑,但它引入了aC0lmBe4ore7heS的当前字符参与运算。 -
sub_1740(密钥旋转):每生成一个字节的密钥,就将密钥字符串循环左移一位 -
sub_16E0(异或):执行标准的流密码异或操作:s[i] ^= keystream[i]
密文:

Exp:
1 | import struct |

Noncesense
IDA分析驱动
DriverEntry -> sub_14000154C

MajorFunction[0/2] = sub_140001000
MajorFunction[14] = sub_1400010C0
驱动核心在sub_1400010C0

0x222000:发 nonce
FsContext 在 open 时通过sub_140001668生成 16 字节随机值
0x222004:验签 + 加密
构造 AUTHv1 || nonce || 0 || len || payload
调 sub_140001A7C 计算 HMAC-SHA256,比较前 16 字节签名
常量 key:VIDAR_HGAME_A0th_HMaC_K1_bu1ld2026
sub_1400016B0:AES 单块加密(可见 Sbox、ShiftRows、MixColumns)
sub_140001908:AES-128 key schedule(Rcon 常量)
明文先 PKCS#7 padding,再每 16 字节块加密(ECB)
会话 AES key 派生
sub_1400019B8:
v10 = HMAC_SHA256(key=00*32, msg=nonce16)
v39 由 byte_140003250 经 xor/ror 变换得到 32 字节
v11 = HMAC_SHA256(key=v10, msg=v39 || 0x01)
AES key = v11[:16]
总结:
KDF(两层 HMAC)
AES-ECB
具体数据可以从.rdata,.text字段dump
IDA分析客户端

main流程:
-
读用户输入
-
CreateFile("\\\\.\\GATE_Driver") -
DeviceIoControl(..., 0x222000, ...)取 nonce -
对输入每字节跑一段小型“指令机”变换(
unk_1400043F3) -
构造
AUTHv1包 + HMAC(sub_140001A80) -
调
0x222004 -
成功后把返回密文写入
Drv_blob.bin
完整解密路线
从 Drv_blob.bin 出发:
-
取前 16 字节为
nonce -
剩余为
ciphertext -
用驱动 KDF 复现 AES key
-
AES-ECB 解密 + 去 PKCS#7 padding,得到client变换后的明文
-
按索引逐字节做指令机逆变换,恢复原始输入(flag)
Exp
1 | from Crypto.Cipher import AES |

Androuge
apktool + jadx

找按键交互
游戏本体在waw和game

分别是程序和数据表
waw是ARM aarch64架构,no stripped
逆一下

根据函数名,是一个lua语言编写的程序
找一下game数据表是在哪里载入的
lua_load → luaD_protectedparser → f_parser → luaU_undump

可以看到,是在对dump数据进行校验,每字节数据都xor0x9C
猜测game数据表不是明文,而是经过异或加密的密文,

将前四字节除了第一字节还原后,刚好是.Waw

dump一下还原后的数据表,发现有不少游戏数据以及flag的信息



通过这些信息,大概能推断出获取flag的机制:
在游戏中到达target_floor,程序就会计算出flag并显示出来
我的思路是直接修改明文数据表,然后再加密回去,用patch后的数据表快速通关获得flag
在game_dec中检索对应游戏数值的字符串,修改数值

这里很容易就能发现数值段有一个03 XX 00 00 00 00 00 00 00 04的固定结构
我第一次尝试的时候直接把target_floor改成了1,结果flag是乱码,说明这个数据影响了flag生成,改为patch一些只优化游戏体验的数值

然后加密回去,在终端用qemu-aarch64跑./waw game_patch
花一点时间通关


Ouroboros
运行 jar

静态分析
TradeService
1 | package com.seal.ouroborosapp.domain; |
RiskPolicy
1 | package com.seal.ouroborosapp.domain; |
return null,显然不是最终逻辑
ShadowLoader
1 | public com.seal.ouroborosapi.RiskEngine initContext() throws java.lang.IllegalAccessException, java.lang.NoSuchMethodException, java.lang.ClassNotFoundException, java.lang.SecurityException, java.io.IOException, java.lang.IllegalArgumentException, java.lang.reflect.InvocationTargetException { |
这里jadx挂了,直接解包用cfr反编译
1 | int k = IntegrityVerifier.getDeriveKey(); |
而核心代码如上
隐藏了两个关键类:RealRiskEngine,OuroborosVM
所以“衔尾蛇”的含义就是
壳中壳 + 动态还原 + 自定义 VM
RealRiskEngine
1 | public class RealRiskEngine |
尝试AES-CBC解密,解密成功给出fake_flag
flag{N0p3_Th1s_1s_A_D3c0y_G0_B4ck}
失败则走OuroborosVM
OuroborosVM
自定义VM,代码附带超长opcode和handlers,就不贴了
token(flag)存进memory
用derive_keydecFIRMWARE获得VM指令流校验token
已知opcode
-
0x10:push immediate (16-bit) -
0x20:push token.length -
0x30:load memory[addr] -
0x35:xor -
0x4A:条件跳 -
0xFF:结束并判断栈顶是否为 1
构造z3约束求解器

Marionette
main

1 | v6 += 2LL; |
输入32个十六进制字符,每两个转成一字节存储,总共16B

最后memcmp密文

分析可知是 fork + ptrace 的木偶

sub_401D4E被int3打断,不可读

xmm1的值为硬编码xmmword_405020,用作aeskeygenassist

trace_parent
可以dump出off_405070,qword_406338,qword_4075F8,喂给ai纯静态还原状态机,但可操性更强的是动调
因为子进程会被父进程ptrace控制,所以需要detach child,单独调父进程
在gdb里直接实现记录idx,打印对应的汇编语块,最后输出idx状态序列
1 | set pagination off |

分析汇编,发现这600个block含有AES makekey/AES rounds/XOR/垃圾块,核心运算不是很多,按照状态序列求逆即可
1 | import re |
