爱飞的猫 发表于 2023-2-6 04:32

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

本帖最后由 爱飞的猫 于 2023-2-6 06:26 编辑

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

1. (https://www.52pojie.cn/thread-1742339-1-1.html)(2、5) ← 你在这里
2. [安卓题](https://www.52pojie.cn/forum.php?mod=redirect&goto=findpost&ptid=1742339&pid=45546422)(3、4、6、7) - 其中 7 挑战失败
3. (https://www.52pojie.cn/forum.php?mod=redirect&goto=findpost&ptid=1742339&pid=45546427) - 缺少 4、8、9、12。

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

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

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

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

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

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

```c
i = 0;
while (1) {
// ... 省略部分
// 遍历字符串
if (str.buffer != (unsigned __int8)(g_password >> 2))
    break;

if (++i >= str.size()) {
    // 遍历结束,提示成功
}
}
```

最后就是回到 `g_password`,将数值导出然后变化一下(js 代码):

```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 数据库,处理得漂漂亮亮的:




#### 验证流程

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

```c
// 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` 的情况:

```c
// 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 = 0;
      break;
    case 2: // ???
      str_err_context = &ctx.strs->str_uid_and_key; // L"e4x#Tgs9T2FU" ???
      break;
    case 3: // 错误: UID 与密钥的组合错误
      str_err_context = ctx.strs->str_uid_and_key;
      ctx.strs->str_uid_and_key = ' ';
      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`,因此优先分析这个函数和它的返回处:

```c
int VerifyKey(uint32_t *p_user_key, int uid, unsigned int sum)
{
int i; // MAPDST
uint32_t tea_delta; // MAPDST
int error_counter; // MAPDST
int err_pos; // MAPDST
int expected_sum; //
flag_data flag; //
wchar_t flag_content; //
uint32_t tea_key; // BYREF

if ( !p_user_key )
    return 0;

tea_delta = 0x11111111;
for ( i = 0; i < 14; ++i )
{
    *(_DWORD *)&flag_content = tea_delta ^ g_flag_encrypted;
    tea_delta += 0x11111111;
}


// "flag{!!!_HAPPY_NEW_YEAR_2023!!!}"
flag.as_str.str = ctx.strs->str_flag;   // 拼接 "flag{"
flag.as_str.str = ctx.strs->str_flag;
flag.as_str.str = ctx.strs->str_flag;
flag.as_str.str = ctx.strs->str_flag;
flag.as_str.str = ctx.strs->str_flag;
for ( i = 1; i < 27; ++i )
    flag.as_str.str = flag_content;   // 写出 26 字节
flag.as_str.end = LOBYTE(ctx.strs->str_flag);// 拷贝 "}\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 + 1) * (tea_delta + 1);

error_counter = 0;
expected_sum = 0;

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

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

    if ( flag.as_u32 == p_user_key )
      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_counter` 与 `expected_sum` 的值需要相等。而 TEA 算法在解密后,`sum` 这个值通常会等于 `0`。

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

```c
for ( i = 0; i < 7; i += 2 ){
    test_sum = tea_decrypt(&p_user_key, tea_key, tea_delta, sum);
    if (test_sum != 0
      || flag.as_u32 != p_user_key
      || flag.as_u32 != p_user_key
    ) {
      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)` 以及加解密前后的值。

对照着解密函数实现一个,然后用抓到的数据做验证。就目前看来,除了 `delta`、`sum` 和 `TEA_ROUND` 这三个参数之外,与标准 TEA 的实现没有什么不同。

```cpp
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;
    uint32_t z = buffer;
    for (int i = 0; i < TEA_ROUND; i++)
    {
      z -= single_round_tea(y, sum, tea_key, tea_key);
      y -= single_round_tea(z, sum, tea_key, tea_key);
      sum -= delta;
    }
    buffer = y;
    buffer = z;
    return sum;
}
```

那解密代码调试好了,就剩下加密代码了。有了 `delta` 和 `tea_decrypt` 的实现后很容易做:

```cpp
uint64_t tea_encrypt(uint32_t* buffer, uint32_t* tea_key, uint32_t delta)
{
    uint32_t y = buffer;
    uint32_t z = buffer;
    uint32_t sum = 0;
    for (int i = 0; i < TEA_ROUND; i++)
    {
      sum += delta;
      y += single_round_tea(z, sum, tea_key, tea_key);
      z += single_round_tea(y, sum, tea_key, tea_key);
    }
    buffer = y;
    buffer = z;
    return sum; // 应等于 0
}
```

此时我们可以得出 CM 的流程:

- (1) 用户输入 UID 和密钥 →
- (2) 密钥被变形/解码 →
- (3) TEA 解密

其中解密后的内容必须与预设内容相同。

因此重新观察 `VerifyKey` 这个函数,发现并没有对加密内容的缓冲区进行额外的操作;回到之前的事件分发函数,可以看到 `HexStringToBlocks` 有两个参数,分别是用户输入的内容与我们的缓冲区。

进去分析,发现就是很普通的十六进制转 u32 数组,没有动过手脚。注意一下大小端以及不能有额外的字符就好。

```cpp
void __fastcall HexStringToBlocks(const wchar_t *src_hex, uint32_t *dst_bin)
{
wchar_t backup; //
int src_idx; //
int dst_idx; //
LARGE_INT_FLAG ptr_check; //

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 )                  // 是否结束
    {
      backup = src_hex;
      src_hex = 0;               // 将 8 字符后的内容改为结束符字符串结束符
      dst_bin = Util::HexToU32(&src_hex);
      src_idx += 8;
      src_hex ^= backup;               // 还原刚才记录的值
    }
}
}
```

输入范例 `L"12345678"`,得到 `0x12345678`,内存中显示为 `78-56-34-12`。

此时已经可以针对自己的 UID 算一个密钥出来了。但是没有完全实现这个加密算法好像不太好,因此还是继续分析一下 `VerifyKey` 这个函数吧。

将无关 `tea_delta` 的代码剔除掉,可以发现计算起来很简单:

```c
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;
}
```

于是改写一番:

```c
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` 值的初始化下方:

```c
for ( i = 0; i < 4; ++i )
    tea_key = (i + 1) * (tea_delta + 1);
```

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

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

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

```cpp
// 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; // 32 个字符 + 结束符
    uint32_t as_u32;
};

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 = { 0 };
    for (int i = 0; i < 4; ++i) {
      tea_key = (i + 1) * (tea_delta + 1);
      std::cout << "tea_key[" << i << "] = 0x" << std::hex << std::setw(8) << std::setfill('0') << tea_key << 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, 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, 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;
    }
    std::cout << "key: " << ss.str() << std::endl;
}
```

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

```cpp
// 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;
    uint32_t z = buffer;
    for (int i = 0; i < TEA_ROUND; i++)
    {
      z -= single_round_tea(y, sum, tea_key, tea_key);
      y -= single_round_tea(z, sum, tea_key, tea_key);
      sum -= delta;
    }
    buffer = y;
    buffer = z;
    return sum;
}

uint64_t tea_encrypt(uint32_t* buffer, uint32_t* tea_key, uint32_t delta)
{
    uint32_t y = buffer;
    uint32_t z = buffer;
    uint32_t sum = 0;
    for (int i = 0; i < TEA_ROUND; i++)
    {
      sum += delta;
      y += single_round_tea(z, sum, tea_key, tea_key);
      z += single_round_tea(y, sum, tea_key, tea_key);
    }
    buffer = y;
    buffer = z;
    return sum; // 应等于 0
}
```

爱飞的猫 发表于 2023-2-6 04:35

本帖最后由 爱飞的猫 于 2023-2-6 05:54 编辑


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

1. (https://www.52pojie.cn/thread-1742339-1-1.html)(2、5)
2. [安卓题](https://www.52pojie.cn/forum.php?mod=redirect&goto=findpost&ptid=1742339&pid=45546422)(3、4、6、7) - 其中 7 挑战失败 ← 你在这里
3. (https://www.52pojie.cn/forum.php?mod=redirect&goto=findpost&ptid=1742339&pid=45546427) - 缺少 4、8、9、12。

### 【春节】解题领红包之三 {Android初级题}

作为 zip 压缩包打开看看,没有发现 so 文件。直接拉到 JEB 分析。

然后用 JEB 一打开就发现解密部分的表达式已经被静态优化了:

```java
if(this$0.check() == 999) {
    Toast.makeText(v4, "快去论坛领CB吧!", 1).show();
    key.setText("flag{zhudajiaxinniankuaile}");
}
```

捡了个漏。

如果硬要分析的话,就得看 smali 代码了:

```text
0000005Econst/4             p2, 2
00000060const-string      v0, "hnci}|jwfclkczkppkcpmwckng\u007F"
00000064invoke-virtual      MainActivity->decrypt(String, I)String, p0, v0, p2
```

传参分别是这个字符串和 `p2`,也就是固定的常数 `2`。

继续分析解密函数,关键点就是这个 for 循环:

```java
for(i = 0; i < txtArray.length; ++i) {
    result.append(((char)(txtArray - delta)));
}
```

每个字符 -2,放到 JS 里也是轻松解密:

```js
"hnci}|jwfclkczkppkcpmwckng\u007F".split('').map(x => String.fromCharCode(x.charCodeAt() - 2)).join('')
```

得到同样的过关密码:`flag{zhudajiaxinniankuaile}`

### 【春节】解题领红包之四 {Android 初级题}

JEB 打开,直接跳到 `MainActivity` 代码。

可以看到顶部有一个签名验证,但是我们是静态分析,无视即可。

往下翻,找到关键函数:

```java
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();
    }
}
```

上面的代码中,我已经对部分混淆过的类名进行了分析。对应类名重更名如下:

```text
A -> Flag
B -> Encoder
C -> Crypto
```

分析基本上没怎么做,因为看代码用到了 MD5,浏览器 JS 跑起来要第三方依赖,还是用 Java 抠代码写注册机简单些。

因为原 APK 用的 Kotlin 加了有很多安全检查代码进去,抠出来后再整理下就是下面这样了:

```java
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;

      int key = 50;
      for (int i = n - 1; i >= 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`:

```java
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 打开,没有混淆,轻松定位到对应的三个函数 - `encrypt`、`decrypt` 和 `get_RealKey`。

注:推荐逆向 arm / arm64 的 so 文件,因为自带了 JNI 的结构信息,不需要自己导入。

首先看 `get_RealKey`,意义不明的一个函数:

```c
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; // 0xFE, 0xFB, ... 重复 ...
    *(_QWORD *)add_mask = 0xFEFBFEFBFEFBFEFBLL;
    *(_QWORD *)&add_mask = 0xFEFBFEFBFEFBFEFBLL;

    // 两个 128 位的数字相加
    *key = vaddq_s8(*(int8x16_t *)key, *(int8x16_t *)add_mask);
    return strcmp(key, "thisiskey") != 0;
}

return 0;
}
```

继续看解密:

```c
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 部分看不懂,但是问题不大。

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

: https://gchq.github.io/CyberChef

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

```c
#include <stdio.h>

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

得到新的密钥 `wuaipojie2023114`,看起来像是走对方向了。

填入正确的密钥,发现能正常解密出来内容,添加一个「From Hex(十六进制解码)」+「To Hexdump」过程,可以看到 `PNG` 头部信息:

```text
0000000089 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52|.PNG........IHDR|
0000001000 00 02 e2 00 00 02 e2 04 03 00 00 00 6e cd ae|...â...â.....nÍ®|
000000200c 00 00 00 04 67 41 4d 41 00 00 b1 8f 0b fc 61|.....gAMA..±..üa|
```

把解密内容保存下来打开,得到一枚嘲讽表情。

继续上网爬文看看 PNG 怎么获得隐写内容,得到 `zsteg` 工具一枚。起一个 Kali 虚拟机,安装这个工具后得到信息:

```text
$ 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 0a00 00 00 0d 49 48 44 52|.PNG........IHDR|
    00000010: 00 00 00 64 00 00 00 6408 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` 内容得到同样的结果。

: https://gchq.github.io/CyberChef/#recipe=From_Base64('A-Za-z0-9%2B/%3D',true,false)AES_Decrypt(%7B'option':'UTF8','string':'wuaipojie2023114'%7D,%7B'option':'Hex','string':''%7D,'ECB','Raw','Raw',%7B'option':'Hex','string':''%7D,%7B'option':'Hex','string':''%7D)From_Hex('Auto')Drop_bytes(0,46866,false)Render_Image('Raw')Parse_QR_Code(false)



#### unidbg 模拟

尝试了一下这玩意,但是只能解出前 160 个字节。不清楚是谜语人 SO 只解密前 160 字节还是哪里调用出毛病了。

```java
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 来自动填充调用 + 去花 + 修正逻辑跳转,但是去花脚本写得太菜了,不少正常的代码也被干掉了。

去花脚本:

```py
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 =


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,
# .text:00000000000112CC 08 01 40 B9                           LDR             W8,
# .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,
# .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,
# .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,
# .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,
# .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,
# .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,
# .text:0000000000012B98 B5 86 45 F9                           LDR             X21,
# .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,
# .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')
```

条件跳转修复(半成品,写的很粗糙):

```py
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, 16)
    if value.startswith('0'):
      return int(value, 8)# should be rare?
    return int(value)


class InstParseException(Exception):
    pass


SPECIAL_REGISTER_ZERO = 100

rMatchRegister = re.compile(r'(\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 =
# 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
      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, 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
      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
            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 == '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[, 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 = 1
            addr_when_take = normalize_addr(self.resolve_operand(next_br, 0))

            self.cset_mapping = 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 = 1
            addr_when_take = normalize_addr(self.resolve_operand(next_br, 0))

            self.csel_mapping = 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

    def patch_csel_br(self, ea):
      result = self.analysis_csel_br(ea)
       = 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 ...` 这样的语句处手动执行,看分析的地址对不对。

例如这一段:

```arm
.text:000000000001ADE0 49 72 41 F9                           LDR             X9,
.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,
.text:000000000001ADF4 6B 01 01 8B                           ADD             X11, X11, X1
.text:000000000001ADF8 60 01 1F D6                           BR            X11
```

选中 `1ADE8` 后在 IDAPython 控制台输入 `analysis_csel_at()` 进行分析:

```text
Python>analysis_csel_at()
------------------------------
0x1ade8 B.LT 0x1adfc -- b'\xab\x00\x00T'
0x1adec B 0x1aea0 -- b'-\x00\x00\x14'
['LT', 0x1adfc, 0x1aea0]
```

确认分析正确,加上 `patch=True` 参数进行自动补丁:

```text
Python>analysis_csel_at(patch=True)
```

自动补丁后:

```arm
text:000000000001ADE0 49 72 41 F9                           LDR             X9,
.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,
.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` 函数的大概逻辑:

```cpp
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; // BYREF
Vector s2; // BYREF
Vector s1; // BYREF
Vector vec_uid_dup; // BYREF
Vector v28; // BYREF
Vector vec_flag; // BYREF
Vector vec_uid; // BYREF
int a1; // BYREF
Vector actual_hash; // BYREF
char v33; // BYREF
__int64 v34; //

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, 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. (https://www.52pojie.cn/thread-1742339-1-1.html)(2、5)
2. [安卓题](https://www.52pojie.cn/forum.php?mod=redirect&goto=findpost&ptid=1742339&pid=45546422)(3、4、6、7) - 其中 7 挑战失败
3. (https://www.52pojie.cn/forum.php?mod=redirect&goto=findpost&ptid=1742339&pid=45546427) - 缺少 4、8、9、12。← 你在这里


### web 题目

感想:

> 谜语人滚啊

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

#### 第 1 个

视频里写了,`flag1{52pojiehappynewyear}`。

#### 第 2 个

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



得到一串地址,地址结尾是 `flag2{878a48f2}`。

#### 第 3 个

25 秒左右右下角名字变了,显示的是 `iodj3{06i95dig}`。

看起来数字和符号没变但是字母变了。

推测 `iodj` 代表 `flag`,得到密码表:

```text
   abcdefghijklmnopqrstuvwxyz
   xyzabcdefghijklmnopqrstuvw
```

最后得到密码 `flag3{06f95afd}`。

#### 第 5 个

30 秒左右处有音频形式的摩斯码,把视频用 Audacity 之类的音频分析工具打开后分析比较容易听。

```text
   ..-.         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 问的方法处理,即:

```js
FLAG_LINE_A_PARTS.map(x => x).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 查询工具填入域名,得到提示:

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

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

```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 一样的东西:

```text
2023_challenge_jwt_token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiIxNzYwMTciLCJyb2xlIjoidXNlciJ9.7O8lMotB3DsaL4ndYA5OMU1rYBRxPXaId4LlqkBSOYQ
```

其实就是 base64 编码后的信息,前两段是认证相关,第三段是签名。

第一段解码后是 `{"alg":"HS256","typ":"JWT"}`,将 `HS256` 改为 `none` 禁用签名即可。

第二段解码后是 `{"uid":"176017","role":"user"}`,将 `user` 替换为 `admin`。

第三段不管,因为没有密钥来生成这个值。

修改后重新 base64 编码,得到新的 Cookie 值来替换:

```text
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1aWQiOiIxNzYwMTciLCJyb2xlIjoiYWRtaW4ifQ.7O8lMotB3DsaL4ndYA5OMU1rYBRxPXaId4LlqkBSOYQ
```

替换 Cookie 后刷新,得到:

```text
欢迎,admin。您的 flag 是 flagC{75c6338f},过期时间是 2023-02-06T05:20:00+08:00
```

bj9ye666 发表于 2023-2-6 06:37

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

amin1994 发表于 2023-2-6 10:57

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

正己 发表于 2023-2-6 13:10

本帖最后由 正己 于 2023-2-6 13:11 编辑

http://pic.rmb.bdstatic.com/bjh/c89ae4857d3c328266e204d94851e894.png
谜语人来了,SO 只解密前 160 字节是故意的,因为在测试的时候发现cv的代码解密不了,就没修挖个坑

lxn13393617553 发表于 2023-2-6 14:35

强的{:1_921:},又学到了

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的伪码是咋处理得这么漂亮的,我是只会重命名,代码结构不会调
页: [1] 2 3 4
查看完整版本: 吾爱破解 - 2023 春节解题领红包 2-6 + 部分 web 题