前言
一年一年, 我又来了, 今年怎么大伙都这么强. . . . . .
在大量梗™里发现了少量题解, 本文还是挺活泼的(梗有点多, 大概我做题时候精神状态不太好)
看不见我: 「 大量梗™」是产品商标,不代表产品最终效果。(这很Coxxs, 对吧)
「 活泼」数据来源于第三方,是指在将本文与2026年央视春晚相比的公开投票结果,参与投票人有10000(单位:10^-4人), 即: 1人
本文无Windows高级/MCP中级(问就是不会/懒), 题解按时间顺序排序, 名称重复会添加题目上线时间进行区分
3.4下午开学, 我快走了
初二Windows初级
依旧是入门题, 还是给新手朋友们说一下 (入门请点我)
使用IDA打开程序, 从start里找到它, 这才是真正的main函数 (啥你问我为什么? ......这么大个result你看不到吗)
int __cdecl sub_D5D130(char a1)
{
__time32_t v1; // eax
char v2; // al
char v3; // al
char v4; // dl
char v5; // cl
int v6; // eax
unsigned __int8 v8; // al
int v9; // edx
int v10; // edx
int v11; // eax
__time32_t *Time; // [esp+0h] [ebp-78h] BYREF
std::string_0 *v13; // [esp+14h] [ebp-64h]
char *Str; // [esp+18h] [ebp-60h]
char v15[4]; // [esp+1Ch] [ebp-5Ch] BYREF
int v16; // [esp+20h] [ebp-58h]
int (__cdecl *v17)(int, int, int, int, int, int); // [esp+34h] [ebp-44h]
int *v18; // [esp+38h] [ebp-40h]
int *v19; // [esp+3Ch] [ebp-3Ch]
void (__noreturn *v20)(); // [esp+40h] [ebp-38h]
__time32_t **p_Time; // [esp+44h] [ebp-34h]
char *v22; // [esp+58h] [ebp-20h] BYREF
int v23; // [esp+5Ch] [ebp-1Ch]
char v24; // [esp+60h] [ebp-18h] BYREF
char *v25; // [esp+70h] [ebp-8h]
int savedregs; // [esp+78h] [ebp+0h] BYREF
v25 = &a1;
v17 = sub_D5C800;
p_Time = &Time;
v18 = dword_D5E3D4;
v19 = &savedregs;
v20 = sub_D5D4EA;
sub_C9C330(v15);
sub_C9A650();
v16 = -1;
v1 = time(0);
srand(v1);
SetConsoleOutputCP(0xFDE9u);
sub_D57E50((int)&dword_D627C0, "========================================");
sub_C915B0(Time);
sub_D57E50((int)&dword_D627C0, " CrackMe Challenge v2.5 - 2026 ");
sub_C915B0(Time);
sub_D57E50((int)&dword_D627C0, "========================================");
sub_C915B0(Time);
sub_D57E50((int)&dword_D627C0, "Keywords: 52pojie, 2026, Happy new year");
sub_C915B0(Time);
sub_D57E50((int)&dword_D627C0, "Hint: Fake flag; length is key");
sub_C915B0(Time);
sub_D57E50((int)&dword_D627C0, "----------------------------------------");
sub_C915B0(Time);
v22 = &v24;
v23 = 0;
v24 = 0;
v16 = 1;
v13 = (std::string_0 *)&v22;
sub_D57E50((int)&dword_D627C0, "\n[?] Enter the password: ");
v13 = (std::string_0 *)&v22;
v2 = sub_C91560();
v13 = (std::string_0 *)&v22;
sub_D55840(&dword_D625E0, &v22, v2);
Str = v22;
v3 = sub_C91740(v22);
v4 = 53;
v5 = v3;
v6 = 0;
if ( !v5 )
{
while ( Str[v6] == v4 )
{
if ( ++v6 == 16 ) // 长度16的假码
{
v16 = 1;
sub_D57E50((int)&dword_D627C0, "\n[!] You're getting closer...");
v16 = 1;
goto LABEL_9;
}
v4 = byte_D63032[v6];
}
if ( strlen(Str) != 31 ) // 真码长度为31
{
v16 = 1;
sub_D57E50((int)&dword_D627C0, "\n[!] Hint: The length is your first real challenge.");
goto LABEL_9;
}
v16 = 1;
if ( sub_C916D0((int)Str, 31) ) // 在这里面进行校验
{
Str = 0;
v8 = *v22;
if ( !*v22 )
goto LABEL_16;
v9 = 0;
do
{
Str += ++v9 * v8;
v8 = v22[v9];
}
while ( v8 );
if ( Str != (char *)44709 ) // 其实这里不需要管的, 直接去上面的函数就行
{
LABEL_16:
v16 = 1;
sub_D57E50((int)&dword_D627C0, "\n[!] Checksum failed! Something is wrong...");
sub_C915B0(Time);
sub_D57E50((int)&dword_D627C0, "[!] Expected: 44709, Got: ");
sub_D0DE70(Str);
sub_C915B0(v10);
LABEL_17:
v16 = 1;
goto LABEL_10;
}
v16 = 1;
sub_D57E50((int)&dword_D627C0, "\n========================================");
sub_C915B0(Time);
sub_D57E50((int)&dword_D627C0, " *** SUCCESS! *** ");
sub_C915B0(Time);
sub_D57E50((int)&dword_D627C0, "========================================");
sub_C915B0(Time);
sub_D57E50((int)&dword_D627C0, "[+] Congratulations! You cracked it!");
sub_C915B0(Time);
v11 = sub_D57E50((int)&dword_D627C0, "[+] Correct flag: ");
sub_D54330(v11, v22, v23);
}
else
{
v16 = 1;
sub_D57E50((int)&dword_D627C0, "\n[X] Access Denied!");
sub_C915B0(Time);
sub_D57E50((int)&dword_D627C0, "[X] Wrong password. Keep trying!");
}
sub_C915B0(Time);
goto LABEL_17;
}
sub_D57E50((int)&dword_D627C0, "\n[!] Nice try, but not quite right...");
LABEL_9:
sub_C915B0(Time);
LABEL_10:
sub_C917C0();
std::string::_M_dispose(v13);
sub_C9C600(v15);
return 0;
}
跟入检查的函数中, 按照历年题目规律, 不出所料是字符串比对
bool __cdecl sub_C916D0(int a1, int a2)
{
unsigned __int8 *Block; // ebp
int v3; // eax
int v4; // ebx
bool v5; // dl
Block = (unsigned __int8 *)sub_D5B710(0x64u);
sub_C91620(Block); // 在这里动态生成Flag
if ( a2 <= 0 ) // 此处下断点断下后直接查看Block的值即可
{
v4 = 0;
}
else
{
v3 = 0;
v4 = 0;
do
{
v5 = *(char *)(a1 + v3) == Block[v3];
++v3;
v4 += v5;
}
while ( a2 != v3 );
}
j_j_free(Block);
return a2 == v4;
}
直接OD/IDA动态调试一下即可
做出这道题的你内心belike: 逆向, 轻而易举啊! (实则后面的题: 坏了, 坏了坏了)
Android 初级
最简单的解法
从APK资源文件(res/drawable)中找到bgm.png (你干嘛哈哈哎哟, 哦对, 我确实目前为止还没感冒)
然后照着拼就行了 (真的很快, 如果正常逆向的话更耗时间)
正常(?应该吧)解法
那当然是直接逆向...这玩意整起来太费时了, 纯大 (Kotlin!!!!)
渲染层
反编译器使用Jadx, 实际Property name与实际情况有出入
搜索flag找到可以找到函数u1.m.d0, 这里是UI的渲染层 (并非实际校验层和flag层)
可以看到flag在构造函数中被传入, 并且在d0中进行一系列判断并显示
package u1;
public final class m extends K1.l implements J1.f {
/* 伪代码经过删减 */
public m(InterfaceC0772V interfaceC0772V, InterfaceC0772V interfaceC0772V2, InterfaceC0772V interfaceC0772V3, InterfaceC0772V interfaceC0772V4, InterfaceC0772V interfaceC0772V5, InterfaceC0772V interfaceC0772V6, C0826g c0826g, InterfaceC0772V interfaceC0772V7, InterfaceC0772V interfaceC0772V8, InterfaceC0772V interfaceC0772V9, Context context, View view, InterfaceC0097w interfaceC0097w) {
super(3);
/* ... */
this.f7612q = interfaceC0772V7; // Flag从这里读, 不过其实我不知道这是个啥结构
}
@Override // J1.f
public final Object d0(Object obj, Object obj2, Object obj3) {
C0810u c0810u = (C0810u) obj2;
int intValue = ((Number) obj3).intValue();
if ((intValue & 14) == 0) {
intValue |= c0810u.f(cVar) ? 4 : 2;
}
if ((intValue & 91) == 18 && c0810u.A()) {
c0810u.T();
} else {
/* ... */
J0 j04 = this.f7612q;
String str = (String) j04.getValue(); // this.q.getValue() 为flag字符串
InterfaceC0772V interfaceC0772V5 = this.f7608m;
if (str != null) { // 如果没拼好的话就是空字符串
H.m f6 = androidx.compose.foundation.layout.d.f(androidx.compose.foundation.layout.a.e(bVar3.a(H.a.f351j), z8 ? 6 : 10, z8 ? -16 : a(g4) - 20), z8 ? 42 : 48);
c0810u.Y(1157296644);
boolean f7 = c0810u.f(interfaceC0772V5);
Object D3 = c0810u.D();
if (f7 || D3 == obj4) {
z6 = false;
D3 = new f(interfaceC0772V5, 0);
c0810u.j0(D3);
} else {
z6 = false;
}
c0810u.t(z6);
/* 代码经过删减 */
c0810u.t(false);
H.m a4 = androidx.compose.ui.graphics.a.a(b4, (J1.c) D4);
c0286i3 = c0286i10;
c0286i4 = c0286i9;
interfaceC0772V = interfaceC0772V5;
j02 = j04;
M1.a.b(Q2, "查看FLAG", a4, null, g6, 0.0f, null, c0810u, 24632, 104);
z2 = true;
z3 = false;
K1.i.y(c0810u, false, true, false, false);
} else {
c0286i2 = c0286i14;
c0286i3 = c0286i11;
interfaceC0772V = interfaceC0772V5;
j02 = j04;
c0286i4 = c0286i13;
z2 = true;
z3 = false;
}
/* ... */
}
return C0847j.f8294a;
}
}
寻找Flag赋值点
接下来通过Frida获取一下该构造函数的调用栈, 往上翻翻flag在哪出现的
脚本如下:
Java.perform(function () {
var Log = Java.use("android.util.Log");
var Throwable = Java.use("java.lang.Throwable");
function printStack(tag) {
var stack = Log.getStackTraceString(Throwable.$new());
console.log("==== " + tag + " ====");
console.log(stack);
}
let m = Java.use("u1.m");
m["$init"].implementation = function (interfaceC0772V, interfaceC0772V2, interfaceC0772V3, interfaceC0772V4, interfaceC0772V5, interfaceC0772V6, c0826g, interfaceC0772V7, interfaceC0772V8, interfaceC0772V9, context, view, interfaceC0097w) {
printStack()
console.log(`m.$init is called: interfaceC0772V=${interfaceC0772V}, interfaceC0772V2=${interfaceC0772V2}, interfaceC0772V3=${interfaceC0772V3}, interfaceC0772V4=${interfaceC0772V4}, interfaceC0772V5=${interfaceC0772V5}, interfaceC0772V6=${interfaceC0772V6}, c0826g=${c0826g}, interfaceC0772V7=${interfaceC0772V7}, interfaceC0772V8=${interfaceC0772V8}, interfaceC0772V9=${interfaceC0772V9}, context=${context}, view=${view}, interfaceC0097w=${interfaceC0097w}`);
this["$init"](interfaceC0772V, interfaceC0772V2, interfaceC0772V3, interfaceC0772V4, interfaceC0772V5, interfaceC0772V6, c0826g, interfaceC0772V7, interfaceC0772V8, interfaceC0772V9, context, view, interfaceC0097w);
};
})
不难得到(部分省略):
java.lang.Throwable
at u1.m.<init>(Native Method) // 构造函数
at t1.k.d(SourceFile:89)
at p1.a.g0(SourceFile:59)
at D.e.g0(SourceFile:57)
at K1.i.s(SourceFile:5)
at w.v.a(SourceFile:177)
at androidx.compose.material3.E0.a(SourceFile:90)
在t1.k.d查看, 可以看到v1(实际是第一个参数)被传入了下一层u1.m中, 也就是说我们还得去看看第一个参数的类
函数签名如下:
public static final void d(g p0,u p1,int p2,int p3);
类g如下:
package w1.g;
public final class g extends a
{
public final B e;
public A f;
public b g;
public boolean h;
public final Q i;
public final z j;
public final Q k; /* 这个是存储Flag的 Property */
public final z l;
public final Q m;
public final z n;
public A o;
public void g(Application p0){
k.e(p0, "application");
super(p0);
Context applicationC = p0.getApplicationContext();
k.d(applicationC, "getApplicationContext\(...\)");
this.e = new B(applicationC);
this.g = b.i;
Q q = D.b(c.a());
this.i = q;
this.j = new z(q);
q = D.b(null);
this.k = q;
this.l = new z(q);
q = D.b(Boolean.FALSE);
this.m = q;
this.n = new z(q);
}
/* 省略了 */
}
由于我并不是很了解kotlin这东西, 所以我还是问了一下Gemini, 加上自己也猜了一下
接下来查看g.k的引用可以找到他在w1.d.i中被赋值为this.o, 而this.o又来自于F.C
w1.d.i代码如下:
package w1.d;
public final class d extends j implements e // class@000c40 from classes.dex
{
public int m;
public final g n;
public final String o;
public void d(g p0,String p1,d p2){ // o又是p1, 在F.C被初始化
this.n = p0;
this.o = p1;
super(2, p2);
}
public final d f(d p0,Object p1){
return new d(this.n, this.o, p0);
}
public final Object g0(Object p0,Object p1){
return this.f(p1, p0).i(j.a);
}
public final Object i(Object p0){
a i = a.i;
d tm = this.m;
d tn = this.n;
boolean b = true;
if (tm != null) {
if (tm == b) {
v.l(p0);
}else {
throw new IllegalStateException("call to \'resume\' before \'invoke\' with coroutine");
}
}else {
/* 播放声音, 不过我并没有听到, 其实如果直接从声音下手更好 */
v.l(p0);
p0 = tn.e;
String str = "mdx.aac";
try{
p0.getClass();
p0.v();
MediaPlayer mediaPlayer = new MediaPlayer();
AssetFileDescriptor uAssetFileDe = p0.j.getAssets().openFd(str);
k.d(uAssetFileDe, "openFd\(...\)");
mediaPlayer.setDataSource(uAssetFileDe.getFileDescriptor(), uAssetFileDe.getStartOffset(), uAssetFileDe.getLength());
uAssetFileDe.close();
mediaPlayer.setLooping(b);
mediaPlayer.prepare();
mediaPlayer.start();
p0.k = mediaPlayer;
label_005f :
this.m = b;
if (x.f(2000, this) == i) {
return i;
}
}catch(java.io.IOException e12){
}catch(java.lang.IllegalStateException e12){
}
e12.printStackTrace();
goto label_005f ;
}
tn.k.k(this.o); /* 这里将k设置为this.o */
return j.a;
}
}
接下来查找引用到F.C, flag就出现了, 这个类类似于一个分发器, 根据数字不同执行不同操作
它在case 28中对拼图进行校验, 如果拼好则跳转到case 25解密flag
package F.C;
public final class C extends l implements c // class@000072 from classes.dex
{
public final int j;
public final Object k;
public void C(int p0,Object p1){
this.j = p0;
this.k = p1;
super(1);
}
public final Object q0(Object p0){
switch (c.j){
/* 没用的case全删了 */
case 25:
k.e(uoa, "part");
StringBuilder str = "";
i = uoa.length;
for (; i3 < i; i3 = i3 + i4) {
e = c.k;
c1 = i3 % e.length;
d = e[c1] & 0x00ff;
b = uoa[i3] ^ d;
str = str.append((char)b);
}
String str1 = str;
k.d(str1, "toString\(...\)");
return str1;
case 28: // 判断
r = uoa.intValue();
k = c.k;
g i8 = k.i;
b value = i8.getValue();
if (value.c == null && (uob = a.a0(value, r)) != null) {
/* ... */
b c2 = j.c;
if (r && c2 == null) {
if ((f8 = k.f) != null) {
f8.a(uoe);
}
k.f = x.r(H.q(k), uoe, i3, new e(k, uoe), 3);
}
if (c2 != null) {
if (c2 != null) {
c2 = j.a;
if (k.a(c2, b.h)) { // 检查是否完成拼图
Iterator iterator1 = c2.iterator();
long l5 = 0;
d = i3;
while (true) {
if (iterator1.hasNext()) {
obj = iterator1.next();
i10 = d + 1;
if (d >= 0) {
l5 = l5 * (long)31;
d = obj.intValue() * i10;
l5 = l5 + (long)d;
d = i10;
}else {
l.k();
throw uoe;
}
}else if((l5 ^ 0x12345678) - 0xe30fe54d0){ // 校验拼图的HASH?
byte[] uobyteArray = new byte[i1]; // 解密密钥
uobyteArray[i3] = (byte)54;
uobyteArray[i4] = (byte)i4;
uobyteArray[i2] = 22;
uobyteArray[3] = 28;
int[][] a2 = a.a; // 这是flag密文
uC = new C(25, uobyteArray); // 跳转到case 25进行解密
Appendable uAppendable = "";
c1 = i3;
i1 = c1;
for (; c1 < 6; c1 = c1 + i4) {
object oobject = a2[c1];
if ((i1 = i1 + i4) > i4) {
uAppendable = uAppendable.append("");
}
c.g(uAppendable, oobject, uC);
}
uoe2 = uAppendable+"";
k.d(uoe2, "joinTo\(StringBuilder\(\), …ed, transform\).toString\(\)");
label_020b :
if (uoe2 != null) {
x.r(H.q(k), uoe, i3, new d(k, uoe2, uoe), 3);
}
}
}
}
}
uoe2 = uoe;
goto label_020b ;
}
}
return j.a;
break;
}
}
}
Python编写解密代码:
key = [54, 1, 22, 28]
cipher_blocks = [
[80, 109, 119, 123, 77],
[97, 116, 34, 45, 105],
[ord('f'), ord('1'), ord('|'), ord('-'), 5, ord('^')],
[4, 49, 36, 42, 105],
[ord('e'), ord('q'), ord('d'), ord('-'), ord('X'), ord('f'), ord('I')],
[ord('p'), ord('2'), ord('e'), ord('h'), 7, ord('w'), ord('"'), ord('p'), ord('K')]
]
flag = ""
for block in cipher_blocks:
for i in range(len(block)):
# 原代码:c1 = i3 % e.length; d = e[c1] & 0x00ff;
current_key = key[i % len(key)]
# 原代码:b = uoa[i3] ^ d;
char_code = block[i] ^ current_key
# 拼接字符串
flag += chr(char_code)
print(flag)
最终得到flag{Wu41_P0j13_2026_Spr1ng_F3st1v4l}
做完这题有不变成小黑子的风险吗 )
两个Python题
(看到题的时候我真绷不住, 绷不了一点)
(请记住我此时的笑容, 到后面就笑不起来了, 后面就炸了)
初四Windows初级
使用Pyinstxtractor解包, 然后直接把PYC扔https://pylingual.io/
接下来自己看代码吧, We all know (We no strangers to love~)
import hashlib
import base64
import sys
def xor_decrypt(data, key):
"""XOR解密"""
result = bytearray()
for i, byte in enumerate(data):
result.append(byte ^ key ^ i & 255)
return result.decode('utf-8', errors='ignore')
def get_encrypted_flag():
"""获取加密的flag"""
enc_data = 'e3w+fiRvfW18fnx4ZAZ6Pj43YwB9OWMXfXo8Dg4O'
return base64.b64decode(enc_data)
def generate_flag():
"""动态生成flag"""
encrypted = get_encrypted_flag()
key = 78
result = bytearray()
for i, byte in enumerate(encrypted):
result.append(byte ^ key)
return result.decode('utf-8')
def calculate_checksum(s):
"""计算校验和"""
total = 0
for i, c in enumerate(s):
total += ord(c) * (i + 1)
return total
def hash_string(s):
"""计算字符串哈希"""
return hashlib.sha256(s.encode()).hexdigest()
def verify_flag(user_input):
"""验证flag"""
correct_flag = generate_flag()
if len(user_input)!= len(correct_flag):
return False
else:
for i in range(len(correct_flag)):
if user_input[i]!= correct_flag[i]:
return False
return True
def fake_check_1(user_input):
"""假检查1"""
fake_hash = 'a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890'
return hash_string(user_input) == fake_hash
def fake_check_2(user_input):
"""假检查2"""
fake_hash = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
return hash_string(user_input) == fake_hash
def main():
"""主函数"""
print('==================================================')
print(' CrackMe Challenge - Python Edition')
print('==================================================')
print('Keywords: 52pojie, 2026, Happy New Year')
print('Hint: Decompile me if you can!')
print('--------------------------------------------------')
user_input = input('\n[?] Enter the password: ').strip()
if fake_check_1(user_input):
print('\n[!] Nice try, but not quite right...')
input('\nPress Enter to exit...')
return None
else:
if fake_check_2(user_input):
print('\n[!] You\'re getting closer...')
input('\nPress Enter to exit...')
else:
if verify_flag(user_input):
checksum = calculate_checksum(user_input)
expected_checksum = calculate_checksum(generate_flag())
if checksum == expected_checksum:
print('\n==================================================')
print(' *** SUCCESS! ***')
print('==================================================')
print('[+] Congratulations! You cracked it!')
print(f'[+] Correct flag: {user_input}')
else:
print('\n[!] Checksum failed!')
else:
print('\n[X] Access Denied!')
print('[X] Wrong password. Keep trying!')
input('\nPress Enter to exit...')
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print('\n\n[!] Interrupted by user')
sys.exit(0)
初五Windows中级
注: 这里其实与吾爱另一位大佬"善良的人鱼"(好奇一下, 这条鱼好吃吗)的文章内容差不多, 你们可以直接去看他的 (严重怀疑这题就是看了这个之后才出的, 不过我也是靠他才做出来的)
DIE查壳:
操作系统: Windows(Server 2003)[AMD64, 64 位, 控制台]
(Heur)语言: C
打包工具: Nuitka[OneFile]
(Heur)打包工具: Compressed or packed data[High entropy + Section 10 (".rsrc") compressed]
提取DLL
使用nuitka-extractor从exe可以提取到crackme_hard.dll
由导出表可知, run_code即为执行字节码的函数
提取字节码
查看函数可知, 字节码从资源中加载
所以我们使用Resource Hacker从他的资源中提取并保存到文件中
编写解析代码
想要具体了解的可以看下面的四篇文章:
https://goatmilkk.notion.site/Nuitka-a3ac9ee7f3f240f3baa345c17f2b8aa3
https://blog.svenskithesource.be/posts/flare10-07-(nuitka)/
https://services.google.com/fh/files/misc/7-flake-flareon10.pdf
https://www.52pojie.cn/forum.php?mod=viewthread&tid=2063208&highlight=nuitka
我这里选择直接使用大佬"善良的人鱼"写的解析代码
# 这个代码在吾爱另一位佬的文章里
import io
import struct
def read_uint32(bio):
return struct.unpack("<I", bio.read(4))[0]
def read_uint16(bio):
return struct.unpack("<H", bio.read(2))[0]
def read_utf8(bio):
bs = b""
while True:
bs += bio.read(1)
if b"\x00" in bs:
break
return bs[:-1].decode("utf-8")
def main():
with open("main.bin", "rb") as f_in: # 这里改bin文件为你保存出来的文件
bs = f_in.read()
bio = io.BytesIO(bs)
hash_ = read_uint32(bio)
size = read_uint32(bio)
print(f"hash: {hex(hash_)}")
print(f"size: {hex(size)}")
while bio.tell() < size:
blob_name = read_utf8(bio)
blob_size = read_uint32(bio)
blob_count = read_uint16(bio)
print(f"name: {blob_name}, size: {hex(blob_size)}, count: {hex(blob_count)}")
bio.seek(bio.tell() + (blob_size - 2))
if __name__ == "__main__":
main()
运行后可以发现输出中有__main__, 接下来需要继续写函数解析__main__
用IDA在DLL中找到loadConstantsBlob函数, 其中字节码被加载后由下述代码进行解析 (由于Nuitka和Python版本影响, 每个程序应该都不一样)
反编译该函数并且让AI写解析的代码 (AI对于这种简单重复的工作还是非常适合的)
import io
import sys
import struct
def read_uint8(bio):
return struct.unpack("<B", bio.read(1))[0]
def read_uint16(bio):
return struct.unpack("<H", bio.read(2))[0]
def read_uint32(bio):
return struct.unpack("<I", bio.read(4))[0]
def read_utf8_size_1(bio):
return bio.read(1).decode("utf-8")
def read_utf8(bio):
bs = b""
while True:
c = bio.read(1)
if c == b"\x00" or not c: # 读取到空字节或文件末尾时停止
break
bs += c
return bs.decode("utf-8")
def read_bytearray(bio):
bs = b""
while True:
c = bio.read(1)
if c == b"\x00" or not c:
break
bs += c
return bs
# 新增:解析伪代码中频繁出现的变长整数 (VarInt / LEB128)
def read_varint(bio):
b = bio.read(1)
if not b:
raise EOFError()
val = b[0] & 0x7F
if b[0] < 128:
return val
shift = 7
while True:
next_b = bio.read(1)
if not next_b:
raise EOFError()
val += (next_b[0] & 0x7F) << shift
shift += 7
if next_b[0] < 128:
break
return val
def read_null_terminated_bytes(bio):
bs = bytearray()
while True:
c = bio.read(1)
if not c or c == b"\x00":
break
bs.extend(c)
return bytes(bs)
# 新增:递归解析单个对象
def decode_one(bio):
type_b = bio.read(1)
if not type_b:
return None
type_ = chr(type_b[0])
if type_ == '.':
raise ValueError("Hit abort type '.'")
elif type_ == ':': # Slice
start = decode_one(bio)
stop = decode_one(bio)
step = decode_one(bio)
return slice(start, stop, step)
elif type_ == ';': # Range
start = decode_one(bio)
stop = decode_one(bio)
step = decode_one(bio)
return f"Range({start}, {stop}, {step})"
elif type_ == 'A': # Py_GenericAlias
arg1 = decode_one(bio)
arg2 = decode_one(bio)
return f"GenericAlias({arg1}, {arg2})"
elif type_ == 'B': # bytearray
size = read_varint(bio)
return bytearray(bio.read(size))
elif type_ == 'C': # PyCode_Type (极度复杂, 包含多个可选属性)
flags = read_varint(bio)
obj1 = decode_one(bio)
var1 = read_varint(bio)
obj2 = decode_one(bio)
var2 = read_varint(bio)
obj3, obj4, var3, var4 = None, None, None, None
if (flags & 1) != 0: obj3 = decode_one(bio)
if (flags & 2) != 0: obj4 = decode_one(bio)
if (flags & 4) != 0: var3 = read_varint(bio)
if (flags & 8) != 0: var4 = read_varint(bio)
return f"Code(flags={flags}, o1={obj1}, v1={var1}, o2={obj2}, v2={var2}, o3={obj3}, o4={obj4}, v3={var3}, v4={var4})"
elif type_ == 'D': # Dict
count = read_varint(bio)
keys = [decode_one(bio) for _ in range(count)]
vals = [decode_one(bio) for _ in range(count)]
return dict(zip(keys, vals))
elif type_ in ('E', 'O'): # PyObject_GetAttrString
bs = read_null_terminated_bytes(bio)
return f"Attr({bs.decode('utf-8', 'replace')})"
elif type_ == 'F': # False
return False
elif type_ in ('G', 'g'): # BigInt (long)
size = read_varint(bio)
digits = [read_varint(bio) for _ in range(size)]
val = f"BigInt({digits})"
if type_ == 'g': val = "-" + val
return val
elif type_ == 'H': # Set / Frozenset InPlaceOr
obj = decode_one(bio)
return f"Union({obj})"
elif type_ == 'J': # Complex from objects
real = decode_one(bio)
imag = decode_one(bio)
return f"Complex({real}, {imag})"
elif type_ == 'L': # List
count = read_varint(bio)
return [decode_one(bio) for _ in range(count)]
elif type_ == 'M': # Builtin Singletons
idx = read_uint8(bio)
names = {0: "None", 1: "Ellipsis", 2: "NotImplemented", 3: "Function", 4: "Gen", 5: "CFunction", 6: "Code", 7: "Module", 10: "Method"}
return f"Builtin({names.get(idx, idx)})"
elif type_ in ('P', 'S'): # P=FrozenSet, S=Set
count = read_varint(bio)
items = [decode_one(bio) for _ in range(count)]
if type_ == 'P': return frozenset(items)
return set(items)
elif type_ == 'Q':
idx = read_uint8(bio)
if idx == 1: return NotImplemented
elif idx == 2: return "Special(2)"
else: return Ellipsis
elif type_ == 'T': # Tuple
count = read_varint(bio)
return tuple(decode_one(bio) for _ in range(count))
elif type_ == 'X': # Raw Byte Buffer slice
size = read_varint(bio)
return bio.read(size)
elif type_ == 'Z': # Float Constants (0.0, 1.0 等)
idx = read_uint8(bio)
return f"FloatConst({idx})"
elif type_ in ('a', 'u'): # UTF8 String
bs = read_null_terminated_bytes(bio)
return bs.decode('utf-8', 'replace')
elif type_ == 'b': # Bytes
size = read_varint(bio)
return bio.read(size)
elif type_ == 'c': # Null-Terminated Bytes
return read_null_terminated_bytes(bio)
elif type_ == 'd': # PyRuntime Object Map
idx = read_uint8(bio)
return f"RuntimeObj({idx})"
elif type_ == 'f': # 8-byte Float
data = bio.read(8)
return struct.unpack("<d", data)[0]
elif type_ == 'j': # 16-byte Complex
data = bio.read(16)
r, i = struct.unpack("<dd", data)
return complex(r, i)
elif type_ in ('l', 'q'): # Small Int (l=正, q=负)
val = read_varint(bio)
if type_ == 'q': val = -val
return val
elif type_ == 'n': # None
return None
elif type_ == 'p': # Reference to previous object (*(a2 - 1))
return "Ref(Prev)"
elif type_ == 's': # Empty String
return ""
elif type_ == 't': # True
return True
elif type_ == 'v': # Varint length UTF8 string
size = read_varint(bio)
return bio.read(size).decode('utf-8', 'replace')
elif type_ == 'w': # Size 1 String
return bio.read(1).decode('utf-8', 'replace')
else:
raise ValueError(f"Unhandled type: {type_} ({hex(ord(type_))}) at {hex(bio.tell())}")
# 修改原本的 decode_blob 以适配上述新函数
def decode_blob(bio, count):
container = []
for i in range(count):
o = decode_one(bio)
container.append(o)
return container
def main():
with open("rc3.bin", "rb") as f_in:
bs = f_in.read()
bio = io.BytesIO(bs)
hash_ = read_uint32(bio)
size = read_uint32(bio)
# print(f"Header Hash: {hex(hash_)}")
# print(f"Total Size: {hex(size)}")
while bio.tell() < size:
blob_name = read_utf8(bio)
blob_size = read_uint32(bio)
blob_count = read_uint16(bio)
# print(f"Found Blob - Name: '{blob_name}', Size: {hex(blob_size)}, Count: {hex(blob_count)}")
print(blob_name)
# 检查是否是我们要找的 '__main__' 数据块
print("Decoding '__main__' blob...")
# 解码 '__main__' 数据块中的所有对象
decoded = decode_blob(bio, blob_count)
# 打印出每个对象的索引和值
for idx, obj in enumerate(decoded):
print(f"{idx}: {obj}")
if __name__ == "__main__":
main()
分析解析结果
执行上述代码, 我们不难得到 (不是, 为什么感觉这个注释好大的AI味):
Decoding '__main__' blob...
0: [b'dc!a;`b', 'RuntimeObj(17)', b'cacg', 'RuntimeObj(47)', b'\x19e!!(', 'RuntimeObj(14)', b'\x1fb&', 'RuntimeObj(14)', b'\x08be#', b'ppp']
1: _parts
2: 81
3: _key
4: 30
5: _total_len
6: 解密单个字符
7: current
8: _decrypt_char
9: 获取指定位置的字符
10: self
11: _get_char_at_position
12: 验证用户输入
13: total
14: 计算校验和
15:
16: flag
17: checksum
18: 获取目标校验和
19: hashlib
20: sha256
21: encode
22: hexdigest
23: slice(None, 8, None)
24: 16
25: 哈希函数
26: 305419896
27: -BigInt([1, 734916353])
28: 1380994890
29: hash_input
30: 假检查
31: print
32: ('==================================================',)
33: (' CrackMe Challenge - Binary Edition',)
34: ('Keywords: 52pojie, 2026, Happy New Year',)
35: ('Hint: 1337 5p34k & 5ymb0l5!',)
36: (' Try to decompile this in IDA!',)
37: ('--------------------------------------------------',)
38: CrackMeCore
39:
[?] Enter the password:
40: fake_check
41: ('\n[!] Close, but not quite there...',)
42:
Press Enter to exit...
43: verify
44: get_target_checksum
45: ('\n==================================================',)
46: (' *** SUCCESS! ***',)
47: ('[+] L33T H4X0R!',)
48: [+] Your answer:
49:
[!] Checksum mismatch:
50: !=
51: ('\n[X] Access Denied!',)
52: ('[X] Wrong password!',)
53: 主函数
54: __doc__
55: __file__
56: __cached__
57: __annotations__
58: sys
59: __main__
60: __module__
61: 核心验证类 - 将被编译成二进制
62: __qualname__
63: __init__
64: CrackMeCore.__init__
65: CrackMeCore._decrypt_char
66: CrackMeCore._get_char_at_position
67: CrackMeCore.verify
68: CrackMeCore.checksum
69: CrackMeCore.get_target_checksum
70: main
71: ('\n\n[!] Interrupted',)
72: crackme_hard.py
73: <module>
74: ('self',)
75: ('self', 'part_idx', 'char_idx', 'encrypted_byte')
76: ('self', 'pos', 'current', 'part_idx', 'part')
77: ('self', 's', 'total', 'i', 'c')
78: ('user_input', 'fake_hashes', 'user_hash')
79: ('self', 'flag', 'i')
80: ('s',)
81: ('core', 'user_input', 'cs', 'target')
82: ('self', 'user_input', 'i', 'expected')
接下来将其扔给AI或者自己分析即可得到flag, 解密代码如下:
import re
def decrypt_flag():
# 从 Nuitka 解析出的常量池索引 0 提取出的被加密的碎片
# 注意:b'dc!a;`b' 中的反引号在 Python 中可以表示为 \x60
encrypted_parts = [
b'dc!a;\x60b', # 对应 52p0j13
'RuntimeObj(17)', # 对应 @
b'cacg', # 对应 2026
'RuntimeObj(47)', # 对应 ~
b'\x19e!!(', # 对应 H4ppy
'RuntimeObj(14)', # 对应 _
b'\x1fb&', # 对应 N3w
'RuntimeObj(14)', # 对应 _
b'\x08be#', # 对应 Y34r
b'ppp' # 对应 !!!
]
# 常量池索引 2 提取出的异或密钥
xor_key = 81
flag = ""
print(" 开始解密 Flag...")
# 遍历每个加密碎片
for index, part in enumerate(encrypted_parts):
decrypted_part = ""
if isinstance(part, bytes):
# 处理连续的字节串碎片
for b in part:
decrypted_part += chr(b ^ xor_key)
elif isinstance(part, str) and part.startswith('RuntimeObj'):
# 处理 Nuitka 的 RuntimeObj(被优化的单字符整型常数)
match = re.search(r'RuntimeObj\((\d+)\)', part)
if match:
char_val = int(match.group(1))
decrypted_part += chr(char_val ^ xor_key)
print(f" [-] 碎片 {index:02d} 解密: {repr(part)} -> '{decrypted_part}'")
flag += decrypted_part
print("-" * 50)
print(f"[+] 最终解密出的 Flag: {flag}")
print(f"[+] 字符总长度: {len(flag)} (符合常量池中的 _total_len: 30)")
if __name__ == '__main__':
decrypt_flag()
还是Python对我好啊uwu
番外篇
不是你们这速度是人吗, 五分钟都没有呢我刷新一下发现已经提交了13个 (非人哉!!!)
Love2D(2D都有Love, 为什么我没有Love)游戏(至少这个可以)直接解压获取Lua源码, 解压后有main.lua
搜索flag可找到:
local function getWinMessage()
local content = nil
if love.filesystem.getInfo("assets/flag.dat") then
content = love.filesystem.read("assets/flag.dat")
end
if not content or currentDifficulty ~= "hard" then
return "You WIN!"
end
local key = "52pojie"
local keyLen = #key
local result = {}
local bit = require("bit")
for i = 1, #content do
local b = string.byte(content, i)
local k = string.byte(key, ((i - 1) % keyLen) + 1)
table.insert(result, string.char(bit.bxor(b, k)))
end
return table.concat(result)
end
显然, 直接运行一下可得: flag{52pojie_2026_Happy_New_Year!_>w<}
你可能觉得这个flag里居然有颜文字, 题目挺可爱的, 没事到后面你就不会这么觉得了
初七Windows中级
(逆向? 密码学! 不是你们是怎么做到20分钟就有10个人做出来的)
窗口&解密初步
IDA载入, 显然可以注意到函数DialogFunc <u>(Attention is all you need)</u>
INT_PTR __fastcall DialogFunc(HWND a1, int a2, __int16 a3)
{
// 类型定义被我吃了
if ( a2 == 272 )
{
/* 这个分支是初始化, 我省略了 */
}
else if ( a2 == 273 )
{
if ( a3 == 10 )
{
MessageBoxW(a1, &::Text, &word_14000A152, 0x40u);
}
else if ( a3 == 11 )
{
*(_QWORD *)Str = 0;
memset(String, 0, 0x200u);
GetDlgItemTextW(a1, 2, String, 255);
v5 = _wfopen(String, aR);
if ( v5 )
{
GetDlgItemTextW(a1, 3, String, 255);
v6 = _wfopen(String, L"wb"); // 打开文件
if ( v6 )
{
GetDlgItemTextA(a1, 4, Str, 79);
v7 = sub_140008720(Str, v5, v6); // 解密函数, 传入Flag, 输入文件, 输出文件
fclose(v5);
fclose(v6);
memset(Text, 0, sizeof(Text));
sub_140008D70((__int64)Text);
if ( v7 )
MessageBoxW(a1, Text, &word_14000A078, 0x10u);
else
MessageBoxW(a1, Text, &Caption, 0x40u);
}
else
{
fclose(v5);
MessageBoxW(a1, &word_14000A0B0, &word_14000A078, 0x10u);
}
}
else
{
MessageBoxW(a1, &word_14000A080, &word_14000A078, 0x10u);
}
}
}
else
{
result = 0;
if ( a2 != 16 )
return result;
EndDialog(a1, 0);
}
return 1;
}
不难发现(这真不难)真正的解密函数其实是sub_140008720
__int64 __fastcall sub_140008720(char *Str, FILE *Stream, FILE *a3)
{
size_t v6; // rax
__int64 v7; // rdi
char v8; // dl
__int64 result; // rax
__int64 v10; // rdi
unsigned __int64 v11; // r13
char *v12; // rax
char *v13; // r12
__int64 v14; // rdx
unsigned int v15; // r8d
unsigned __int64 v16; // rax
_DWORD Buffer[4]; // [rsp+20h] [rbp-858h] BYREF
unsigned __int64 v18[265]; // [rsp+30h] [rbp-848h] BYREF
CRC32_Init((__int64)v18); // 打开之后有一个很经典的CRC常数, 那很显然了吧?
CRC32_Update(v18, "52pojie_2026_", 14);
v6 = strlen(Str);
CRC32_Update(v18, Str, v6);
v7 = CRC32_Final(v18);// 将"52pojie_2026_"和输入的Flag进行CRC, 然后存入v18
fread(Buffer, 0x10u, 1u, Stream); // 此处读取了输入文件的前0x10(16)个字节
v8 = sub_140008310((__int64)v18, v7, Buffer); // 这里是密钥派生部分吧 (具体跟进去看)
result = 1;
if ( v8 )
{
fseek(Stream, 0, 2);
v10 = ftell(Stream);
result = 2;
v11 = v10 - 16;
if ( (v10 & 7) == 0 )
{
fseek(Stream, 16, 0);
v12 = (char *)malloc(v10 - 16);
v13 = v12;
if ( v12 )
{
fread(v12, 1u, v10 - 16, Stream);
sub_1400081E0((__int64)v18, (__int64)v13, v10 - 16); // v18: 大数组, v13: 输入, v10-16: payload长度(?)
if ( (unsigned __int8)sub_1400082E0(v18, v14, v15) ) // 这里是一个校验
{
v16 = (unsigned __int8)v13[v10 - 17]; // PKCS#7填充
if ( v11 < v16 )
{
free(v13);
return 5;
}
else
{
fwrite(v13, 1u, v11 - v16, a3);
free(v13);
return 0;
}
}
else
{
free(v13);
return 4;
}
}
else
{
return 3;
}
}
}
return result;
}
在这里我们可以看到我们输入的Flag与52pojie_2026_一起经过了CRC32的处理, 被当做了解密的Key
密钥流派生&文件结构
__int64 __fastcall sub_140008310(__int64 a1, __int64 a2, _DWORD *a3)
{
__int64 result; // rax
int v6; // eax
result = 0;
if ( a3 )
{
if ( *a3 == 909266243 ) // a3是输入文件, 比较开头前四字节是否为CM26
{
sub_140008360((_QWORD *)a1, a2, (__int64)(a3 + 2)); // 后八字节作为IV
v6 = a3[1]; // 再之后四字节被存储到v6, 这里反编译应该是错了, 这是实际上是个CRC校验和
*(_DWORD *)(a1 + 288) = -1;
*(_DWORD *)(a1 + 292) = v6;
return 1;
}
}
return result;
}
void *__fastcall sub_140008360(_QWORD *a1, __int64 a2, __int64 a3) // a1: Key, a3: IV
{
__int64 v3; // rax
v3 = 0;
*a1 = a2;
do
{
*((_BYTE *)a1 + v3 + 16) = *(_BYTE *)(a3 + v3);
++v3;
}
while ( v3 != 16 );
return memcpy(a1 + 4, &unk_14000A270, 0x100u); // unk_14000A270其实就是AES S-BOX...
}
由此可知其文件头是这样的(总共16个字节):
CM26 (前4) CRC校验和(中间4) IV(后4)
看到这里就有人要问了: 哎呀, Flag在图片里, 但是解密图片又需要Flag, 这题是不是没法做了, 我要停在这了
别急, 继续往下看, 有反转 (建议手动将手机/电脑屏幕反转)
块解密函数
// 哇哦, 这是什么东西
// a1, 大数组, a2输入, a3长度
__int64 __fastcall sub_1400081E0(unsigned __int64 *a1, __int64 a2, __int64 a3)
{
unsigned __int64 v3; // rbx
unsigned __int64 v5; // rdi
_DWORD *v6; // r12
__int64 result; // rax
v3 = a2 - 24627;
v5 = a2 - 24627 + (a3 & 0xFFFFFFFFFFFFFFF8uLL);
if ( a2 - 24627 < v5 )
{
v6 = a1 + 36;
do
{
v3 += 8LL; // 从这里我们可以发现, 他是8个字节为一组进行解密的
sub_140008080(a1, (__int64 *)(v3 + 24619));
result = sub_140008480(v6, v3 + 24619, 8);
}
while ( v3 < v5 );
}
return result;
}
char __fastcall sub_140008080(unsigned __int64 *a1, __int64 *a2)
{
int v2; // r8d
unsigned __int64 v3; // r11
__int64 v5; // rax
char *v6; // rcx
unsigned __int64 v7; // rax
__int64 *v8; // rdx
char v9; // al
char result; // al
v2 = 8;
v3 = *a2;
v5 = *a1;
v6 = (char *)a1 - 21569;
v7 = __ROL8__(v5, 3);
do
{
v7 = (v7 << 8) | *((unsigned __int8 *)a1 + (HIBYTE(v7) | 0x221300) - 2233056);
--v2;
}
while ( v2 );
*a1 = v7;
v8 = a2 + 4711;
do
{
v9 = *((_BYTE *)v8 - 37688);
++v6;
v8 = (__int64 *)((char *)v8 + 1);
result = v6[21568] ^ v6[21584] ^ v9; // 偏移相差 16 字节. 回到初始化那边看的话其实就是密钥流和IV, v9是密文
*((_BYTE *)v8 - 37689) = result;
}
while ( v6 != (char *)a1 - 21561 );
a1[2] = v3; // 这里是解密之前的密文块
return result;
}
在这里a1[2] = v3; 将当前的密文块存入了 a1[2](也就是IV的位置, 我滴妈IDA把这一块内存区域给我加过来减过去, 还换类型, 算大小算半天). 这证明它使用了 密文反馈,下一个块的 IV 就是当前的密文.
这么一看我们就能知道个大概, 看不出来的同学可以把函数扔给Gemini
整个解密算法如下:
K_stream = SBox_Substitute( ROL(Key, 3))
Plaintext = K_stream ^ IV ^ Ciphertext
好机会!
理论上讲, 你没密钥是不可能解密的, 只能去爆破CRC的结果. <u>但是!</u>
题目文件里的Flag告诉我们是flag.png.encrypted (这个PNG可不是白给的)
众所周知, PNG文件头前八个字节是固定的89 50 4E 47 0D 0A 1A 0A
而回到刚才, 他这个算法也是八个一组哦!!!
由于异或的特性(见下方公式, 而非图), 我们可以直接推出第一块的密钥流, 进而通过AES Inverse S-Box 得到整个密钥流
$$
K\_stream = Plaintext \oplus IV \oplus Ciphertext
$$
接下来请AI生成代码(我懒):
import struct
import zlib
import os
# 标准 AES S-Box
AES_SBOX = [
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16
]
# 生成 AES 逆 S-Box (用于倒推密钥)
AES_INV_SBOX = [0] * 256
for i, val in enumerate(AES_SBOX):
AES_INV_SBOX[val] = i
def rotl64(x, y):
"""64位循环左移"""
return ((x << y) | (x >> (64 - y))) & 0xFFFFFFFFFFFFFFFF
def rotr64(x, y):
"""64位循环右移"""
return ((x >> y) | (x << (64 - y))) & 0xFFFFFFFFFFFFFFFF
def recover_key_from_png(iv_bytes, ciphertext_block0):
"""
已知明文攻击 (KPA):通过 PNG 文件头逆推 64位主密钥
"""
# 所有的 PNG 文件必然以这 8 个字节开头,刚好满足一个加密块大小 (8 bytes)
png_magic = b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A'
# 1. 因为 Plaintext = Keystream ^ IV ^ Ciphertext
# 所以 Keystream = Plaintext ^ IV ^ Ciphertext
k_bytes = bytearray(8)
for i in range(8):
k_bytes[i] = png_magic[i] ^ iv_bytes[i] ^ ciphertext_block0[i]
# 2. 逆推 Key 生成算法
# 在原算法中,rotl64 是在 8 次 S-Box 循环的外部执行的。
# 所以最终生成的 Keystream 实际上就是 rotl64(Key, 3) 的每一个独立字节经过 SBox 的结果。
t0_bytes = bytearray(8)
for i in range(8):
# 通过逆向 S-Box 还原出 rotl64(Key, 3) 时的原始字节
t0_bytes[i] = AES_INV_SBOX[k_bytes[i]]
# 组合成 64 位整数 T0
t0 = struct.unpack('<Q', t0_bytes)[0]
# 逆向循环左移 3 位 (即循环右移 3 位) 恢复出完全准确的初始 Key64
k_current = rotr64(t0, 3)
return k_current
def decrypt_cm26_file_kpa(input_path, output_path):
"""基于已知明文攻击的无密码解密主逻辑"""
with open(input_path, 'rb') as f:
file_data = f.read()
if len(file_data) < 16:
raise ValueError("文件太小,不是有效的 CM26 文件")
# 1. 解析头部
magic, expected_crc, iv = struct.unpack('<4sI8s', file_data[:16])
if magic != b'CM26':
raise ValueError(f"文件特征码不匹配,预期为 CM26,实际为: {magic}")
iv_array = bytearray(iv)
ciphertext = file_data[16:]
if len(ciphertext) % 8 != 0:
raise ValueError("加密数据大小不是 8 的倍数,文件可能已损坏")
# 2. 实施已知明文攻击,直接从加密文件中逆推初始密钥
print(" 正在执行已知明文攻击 (PNG Magic KPA)...")
key64 = recover_key_from_png(iv_array, ciphertext[:8])
print(f"[+] 攻击成功!恢复出原始 64 位 Key: 0x{key64:016X}")
plaintext_padded = bytearray()
# 3. 逐块解密循环
for offset in range(0, len(ciphertext), 8):
block = ciphertext[offset:offset+8]
# 3.1 生成当前块的 64 位 Keystream
# 修复:循环左移 3 位是在 8 次 S-Box 替换循环的 *外部* 发生的
k = rotl64(key64, 3)
for _ in range(8):
idx = (k >> 56) & 0xFF
sbox_val = AES_SBOX[idx]
k = ((k << 8) & 0xFFFFFFFFFFFFFFFF) | sbox_val
key64 = k
# 3.2 异或解密
k_bytes = struct.pack('<Q', k)
pt_block = bytearray(8)
for i in range(8):
pt_block[i] = k_bytes[i] ^ iv_array[i] ^ block[i]
plaintext_padded.extend(pt_block)
# 3.3 更新 IV
iv_array = bytearray(block)
# 校验 CRC32
actual_crc = zlib.crc32(plaintext_padded) & 0xFFFFFFFF
if actual_crc != expected_crc:
print(f"警告: CRC32 校验失败! (预期: {hex(expected_crc)}, 实际: {hex(actual_crc)})")
# PKCS#7
if plaintext_padded:
padding_len = plaintext_padded[-1]
if padding_len > 0 and padding_len <= 8:
plaintext = plaintext_padded[:-padding_len]
else:
plaintext = plaintext_padded
else:
plaintext = plaintext_padded
with open(output_path, 'wb') as f:
f.write(plaintext)
if __name__ == "__main__":
in_file = "flag.png.encrypted"
out_file = "flag.png"
decrypt_cm26_file_kpa(in_file, out_file)
然后我们可以得到下图:
记事本或者什么二进制查看工具打开, 可以得到flag{EncrypTIoN_Is_haRd_52p0jIE_2o26_m62Tc4uj78maAq1C}
Android中级题
做完这题直接力竭了。。。半个小时做出来这题的究竟是什么神人 (大佬)
反正我刚开始是这样的:
Java代码分析
package com.zj.wuaipojie2026_2;
import f1.h;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/* loaded from: classes.dex */
public final class NativeBridge {
public static final int $stable = 0;
public static final int ERR_CHEAT = -7;
public static final NativeBridge INSTANCE = new NativeBridge();
public static final int SCORE_GOOD = 1;
public static final int SCORE_MISS = 0;
public static final int SCORE_PERFECT = 2;
static {
System.loadLibrary("hajimi");
}
private NativeBridge() {
}
private final native void startSessionBytes(long j2, byte[] bArr, int i2);
public final native int checkRhythm(long j2, int i2, long j3, int i3);
public final native byte[] decryptFrames(byte[] bArr, long j2);
public final native void setDebugBypass(boolean z2); // 设置调试模式(?) 反正这玩意必须得自己设置
public final void startSession(long j2, int[] iArr, int i2) {
h.e(iArr, "beatMapMs");
ByteBuffer order = ByteBuffer.allocate(iArr.length * 4).order(ByteOrder.LITTLE_ENDIAN);
for (int i3 : iArr) {
order.putInt(i3);
}
byte[] array = order.array();
h.d(array, "array(...)");
startSessionBytes(j2, array, i2);
}
public final native long updateExp(int i2, int i3, long j2);
public final native byte[] verifyAndDecrypt(byte[] bArr, String str); // 需要逆向的是这个函数, bArr是hjm_pack.bin文件内容, Str是用户输入的Flag
}
显然, 我们需要逆向verifyAndDecrypt并且设置setDebugBypass(设置他的原因在最后会讲到, 这玩意坑我俩小时啊啊啊啊啊)
JNI_OnLoad
非常显然的是又是动态注册, 不过无所谓习惯了, 好在没OLLVM他全家...
jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
JavaVM v2; // x8
__int64 v4; // x0
_QWORD v5[2]; // [xsp+0h] [xbp-10h] BYREF
v5[1] = *(_QWORD *)(_ReadStatusReg(TPIDR_EL0) + 40);
v2 = *vm;
v5[0] = 0;
if ( v2->GetEnv(vm, (void **)v5, 65542) )
return -1;
v4 = (*(__int64 (__fastcall **)(_QWORD, const char *))(*(_QWORD *)v5[0] + 48LL))(
v5[0],
"com/zj/wuaipojie2026_2/NativeBridge");
if ( !v4 )
return -1;
if ( (*(unsigned int (__fastcall **)(_QWORD, __int64, char **, __int64))(*(_QWORD *)v5[0] + 1720LL))(
v5[0],
v4,
off_5E6F8,
6) ) // 此处动态注册, off_5E6F8是表
{
return -1;
}
return 65542;
}
通过观察这个二维数组可以看到这些函数的本体与签名(虽然签名没卵用)
需要注意的是, 他的排列顺序为:
名称 (字符串)
签名 (字符串)
函数
不要搞反了... (嗯对我一开始就搞成了decryptFrames, 然后浪费了半小时)
setDebugBypass
在表里找到函数本体
void __fastcall sub_25C90(JNIEnv *a1, void *a2, jboolean a3)
{
DebugFlag = a3 == 1; // 先给他改个名, 不然逆向verifyAndDecrypt的时候得晕
}
嗯对就这么朴实无华, 调用直接传个true就行
verifyAndDecrypt
主体
直接看代码吧, 刚开始就是一坨大的。。。
jbyteArray __fastcall verifyAndDecrypt(JNIEnv *vm, void *reserved, jbyteArray *bArr, jstring *Str)
{
JNIEnv v5; // x8
int v8; // w0
size_t v9; // x23
unsigned int v10; // w21
char *v11; // x0
char *v12; // x25
char *v13; // x19
JNIEnv v14; // x8
jbyteArray v15; // x0
jbyteArray v16; // x22
unsigned __int64 v18; // x0
int v19; // w10
__int64 v20; // x9
__int64 v21; // x8
unsigned __int64 v22; // x9
unsigned __int64 v23; // x9
unsigned __int64 v24; // x10
__int128 v25; // t2
int v26; // w26
int v27; // w10
int v28; // w24
int v29; // w25
int v30; // w8
__int64 v31; // x0
double v32; // d0
double v33; // d1
long double v34; // q2
int8x16_t v35; // q3
int8x16_t v36; // q4
int8x8_t v37; // d5
const char *InpStr; // x26
int v39; // w28
char v40; // w24
size_t v41; // x2
unsigned __int64 v42; // x0
unsigned __int64 v43; // x1
__int64 a1; // [xsp+8h] [xbp-38h] BYREF
int v45[2]; // [xsp+10h] [xbp-30h]
int v46[2]; // [xsp+18h] [xbp-28h]
void *s2[4]; // [xsp+20h] [xbp-20h] BYREF
s2[3] = *(void **)(_ReadStatusReg(TPIDR_EL0) + 40);
v5 = *vm;
if ( bArr && Str )
{
v8 = v5->GetArrayLength(vm, bArr);
if ( v8 > 0 )
{
v9 = (unsigned int)v8;
v10 = v8;
v11 = (char *)operator new((unsigned int)v8);
v12 = &v11[v9];
v13 = v11;
a1 = (__int64)v11;
*(_QWORD *)v46 = &v11[v9];
memset(v11, 0, v9);
v14 = *vm;
*(_QWORD *)v45 = v12;
v14->GetByteArrayRegion(vm, bArr, 0, v10, v13);
if ( v10 <= 0x33 || *(_DWORD *)v13 != 827148872 ) // 长度检测&文件头检测
goto fail;
v18 = env_check(vm); // 就这个环境检测, 老长了...
v19 = dword_5EA50 + HIDWORD(v18);
v20 = dword_5EA4C | (unsigned int)v18;
if ( dword_5EA50 + HIDWORD(v18) >= 12 )
v19 = 12;
dword_5EA4C |= v18;
dword_5EA50 = v19;
if ( v19 < 4 )
{
v21 = qword_5EA28;
if ( byte_5EA54 != 1 )
{
v26 = 0;
goto LABEL_20;
}
}
else
{
v21 = qword_5EA28;
byte_5EA54 = 1;
}
v22 = (v19 ^ (unsigned __int64)(v20 << 32) ^ 0x1A8CBC5B802E097CLL) - 0x61C8864680B583EBLL;
v23 = 0x94D049BB133111EBLL
* ((0xBF58476D1CE4E5B9LL * (v22 ^ (v22 >> 30))) ^ ((0xBF58476D1CE4E5B9LL * (v22 ^ (v22 >> 30))) >> 27));
v24 = v23 ^ (v23 >> 31);
if ( v24 )
{
*((_QWORD *)&v25 + 1) = v23 ^ (v23 >> 31);
*(_QWORD *)&v25 = v23;
v21 ^= (v25 >> 35) ^ v24;
}
v26 = 1;
LABEL_20:
v27 = *((_DWORD *)v13 + 2);
qword_5EA30 = v21;
if ( v27 )
{
v28 = *((_DWORD *)v13 + 3);
if ( v28 )
{
v29 = *((_DWORD *)v13 + 4);
if ( v29 )
{
v30 = *((_DWORD *)v13 + 1);
if ( v30 == 2 )
{
if ( (v26 | (unsigned __int8)byte_5EA40) & 1 | (DebugFlag != 0) ) // 启用调试之后可以在直接把环境监测数值改为0的情况下, 走正常分支
{
if ( DebugFlag )
v42 = sub_2DCDC();
else
v42 = qword_5EA38;
if ( v26 )
v43 = v42 ^ 0xA5A5A5A5A5A5A5A5LL;
else
v43 = v42;
if ( (sub_2DDF8(&a1, v43) & 1) == 0 )
{
LABEL_47:
v15 = (*vm)->NewByteArray(vm, 0);
goto LABEL_7;
}
LABEL_29:
InpStr = (*vm)->GetStringUTFChars(vm, Str, 0);
if ( InpStr )
{
v39 = v29 * v28;
sub_2D46C(s2, (unsigned int)(v29 * v28) >> 3);
v40 = sub_2E5FC(InpStr, v28, v29, s2[0], (char *)s2[1] - (char *)s2[0]); // 将输入的字符串"渲染"成点阵图, 后续直接将其与解密得到的位图进行比较)
(*vm)->ReleaseStringUTFChars(vm, Str, InpStr);
if ( (v40 & 1) != 0
&& (unsigned int)v39 >= 8
&& (v41 = (unsigned __int64)v39 >> 3, v41 + 52 <= v9)
&& !memcmp(v13 + 52, s2[0], v41) ) // 此处为比较代码, v13 + 52是FLAG的内容的地址, s2[0]是输入的字符串渲染成的点阵图, 比较长度(v41)为512
{
v16 = (*vm)->NewByteArray(vm, v10);
(*vm)->SetByteArrayRegion(vm, v16, 0, v10, v13);
}
else
{
v16 = (*vm)->NewByteArray(vm, 0);
}
if ( s2[0] )
{
s2[1] = s2[0];
operator delete(s2[0]);
}
goto LABEL_8;
}
goto LABEL_47;
}
}
else if ( v30 == 1 )
{
if ( v26 )
v31 = 1515870653;
else
v31 = 999;
sub_2D4F0(v31, (_QWORD *)v13 + 3, (__int64 *)v13 + 5, (unsigned __int64 *)s2);
sub_2D678((__int64)(v13 + 52), v9 - 52, s2, v13 + 40, v32, v33, v34, v35, v36, v37);// AES-128?
goto LABEL_29;
}
}
}
}
fail:
v15 = (*vm)->NewByteArray(vm, 0);
LABEL_7:
v16 = v15;
LABEL_8:
operator delete(v13);
return v16;
}
v5 = *vm;
}
return v5->NewByteArray(vm, 0);
}
从中我们可以不难可以看到, 这个函数大致的流程如下:
- 环境检测, 如果异常则影响下一步解密的结果
- 使用AES(大概是)解密输入文件, 解密后得到文字点阵图(图片内容就是Flag)
- 将用户输入进行渲染, 并与(2)中的点阵图比较
反调试/环境监测
代码太长太恶心了, 这里就不贴出来了, 总之我直接扔给AI大概讲一下
return v332 | (unsigned int)v340 | (unsigned __int64)(v324 << 32);
返回值是一个 64 位整数,低 32 位为检测标志位,高 32 位为检测评分。
各分量在干净环境下的推导
低 32 位标志(v332 | v340)
| 变量 |
含义 |
干净环境值 |
v332 |
JNI 环境异常标志(FindClass/GetMethodID/NewStringUTF/GetObjectRefType 检测) |
0(JNI 全部正常) |
v343(即 v152) |
/proc/self/maps 首次读取相关异常 |
0 |
v344(即 v161) |
可执行库 hook 检测(Frida/Xposed inline hook) |
0(无 hook) |
v197 |
readlink + readlinkat 路径一致性 + maps/status 模拟器特征检测 |
0 |
v224 & 1 |
maps 中是否存在黑名单字符串(frida/magisk 等) |
0 |
v340 \|= 2u |
设备指纹命中模拟器列表(v263 == 1) |
不触发 |
v340 \|= 4u |
/proc/zoneinfo、/sys/cpu 等路径 access 失败超限 |
不触发(真机均可访问) |
→ v332 | v340 = 0
高 32 位评分(v324)
v324 = v239,而 v239 = v114(无额外检测加分时)。
v114 是整个函数中累积的异常评分:
- 首次 maps 解析:检测
[vdso]/[vsyscall] / 调试器路径 / frida-agent 路径等 → 干净环境无命中,v114 = 0
- 后续各阶段(maps hash 校验、emulator 文件系统检测、可执行文件符号链接校验等) → 全部通过,无加分
→ v324 = 0,高 32 位 = 0
结论
干净环境返回值 = 0x0000000000000000
在这里我们需要使用Frida将该函数的返回值设置为0, 但需要注意的是如果直接替换则会导致无法走入正常分支, 需要同时setDebugBypass
而且还有一个坑: 程序一打开就会调用一次环境检测, 并且后续每次验证flag都会调用
所以我们要在so刚加载的时候就hook掉他
const TARGET_LIB = "libhajimi.so";
const ANTI_DEBUG_OFFSET = 0x25ef8; // 此处填入从IDA中找到的环境检测函数的偏移地址
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
onEnter: function (args) {
this.libName = args[0].readCString();
},
onLeave: function (retval) {
if (this.libName && this.libName.indexOf(TARGET_LIB) !== -1) {
let baseAddr = Module.getBaseAddress(TARGET_LIB);
let antiDebugPtr = baseAddr.add(ANTI_DEBUG_OFFSET);
Interceptor.replace(antiDebugPtr, new NativeCallback(function (a) {
return ptr(0);
}, 'pointer', ['pointer']));
}
}
});
Java.perform(function () {
let NativeBridge = Java.use("com.zj.wuaipojie2026_2.NativeBridge");
NativeBridge.INSTANCE.value["setDebugBypass"](true) // 启用Debug
})
点阵图渲染
函数签名及参数解释:
__int64 sub_2E5FC(
const char *text, // a1: 输入的源字符串 (例如: Flag)
int width, // a2: 画布总像素宽度
int height, // a3: 画布总像素高度
void *out_buffer, // s: 输出位图缓冲区 (每 bit 代表一像素)
size_t buffer_n // n: 缓冲区字节大小
);
这个函数实现了自动换行, 居中渲染, 字模查找与绘制...... 实际上我们根本不需要逆向它, 但是还是稍微了解一下比较好 (下方三点为AI生成, 仅作为了解)
-
字模库: 位于 byte_16A58.
-
查找方式: 线性搜索. 每个字模占用 8 字节: 第一个字节是 ASCII 码, 后 7 字节代表 7 行点阵数据 (每行取低 5 位).
-
绘图循环:
do {
v58 = v56[v55]; // 获取字模的一行数据
// ... 检查 v58 的每一位 (0x10, 0x08, 0x04, 0x02, 0x01)
// ... 如果位被点亮,计算全局坐标并写入缓冲区
} while ( v55 != 7 );
点阵图比较
直接看代码吧
/*...*/
InpStr = (*vm)->GetStringUTFChars(vm, Str, 0);
if ( InpStr )
{
v39 = v29 * v28;
sub_2D46C(s2, (unsigned int)(v29 * v28) >> 3);
v40 = sub_2E5FC(InpStr, v28, v29, s2[0], (char *)s2[1] - (char *)s2[0]); // 这里根据输入绘制
(*vm)->ReleaseStringUTFChars(vm, Str, InpStr);
if ( (v40 & 1) != 0
&& (unsigned int)v39 >= 8
&& (v41 = (unsigned __int64)v39 >> 3, v41 + 52 <= v9)
&& !memcmp(v13 + 52, s2[0], v41) ) // 这里将两个图进行比较, 如果相同就返回FLAG图
{
v16 = (*vm)->NewByteArray(vm, v10);
(*vm)->SetByteArrayRegion(vm, v16, 0, v10, v13);
}
else
{
v16 = (*vm)->NewByteArray(vm, 0);
}
if ( s2[0] )
{
s2[1] = s2[0];
operator delete(s2[0]);
}
goto LABEL_8;
}
goto LABEL_47;
/*...*/
拿Flag
通过刚才的环节, 我们知道了: 程序如果判断绘制的点阵图与FLAG图一致的话, 那么就会返回FLAG图, 并且在界面上显示
能想到这么折腾FLAG我只能说:
那不就好说了, 直接让memcmp返回0, 强制让判断通过即可让FLAG回显
/* 注意, 此处代码需要配合上面移除环境检测的代码一起使用, 如果hook不上可以和上面的代码在同一时机hook */
const TARGET_LIB = "libhajimi.so";
const MEMCMP_OFFSET = 0x25b24; // 此处填入调用MEMCMP的地址
let baseAddr = Module.findBaseAddress(TARGET_LIB);
let memcmpPtr = baseAddr.add(MEMCMP_OFFSET);
Interceptor.attach(memcmpPtr.add(4) /* +4 是因为在下一条指令处修改寄存器 (MEMCMP返回值寄存器为x0) */, {
onEnter: function (args) {
console.log(this.context.w0)
console.log(this.context.x0)
this.context.w0 = 0
this.context.x0 = 0
}
})
点投喂 flag然后随便输入点什么就行, 然后就行了:
这题真的让我做的想哈气了... 哦没事后面我哈了个够
哈基米哦南北绿豆~~
Web题
我本以为上一道题已经足够奇怪了, 没想到还有WASM高手
千算万算结果我还是没算到: 「 如呼吸一样轻松」是产品设计目标。 (照应了我开头的话)
那我交的FLAG能不能解释权归我所有, 我说对就对
提取WASM&反编译
将verify.wasm.js中的base64提取出来并解码保存, 即可得到WASM文件
并从Github上下载wabt, 使用其中的wasm-decompile进行反编译 (不用wasm2c的原因是那玩意太费眼了...你自己试一下就知道了...)
wasm-decompile FileName.wasm -o Result .dcmp
分析JS:
async function init() {
let i = false, w = document.createTreeWalker(document, 128), n
try {
await wasm_bindgen('f.wasm') /* 这里被我改过 */
const audio = document.getElementById('audioPlayer')
audio.volume = 0.3
checkboxText.addEventListener('click', async () => {
const uidInput = document.getElementById('uid')
if (!uidInput.value) {
uidInput.focus()
return
}
const uid = parseInt(uidInput.value) || 0
const voice = document.getElementById('voice').value
try {
// 将uid和音色传入wasm导出的gen函数中
const challenge = wasm_bindgen.gen(uid, voice)
/* ... */
}
})
/* ... */
}
/* ... */
}
我们可以知道, 我们接下来需要逆向WASM中的gen函数
结构分析
首先在反编译结果中可以看到如下函数签名:
// a: 传入的UID
// b: voice字符串指针
// c: voice字符串长度(?)
// return: 一堆指针(音频, Hash)
export function gen(a:int, b:int, c:int): (int, int, int)
UID异或+随机值
export function gen(a:int, b:int, c:int):(int, int, int) { // func50
/* 省略定义 */
wbg_wbg_getRandomValues_1c61fac11405ffdc(d + 80, 17); // 获取随机字节, 储存在地址d[80]到d[97]
c = 5412468[0]:int;
var e:int = 5412472[0]:int;
5412468[0]:long@4 = 0L;
var g:int = d + 72;
g[1]:int = e;
g[0]:int = c == 1;
j = f_zb(37, 1); // malloc申请内存, 这里存了Payload
if (j) {
// ----
j[3]:byte = (b = d[83]:ubyte ^ a >> 24);
j[2]:byte = (c = d[82]:ubyte ^ a >> 16);
j[1]:byte = (e = d[81]:ubyte ^ a >> 8);
j[0]:byte = (a = d[80]:ubyte ^ a);
// ---- 上方将UID的4字节与获取的前四个随机字节进行逐字节异或, 并且放入j中前四个字节
// 下方将剩余随机字节复制到j中
j[4]:long@1 = d[10]:long; // 这里的long是8字节, 所以其实还是复制
(j + 12)[0]:long@1 = (d + 88)[0]:long;
(j + 20)[0]:byte = (d + 96)[0]:ubyte;
/*...*/
}
}
怕你不理解, 给你表示一下此时的j中的布局 (这里的j实际上就是payload所存储的位置)
+----------------+------------------+
| UID(j[0..3]) | 随机字节(j[4..20])|
| 4字节, 小端序 | 17字节 |
+----- -----------++----------------+
HMAC-SHA256
export function gen(a:int, b:int, c:int):(int, int, int) { // func50
/* 省略定义 */
wbg_wbg_getRandomValues_1c61fac11405ffdc(d + 80, 17); // 获取随机字节, 储存在地址d[80]到d[97]
c = 5412468[0]:int;
var e:int = 5412472[0]:int;
5412468[0]:long@4 = 0L;
var g:int = d + 72;
g[1]:int = e;
g[0]:int = c == 1;
j = f_zb(37, 1); // malloc申请内存, 这里存了Payload
if (j) {
/*...*/
memory_copy(a, 1295967, 14); // 从1295967复制14字节Key
b = d + 416;
b[0]:long@1 = a[0]:long;
(b + 56)[0]:long@1 = c[0]:long;
(b + 48)[0]:long@1 = e[0]:long;
(b + 40)[0]:long@1 = f[0]:long;
(b + 32)[0]:long@1 = g[0]:long;
(b + 24)[0]:long@1 = l[0]:long;
(b + 16)[0]:long@1 = i[0]:long;
(b + 8)[0]:long@1 = k[0];
g_a = a + 352;
a = 0;
/* 0x36 HMAC标准中的ipad */
loop L_w {
c = d + 416;
b = c + a;
b[0]:byte = b[0]:ubyte ^ 54;
e = b + 1;
e[0]:byte = e[0]:ubyte ^ 54;
e = b + 2;
e[0]:byte = e[0]:ubyte ^ 54;
b = b + 3;
b[0]:byte = b[0]:ubyte ^ 54;
a = a + 4;
if (a != 64) continue L_w;
}
a = 0;
// 下方加载了SHA-256的常量
(d + 504)[0]:long = d_Cahabcdefghijklmnopqrstuvwxy[113@8]:long;
(d + 496)[0]:long = d_Cahabcdefghijklmnopqrstuvwxy[105@8]:long;
(d + 488)[0]:long = d_Cahabcdefghijklmnopqrstuvwxy[97@8]:long;
d[64]:long = 1L;
d[60]:long = d_Cahabcdefghijklmnopqrstuvwxy[89@8]:long;
f_j(d + 480, c, 1);
/* 再次异或正好得到0x5c, HMAC标准中的opad */
loop L_x {
c = d + 416;
b = c + a;
b[0]:byte = b[0]:ubyte ^ 106;
e = b + 1;
e[0]:byte = e[0]:ubyte ^ 106;
e = b + 2;
e[0]:byte = e[0]:ubyte ^ 106;
b = b + 3;
b[0]:byte = b[0]:ubyte ^ 106;
a = a + 4;
if (a != 64) continue L_x;
}
/* ... 省略很长的代码... 下方将结果大端序的后16字节转为小端序, 组装到Payload */
j[33]:int@1 =
((e = d[77]:int) << 24 | (e & 65280) << 8) |
((e >> 8 & 65280) | e >> 24);
j[29]:int@1 =
(c << 24 | (c & 65280) << 8) | ((c >> 8 & 65280) | c >> 24);
j[25]:int@1 =
(b << 24 | (b & 65280) << 8) | ((b >> 8 & 65280) | b >> 24);
j[21]:int@1 =
(a << 24 | (a & 65280) << 8) | ((a >> 8 & 65280) | a >> 24);
}
}
这段代码将上一步中的UID+随机值进行了HMAC-SHA256计算(Key: b'\x00\x01\x01\x01\x01\x01\x01\x00\x01\x00\x01\x00\x05\x02'), 并将其结果(大端序后16字节 或 小端序前16字节)添加到j(Payload)中, 那么现在整个Payload的结构就很清晰了:
+----------------+------------------+----------------------+
| UID(j[0..3]) | 随机字节(j[4..20])| HMAC-SHA256(j[21..36])|
| 4字节, 小端序 | 17字节 | 16字节, 强调是小端序 |
+----------------+-----------------+-----------------------+
到这里你是不是以为结束了? 不不不还没完, 不要忘了最后出来的是语音(而且是念出来的FLAG哦)
还是魔改Base64
继续看, 又是老朋友了:
export function gen(a:int, b:int, c:int):(int, int, int) { // func50
/* 省略定义 */
wbg_wbg_getRandomValues_1c61fac11405ffdc(d + 80, 17); // 获取随机字节, 储存在地址d[80]到d[97]
c = 5412468[0]:int;
var e:int = 5412472[0]:int;
5412468[0]:long@4 = 0L;
var g:int = d + 72;
g[1]:int = e;
g[0]:int = c == 1;
j = f_zb(37, 1); // 这里存了Payload
if (j) {
/* 上方代码省略 */
if (b) {
a = 0;
d[106]:int = 0;
d[105]:int = b;
d[104]:int = 50;
f = 1;
b = 0;
e = j;
loop L_ga {
let t0 = e;
g = a << 2;
e = f + j;
l = t0[0]:ubyte;
h = l | h << 8;
c = b;
loop L_ha {
i = (h >> (b = c + 2) & 63)[1295903]:ubyte; // base64查表, 经典操作了属于是
if (d[104]:int == a) { f_na(d + 416) }
(d[105]:int + g)[0]:int = i;
d[106]:int = (a = a + 1);
c = c - 6;
g = g + 4;
if (b > 5) continue L_ha;
}
b = c + 8;
f = f + (i = f != 37);
if (i) continue L_ga;
}
if (c == -8) goto B_u;
b = (l << -2 - c & 63)[1295903]:ubyte; // base64结束
if (a == d[104]:int) { f_na(d + 416) }
(d[105]:int + g)[0]:int = b;
d[106]:int = (a = a + 1);
goto B_u;
}
f_nb(4, 200);
}
}
由于标准Base64里没有问号和感叹号, 但是我们在听语音的时候能听到, 所以一定又是改了字典表的Base64!!
我们不难注意到, 地址1295903存放了Base64字典 (abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789?!)
Base64笑转之查查表
得到Flag
既然知道了结构, 又知道了编码, 我们不妨写如下代码计算Flag:
import os
import hmac
import hashlib
import base64
TRANS = str.maketrans('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789?!')
KEY = b'\x00\x01\x01\x01\x01\x01\x01\x00\x01\x00\x01\x00\x05\x02'
def MyFlag(UID: int) -> str:
RandomBytes = os.urandom(17)
j = bytearray(37)
UIDBytes = UID.to_bytes(4, 'little')
# j[0..3]
for i in range(4):
j[i] = UIDBytes[i] ^ RandomBytes[i]
j[4:21] = RandomBytes
h = hmac.new(KEY, j[:21], hashlib.sha256).digest()
j[21:37] = h[:16] # 小端序, 所以前16
return base64.b64encode(j).decode().translate(TRANS).replace('=', '') # 魔改
print('flag{' + MyFlag(1354181) + '}') # 这写自己UID
看到这么简短的代码, 你是不是有了一种感觉:
WAV合成
这个WASM本身并不能动态合成语音, 语音实际上是硬编码的PCM音轨, 通过直接拼接来实现TTS效果, 不过不得不说, 广东话的TTS是什么啊!!!
音轨寻址:
if (q != 1) goto B_eb;
br_table[...](r[0] - 99)
// 命中 'c' (99) -> 指向 1048577, 长度 76762
// 命中 'e' (101) -> 指向 1125339, 长度 86076
// ...
硬写WAV文件头:
(a + f)[0]:int@1 = 1179011410; // 'RIFF'
(d[65]:int + a)[0]:int@1 = 1163280727; // 'WAVE'
(d[65]:int + a)[0]:int@1 = 544501094; // 'fmt '
(d[65]:int + a)[0]:int@1 = 16; // Subchunk1Size
(d[65]:int + a)[0]:short@1 = 1; // AudioFormat (PCM)
(d[65]:int + a)[0]:short@1 = 1; // NumChannels (单声道)
(d[65]:int + a)[0]:int@1 = 24000; // SampleRate
(d[65]:int + a)[0]:int@1 = 48000; // ByteRate (24000 * 1 * 16/8)
(d[65]:int + a)[0]:short@1 = 2; // BlockAlign
(d[65]:int + a)[0]:short@1 = 16; // BitsPerSample
(d[65]:int + a)[0]:int@1 = 1635017060; // 'data'
(哦对了值得一提的是, 如果你想要通过在合成音频的函数打日志断点的话, 那你还是别想了, 他发声的时候全部传的小写字母)
后记
剩下两道我真不会做了, 尤其MCP(去年是番外CTF-Misc我能理解, 今年咋是MCP猜谜了, 我语文80多分, 我看不懂啊; 妈妈我再也不玩MCP了), Windows高级一个混淆给我干废了
今年全是大佬, 只能拿个50开外了, 给我的自信心大大挫败了一下 ( 做完这些题有活着的风险吗 )
哈!