吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 6226|回复: 28
收起左侧

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

[复制链接]
爱飞的猫 发表于 2023-2-6 04:32
本帖最后由 爱飞的猫 于 2023-2-6 06:26 编辑

吾爱破解 - 2023 春节解题领红包

  1. Windows 题(2、5) ← 你在这里
  2. 安卓题(3、4、6、7) - 其中 7 挑战失败
  3. Web 题 - 缺少 4、8、9、12。

因为安卓部分有些代码太长了在编辑器会被截断,所以拆出来发了…

【春节】解题领红包之二 {Windows 初级题}

打开程序,随便输入个 12345,提示长度错误。

偷懒直接用 IDA 看伪代码,看到一个像检查长度的:

if ( *((_DWORD *)str.buffer - 3) == 29 )      // str len

以及附近有 "Success" 字样,继续分析该分支,可以看到一个 for 循环(IDA 识别为 while)遍历每一个字符:

i = 0;
while (1) {
  // ... 省略部分
  // 遍历字符串
  if (str.buffer[i] != (unsigned __int8)(g_password[i] >> 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 慢慢分析。

需要注意:

  1. 下方的伪代码含有剧透内容,因为是从我分析结束后的 IDA 数据库里提取的内容,部分意义不明的地方已经加上了注解与更名。
  2. 软件有尝试藏匿 API 调用与字符串来对抗静态分析,但是因为可以直接从调试器获取到对应的信息,所以没有加到文章中。部分字符串解密可以在事件处理函数的 WM_INITDIALOG 事件处理分支找到。有一部分的 API 调用是在需要调用的时候使用 GetProcAddress 动态获取函数地址。

看了下其他人对这题的分析好像都没有在 IDA 里面做笔记 / 改变量名。这个是我 dump 的 exe + i64 数据库,处理得漂漂亮亮的:

处理得漂漂亮亮的第五题 IDA i64 数据库.7z (495.8 KB, 下载次数: 18)


wndproc.png

验证流程

首先看看「确认」按钮按下后的流程:

// 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 < 14; ++i )
  {
    *(_DWORD *)&flag_content[2 * i] = tea_delta ^ g_flag_encrypted[i];
    tea_delta += 0x11111111;
  }

  // "flag{!!!_HAPPY_NEW_YEAR_2023!!!}"
  flag.as_str.str[0] = ctx.strs->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 < 27; ++i )
    flag.as_str.str[i + 4] = flag_content[i];   // 写出 26 字节
  flag.as_str.end = LOBYTE(ctx.strs->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 < 4; ++i )
    tea_key[i] = (i + 1) * (tea_delta + 1);

  error_counter = 0;
  expected_sum = 0;

  // m = [0, 2, 4, 6]
  // 每次处理 8 字节
  // 输入应为 4 * 8 = 32 字节长
  for ( i = 0; i < 7; i += 2 )
  {
    // 参数顺序
    // rcx, rdx, r8d(0xABE63FF7), r9d(0x7CC7FEE0)
    expected_sum = tea_decrypt(&p_user_key[i], tea_key, tea_delta, sum);// 返回值必须是 0

    if ( flag.as_u32[i] == p_user_key[i] )      // 检查是否相等,下同
      err_pos = 0;
    else
      err_pos = i + 1;
    error_counter += err_pos;

    if ( flag.as_u32[i + 1] == p_user_key[i + 1] )
      err_pos = 0;
    else
      err_pos = i + 1;
    error_counter += err_pos;
  }
  if ( error_counter == expected_sum )
    return i >> 1;                              // ret 4, 成功
  else
    return 3;                                   // fail
}

我在看这个函数的时候是从下向上反推的。因为已知返回值 3 是失败,因此 error_counterexpected_sum 的值需要相等。而 TEA 算法在解密后,sum 这个值通常会等于 0

因此最后面这一段循环可以这么改写/理解:

for ( i = 0; i < 7; i += 2 ){
    test_sum = tea_decrypt(&p_user_key[i], tea_key, tea_delta, sum);
    if (test_sum != 0
        || flag.as_u32[i + 0] != p_user_key[i + 0]
        || flag.as_u32[i + 1] != p_user_key[i + 1]
    ) {
        return 3; // 失败
    }
}

return 4;

再这么一看,不就是把用户输入的内容解密后看是不是等于固定的一个值?

分析到这里,我就去调用 tea_decrypt 前后位置分别下了一个断点,然后得到了 flag ("flag{!!!_HAPPY_NEW_YEAR_2023!!!}")tea_key (LittleEndian {0xabe63ff8, 0x57cc7ff0, 0x03b2bfe8, 0xaf98ffe0)tea_sum (0x7CC7FEE0)tea_delta (0xABE63FF7) 以及加解密前后的值。

对照着解密函数实现一个,然后用抓到的数据做验证。就目前看来,除了 deltasumTEA_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 << 4) + key1) ^ (value + sum) ^ ((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 < TEA_ROUND; i++)
    {
        z -= single_round_tea(y, sum, tea_key[2], tea_key[3]);
        y -= single_round_tea(z, sum, tea_key[0], tea_key[1]);
        sum -= delta;
    }
    buffer[0] = y;
    buffer[1] = z;
    return sum;
}

