吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 553|回复: 17
上一主题 下一主题
收起左侧

[CTF] 2026解题领红包基本完成(少MCP和Win高级, 但很有梗)

  [复制链接]
跳转到指定楼层
楼主
Command 发表于 2026-3-4 00:25 回帖奖励
本帖最后由 Command 于 2026-3-4 00:25 编辑

前言

一年一年, 我又来了, 今年怎么大伙都这么强. . . . . .

在大量梗™里发现了少量题解, 本文还是挺活泼的(梗有点多, 大概我做题时候精神状态不太好)

看不见我: 「 大量梗™」是产品商标,不代表产品最终效果。(这很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

整个解密算法如下:

  1. K_stream = SBox_Substitute( ROL(Key, 3))
  2. 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);
}

从中我们可以不难可以看到, 这个函数大致的流程如下:

  1. 环境检测, 如果异常则影响下一步解密的结果
  2. 使用AES(大概是)解密输入文件, 解密后得到文字点阵图(图片内容就是Flag)
  3. 将用户输入进行渲染, 并与(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开外了, 给我的自信心大大挫败了一下 ( 做完这些题有活着的风险吗 )

哈!

免费评分

参与人数 8吾爱币 +11 热心值 +8 收起 理由
蚯蚓翔龙 + 1 + 1 热心回复!
Coxxs + 3 + 1 大佬太强了!
伤城幻化 + 1 + 1 虽然看不懂具体内容,步骤都挺详细的
nanaqilin + 1 + 1 我很赞同!
collinchen1218 + 3 + 1 厉害
代陌 + 1 我很赞同!
cattie + 1 + 1 大佬好厉害~
Bambi5 + 1 + 1 大佬厉害

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
Hmily 发表于 2026-3-4 00:32
语文还得好好学,高考很重要
推荐
 楼主| Command 发表于 2026-3-4 00:41 |楼主
本帖最后由 Command 于 2026-3-4 00:45 编辑
Hmily 发表于 2026-3-4 00:39
你该睡觉了,早上得早起上课哦。

今天报道得下午5点了, 不着急, 我之前因为心理原因就请了半学期假, 学习对于我来说可能已经不是特别重要(?), 主要是河北高中我实在...受不了... 我其实以后也想就在逆向这行干下去, 但是也挺迷茫, 我在吾爱发文章也是为了以后攒点简历
3#
 楼主| Command 发表于 2026-3-4 00:33 |楼主
Hmily 发表于 2026-3-4 00:32
语文还得好好学,高考很重要

收到收到!!! 我还有两年呢, 到那时候会是啥样子我都不知道, 现在我状态其实就不很好的

点评

你该睡觉了,早上得早起上课哦。  详情 回复 发表于 2026-3-4 00:39
4#
Hmily 发表于 2026-3-4 00:39
Command 发表于 2026-3-4 00:33
收到收到!!! 我还有两年呢, 到那时候会是啥样子我都不知道, 现在我状态其实就不很好的

你该睡觉了,早上得早起上课哦。

点评

H大也早点休息, 我现在的情况其实也挺复杂  发表于 2026-3-4 00:43
6#
linhzye 发表于 2026-3-4 00:48
Command 发表于 2026-3-4 00:33
收到收到!!! 我还有两年呢, 到那时候会是啥样子我都不知道, 现在我状态其实就不很好的

膜拜大神!
7#
仿佛_一念成佛 发表于 2026-3-4 01:10
太强了太强了
8#
silent 发表于 2026-3-4 01:12
厉害,在这又学到新知识了,又能去再折腾一下没解出来的题
9#
nanaqilin 发表于 2026-3-4 08:42
哎,Frida我不咱会作,只会用IDA,结果一附上就崩了,结果,就没有结果了
10#
geesehoward 发表于 2026-3-4 09:47
本帖最后由 geesehoward 于 2026-3-4 09:49 编辑

看了几篇文章的解题思路,一直没注意,输出文件是png后缀,一直卡着进行 不下去。
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-3-4 15:47

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表