吾爱破解 - 2023 春节解题领红包 2-6 + 部分 web 题

查看 167|回复 9
作者:bj9ye666   
吾爱破解 - 2023 春节解题领红包
[ol]
  • Windows 题(2、5) ← 你在这里
  • 安卓题(3、4、6、7) - 其中 7 挑战失败
  • Web 题 - 缺少 4、8、9、12。
    [/ol]
    因为安卓部分有些代码太长了在编辑器会被截断,所以拆出来发了…
    【春节】解题领红包之二 {Windows 初级题}
    打开程序,随便输入个 12345,提示长度错误。
    偷懒直接用 IDA 看伪代码,看到一个像检查长度的:
    if ( *((_DWORD *)str.buffer - 3) == 29 )      // str len
    以及附近有 "Success" 字样,继续分析该分支,可以看到一个 for 循环(IDA 识别为 while)遍历每一个字符:
    i = 0;
    while (1) {
      // ... 省略部分
      // 遍历字符串
      if (str.buffer != (unsigned __int8)(g_password >> 2))
        break;
      if (++i >= str.size()) {
        // 遍历结束,提示成功
      }
    }
    最后就是回到 g_password,将数值导出然后变化一下(js 代码):
    [
      408, 432, 388, 412, 492, 212, 200, 320, 444, 296, 420, 404, 200, 192, 200,
      204, 288, 388, 448, 448, 484, 312, 404, 476, 356, 404, 388, 456, 500,
    ].map((v) => String.fromCharCode(v >> 2)).join("");
    得到答案 flag{52PoJie2023HappyNewYear}。
    【春节】解题领红包之五 {Windows  中级题}
    首先,这玩意脱壳我不会,即便是魔改过的 UPX _(:3__
    于是让程序在 x64dbg 内跑起来,直接用 Scylla 转储内存来静态分析。
    用 IDA 载入,查看字符串列表并没有什么能用的东西。于是在调试器附加后找字符串引用,把找到的提示错误都下个断点然后按下确认按钮,断下。
    此时断下的地址是比较迟的了,但是也因祸得福找到了事件派发函数 WndProc,于是继续在 IDA 慢慢分析。
    需要注意:
    [ol]
  • 下方的伪代码含有剧透内容,因为是从我分析结束后的 IDA 数据库里提取的内容,部分意义不明的地方已经加上了注解与更名。
  • 软件有尝试藏匿 API 调用与字符串来对抗静态分析,但是因为可以直接从调试器获取到对应的信息,所以没有加到文章中。部分字符串解密可以在事件处理函数的 WM_INITDIALOG 事件处理分支找到。有一部分的 API 调用是在需要调用的时候使用 GetProcAddress 动态获取函数地址。
    [/ol]
    看了下其他人对这题的分析好像都没有在 IDA 里面做笔记 / 改变量名。这个是我 dump 的 exe + i64 数据库,处理得漂漂亮亮的:

    处理得漂漂亮亮的第五题 IDA i64 数据库.7z
    (495.8 KB, 下载次数: 1, 售价: 1 CB吾爱币)
    2023-2-6 06:05 上传
    点击文件名下载附件
    收 1CB 不过分吧
    售价: 1 CB吾爱币         [记录]
    [购买]
    下载积分: 吾爱币 -1 CB



    wndproc.png (60.07 KB, 下载次数: 0)
    下载附件
    2023-2-6 06:05 上传

    验证流程
    首先看看「确认」按钮按下后的流程:
    // WM_COMMAND 分支下
    switch (wParam) {
    case 1: // 1 是 [ 确认 ] 按钮
        uid = GetUID_0(hWnd);
        if ( uid != 0 && GetKeyStr(hWnd, key_buff) > 0 ) {
            tea_sum = encrypt_payload(key_buff, uid); // 初始化?
            PostMessageW(hWnd, WM_COMMAND, 0x300/* wParam */, tea_sum);
            return 1;
        }
        return 0;
    // 忽略其他情况
    }
    encrypt_payload 这个函数其实并没有很明白在干什么,不过里面调用了一个 TEA 加密的变形,
    返回了一个值。
    然后就带着这个值一起提交到窗口事件列表,结束当前操作。
    经过了一段时间后,事件派发函数再次被执行。这次是 wParam === 0x300 的情况:
    // WM_COMMAND 分支下
    switch (wParam) {
    case 0x300u: // 自定义组件 ID
        switch (lParam) {
        case 1: // 错误: UID 是空的
            str_err_context = ctx.strs->str_uid_and_key;
            ctx.strs->str_uid_and_key[3] = 0;
            break;
        case 2: // ???
            str_err_context = &ctx.strs->str_uid_and_key[8]; // L"e4x#Tgs9T2FU" ???
            break;
        case 3: // 错误: UID 与密钥的组合错误
            str_err_context = ctx.strs->str_uid_and_key;
            ctx.strs->str_uid_and_key[3] = ' ';
            break;
        case 4: // 弹窗: 挑战成功
            MessageBoxSuccess(hWnd);
            return 1;
        default: // 默认
            str_err_context = L"";
            break;
        }
        if (wcscmp(str_err_context, L"") != 0) { // 错误显示
            str_error = ctx.strs->str_error;
            wsprintfW(str_message, ctx.strs->str_missing_field, str_err_context);
            user32.MessageBoxW(hWnd, str_message, str_error, MB_ICONERROR);
            return 1;
        } else if ((uid = GetUID(hWnd)) && GetUserKey(hWnd, buf_key_str) > 0) {
            // 取到的 uid 不得为 0,且 key 的长度必须大于 0
            // 基本上不需要管它…
            // 将十六进制字符串转换为 u32 数组
            HexStringToBlocks(buf_key_str, key);
            // 验证密钥,返回值可以是 3 或 4
            next_error_code = VerifyKey(key, uid, lParam_dup); // 3 or 4
            // 投递下一则消息
            // 3 = 失败
            // 4 = 成功
            PostMessageW(hWnd, WM_COMMAND, 0x300, next_error_code);
            return 1;
        }
        return 0;
    // 忽略其他情况
    }
    一开始看到这的时候还以为是个状态机,虚惊一场。前几个判断的情况都是根据代码显示对应的信息。
    刚才的 tea_sum 作为 lParam 传了进来,而这个值最高位会被设定(0x8000_0000),因此必定会跑到 default 的情况,然后到下面的 else if 分支继续。
    最终成功或失败则是取决于 VerifyKey 的返回值。
    算法分析
    因为最终成功或失败取决于 VerifyKey,因此优先分析这个函数和它的返回处:
    int VerifyKey(uint32_t *p_user_key, int uid, unsigned int sum)
    {
      int i; // [rsp+20h] [rbp-188h] MAPDST
      uint32_t tea_delta; // [rsp+24h] [rbp-184h] MAPDST
      int error_counter; // [rsp+28h] [rbp-180h] MAPDST
      int err_pos; // [rsp+38h] [rbp-170h] MAPDST
      int expected_sum; // [rsp+3Ch] [rbp-16Ch]
      flag_data flag; // [rsp+40h] [rbp-168h]
      wchar_t flag_content[104]; // [rsp+B0h] [rbp-F8h]
      uint32_t tea_key[4]; // [rsp+180h] [rbp-28h] BYREF
      if ( !p_user_key )
        return 0;
      tea_delta = 0x11111111;
      for ( i = 0; i str_flag[0];   // 拼接 "flag{"
      flag.as_str.str[1] = ctx.strs->str_flag[1];
      flag.as_str.str[2] = ctx.strs->str_flag[2];
      flag.as_str.str[3] = ctx.strs->str_flag[3];
      flag.as_str.str[4] = ctx.strs->str_flag[4];
      for ( i = 1; i str_flag[6]);// 拷贝 "}\x00"
      // 等价代码
      // tea_delta += uid;
      // while ((tea_delta >> 31) == 0) {
      //   tea_delta = tea_delta * 2 + 9;
      // }
      for ( tea_delta += uid; (tea_delta & 0x80000000) == 0; tea_delta = 2 * tea_delta + 9 )
        ;
      for ( i = 0; i > 1;                              // ret 4, 成功
      else
        return 3;                                   // fail
    }
    我在看这个函数的时候是从下向上反推的。因为已知返回值 3 是失败,因此 error_counter 与 expected_sum 的值需要相等。而 TEA 算法在解密后,sum 这个值通常会等于 0。
    因此最后面这一段循环可以这么改写/理解:
    for ( i = 0; i
    再这么一看,不就是把用户输入的内容解密后看是不是等于固定的一个值?
    分析到这里,我就去调用 tea_decrypt 前后位置分别下了一个断点,然后得到了 flag ("flag{!!!_HAPPY_NEW_YEAR_2023!!!}")、tea_key (LittleEndian {0xabe63ff8, 0x57cc7ff0, 0x03b2bfe8, 0xaf98ffe0)、tea_sum (0x7CC7FEE0)、tea_delta (0xABE63FF7) 以及加解密前后的值。
    对照着解密函数实现一个,然后用抓到的数据做验证。就目前看来,除了 delta、sum 和 TEA_ROUND 这三个参数之外,与标准 TEA 的实现没有什么不同。
    constexpr int TEA_ROUND = 32; // 这个值一般是 16
    // 其实用宏也可以,这段是从我以前写的 TEA 实现里抠出来的。
    // 让编译器自己优化就好。
    inline uint32_t single_round_tea(uint32_t value, uint32_t sum, uint32_t key1, uint32_t key2) {
        return ((value > 5) + key2);
    }
    // delta 和 sum 一般是固定的值
    uint64_t tea_decrypt(uint32_t* buffer, uint32_t* tea_key, uint32_t delta, uint32_t sum) {
        uint32_t y = buffer[0];
        uint32_t z = buffer[1];
        for (int i = 0; i
    那解密代码调试好了,就剩下加密代码了。有了 delta 和 tea_decrypt 的实现后很容易做:
    uint64_t tea_encrypt(uint32_t* buffer, uint32_t* tea_key, uint32_t delta)
    {
        uint32_t y = buffer[0];
        uint32_t z = buffer[1];
        uint32_t sum = 0;
        for (int i = 0; i
    此时我们可以得出 CM 的流程:
  • (1) 用户输入 UID 和密钥 →
  • (2) 密钥被变形/解码 →
  • (3) TEA 解密

    其中解密后的内容必须与预设内容相同。
    因此重新观察 VerifyKey 这个函数,发现并没有对加密内容的缓冲区进行额外的操作;回到之前的事件分发函数,可以看到 HexStringToBlocks 有两个参数,分别是用户输入的内容与我们的缓冲区。
    进去分析,发现就是很普通的十六进制转 u32 数组,没有动过手脚。注意一下大小端以及不能有额外的字符就好。
    void __fastcall HexStringToBlocks(const wchar_t *src_hex, uint32_t *dst_bin)
    {
      wchar_t backup; // [rsp+20h] [rbp-28h]
      int src_idx; // [rsp+24h] [rbp-24h]
      int dst_idx; // [rsp+28h] [rbp-20h]
      LARGE_INT_FLAG ptr_check; // [rsp+2Ch] [rbp-1Ch]
      ptr_check.as_u32.lo = src_hex == nullptr;
      ptr_check.as_u32.hi = dst_bin == nullptr;
      // 两个参数不能是空指针,且 key 的长度必须是 8 的倍数
      if ( ptr_check.as_u64 == 0 && (wcslen(src_hex) % 8) == 0 )
      {
        src_idx = 0;
        dst_idx = 0;
        while ( src_hex[src_idx] )                  // 是否结束
        {
          backup = src_hex[src_idx + 8];
          src_hex[src_idx + 8] = 0;                 // 将 8 字符后的内容改为结束符字符串结束符
          dst_bin[dst_idx++] = Util::HexToU32(&src_hex[src_idx]);
          src_idx += 8;
          src_hex[src_idx] ^= backup;               // 还原刚才记录的值
        }
      }
    }
    输入范例 L"12345678",得到 0x12345678,内存中显示为 78-56-34-12。
    此时已经可以针对自己的 UID 算一个密钥出来了。但是没有完全实现这个加密算法好像不太好,因此还是继续分析一下 VerifyKey 这个函数吧。
    将无关 tea_delta 的代码剔除掉,可以发现计算起来很简单:
      tea_delta = 0x11111111;
      for ( i = 0; i > 31) == 0) {
        tea_delta = tea_delta * 2 + 9;
      }
    于是改写一番:
    uint32_t uid_to_delta(uint32_t uid) {
        uint32_t delta = uid + uint32_t{ 0x11111111 } *15;
        while ((delta >> 31) == 0) { // 最高位为 1 时停止
            delta = 2 * delta + 9;
        }
        return delta;
    }
    而 tea_key 的密钥则是紧随 delta 值的初始化下方:
      for ( i = 0; i
    这个就没什么好说的了,直接照抄就行。
    最后的 tea_sum 参数在加密的时一般是 0,姑且不管它。
    最后就是完整的算法注册机:
    // main.cpp
    #include "tea.h"
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    // 填写从调试器得到的值用于检查
    constexpr uint32_t expected_delta = 0xABE63FF7;
    constexpr uint32_t expected_sum = 0x7CC7FEE0;
    union FlagMagic {
        char as_str[33]; // 32 个字符 + 结束符
        uint32_t as_u32[8];
    };
    int main() {
        uint32_t tea_delta = uid_to_delta(176017); // 我的 UID :D
        std::cout
    虽然只是重复,但是为了代码的完整性还是提供吧:
    // tea.h
    #pragma once
    #include
    uint32_t uid_to_delta(uint32_t uid);
    uint64_t tea_decrypt(uint32_t* buf, uint32_t* tea_key, uint32_t delta, uint32_t sum);
    uint64_t tea_encrypt(uint32_t* out_buf, uint32_t* key, uint32_t delta);
    // tea.cpp
    #include "tea.h"
    uint32_t uid_to_delta(uint32_t uid) {
        uint32_t delta = uid + uint32_t{ 0x11111111 } *15;
        while ((delta >> 31) == 0) { // 最高位为 1 时停止
            delta = 2 * delta + 9;
        }
        return delta;
    }
    constexpr int TEA_ROUND = 32; // 这个值一般是 16
    inline uint32_t single_round_tea(uint32_t value, uint32_t sum, uint32_t key1, uint32_t key2) {
        return ((value > 5) + key2);
    }
    uint64_t tea_decrypt(uint32_t* buffer, uint32_t* tea_key, uint32_t delta, uint32_t sum)
    {
        uint32_t y = buffer[0];
        uint32_t z = buffer[1];
        for (int i = 0; i

    函数, 字符串

  • amin1994   

    吾爱破解 - 2023 春节解题领红包
    [ol]
  • Windows 题(2、5)
  • 安卓题(3、4、6、7) - 其中 7 挑战失败 ← 你在这里
  • Web 题 - 缺少 4、8、9、12。
    [/ol]
    【春节】解题领红包之三 {Android  初级题}
    作为 zip 压缩包打开看看,没有发现 so 文件。直接拉到 JEB 分析。
    然后用 JEB 一打开就发现解密部分的表达式已经被静态优化了:
    if(this$0.check() == 999) {
        Toast.makeText(v4, "快去论坛领CB吧!", 1).show();
        key.setText("flag{zhudajiaxinniankuaile}");
    }
    捡了个漏。
    如果硬要分析的话,就得看 smali 代码了:
    0000005E  const/4             p2, 2
    00000060  const-string        v0, "hnci}|jwfclkczkppkcpmwckng\u007F"
    00000064  invoke-virtual      MainActivity->decrypt(String, I)String, p0, v0, p2
    传参分别是这个字符串和 p2,也就是固定的常数 2。
    继续分析解密函数,关键点就是这个 for 循环:
    for(i = 0; i
    每个字符 -2,放到 JS 里也是轻松解密:
    "hnci}|jwfclkczkppkcpmwckng\u007F".split('').map(x => String.fromCharCode(x.charCodeAt() - 2)).join('')
    得到同样的过关密码:flag{zhudajiaxinniankuaile}
    【春节】解题领红包之四 {Android 初级题}
    JEB 打开,直接跳到 MainActivity 代码。
    可以看到顶部有一个签名验证,但是我们是静态分析,无视即可。
    往下翻,找到关键函数:
    private static final void onCreate$lambda-0(MainActivity this$0, View arg4) {
        // ... 算法无关代码 ...
        String uid = this$0.edit_uid.getText().trim();
        if( Flag.INSTANCE.check( uid, this$0.edit_flag.getText().trim() ) ) {
            Toast.makeText( ((Context)this$0), "恭喜你,flag正确!", 1 ).show();
        } else {
            Toast.makeText( ((Context)this$0), "flag错误哦,再想想!", 1 ).show();
        }
    }
    上面的代码中,我已经对部分混淆过的类名进行了分析。对应类名重更名如下:
    A -> Flag
    B -> Encoder
    C -> Crypto
    分析基本上没怎么做,因为看代码用到了 MD5,浏览器 JS 跑起来要第三方依赖,还是用 Java 抠代码写注册机简单些。
    因为原 APK 用的 Kotlin 加了有很多安全检查代码进去,抠出来后再整理下就是下面这样了:
    package cn.lcg.flyingcat;
    import java.nio.charset.StandardCharsets;
    import java.security.MessageDigest;
    import java.security.NoSuchAlgorithmException;
    import java.util.Base64;
    class Crypto {
        private static char cipher(char c, int delta) {
            char base = 0;
            if (c >= 'A' && c = 'a' && c = 0; i--) {
                key ^= (50 ^ 53); // 切换密钥
                result = (char) (src.charAt(i) ^ key);
            }
            return new String(result);
        }
    }
    public class Main {
        public static void main(String[] args) throws Exception {
            String uid = "176017"; // uid
            var flag = Crypto.encode(uid + "Wuaipojie2023").getBytes(StandardCharsets.UTF_8);
            var result = Crypto.cipher(Crypto.md5(Crypto.encodeBase64(flag)), 5);
            // result == "4k65807686gg2k149h4k338211hi8643"
            System.out.println("flag{" + result + "}");
        }
    }
    得到过关密码 flag{4k65807686gg2k149h4k338211hi8643}。
    【春节】解题领红包之六 {Android 中级题}
    此题感想:

    谜语人滚啊!

    JEB 打开 APK,没发现什么东西。有三个 Native 函数,但是并没有调用。
    然后有一个函数会判断麦克风音量,根据分贝(?)等级做不同的事情,其中一个情况是写出 aes.png:
    private final void Check_Volume(double vol) {
        // 无关代码跳过
        int showHint = 0;
        if(100
    IDA 打开,没有混淆,轻松定位到对应的三个函数 - encrypt、decrypt 和 get_RealKey。
    注:推荐逆向 arm / arm64 的 so 文件,因为自带了 JNI 的结构信息,不需要自己导入。
    首先看 get_RealKey,意义不明的一个函数:
    BOOL __fastcall get_RealKey(JNIEnv *env, int a2, int a3) {
      char *key = (char *)(*env)->GetStringUTFChars(env, a3, 0);
      if ( strlen(key) == 16 ) { // 输入必须是 16 位
        char add_mask[16]; // 0xFE, 0xFB, ... 重复 ...
        *(_QWORD *)add_mask = 0xFEFBFEFBFEFBFEFBLL;
        *(_QWORD *)&add_mask[8] = 0xFEFBFEFBFEFBFEFBLL;
        // 两个 128 位的数字相加
        *key = vaddq_s8(*(int8x16_t *)key, *(int8x16_t *)add_mask);
        return strcmp(key, "thisiskey") != 0;
      }
      return 0;
    }
    继续看解密:
    jstring /*省略*/_MainActivity_decrypt(JNIEnv *env, int a2, jstring a3)
    {
      char *c_str_input = (*env)->GetStringUTFChars(env, a3, 0);
      char *c_str_result = j_AES_ECB_PKCS7_Decrypt(c_str_input, "|wfkuqokj4548366");
      (*env)->ReleaseStringUTFChars(env, a3, c_str_input);
      return (*env)->NewStringUTF(env, c_str_result);
    }
    进去 j_AES_ECB_PKCS7_Decrypt 和 AES_ECB_PKCS7_Decrypt 看了下,就是 Base64 解码然后进行解密。
    后面的 AES 部分看不懂,但是问题不大。
    尝试在 CyberChef 的流程添加了「From Base64(Base64 解码)」和「AES Decrypt(AES 解密)」,填入密钥,选择 ECB 模式,提示无法解密。
    冷静一会,发现这个长度是 16,刚好和 get_RealKey 的要求一致,于是把代码抠出来试试:
    #include
    int main() {
        char key[] = "|wfkuqokj4548366";
        char add_mask[] = { 0xFB, 0xFE };
        for (int i = 0; i
    得到新的密钥 wuaipojie2023114,看起来像是走对方向了。
    填入正确的密钥,发现能正常解密出来内容,添加一个「From Hex(十六进制解码)」+「To Hexdump」过程,可以看到 PNG 头部信息:
    00000000  89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52  |.PNG........IHDR|
    00000010  00 00 02 e2 00 00 02 e2 04 03 00 00 00 6e cd ae  |...â...â.....nÍ®|
    00000020  0c 00 00 00 04 67 41 4d 41 00 00 b1 8f 0b fc 61  |.....gAMA..±..üa|
    把解密内容保存下来打开,得到一枚嘲讽表情。
    继续上网爬文看看 PNG 怎么获得隐写内容,得到 zsteg 工具一枚。起一个 Kali 虚拟机,安装这个工具后得到信息:
    $ zsteg aes.png
    [?] 994 bytes of extra data after image end (IEND), offset = 0xb712
    extradata:0         .. file: PNG image data, 100 x 100, 8-bit/color RGBA, non-interlaced
        00000000: 89 50 4e 47 0d 0a 1a 0a  00 00 00 0d 49 48 44 52  |.PNG........IHDR|
        00000010: 00 00 00 64 00 00 00 64  08 06 00 00 00 70 e2 95  |...d...d.....p..|
        ... 省略 ...
    报告说在图片结尾处有另一张 PNG 图片在文件偏移 0xb712 (46866) 处。于是将「To Hexdump」过程禁用,添加新的「Drop bytes(删除字节)」过程,将前 46866 个字节剔除;再根据提示分别添加「Render Image(渲染图片)」、「Parse QR Code(解析 QR 二维码)」,最终得到过关密码 flag{Happy_New_Year_Wuaipojie2023}。
    你也可以直接打开这个解密流程,粘贴 aes.png 内容得到同样的结果。


    aes_png.png (48.91 KB, 下载次数: 0)
    下载附件
    2023-2-6 05:54 上传

    unidbg 模拟
    尝试了一下这玩意,但是只能解出前 160 个字节。不清楚是谜语人 SO 只解密前 160 字节还是哪里调用出毛病了。
    package com.bytedance.frameworks.core.encrypt;
    import com.alibaba.fastjson.util.IOUtils;
    import com.github.unidbg.AndroidEmulator;
    import com.github.unidbg.Module;
    import com.github.unidbg.Symbol;
    import com.github.unidbg.arm.backend.Unicorn2Factory;
    import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
    import com.github.unidbg.linux.android.AndroidResolver;
    import com.github.unidbg.linux.android.dvm.DalvikModule;
    import com.github.unidbg.linux.android.dvm.VM;
    import com.github.unidbg.memory.Memory;
    import com.github.unidbg.memory.MemoryBlock;
    import com.github.unidbg.utils.Inspector;
    import org.apache.commons.io.FileUtils;
    import java.io.File;
    import java.nio.charset.StandardCharsets;
    public class lcg_2023_spring {
        private final AndroidEmulator emulator;
        private final VM vm;
        private final Module module;
        private final boolean logging;
        private final Memory memory;
        lcg_2023_spring(boolean logging) {
            this.logging = logging;
            emulator = AndroidEmulatorBuilder.for32Bit()
                    .setProcessName("com.zj.wuaipojie2023_2")
                    .addBackendFactory(new Unicorn2Factory(true))
                    .build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
            memory = emulator.getMemory(); // 模拟器的内存操作接口
            memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析
            vm = emulator.createDalvikVM(); // 创建Android虚拟机
            vm.setVerbose(logging); // 设置是否打印Jni调用细节
            DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/example_binaries/armeabi-v7a/lib52pj.so"), false); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
            dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数
            module = dm.getModule(); // 加载好的 so 对应一个模块
        }
        void destroy() {
            IOUtils.close(emulator);
        }
        void DoWork() throws Exception {
            Symbol AES_ECB_PKCS7_DecryptSym = module.findSymbolByName("AES_ECB_PKCS7_Decrypt");
            File file = new File("unidbg-android/src/test/resources/aes.png.txt");
            byte[] payload_utf8 = FileUtils.readFileToString(file, "UTF-8").trim().getBytes(StandardCharsets.UTF_8);
            MemoryBlock payload = memory.malloc(payload_utf8.length + 1, false);
            payload.getPointer().write(payload_utf8);
            MemoryBlock key = memory.malloc(17, false);
            key.getPointer().write("wuaipojie2023114".getBytes(StandardCharsets.UTF_8));
            Number ret = AES_ECB_PKCS7_DecryptSym.call(emulator, payload.getPointer(), key.getPointer());
            byte[] result = emulator.getBackend().mem_read(ret.longValue(), 0x100);
            Inspector.inspect(result, "result (ECB)");
            payload.free();
            key.free();
        }
        public static void main(String[] args) throws Exception {
            lcg_2023_spring test = new lcg_2023_spring(true);
            test.DoWork();
            test.destroy();
        }
    }
    【春节】解题领红包之七 {Android 高级题}
    混淆太厉害了,不太懂如何对抗。过两天偷学一手别人的 Writeup 把混淆部分去掉后再分析吧。当初整了几个钟头后摆烂了。
    试了下跟踪生成 log 来自动填充调用 + 去花 + 修正逻辑跳转,但是去花脚本写得太菜了,不少正常的代码也被干掉了。
    去花脚本:
    import idc
    import ida_bytes
    def patch_bytes(ea, data):
        for i in range(len(data)):
            ida_bytes.patch_byte(ea + i, data)
    INST_NOP = [0x1F, 0x20, 0x03, 0xD5]
    def nop_factory(count):
        nop = INST_NOP * count
        return lambda ea: patch_bytes(ea, nop)
    def remove_junk_all_inst(action, pattern):
        text_beg = 0x0000_D5C0
        text_end = 0x0004_D21C
        cur_addr = text_beg
        while cur_addr  4E
    # .text:0000000000012898 C8 02 40 B9                             LDR             W8, [X22]
    # .text:000000000001289C 1F 29 00 71                             CMP             W8, #0xA
    # .text:00000000000128A0 AB 00 00 54                             B.LT            loc_128B4
    # .text:0000000000012E20 A8 02 40 B9                             LDR             W8, [X21]
    # .text:0000000000012E24 1F 29 00 71                             CMP             W8, #0xA
    # .text:0000000000012E28 AB 00 00 54                             B.LT            loc_12E3C
    # .text:0000000000010080 08 01 40 B9                             LDR             W8, [X8]
    # .text:0000000000010084 1F 29 00 71                             CMP             W8, #0xA
    # .text:0000000000010088 EB 00 00 54                             B.LT            loc_100A4
    remove_junk_all_inst(blt_to_bal_factory(8),
                         '?? ?? 40 B9 1F 29 00 71 ?? ?? 00 54')
    # .text:0000000000010104 3F 29 00 71                             CMP             W9, #0xA
    # .text:0000000000010108 08 01 40 B9                             LDR             W8, [X8]
    # .text:000000000001010C 8B 00 00 54                             B.LT            loc_1011C
    remove_junk_all_inst(blt_to_bal_factory(8),
                         '3F 29 00 71 08 01 40 B9 8B 00 00 54')
    # .text:0000000000012B94 88 02 40 B9                             LDR             W8, [X20]
    # .text:0000000000012B98 B5 86 45 F9                             LDR             X21, [X21,#Oo0O.123_ptr@PAGEOFF]
    # .text:0000000000012B9C 1F 29 00 71                             CMP             W8, #0xA
    # .text:0000000000012BA0 AB 00 00 54                             B.LT            loc_12BB4
    remove_junk_all_inst(blt_to_bal_factory(12),
                         '?? 02 40 B9 ?? ?? ?? ?? ?? 29 00 71 ?? ?? 00 54')
    # .text:00000000000158E8 1F 29 00 71                             CMP             W8, #0xA
    # .text:00000000000158EC AB F1 FF 54                             B.LT            loc_15720
    # .text:00000000000158F0 2B 05 00 51                             SUB             W11, W9, #1
    # .text:00000000000158F4 6B 7D 09 1B                             MUL             W11, W11, W9
    # .text:00000000000158F8 4B F1 07 36                             TBZ             W11, #0, loc_15720
    remove_junk_all_inst(
        blt_to_bal_factory(4),
        '?? 29 00 71 ?? ?? ?? 54 ?? 05 00 51 ?? ?? ?? 1B ?? ?? ?? 36')
    # .text:00000000000157F4 1F 29 00 71                             CMP             W8, #0xA
    # .text:00000000000157F8 4A 01 0B 0B                             ADD             W10, W10, W11
    # .text:00000000000157FC 6B 07 00 54                             B.LT            loc_158E8
    # .text:0000000000015800 2B 05 00 51                             SUB             W11, W9, #1
    # .text:0000000000015804 6B 7D 09 1B                             MUL             W11, W11, W9
    # .text:0000000000015808 0B 07 00 36                             TBZ             W11, #0, loc_158E8
    remove_junk_all_inst(
        blt_to_bal_factory(8),
        '?? 29 00 71 ?? ?? ?? ?? ?? ?? ?? 54 ?? 05 00 51 ?? ?? ?? 1B ?? ?? ?? 36')
    # .text:00000000000159C8 3F 29 00 71                             CMP             W9, #0xA
    # .text:00000000000159CC AB 00 00 54                             B.LT            loc_159E0
    # .text:00000000000159D0 C9 02 40 B9                             LDR             W9, [X22]
    # .text:00000000000159D4 2A 05 00 51                             SUB             W10, W9, #1
    # .text:00000000000159D8 49 7D 09 1B                             MUL             W9, W10, W9
    # .text:00000000000159DC C9 FC 07 37                             TBNZ            W9, #0, loc_15974
    remove_junk_all_inst(
        blt_to_bal_factory(4),
        '?? 29 00 71 ?? ?? ?? 54 ?? ?? ?? ?? ?? 05 00 51 ?? ?? ?? 1B ?? ?? ?? 36')
    条件跳转修复(半成品,写的很粗糙):
    from typing import Callable
    import re
    import idc
    import ida_bytes
    from struct import pack
    verbose = False
    def istn_name(ea):
        return print_insn_mnem(ea)
    def istn_operand(ea, pos):
        return print_operand(ea, pos)
    def patch_bytes(ea, data):
        for i in range(len(data)):
            ida_bytes.patch_byte(ea + i, data)
    def normalize_addr(addr):
        return addr & 0xFFFF_FFFF
    def get_dword(addr):
        return normalize_addr(ida_bytes.get_dword(normalize_addr(addr)))
    def get_register_without_prefix(reg_name):
        m = rMatchRegister.fullmatch(reg_name)
        if m == None:
            if verbose:
                print(f"WARN: failed to match reg: {reg_name}")
            return -1
        if m.group(1) == 'ZR':
            return SPECIAL_REGISTER_ZERO
        return int(m.group(1))
    def parse_ida_int(value: str):
        if value == '0': return 0
        if value.startswith('0x'):
            return int(value[2:], 16)
        if value.startswith('0'):
            return int(value[1:], 8)  # should be rare?
        return int(value)
    class InstParseException(Exception):
        pass
    SPECIAL_REGISTER_ZERO = 100
    rMatchRegister = re.compile(r'[XW](\d+|ZR)')
    rOperandIndirectRegImm = re.compile(r'\[(X\d+|ZR),#?(-?0x[\da-fA-F]+|\d+)\]')
    rOperandIndirectRegReg = re.compile(r'\[(X\d+|ZR),(X\d+|ZR)\]')
    rOperandIndirectRegX = re.compile(r'\[(X\d+|ZR)\]')
    rOperandRegX = re.compile(r'X(\d+|ZR)')
    rOperandRegW = re.compile(r'W(\d+|ZR)')
    rOperandImm = re.compile(r'#?(-?0x[\da-fA-F]+|\d+)')
    rOperandWithLSL16 = re.compile(r'#?(-?0x[\da-fA-F]+|\d+),LSL#16')
    INST_NOP = [0x1F, 0x20, 0x03, 0xD5]
    # https://developer.arm.com/documentation/ddi0406/c/Application-Level-Architecture/ARM-Instruction-Set-Encoding/ARM-instruction-set-encoding
    condition_encode = {
        'EQ': 0b0000,
        'NE': 0b0001,
        'CS': 0b0010,
        'CC': 0b0011,
        'MI': 0b0100,
        'PL': 0b0101,
        'VS': 0b0110,
        'VC': 0b0111,
        'HI': 0b1000,
        'LS': 0b1001,
        'GE': 0b1010,
        'LT': 0b1011,
        'GT': 0b1100,
        'LE': 0b1101,
        'AL': 0b1110,  # unconditional
    }
    def encode_jump(curr_addr, jump_addr, condition=''):
        # .text:000000000000E8FC 60 02 00 54                             B.EQ            loc_E948
        # .text:000000000000E900 04 00 00 14                             B               loc_E910
        # .text:000000000000E904                         ; ---------------------------------------------------------------------------
        # .text:000000000000E904 1F 20 03 D5                             NOP
        # .text:000000000000E908 1F 20 03 D5                             NOP
        # .text:000000000000E90C 1F 20 03 D5                             NOP                     ; loc_E910
        delta = jump_addr - curr_addr
        if delta > 2
            delta &= 0x03_FF_FF_FF
        else:
            opcode |= 0b0101_01
            delta = delta  {value}')
                return value
            curr_addr = ea
            max_look_back = max(curr_addr - 1000 * 4, self.text_start)
            while curr_addr >= max_look_back:
                curr_addr -= 4
                name = istn_name(curr_addr)
                if name == '' or name[0] == 'B': continue
                if name == 'LDP':
                    curr_reg = get_register_without_prefix(
                        istn_operand(curr_addr, 1))
                    if curr_reg == target_reg:
                        result = self.resolve_istn_value(curr_addr,
                                                         descend_max,
                                                         ldp_offset=1)
                        return result
                curr_reg = get_register_without_prefix(istn_operand(curr_addr, 0))
                if curr_reg == target_reg:
                    if verbose:
                        print(f'found assignment to {reg_name}'
                              f' in {hex(curr_addr)}')
                    result = self.resolve_istn_value(curr_addr, descend_max)
                    if verbose:
                        print(f'{hex(curr_addr)}: '
                              f'{reg_name} resolved to {hex(result)}')
                    return result
            raise InstParseException(
                f'could not find assignment to {reg_name}: {hex(ea)}')
        def find_prev_cmp(self, ea):
            curr_addr = ea
            max_look_back = max(curr_addr - 1000 * 4, self.text_start)
            while curr_addr >= max_look_back:
                curr_addr -= 4
                if istn_name(curr_addr) == 'CMP':
                    return [
                        curr_addr,
                        istn_operand(curr_addr, 0),
                        istn_operand(curr_addr, 1),
                    ]
            print(f'CMP inst not found :/')
        def find_next_matching_istn(self,
                                    ea,
                                    filter: Callable[[int], bool],
                                    max_itsn_distance: int = 1000):
            max_look_ahead = min(ea + max_itsn_distance * 4, self.text_end)
            for addr in range(ea + 4, max_look_ahead, 4):
                # print(f'checking {hex(addr)}: {istn_name(addr)}')
                if filter(addr):
                    return addr
            raise InstParseException(f'could not find expected instruction')
        def analysis_csel_br(self, ea):
            # Search for BR instruction
            next_br = self.find_next_matching_istn(
                ea, lambda addr: istn_name(addr) == 'BR', 40)
            name = istn_name(ea)
            if name == 'CSET':
                reg_cond_name = istn_operand(ea, 1)
                self.cset_mapping[ea] = 1
                addr_when_take = normalize_addr(self.resolve_operand(next_br, 0))
                self.cset_mapping[ea] = 0
                addr_when_miss = normalize_addr(self.resolve_operand(next_br, 0))
            elif name == 'CSEL':
                reg_cond_name = istn_operand(ea, 3)
                self.csel_mapping[ea] = 1
                addr_when_take = normalize_addr(self.resolve_operand(next_br, 0))
                self.csel_mapping[ea] = 2
                addr_when_miss = normalize_addr(self.resolve_operand(next_br, 0))
            else:
                raise InstParseException('unsupported instruction')
            print('-' * 30)
            print(f'{hex(ea + 0)} B.{reg_cond_name} {hex(addr_when_take)}'
                  f' -- {encode_jump(ea + 0, addr_when_take, reg_cond_name)}')
            print(f'{hex(ea + 4)} B {hex(addr_when_miss)}'
                  f' -- {encode_jump(ea + 4, addr_when_miss)}')
            return [reg_cond_name, addr_when_take, addr_when_miss]
        def patch_csel_br(self, ea):
            result = self.analysis_csel_br(ea)
            [reg_cond_name, addr_when_take, addr_when_miss] = result
            encoded_jump = encode_jump(ea + 0, addr_when_take, reg_cond_name)
            patch_bytes(ea + 0, encoded_jump)
            encoded_jump = encode_jump(ea + 4, addr_when_miss)
            patch_bytes(ea + 4, encoded_jump)
            print(f'... patched')
            print('-' * 30)
            return result
    # .text        000000000000D5C0        000000000004D21C        R        .        X        .        L        dword        06        public        CODE        64        00        0F
    def analysis_csel_at(ea=None, patch=False, register_override=None):
        if ea == None: ea = idc.get_screen_ea()
        fixer = ConditionFixer(0x000000000000D5C0,
                               0x000000000004D21C,
                               register_override=register_override)
        if patch: return fixer.patch_csel_br(ea)
        else: return fixer.analysis_csel_br(ea)
    def analysis_csel_in_function(ea=None, patch=False):
        if ea == None: ea = idc.get_screen_ea()
        search_beg = idc.get_func_attr(ea, idc.FUNCATTR_START)
        search_end = idc.get_func_attr(ea, idc.FUNCATTR_END)
        result = []
        for addr in range(search_beg, search_end, 4):
            if istn_name(addr) == 'CSEL':
                result.append(analysis_csel_at(addr, patch))
        return result
    def analysis_resolve_call_fn(ea=None):
        if ea == None: ea = idc.get_screen_ea()
        fixer = ConditionFixer(0x000000000000D5C0, 0x000000000004D21C)
        addr = fixer.resolve_operand(ea, 0)
        print(f'param1 resolved at: {hex(addr)}')
    def main(analysis_only=True):
        print(f' {"-" * 30} begin new session {"-" * 30}')
        text_start = 0x000000000000D5C0
        text_end = 0x000000000004D21C
        ok = 0
        fail = 0
        ea = text_start
        while ea  10:
                #     print('terminated: too many failure.')
                #     break
        print(f'ok({ok}) fail({fail})')
    # main(False)
    # ConditionFixer(0x000000000000D5C0, 0x000000000004D21C).analysis_csel_br(0xE8FC)
    上面这个脚本在处理数据的时候有毛病,要手动到 CSEL ... 这样的语句处手动执行,看分析的地址对不对。
    例如这一段:
    .text:000000000001ADE0 49 72 41 F9                             LDR             X9, [X18,#0x2E0]
    .text:000000000001ADE4 1F 01 0F 6B                             CMP             W8, W15
    .text:000000000001ADE8 2B B2 90 9A                             CSEL            X11, X17, X16, LT
    .text:000000000001ADEC 29 01 00 8B                             ADD             X9, X9, X0
    .text:000000000001ADF0 2B 69 6B F8                             LDR             X11, [X9,X11]
    .text:000000000001ADF4 6B 01 01 8B                             ADD             X11, X11, X1
    .text:000000000001ADF8 60 01 1F D6                             BR              X11
    选中 1ADE8 后在 IDAPython 控制台输入 analysis_csel_at() 进行分析:
    Python>analysis_csel_at()
    ------------------------------
    0x1ade8 B.LT 0x1adfc -- b'\xab\x00\x00T'
    0x1adec B 0x1aea0 -- b'-\x00\x00\x14'
    ['LT', 0x1adfc, 0x1aea0]
    确认分析正确,加上 patch=True 参数进行自动补丁:
    Python>analysis_csel_at(patch=True)
    自动补丁后:
    text:000000000001ADE0 49 72 41 F9                             LDR             X9, [X18,#0x2E0]
    .text:000000000001ADE4 1F 01 0F 6B                             CMP             W8, W15
    .text:000000000001ADE8 AB 00 00 54                             B.LT            loc_1ADFC
    .text:000000000001ADEC 2D 00 00 14                             B               loc_1AEA0
    .text:000000000001ADF0 2B 69 6B F8                             LDR             X11, [X9,X11]
    .text:000000000001ADF4 6B 01 01 8B                             ADD             X11, X11, X1
    .text:000000000001ADF8 60 01 1F D6                             BR              X11
    然后按下 alt-p 让 IDA 重新分析该函数。
    缺点就是,效率很低;遇到非常数形式的指令也不懂(如 .text:1AE34   ADRP X8, #off_6A2F0@PAGE)。也不懂得做优化/分析,只能单纯的向上找。
    之前顶着混淆分析,结果发现分析的是一堆类似 Vector 数据类型相关的函数… 吐血。
    checkSN 函数的大概逻辑:
    BOOL __fastcall checkSN(JNIEnv *env, __int64 a2, jstring jstr_uid, void *jstr_flag)
    {
      bool check_ok; // w19
      const char *str_uid; // x21
      const char *str_flag; // x23
      uint64_t flag_len; // x0
      __int64 v12; // x0
      uint8_t *DataPointer_0; // x19
      uint64_t Length_0; // x0
      uint8_t *p_flag_data; // x19
      __int64 p_flag_len; // x0
      uint8_t *ptr_s1; // x20
      uint8_t *ptr_s2; // x0
      unsigned __int64 flag_len_1; // x0
      int v20; // w0
      int v21; // w20
      uint8_t *expected_hash; // x19
      unsigned __int64 Length_1; // x0
      char v24[8]; // [xsp+8h] [xbp-578h] BYREF
      Vector s2; // [xsp+10h] [xbp-570h] BYREF
      Vector s1; // [xsp+28h] [xbp-558h] BYREF
      Vector vec_uid_dup; // [xsp+40h] [xbp-540h] BYREF
      Vector v28; // [xsp+58h] [xbp-528h] BYREF
      Vector vec_flag; // [xsp+70h] [xbp-510h] BYREF
      Vector vec_uid; // [xsp+88h] [xbp-4F8h] BYREF
      int a1; // [xsp+A4h] [xbp-4DCh] BYREF
      Vector actual_hash; // [xsp+A8h] [xbp-4D8h] BYREF
      char v33[88]; // [xsp+4C8h] [xbp-B8h] BYREF
      __int64 v34; // [xsp+520h] [xbp-60h]
      v34 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
      if ( JNI::GetStringUTFLength(env, jstr_flag) == 44 && JNI::GetStringUTFLength(env, jstr_uid) == 8 )
      {
        str_uid = JNI::GetStringUTFChars(env, jstr_uid, 0LL);
        Vector::InitFromString(&vec_uid, str_uid);
        str_flag = (*env)->GetStringUTFChars(env, jstr_flag, 0LL);
        Vector::Reset2(&vec_flag);
        flag_len = strlen_0(str_flag);
        Vector::AddData(&vec_flag, (uint8_t *)str_flag, flag_len);
        (*env)->ReleaseStringUTFChars(env, jstr_flag, str_flag);
        (*env)->ReleaseStringUTFChars(env, jstr_uid, str_uid);
        Vector::Copy(&vec_uid_dup, &vec_uid);
        sub_1B32C(&vec_uid_dup);
        Vector::SafeFree(&vec_uid_dup);
        // Initialize with data from a table?
        Vector::Reset2(&s1);
        Vector::AddData(&s1, byte_4D220, 10uLL);
        Vector::Reset2(&s2);
        Vector::AddData(&s2, &byte_4D220[11], 0x10uLL);
        a1 = 0x69AB81DE;
        v12 = sub_10214(&a1);                       // 时间相关
        sub_1F588(v12 / 20000000, (__int64)&actual_hash);
        DataPointer_0 = Vector::GetDataPointer_0(&actual_hash);
        Length_0 = Vector::GetLength_0(&actual_hash);
        sub_13318(&s1, DataPointer_0, Length_0);
        Vector::SafeFree(&actual_hash);
        p_flag_data = Vector::GetDataPointer_1(&vec_flag);
        p_flag_len = Vector::GetLength_1(&vec_flag);
        if ( sub_1B788((__int64)p_flag_data, p_flag_len) )
        {
          ptr_s1 = Vector::GetDataPointer_1(&s1);
          ptr_s2 = Vector::GetDataPointer_1(&s2);
          if ( sub_165F0((uint64_t)v33, (__int64)ptr_s1, (__int64)ptr_s2) != 1 )
          {
            check_ok = 1;
    LB_CHECK_COMPLETE:
            Vector::SafeFree(&s2);
            Vector::SafeFree(&s1);
            Vector::SafeFree(&v28);
            Vector::SafeFree(&vec_flag);
            Vector::SafeFree(&vec_uid);
            return check_ok;
          }
          memset(&actual_hash, 0, 0x420u);
          flag_len_1 = strlen((const char *)p_flag_data);
          sub_168E4((__int64)v33, p_flag_data, flag_len_1, (__int64)&actual_hash, v24);
          v21 = v20;
          free(p_flag_data);
          if ( v21 == 1 )
          {
            expected_hash = Vector::GetDataPointer_1(&v28);
            Length_1 = Vector::GetLength_1(&v28);
            a1 = 0x37FA57CD;
            check_ok = sub_104C8(&a1, (uint8_t *)&actual_hash, expected_hash, Length_1) == 0;
            if ( sub_173B8() == 1 )
              goto LB_CHECK_COMPLETE;
          }
        }
        check_ok = 0;
        goto LB_CHECK_COMPLETE;
      }
      return 0;
    }
    最坑的是里面还有状态机打乱执行流程。去掉混淆后这个倒还也能看,加点注释就好。
  • lxn13393617553   

    吾爱破解 - 2023 春节解题领红包
    导航:
    [ol]
  • Windows 题(2、5)
  • 安卓题(3、4、6、7) - 其中 7 挑战失败
  • Web 题 - 缺少 4、8、9、12。← 你在这里
    [/ol]
    web 题目
    感想:

    谜语人滚啊

    第 4、8、9、12 问藏的 flag 没找到。
    第 1 个
    视频里写了,flag1{52pojiehappynewyear}。
    第 2 个
    视频里 20 秒左右的二维码,截图后放到 PS 内加个黑白通道过滤来加强画质,方便扫描。


    threshold_filter.png (29.22 KB, 下载次数: 0)
    下载附件
    2023-2-6 04:57 上传

    得到一串地址,地址结尾是 flag2{878a48f2}。
    第 3 个
    25 秒左右右下角名字变了,显示的是 iodj3{06i95dig}。
    看起来数字和符号没变但是字母变了。
    推测 iodj 代表 flag,得到密码表:
         abcdefghijklmnopqrstuvwxyz
         xyzabcdefghijklmnopqrstuvw
    最后得到密码 flag3{06f95afd}。
    第 5 个
    30 秒左右处有音频形式的摩斯码,把视频用 Audacity 之类的音频分析工具打开后分析比较容易听。
         ..-.           F
         .-..           L
         .-             A
         --.            G
         .....          5
                   {
         .              E
         .-             A
         ..             I
         -              T
                   }
    得到 flag5{eait}。
    第 6 个
    视频开始时给了提示,是电话号码的声音。
    这个我不懂,给 Audacity 装了个插件自动识别的。
    插件 rjh-dtmfdec.ny,地址: https://forum.audacityteam.org/viewtopic.php?t=79168#p245364
    选中区域后点击 Analyze → DTMF Decoder,确认使用默认设定,得到结果 590124,即 flag6{590124}。
    第 7 个
    从这个问题开始(应该),对抗到了 2023challenge.52pojie.cn 这个域名下。
    打开首页查看源码,得到两个 flag 提示:
            const FLAG_LINE_A = '|01 1 001 1 001 1 01 1 0001 1 00001 01 1 001 1 1 001 1 0111 011 1 101100 1 1 0 10 1 011 0 01 0000 1 10000 001 1 01 1 0 011 0 00 10 011 0 010 100 1 1011 000 1 1 0 0 11 01111101==========|'; // 这里面藏着两个 flag 哦~
            const FLAG_LINE_B = '|++++++++++[>++++++++++>++++++++++>+++++>++++++++++++++.++++++.>---.>-..>+++.-.>++.|';
            const FLAG_LINE_A2 = FLAG_LINE_A.replaceAll(' ', '');
            const FLAG_LINE_A_PARTS = Array.from(FLAG_LINE_A.matchAll(/. ?/g));
    其中 7 是将 FLAG_LINE_A 的字符串只保留 0 和 1,然后 8 位一组合并起来转 16 进制,然后转文字,得到 flag7{5d06be63}。
    第 10 个
    这个是瞎蒙出来的。
    将 FLAG_LINE_A_PARTS 的值转换,可以得到一堆长度是 1 和 2 的数组内容。
    因此将长度 - 1 后同第 7 问的方法处理,即:
    FLAG_LINE_A_PARTS.map(x => x[0]).slice(1, -11).map(x => x.length - 1).join('')
    // 得到 011001100110110001100001011001110011000100110000011110110011010001100001001101110011010100110010011000100111110100000000
    解码后得到 flag10{4a752b}。
    第 11 个
    将 FLAG_LINE_B 的竖杠去掉后随便找个 BrainFuck 执行器跑一下就好,得到 flag11{63418de7}。
    Web-A
    观察首页请求头,发现提示 X-Dynamic-Flag: flagA{Header X-52PoJie-Uid Not Found}。
    带上请求,得到 flag。
    fetch('/', { headers: {'X-52PoJie-Uid': 176017}}).then(r => r.headers.get('X-Dynamic-Flag')).then(console.log)
    // flagA{2c25aba2} ExpiredAt: 2023-02-06T05:20:00+08:00
    Web-B
    首页查看源代码,得到提示:
    已知直接访问不能进入网站,因此不可能是 A/CNAME。网上随便找了个 DNS TXT Record 查询工具填入域名,得到提示:
    _52pojie_2023_happy_new_year=flagB{substr(md5(uid+"_happy_new_year_"+floor(timestamp/600)),0,8)}
    改写到 php,在网上随便找了个解释器跑一下:
    Web-C
    访问登陆页面 /login,用开发者工具删除掉 disabled 属性,填入 UID 继续。
    提示 您不是 admin,你没有权限获取 flag,观察 Cookie,发现一个 JWT 一样的东西:
    2023_challenge_jwt_token:
    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiIxNzYwMTciLCJyb2xlIjoidXNlciJ9.7O8lMotB3DsaL4ndYA5OMU1rYBRxPXaId4LlqkBSOYQ
    其实就是 base64 编码后的信息,前两段是认证相关,第三段是签名。
    第一段解码后是 {"alg":"HS256","typ":"JWT"},将 HS256 改为 none 禁用签名即可。
    第二段解码后是 {"uid":"176017","role":"user"},将 user 替换为 admin。
    第三段不管,因为没有密钥来生成这个值。
    修改后重新 base64 编码,得到新的 Cookie 值来替换:
    eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1aWQiOiIxNzYwMTciLCJyb2xlIjoiYWRtaW4ifQ.7O8lMotB3DsaL4ndYA5OMU1rYBRxPXaId4LlqkBSOYQ
    替换 Cookie 后刷新,得到:
    欢迎,admin。您的 flag 是 flagC{75c6338f},过期时间是 2023-02-06T05:20:00+08:00
  • 5ctw   

    可以可惜看到晚了留着明年用
    amin1994   

    解题4  反编译后得到的flag 填写上提示不对 跟你这个弄的差不多  不知道问题出在哪里,直接复制出来的
    lxn13393617553   

    [i]
    谜语人来了,SO 只解密前 160 字节是故意的,因为在测试的时候发现cv的代码解密不了,就没修挖个坑
    5ctw   

    强的,又学到了
    lxn13393617553   

    大佬牛逼!
    amin1994   


    正己 发表于 2023-2-6 13:10
    谜语人来了,SO 只解密前 160 字节是故意的,因为在测试的时候发现cv的代码解密不了,就没修挖个坑

    这个理由我可真没想到
    5ctw   

    IDA的伪码是咋处理得这么漂亮的,我是只会重命名,代码结构不会调
    您需要登录后才可以回帖 登录 | 立即注册