那解密代码调试好了,就剩下加密代码了。有了 deltatea_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 < TEA_ROUND; i++)
    {
        sum += delta;
        y += single_round_tea(z, sum, tea_key[0], tea_key[1]);
        z += single_round_tea(y, sum, tea_key[2], tea_key[3]);
    }
    buffer[0] = y;
    buffer[1] = z;
    return sum; // 应等于 0
}

此时我们可以得出 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 < 14; ++i ) {
    tea_delta += 0x11111111;
  }

  tea_delta += uid;
  while ((tea_delta >> 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 < 4; ++i )
    tea_key[i] = (i + 1) * (tea_delta + 1);

这个就没什么好说的了,直接照抄就行。

最后的 tea_sum 参数在加密的时一般是 0,姑且不管它。

最后就是完整的算法注册机:

// main.cpp
#include "tea.h"

#include <cassert>
#include <cstdint>
#include <cstring>
#include <cstdlib>

#include <string>
#include <sstream>
#include <iostream>
#include <iomanip>

// 填写从调试器得到的值用于检查
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_delta = 0x" << std::hex << std::setw(8) << std::setfill('0') << tea_delta << std::endl;
    assert(tea_delta == expected_delta);

    uint32_t tea_key[4] = { 0 };
    for (int i = 0; i < 4; ++i) {
        tea_key[i] = (i + 1) * (tea_delta + 1);
        std::cout << "tea_key[" << i << "] = 0x" << std::hex << std::setw(8) << std::setfill('0') << tea_key[i] << std::endl;
    }

    // 无加密的内容
    const char expected_flag_str[] = "flag{!!!_HAPPY_NEW_YEAR_2023!!!}";

    // 开始准备
    auto flag = FlagMagic{ 0 };
    memcpy(flag.as_str, expected_flag_str, sizeof(expected_flag_str));
    assert(strlen(flag.as_str) == 32);

    // 进行加密
    for (int i = 0; i < 8; i += 2) {
        auto sum = tea_encrypt(&flag.as_u32[i], tea_key, tea_delta);
        assert(sum == expected_sum);
    }

    // 看看解密后的效果
    auto flag_decrypted = flag; // 拷贝一份出来试试解密
    for (int i = 0; i < 8; i += 2) {
        auto next_sum = tea_decrypt(&flag_decrypted.as_u32[i], tea_key, expected_delta, expected_sum);
        assert(next_sum == 0);
    }
    std::cout << "decrypted: " << flag_decrypted.as_str << std::endl;
    assert(memcmp(flag_decrypted.as_str, expected_flag_str, 32) == 0); // 解密出来的内容应当相等

    // 因为是小端序编码的十六进制字符串,直接以 u32 为单位输出即可。
    // 如果是以 u8 为单位输出,需要提前调整。
    std::stringstream ss;
    for (int i = 0; i < 8; i++) {
        ss << std::hex << std::setw(8) << std::setfill('0') << flag.as_u32[i];
    }
    std::cout << "key: " << ss.str() << std::endl;
}

虽然只是重复,但是为了代码的完整性还是提供吧:

// tea.h
#pragma once
#include <cstdint>

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 << 4) + key1) ^ (value + sum) ^ ((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 < TEA_ROUND; i++)
    {
        z -= single_round_tea(y, sum, tea_key[2], tea_key[3]);
        y -= single_round_tea(z, sum, tea_key[0], tea_key[1]);
        sum -= delta;
    }
    buffer[0] = y;
    buffer[1] = z;
    return sum;
}

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 < TEA_ROUND; i++)
    {
        sum += delta;
        y += single_round_tea(z, sum, tea_key[0], tea_key[1]);
        z += single_round_tea(y, sum, tea_key[2], tea_key[3]);
    }
    buffer[0] = y;
    buffer[1] = z;
    return sum; // 应等于 0
}

