吾爱破解 - 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 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: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
```
可以可惜看到晚了留着明年用 解题4反编译后得到的flag 填写上提示不对 跟你这个弄的差不多 {:301_999:} 不知道问题出在哪里,直接复制出来的 本帖最后由 正己 于 2023-2-6 13:11 编辑
http://pic.rmb.bdstatic.com/bjh/c89ae4857d3c328266e204d94851e894.png
谜语人来了,SO 只解密前 160 字节是故意的,因为在测试的时候发现cv的代码解密不了,就没修挖个坑 强的{:1_921:},又学到了 大佬牛逼! 正己 发表于 2023-2-6 13:10
谜语人来了,SO 只解密前 160 字节是故意的,因为在测试的时候发现cv的代码解密不了,就没修挖个坑
这个理由我可真没想到 IDA的伪码是咋处理得这么漂亮的,我是只会重命名,代码结构不会调