免费评分

参与人数 1吾爱币 +1 热心值 +1 收起 理由
侃遍天下无二人 + 1 + 1 我很赞同!

查看全部评分

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

 楼主| 爱飞的猫 发表于 2023-2-6 04:35
本帖最后由 爱飞的猫 于 2023-2-6 05:54 编辑

吾爱破解 - 2023 春节解题领红包

  1. Windows 题(2、5)
  2. 安卓题(3、4、6、7) - 其中 7 挑战失败 ← 你在这里
  3. Web 题 - 缺少 4、8、9、12。

【春节】解题领红包之三 {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 < txtArray.length; ++i) {
    result.append(((char)(txtArray[i] - delta)));
}

每个字符 -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 <= 'Z') base = 'A';
        else if (c >= 'a' && c <= 'z') base = 'a';
        else return c; // 不处理

        int code = c - base;
        code = code + delta % 26;
        return (char) (code + base);
    }

    public static String cipher(String str, int delta) {
        StringBuilder sb = new StringBuilder();
        int n = str.length();
        for (int i = 0; i < n; ++i) {
            sb.append(cipher(str.charAt(i), delta));
        }

        return sb.toString();
    }

    public static String encodeBase64(byte[] src) {
        return Base64.getEncoder().encodeToString(src);
    }

    public static String md5(String data) throws NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance("MD5");
        md.update(data.getBytes());
        byte[] digest = md.digest();

        StringBuilder sb = new StringBuilder();
        sb.ensureCapacity(32);
        for (int b : digest) {
            int c = b;
            if (c < 0) c += 256; // 溢出
            if (c <= 0x0F) sb.append('0'); // 补 0
            sb.append(Integer.toHexString(c));
        }
        return sb.toString();
    }

    public static String encode(String src) {
        int n = src.length();
        char[] result = new char[n];

        int key = 50;
        for (int i = n - 1; i >= 0; i--) {
            key ^= (50 ^ 53); // 切换密钥
            result[i] = (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 <= vol && vol <= 101) showHint = 1;
    if(showHint != 0) {
        Toast.makeText(((Context)this), "快去找flag吧", 1).show();
        this.write_img(); // 写出 aes.png 到安卓储存区的某个地方,反正能从 APK 里找到
    }
}

IDA 打开,没有混淆,轻松定位到对应的三个函数 - encryptdecryptget_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_DecryptAES_ECB_PKCS7_Decrypt 看了下,就是 Base64 解码然后进行解密。

后面的 AES 部分看不懂,但是问题不大。

尝试在 CyberChef 的流程添加了「From Base64(Base64 解码)」和「AES Decrypt(AES 解密)」,填入密钥,选择 ECB 模式,提示无法解密。

冷静一会,发现这个长度是 16,刚好和 get_RealKey 的要求一致,于是把代码抠出来试试:

#include <stdio.h>

int main() {
    char key[] = "|wfkuqokj4548366";
    char add_mask[] = { 0xFB, 0xFE };
    for (int i = 0; i < sizeof(key) - 1; i ++)
        key[i] += add_mask[i & 1];
    printf("key = '%s'\n", key);
}

得到新的密钥 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

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[i])

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 < text_end:
        cur_addr = idc.find_binary(cur_addr, idc.SEARCH_DOWN, pattern)
        if cur_addr == idc.BADADDR: break
        action(cur_addr)
        cur_addr += 1

remove_junk_all_inst(
    nop_factory(7),
    '88 02 40 B9 1F 29 00 71 AB 00 00 54 A8 02 40 B9 09 05 00 51 28 7D 08 1B 48 FE 07 37'
)
remove_junk_all_inst(
    nop_factory(7),
    '1F 29 00 71 CB 00 00 54 A8 02 40 B9 09 05 00 51 28 7D 08 1B 48 00 00 36 00 00 00 14'
)

# .text:00000000000112BC 1F 29 00 71                             CMP             W8, #0xA
# .text:00000000000112C0 0B 01 00 54                             B.LT            loc_112E0
# .text:00000000000112C4 A8 02 00 F0                             ADRP            X8, #Oo0O.57_ptr@PAGE
# .text:00000000000112C8 08 11 46 F9                             LDR             X8, [X8,#Oo0O.57_ptr@PAGEOFF]
# .text:00000000000112CC 08 01 40 B9                             LDR             W8, [X8]
# .text:00000000000112D0 09 05 00 51                             SUB             W9, W8, #1
# .text:00000000000112D4 28 7D 08 1B                             MUL             W8, W9, W8
# .text:00000000000112D8 48 00 00 36                             TBZ             W8, #0, loc_112E0
# .text:00000000000112DC
# .text:00000000000112DC                         loc_112DC                               ; CODE XREF: sub_112B0:loc_112DC↓j
# .text:00000000000112DC 00 00 00 14                             B               loc_112DC

remove_junk_all_inst(
    nop_factory(9),
    '1F 29 00 71 0B 01 00 54 ?? ?? ?? ?? ?? ?? ?? ?? 08 01 40 B9 09 05 00 51 28 7D 08 1B 48 00 00 36 00 00 00 14'
)

# .text:0000000000011B18 3F 29 00 71                             CMP             W9, #0xA
# .text:0000000000011B1C 8B 00 00 54                             B.LT            loc_11B2C
# .text:0000000000011B20 0B 05 00 51                             SUB             W11, W8, #1
# .text:0000000000011B24 6B 7D 08 1B                             MUL             W11, W11, W8
# .text:0000000000011B28 8B 03 00 37                             TBNZ            W11, #0, loc_11B98

# .text:0000000000011AF8 3F 29 00 71                             CMP             W9, #0xA
# .text:0000000000011AFC 8B 00 00 54                             B.LT            loc_11B0C
# .text:0000000000011B00 0B 05 00 51                             SUB             W11, W8, #1
# .text:0000000000011B04 6B 7D 08 1B                             MUL             W11, W11, W8
# .text:0000000000011B08 0B FE 07 37                             TBNZ            W11, #0, loc_11AC8
remove_junk_all_inst(
    nop_factory(5),
    '3F 29 00 71 8B 00 00 54 0B 05 00 51 6B 7D 08 1B ?? ?? ?? 37')

# .text:0000000000012C78 E9 A7 9F 1A                             CSET            W9, LT
# .text:0000000000012C7C 28 01 08 2A                             ORR             W8, W9, W8
# .text:0000000000012C80
# .text:0000000000012C80                         loc_12C80                               ; CODE XREF: sub_12C38:loc_12C80↓j
# .text:0000000000012C80 08 00 00 34                             CBZ             W8, loc_12C80
remove_junk_all_inst(nop_factory(3), 'E9 A7 9F 1A 28 01 08 2A 08 00 00 34')

# .text:0000000000010140 3F 29 00 71                             CMP             W9, #0xA
# .text:0000000000010144 CA 04 00 54                             B.GE            loc_101DC
# .text:0000000000010148 9F FF FF 17                             B               loc_FFC4
remove_junk_all_inst(nop_factory(2), '3F 29 00 71 ?? ?? 00 54 ?? ?? ?? 17')

###

def blt_to_bal_factory(offset):

    def blt_to_bal(ea):
        cond_flag = ida_bytes.get_byte(ea + offset)
        cond_flag &= 0b1110_0000
        cond_flag |= 0b0000_1110
        ida_bytes.patch_byte(ea + offset, cond_flag)
        # print(f'patching {hex(ea + offset)}: {hex(cond_flag)}')

    return blt_to_bal

# .text:0000000000012ABC 88 02 40 B9                             LDR             W8, [X20]
# .text:0000000000012AC0 1F 29 00 71                             CMP             W8, #0xA
# .text:0000000000012AC4 4B 03 00 54                             B.LT            loc_12B2C
# 4B -> 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[i])

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 < 0:
        delta += 0x1_0000_0000_0000_0000

    if verbose:
        print(f'delta = {hex(delta)}: {hex(jump_addr)} - {hex(curr_addr)}')

    opcode = 0b000000  # 6 bits
    if condition == '':  # unconditional jump
        opcode |= 0b0001_01
        delta = delta >> 2
        delta &= 0x03_FF_FF_FF
    else:
        opcode |= 0b0101_01
        delta = delta << 3 | condition_encode[condition]
        delta &= 0xFF_FF_FF
    operand = delta | (opcode << 26)
    byte_code = pack('<I', operand)
    return byte_code

class ConditionFixer:
    text_start: int
    text_end: int
    csel_mapping = {}
    cset_mapping = {}
    register_override = {}

    def __init__(self, text_start, text_end, register_override=None):
        self.text_start = text_start
        self.text_end = text_end
        if register_override is not None:
            self.register_override = register_override
            print(f'using reg override: {register_override}')

    def resolve_indirect_operand_addr(self, ea, op_pos, descend_max=10):
        op = istn_operand(ea, op_pos)
        if m := rOperandIndirectRegImm.fullmatch(op):
            v_first = self.find_prev_assign(ea, m.group(1), descend_max - 1)
            v_second = parse_ida_int(m.group(2))
            return v_first + v_second
        elif m := rOperandIndirectRegReg.fullmatch(op):
            v_first = self.find_prev_assign(ea, m.group(1), descend_max - 1)
            v_second = self.find_prev_assign(ea, m.group(2), descend_max - 1)
            return v_first + v_second
        elif m := rOperandIndirectRegX.fullmatch(op):
            reg_value = self.find_prev_assign(ea, m.group(1), descend_max - 1)
            return reg_value
        return None

    def resolve_operand(self, ea, op_pos, descend_max=10):
        indirect_addr = self.resolve_indirect_operand_addr(
            ea, op_pos, descend_max)
        if indirect_addr is not None:
            return get_dword(indirect_addr)

        op = istn_operand(ea, op_pos)
        if m := rOperandRegX.fullmatch(op):
            return normalize_addr(
                self.find_prev_assign(ea, m.group(0), descend_max - 1))
        elif m := rOperandRegW.fullmatch(op):
            v = self.find_prev_assign(ea, m.group(0), descend_max - 1)
            return normalize_addr(v)
        elif m := rOperandImm.fullmatch(op):
            return normalize_addr(parse_ida_int(m.group(1)))
        elif m := rOperandWithLSL16.fullmatch(op):
            return normalize_addr(parse_ida_int(m.group(1)) << 16)
        else:
            raise InstParseException(
                f'unsupported operand to resolve: {hex(ea)} / op = {op}')

    def resolve_istn_value(self, ea, descend_max=10, ldp_offset=0):
        name = istn_name(ea)
        if name == 'LDR':
            return self.resolve_operand(ea, 1, descend_max)
        if name == 'LDP':
            addr = self.resolve_indirect_operand_addr(ea, 2)
            if addr == None:
                raise InstParseException(
                    f'could not resolve ptr for LDP instruction at {hex(ea)}')
            return get_dword(addr + ldp_offset * 8)
        elif name == 'MOV':
            return self.resolve_operand(ea, 1, descend_max)
        elif name == 'ADRL':
            return self.resolve_operand(ea, 1, descend_max)
        elif name == 'ADRP':
            return self.resolve_operand(ea, 1, descend_max)
        elif name == 'ADD':
            param1 = self.resolve_operand(ea, 1, descend_max)
            param2 = self.resolve_operand(ea, 2, descend_max)
            if verbose:
                print(
                    f'{hex(ea)}: ADD {hex(param1)}, {hex(param2)} => {hex(param1 + param2)}'
                )
            return param1 + param2
        elif name == 'SUB':
            param1 = self.resolve_operand(ea, 1, descend_max)
            param2 = self.resolve_operand(ea, 2, descend_max)
            return param1 - param2
        elif name == 'LSL':
            param1 = self.resolve_operand(ea, 1, descend_max)
            param2 = self.resolve_operand(ea, 2, descend_max)
            return param1 << param2
        elif name == 'MOVK':
            low_16_bit = self.find_prev_assign(ea, istn_operand(ea, 0),
                                               descend_max - 1)
            low_16_bit &= 0xFFFF
            high_16_bit = self.resolve_operand(ea, 1, descend_max)
            high_16_bit &= 0xFFFF_0000
            return (high_16_bit | low_16_bit)
        elif name == 'CSEL':
            if ea not in self.csel_mapping:
                raise InstParseException(
                    f'CSEL selection not defined in {hex(ea)}')
            return self.resolve_operand(ea, self.csel_mapping[ea], descend_max)
        elif name == 'CSET':
            if ea not in self.cset_mapping:
                raise InstParseException(
                    f'CSET selection not defined in {hex(ea)}')
            # should be 1 (taken) or 0 (not taken)
            return self.cset_mapping[ea]
        else:
            raise InstParseException(
                f'unknown opcode when attempting to resolve: {hex(ea)}')

    def find_prev_assign(self, ea, reg_name, descend_max=10):
        if descend_max <= 0:
            raise InstParseException('reached max descend limit.')
        target_reg = get_register_without_prefix(reg_name)
        assert target_reg != -1, f"could not find reg name from {reg_name}"

        if target_reg == SPECIAL_REGISTER_ZERO:
            return 0

        if reg_name in self.register_override:
            value = self.register_override[reg_name]
            print(f'{hex(ea)}: REG OVERRIDE {reg_name} => {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 < text_end:
        ea += 4
        if istn_name(ea) == 'CSEL':
            try:
                fixer = ConditionFixer(0x000000000000D5C0, 0x000000000004D21C)
                if analysis_only:
                    fixer.analysis_csel_br(ea)
                else:
                    fixer.patch_csel_br(ea)
                print(f'OK: {hex(ea)}')
                ok += 1
            except InstParseException as ex:
                fail += 1
                # print(f'could not handle {hex(ea)}: {ex}')
            except Exception as ex:
                print(f'crashed at {hex(ea)}: {ex}')
                return

            # if fail > 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;
}

最坑的是里面还有状态机打乱执行流程。去掉混淆后这个倒还也能看,加点注释就好。

 楼主| 爱飞的猫 发表于 2023-2-6 05:19
本帖最后由 爱飞的猫 于 2023-2-6 05:27 编辑

吾爱破解 - 2023 春节解题领红包

导航:

  1. Windows 题(2、5)
  2. 安卓题(3、4、6、7) - 其中 7 挑战失败
  3. Web 题 - 缺少 4、8、9、12。← 你在这里

web 题目

感想:

谜语人滚啊

第 4、8、9、12 问藏的 flag 没找到。

第 1 个

视频里写了,flag1{52pojiehappynewyear}

第 2 个

视频里 20 秒左右的二维码,截图后放到 PS 内加个黑白通道过滤来加强画质,方便扫描。

threshold_filter.png

得到一串地址,地址结尾是 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

首页查看源代码,得到提示: <!-- 提示:你可以去 DNS 中寻找一些信息 -->

已知直接访问不能进入网站,因此不可能是 A/CNAME。网上随便找了个 DNS TXT Record 查询工具填入域名,得到提示:

_52pojie_2023_happy_new_year=flagB{substr(md5(uid+"_happy_new_year_"+floor(timestamp/600)),0,8)}

改写到 php,在网上随便找了个解释器跑一下:

<?php
$ts = floor(time() / 600);
$sig = substr(md5("176017_happy_new_year_{$ts}"),0,8);
$flagB = "flagB{{$sig}}";

echo($flagB); // flagB{30ade15d}

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

免费评分

参与人数 2热心值 +2 收起 理由
pjy612 + 1 我很赞同!
anwen + 1 看了几个都没解出第十个,大佬解滴真滴 随缘 哈哈哈

查看全部评分

bj9ye666 发表于 2023-2-6 06:37
amin1994 发表于 2023-2-6 10:57
解题4  反编译后得到的flag 填写上提示不对 跟你这个弄的差不多 不知道问题出在哪里,直接复制出来的
正己 发表于 2023-2-6 13:10
本帖最后由 正己 于 2023-2-6 13:11 编辑


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

点评

这个理由我可真没想到  详情 回复 发表于 2023-2-6 18:33
lxn13393617553 发表于 2023-2-6 14:35
强的,又学到了
5ctw 发表于 2023-2-6 17:55
大佬牛逼!
 楼主| 爱飞的猫 发表于 2023-2-6 18:33
正己 发表于 2023-2-6 13:10
谜语人来了,SO 只解密前 160 字节是故意的,因为在测试的时候发现cv的代码解密不了,就没修挖个坑

这个理由我可真没想到
侃遍天下无二人 发表于 2023-2-6 21:12
IDA的伪码是咋处理得这么漂亮的,我是只会重命名,代码结构不会调

点评

加结构体,按 y 改变量类型到结构体,函数传参类型也可以改 常数按 m 改为宏,让数字显示为宏的值 行末换行可以添加空白行(或按 ins 手动插入空白注释) 等等等等…  详情 回复 发表于 2023-2-6 22:57
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2024-11-24 09:15

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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