作者论坛账号:爱飞的猫
Windows 题(2、5) ← 你在这里
安卓题(3、4、6、7) - 其中 7 挑战失败
Web 题 - 缺少 4、8、9、12。
因为安卓部分有些代码太长了在编辑器会被截断,所以拆出来发了…
打开程序,随便输入个 12345
,提示长度错误。
偷懒直接用 IDA 看伪代码,看到一个像检查长度的:
复制代码 隐藏代码if ( *((_DWORD *)str.buffer - 3) == 29 ) // str len
以及附近有 "Success"
字样,继续分析该分支,可以看到一个 for
循环(IDA 识别为 while
)遍历每一个字符:
复制代码 隐藏代码i = 0;
while (1) {
// ... 省略部分
// 遍历字符串
if (str.buffer[i] != (unsigned __int8)(g_password[i] >> 2))
break;if (++i >= str.size()) {
// 遍历结束,提示成功
}
}
最后就是回到 g_password
,将数值导出然后变化一下(js 代码):
复制代码 隐藏代码[
408, 432, 388, 412, 492, 212, 200, 320, 444, 296, 420, 404, 200, 192, 200,
204, 288, 388, 448, 448, 484, 312, 404, 476, 356, 404, 388, 456, 500,
].map((v) => String.fromCharCode(v >> 2)).join("");
得到答案 flag{52PoJie2023HappyNewYear}
。
首先,这玩意脱壳我不会,即便是魔改过的 UPX _(:3__
于是让程序在 x64dbg 内跑起来,直接用 Scylla 转储内存来静态分析。
用 IDA 载入,查看字符串列表并没有什么能用的东西。于是在调试器附加后找字符串引用,把找到的提示错误都下个断点然后按下确认按钮,断下。
此时断下的地址是比较迟的了,但是也因祸得福找到了事件派发函数 WndProc
,于是继续在 IDA 慢慢分析。
需要注意:
下方的伪代码含有剧透内容,因为是从我分析结束后的 IDA 数据库里提取的内容,部分意义不明的地方已经加上了注解与更名。
软件有尝试藏匿 API 调用与字符串来对抗静态分析,但是因为可以直接从调试器获取到对应的信息,所以没有加到文章中。部分字符串解密可以在事件处理函数的 WM_INITDIALOG
事件处理分支找到。有一部分的 API 调用是在需要调用的时候使用 GetProcAddress
动态获取函数地址。
看了下其他人对这题的分析好像都没有在 IDA 里面做笔记 / 改变量名。这个是我 dump 的 exe + i64 数据库,处理得漂漂亮亮的:
首先看看「确认」按钮按下后的流程:
复制代码 隐藏代码// WM_COMMAND 分支下
switch (wParam) {
case 1: // 1 是 [ 确认 ] 按钮
uid = GetUID_0(hWnd);
if ( uid != 0 && GetKeyStr(hWnd, key_buff) > 0 ) {
tea_sum = encrypt_payload(key_buff, uid); // 初始化?
PostMessageW(hWnd, WM_COMMAND, 0x300/* wParam */, tea_sum);
return 1;
}
return 0;// 忽略其他情况
}
encrypt_payload
这个函数其实并没有很明白在干什么,不过里面调用了一个 TEA 加密的变形,
返回了一个值。
然后就带着这个值一起提交到窗口事件列表,结束当前操作。
经过了一段时间后,事件派发函数再次被执行。这次是 wParam === 0x300
的情况:
复制代码 隐藏代码// WM_COMMAND 分支下
switch (wParam) {
case 0x300u: // 自定义组件 ID
switch (lParam) {
case 1: // 错误: UID 是空的
str_err_context = ctx.strs->str_uid_and_key;
ctx.strs->str_uid_and_key[3] = 0;
break;
case 2: // ???
str_err_context = &ctx.strs->str_uid_and_key[8]; // L"e4x#Tgs9T2FU" ???
break;
case 3: // 错误: UID 与密钥的组合错误
str_err_context = ctx.strs->str_uid_and_key;
ctx.strs->str_uid_and_key[3] = ' ';
break;
case 4: // 弹窗: 挑战成功
MessageBoxSuccess(hWnd);
return 1;
default: // 默认
str_err_context = L"";
break;
}if (wcscmp(str_err_context, L"") != 0) { // 错误显示
str_error = ctx.strs->str_error;
wsprintfW(str_message, ctx.strs->str_missing_field, str_err_context);
user32.MessageBoxW(hWnd, str_message, str_error, MB_ICONERROR);
return 1;
} else if ((uid = GetUID(hWnd)) && GetUserKey(hWnd, buf_key_str) > 0) {
// 取到的 uid 不得为 0,且 key 的长度必须大于 0
// 基本上不需要管它…// 将十六进制字符串转换为 u32 数组
HexStringToBlocks(buf_key_str, key);// 验证密钥,返回值可以是 3 或 4
next_error_code = VerifyKey(key, uid, lParam_dup); // 3 or 4// 投递下一则消息
// 3 = 失败
// 4 = 成功
PostMessageW(hWnd, WM_COMMAND, 0x300, next_error_code);
return 1;
}
return 0;// 忽略其他情况
}
一开始看到这的时候还以为是个状态机,虚惊一场。前几个判断的情况都是根据代码显示对应的信息。
刚才的 tea_sum
作为 lParam
传了进来,而这个值最高位会被设定(0x8000_0000
),因此必定会跑到 default
的情况,然后到下面的 else if
分支继续。
最终成功或失败则是取决于 VerifyKey
的返回值。
因为最终成功或失败取决于 VerifyKey
,因此优先分析这个函数和它的返回处:
复制代码 隐藏代码int VerifyKey(uint32_t *p_user_key, int uid, unsigned int sum)
{
int i; // [rsp+20h] [rbp-188h] MAPDST
uint32_t tea_delta; // [rsp+24h] [rbp-184h] MAPDST
int error_counter; // [rsp+28h] [rbp-180h] MAPDST
int err_pos; // [rsp+38h] [rbp-170h] MAPDST
int expected_sum; // [rsp+3Ch] [rbp-16Ch]
flag_data flag; // [rsp+40h] [rbp-168h]
wchar_t flag_content[104]; // [rsp+B0h] [rbp-F8h]
uint32_t tea_key[4]; // [rsp+180h] [rbp-28h] BYREFif ( !p_user_key )
return 0;tea_delta = 0x11111111;
for ( i = 0; i < 14; ++i )
{
*(_DWORD *)&flag_content[2 * i] = tea_delta ^ g_flag_encrypted[i];
tea_delta += 0x11111111;
}// "flag{!!!_HAPPY_NEW_YEAR_2023!!!}"
flag.as_str.str[0] = ctx.strs->str_flag[0]; // 拼接 "flag{"
flag.as_str.str[1] = ctx.strs->str_flag[1];
flag.as_str.str[2] = ctx.strs->str_flag[2];
flag.as_str.str[3] = ctx.strs->str_flag[3];
flag.as_str.str[4] = ctx.strs->str_flag[4];
for ( i = 1; i < 27; ++i )
flag.as_str.str[i + 4] = flag_content[i]; // 写出 26 字节
flag.as_str.end = LOBYTE(ctx.strs->str_flag[6]);// 拷贝 "}\x00"// 等价代码
// tea_delta += uid;
// while ((tea_delta >> 31) == 0) {
// tea_delta = tea_delta * 2 + 9;
// }
for ( tea_delta += uid; (tea_delta & 0x80000000) == 0; tea_delta = 2 * tea_delta + 9 )
;for ( i = 0; i < 4; ++i )
tea_key[i] = (i + 1) * (tea_delta + 1);error_counter = 0;
expected_sum = 0;// m = [0, 2, 4, 6]
// 每次处理 8 字节
// 输入应为 4 * 8 = 32 字节长
for ( i = 0; i < 7; i += 2 )
{
// 参数顺序
// rcx, rdx, r8d(0xABE63FF7), r9d(0x7CC7FEE0)
expected_sum = tea_decrypt(&p_user_key[i], tea_key, tea_delta, sum);// 返回值必须是 0if ( flag.as_u32[i] == p_user_key[i] ) // 检查是否相等,下同
err_pos = 0;
else
err_pos = i + 1;
error_counter += err_pos;if ( flag.as_u32[i + 1] == p_user_key[i + 1] )
err_pos = 0;
else
err_pos = i + 1;
error_counter += err_pos;
}
if ( error_counter == expected_sum )
return i >> 1; // ret 4, 成功
else
return 3; // fail
}
我在看这个函数的时候是从下向上反推的。因为已知返回值 3
是失败,因此 error_counter
与 expected_sum
的值需要相等。而 TEA 算法在解密后,sum
这个值通常会等于 0
。
因此最后面这一段循环可以这么改写/理解:
复制代码 隐藏代码for ( i = 0; i < 7; i += 2 ){
test_sum = tea_decrypt(&p_user_key[i], tea_key, tea_delta, sum);
if (test_sum != 0
|| flag.as_u32[i + 0] != p_user_key[i + 0]
|| flag.as_u32[i + 1] != p_user_key[i + 1]
) {
return 3; // 失败
}
}return 4;
再这么一看,不就是把用户输入的内容解密后看是不是等于固定的一个值?
分析到这里,我就去调用 tea_decrypt
前后位置分别下了一个断点,然后得到了 flag ("flag{!!!_HAPPY_NEW_YEAR_2023!!!}")
、tea_key (LittleEndian {0xabe63ff8, 0x57cc7ff0, 0x03b2bfe8, 0xaf98ffe0)
、tea_sum (0x7CC7FEE0)
、tea_delta (0xABE63FF7)
以及加解密前后的值。
对照着解密函数实现一个,然后用抓到的数据做验证。就目前看来,除了 delta
、sum
和 TEA_ROUND
这三个参数之外,与标准 TEA 的实现没有什么不同。
复制代码 隐藏代码constexpr int TEA_ROUND = 32; // 这个值一般是 16
// 其实用宏也可以,这段是从我以前写的 TEA 实现里抠出来的。
// 让编译器自己优化就好。
inline uint32_t single_round_tea(uint32_t value, uint32_t sum, uint32_t key1, uint32_t key2) {
return ((value << 4) + key1) ^ (value + sum) ^ ((value >> 5) + key2);
}// delta 和 sum 一般是固定的值
uint64_t tea_decrypt(uint32_t* buffer, uint32_t* tea_key, uint32_t delta, uint32_t sum) {
uint32_t y = buffer[0];
uint32_t z = buffer[1];
for (int i = 0; i < TEA_ROUND; i++)
{
z -= single_round_tea(y, sum, tea_key[2], tea_key[3]);
y -= single_round_tea(z, sum, tea_key[0], tea_key[1]);
sum -= delta;
}
buffer[0] = y;
buffer[1] = z;
return sum;
}
那解密代码调试好了,就剩下加密代码了。有了 delta
和 tea_decrypt
的实现后很容易做:
复制代码 隐藏代码uint64_t tea_encrypt(uint32_t* buffer, uint32_t* tea_key, uint32_t delta)
{
uint32_t y = buffer[0];
uint32_t z = buffer[1];
uint32_t sum = 0;
for (int i = 0; i < TEA_ROUND; i++)
{
sum += delta;
y += single_round_tea(z, sum, tea_key[0], tea_key[1]);
z += single_round_tea(y, sum, tea_key[2], tea_key[3]);
}
buffer[0] = y;
buffer[1] = z;
return sum; // 应等于 0
}
此时我们可以得出 CM 的流程:
(1) 用户输入 UID 和密钥 →
(2) 密钥被变形/解码 →
(3) TEA 解密
其中解密后的内容必须与预设内容相同。
因此重新观察 VerifyKey
这个函数,发现并没有对加密内容的缓冲区进行额外的操作;回到之前的事件分发函数,可以看到 HexStringToBlocks
有两个参数,分别是用户输入的内容与我们的缓冲区。
进去分析,发现就是很普通的十六进制转 u32 数组,没有动过手脚。注意一下大小端以及不能有额外的字符就好。
复制代码 隐藏代码void __fastcall HexStringToBlocks(const wchar_t *src_hex, uint32_t *dst_bin)
{
wchar_t backup; // [rsp+20h] [rbp-28h]
int src_idx; // [rsp+24h] [rbp-24h]
int dst_idx; // [rsp+28h] [rbp-20h]
LARGE_INT_FLAG ptr_check; // [rsp+2Ch] [rbp-1Ch]ptr_check.as_u32.lo = src_hex == nullptr;
ptr_check.as_u32.hi = dst_bin == nullptr;
// 两个参数不能是空指针,且 key 的长度必须是 8 的倍数
if ( ptr_check.as_u64 == 0 && (wcslen(src_hex) % 8) == 0 )
{
src_idx = 0;
dst_idx = 0;
while ( src_hex[src_idx] ) // 是否结束
{
backup = src_hex[src_idx + 8];
src_hex[src_idx + 8] = 0; // 将 8 字符后的内容改为结束符字符串结束符
dst_bin[dst_idx++] = Util::HexToU32(&src_hex[src_idx]);
src_idx += 8;
src_hex[src_idx] ^= backup; // 还原刚才记录的值
}
}
}
输入范例 L"12345678"
,得到 0x12345678
,内存中显示为 78-56-34-12
。
此时已经可以针对自己的 UID 算一个密钥出来了。但是没有完全实现这个加密算法好像不太好,因此还是继续分析一下 VerifyKey
这个函数吧。
将无关 tea_delta
的代码剔除掉,可以发现计算起来很简单:
复制代码 隐藏代码tea_delta = 0x11111111;
for ( i = 0; i < 14; ++i ) {
tea_delta += 0x11111111;
}tea_delta += uid;
while ((tea_delta >> 31) == 0) {
tea_delta = tea_delta * 2 + 9;
}
于是改写一番:
复制代码 隐藏代码uint32_t uid_to_delta(uint32_t uid) {
uint32_t delta = uid + uint32_t{ 0x11111111 } *15;
while ((delta >> 31) == 0) { // 最高位为 1 时停止
delta = 2 * delta + 9;
}
return delta;
}
而 tea_key
的密钥则是紧随 delta
值的初始化下方:
复制代码 隐藏代码for ( i = 0; i < 4; ++i )
tea_key[i] = (i + 1) * (tea_delta + 1);
这个就没什么好说的了,直接照抄就行。
最后的 tea_sum
参数在加密的时一般是 0
,姑且不管它。
最后就是完整的算法注册机:
复制代码 隐藏代码// main.cpp
#include "tea.h"#include <cassert>
#include <cstdint>
#include <cstring>
#include <cstdlib>#include <string>
#include <sstream>
#include <iostream>
#include <iomanip>// 填写从调试器得到的值用于检查
constexpr uint32_t expected_delta = 0xABE63FF7;
constexpr uint32_t expected_sum = 0x7CC7FEE0;union FlagMagic {
char as_str[33]; // 32 个字符 + 结束符
uint32_t as_u32[8];
};int main() {
uint32_t tea_delta = uid_to_delta(176017); // 我的 UID :D
std::cout << "tea_delta = 0x" << std::hex << std::setw(8) << std::setfill('0') << tea_delta << std::endl;
assert(tea_delta == expected_delta);uint32_t tea_key[4] = { 0 };
for (int i = 0; i < 4; ++i) {
tea_key[i] = (i + 1) * (tea_delta + 1);
std::cout << "tea_key[" << i << "] = 0x" << std::hex << std::setw(8) << std::setfill('0') << tea_key[i] << std::endl;
}// 无加密的内容
const char expected_flag_str[] = "flag{!!!_HAPPY_NEW_YEAR_2023!!!}";// 开始准备
auto flag = FlagMagic{ 0 };
memcpy(flag.as_str, expected_flag_str, sizeof(expected_flag_str));
assert(strlen(flag.as_str) == 32);// 进行加密
for (int i = 0; i < 8; i += 2) {
auto sum = tea_encrypt(&flag.as_u32[i], tea_key, tea_delta);
assert(sum == expected_sum);
}// 看看解密后的效果
auto flag_decrypted = flag; // 拷贝一份出来试试解密
for (int i = 0; i < 8; i += 2) {
auto next_sum = tea_decrypt(&flag_decrypted.as_u32[i], tea_key, expected_delta, expected_sum);
assert(next_sum == 0);
}
std::cout << "decrypted: " << flag_decrypted.as_str << std::endl;
assert(memcmp(flag_decrypted.as_str, expected_flag_str, 32) == 0); // 解密出来的内容应当相等// 因为是小端序编码的十六进制字符串,直接以 u32 为单位输出即可。
// 如果是以 u8 为单位输出,需要提前调整。
std::stringstream ss;
for (int i = 0; i < 8; i++) {
ss << std::hex << std::setw(8) << std::setfill('0') << flag.as_u32[i];
}
std::cout << "key: " << ss.str() << std::endl;
}
虽然只是重复,但是为了代码的完整性还是提供吧:
复制代码 隐藏代码// tea.h
#pragma once
#include <cstdint>uint32_t uid_to_delta(uint32_t uid);
uint64_t tea_decrypt(uint32_t* buf, uint32_t* tea_key, uint32_t delta, uint32_t sum);
uint64_t tea_encrypt(uint32_t* out_buf, uint32_t* key, uint32_t delta);// tea.cpp
#include "tea.h"uint32_t uid_to_delta(uint32_t uid) {
uint32_t delta = uid + uint32_t{ 0x11111111 } *15;
while ((delta >> 31) == 0) { // 最高位为 1 时停止
delta = 2 * delta + 9;
}
return delta;
}constexpr int TEA_ROUND = 32; // 这个值一般是 16
inline uint32_t single_round_tea(uint32_t value, uint32_t sum, uint32_t key1, uint32_t key2) {
return ((value << 4) + key1) ^ (value + sum) ^ ((value >> 5) + key2);
}uint64_t tea_decrypt(uint32_t* buffer, uint32_t* tea_key, uint32_t delta, uint32_t sum)
{
uint32_t y = buffer[0];
uint32_t z = buffer[1];
for (int i = 0; i < TEA_ROUND; i++)
{
z -= single_round_tea(y, sum, tea_key[2], tea_key[3]);
y -= single_round_tea(z, sum, tea_key[0], tea_key[1]);
sum -= delta;
}
buffer[0] = y;
buffer[1] = z;
return sum;
}uint64_t tea_encrypt(uint32_t* buffer, uint32_t* tea_key, uint32_t delta)
{
uint32_t y = buffer[0];
uint32_t z = buffer[1];
uint32_t sum = 0;
for (int i = 0; i < TEA_ROUND; i++)
{
sum += delta;
y += single_round_tea(z, sum, tea_key[0], tea_key[1]);
z += single_round_tea(y, sum, tea_key[2], tea_key[3]);
}
buffer[0] = y;
buffer[1] = z;
return sum; // 应等于 0
}
Windows 题(2、5)
安卓题(3、4、6、7) - 其中 7 挑战失败 ← 你在这里
Web 题 - 缺少 4、8、9、12。
作为 zip 压缩包打开看看,没有发现 so 文件。直接拉到 JEB 分析。
然后用 JEB 一打开就发现解密部分的表达式已经被静态优化了:
复制代码 隐藏代码if(this$0.check() == 999) {
Toast.makeText(v4, "快去论坛领CB吧!", 1).show();
key.setText("flag{zhudajiaxinniankuaile}");
}
捡了个漏。
如果硬要分析的话,就得看 smali 代码了:
复制代码 隐藏代码0000005E const/4 p2, 2
00000060 const-string v0, "hnci}|jwfclkczkppkcpmwckng\u007F"
00000064 invoke-virtual MainActivity->decrypt(String, I)String, p0, v0, p2
传参分别是这个字符串和 p2
,也就是固定的常数 2
。
继续分析解密函数,关键点就是这个 for 循环:
复制代码 隐藏代码for(i = 0; i < txtArray.length; ++i) {
result.append(((char)(txtArray[i] - delta)));
}
每个字符 -2,放到 JS 里也是轻松解密:
复制代码 隐藏代码"hnci}|jwfclkczkppkcpmwckng\u007F".split('').map(x => String.fromCharCode(x.charCodeAt() - 2)).join('')
得到同样的过关密码:flag{zhudajiaxinniankuaile}
JEB 打开,直接跳到 MainActivity
代码。
可以看到顶部有一个签名验证,但是我们是静态分析,无视即可。
往下翻,找到关键函数:
复制代码 隐藏代码private static final void onCreate$lambda-0(MainActivity this$0, View arg4) {
// ... 算法无关代码 ...
String uid = this$0.edit_uid.getText().trim();
if( Flag.INSTANCE.check( uid, this$0.edit_flag.getText().trim() ) ) {
Toast.makeText( ((Context)this$0), "恭喜你,flag正确!", 1 ).show();
} else {
Toast.makeText( ((Context)this$0), "flag错误哦,再想想!", 1 ).show();
}
}
上面的代码中,我已经对部分混淆过的类名进行了分析。对应类名重更名如下:
复制代码 隐藏代码A -> Flag
B -> Encoder
C -> Crypto
分析基本上没怎么做,因为看代码用到了 MD5,浏览器 JS 跑起来要第三方依赖,还是用 Java 抠代码写注册机简单些。
因为原 APK 用的 Kotlin 加了有很多安全检查代码进去,抠出来后再整理下就是下面这样了:
复制代码 隐藏代码package cn.lcg.flyingcat;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;class Crypto {
private static char cipher(char c, int delta) {
char base = 0;
if (c >= 'A' && c <= 'Z') base = 'A';
else if (c >= 'a' && c <= 'z') base = 'a';
else return c; // 不处理int code = c - base;
code = code + delta % 26;
return (char) (code + base);
}public static String cipher(String str, int delta) {
StringBuilder sb = new StringBuilder();
int n = str.length();
for (int i = 0; i < n; ++i) {
sb.append(cipher(str.charAt(i), delta));
}return sb.toString();
}public static String encodeBase64(byte[] src) {
return Base64.getEncoder().encodeToString(src);
}public static String md5(String data) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(data.getBytes());
byte[] digest = md.digest();StringBuilder sb = new StringBuilder();
sb.ensureCapacity(32);
for (int b : digest) {
int c = b;
if (c < 0) c += 256; // 溢出
if (c <= 0x0F) sb.append('0'); // 补 0
sb.append(Integer.toHexString(c));
}
return sb.toString();
}public static String encode(String src) {
int n = src.length();
char[] result = new char[n];int key = 50;
for (int i = n - 1; i >= 0; i--) {
key ^= (50 ^ 53); // 切换密钥
result[i] = (char) (src.charAt(i) ^ key);
}return new String(result);
}
}public class Main {
public static void main(String[] args) throws Exception {
String uid = "176017"; // uidvar 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}
。
此题感想:
谜语人滚啊!
JEB 打开 APK,没发现什么东西。有三个 Native 函数,但是并没有调用。
然后有一个函数会判断麦克风音量,根据分贝(?)等级做不同的事情,其中一个情况是写出 aes.png
:
复制代码 隐藏代码private final void Check_Volume(double vol) {
// 无关代码跳过
int showHint = 0;
if(100 <= vol && vol <= 101) showHint = 1;
if(showHint != 0) {
Toast.makeText(((Context)this), "快去找flag吧", 1).show();
this.write_img(); // 写出 aes.png 到安卓储存区的某个地方,反正能从 APK 里找到
}
}
IDA 打开,没有混淆,轻松定位到对应的三个函数 - encrypt
、decrypt
和 get_RealKey
。
注:推荐逆向 arm / arm64 的 so 文件,因为自带了 JNI 的结构信息,不需要自己导入。
首先看 get_RealKey
,意义不明的一个函数:
复制代码 隐藏代码BOOL __fastcall get_RealKey(JNIEnv *env, int a2, int a3) {
char *key = (char *)(*env)->GetStringUTFChars(env, a3, 0);
if ( strlen(key) == 16 ) { // 输入必须是 16 位
char add_mask[16]; // 0xFE, 0xFB, ... 重复 ...
*(_QWORD *)add_mask = 0xFEFBFEFBFEFBFEFBLL;
*(_QWORD *)&add_mask[8] = 0xFEFBFEFBFEFBFEFBLL;// 两个 128 位的数字相加
*key = vaddq_s8(*(int8x16_t *)key, *(int8x16_t *)add_mask);
return strcmp(key, "thisiskey") != 0;
}return 0;
}
继续看解密:
复制代码 隐藏代码jstring /*省略*/_MainActivity_decrypt(JNIEnv *env, int a2, jstring a3)
{
char *c_str_input = (*env)->GetStringUTFChars(env, a3, 0);
char *c_str_result = j_AES_ECB_PKCS7_Decrypt(c_str_input, "|wfkuqokj4548366");
(*env)->ReleaseStringUTFChars(env, a3, c_str_input);
return (*env)->NewStringUTF(env, c_str_result);
}
进去 j_AES_ECB_PKCS7_Decrypt
和 AES_ECB_PKCS7_Decrypt
看了下,就是 Base64 解码然后进行解密。
后面的 AES 部分看不懂,但是问题不大。
尝试在 CyberChef 的流程添加了「From Base64(Base64 解码)」和「AES Decrypt(AES 解密)」,填入密钥,选择 ECB 模式,提示无法解密。
冷静一会,发现这个长度是 16,刚好和 get_RealKey
的要求一致,于是把代码抠出来试试:
复制代码 隐藏代码#include <stdio.h>
int main() {
char key[] = "|wfkuqokj4548366";
char add_mask[] = { 0xFB, 0xFE };
for (int i = 0; i < sizeof(key) - 1; i ++)
key[i] += add_mask[i & 1];
printf("key = '%s'\n", key);
}
得到新的密钥 wuaipojie2023114
,看起来像是走对方向了。
填入正确的密钥,发现能正常解密出来内容,添加一个「From Hex(十六进制解码)」+「To Hexdump」过程,可以看到 PNG
头部信息:
复制代码 隐藏代码00000000 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 |.PNG........IHDR|
00000010 00 00 02 e2 00 00 02 e2 04 03 00 00 00 6e cd ae |...â...â.....nÍ®|
00000020 0c 00 00 00 04 67 41 4d 41 00 00 b1 8f 0b fc 61 |.....gAMA..±..üa|
把解密内容保存下来打开,得到一枚嘲讽表情。
继续上网爬文看看 PNG 怎么获得隐写内容,得到 zsteg
工具一枚。起一个 Kali 虚拟机,安装这个工具后得到信息:
复制代码 隐藏代码$ zsteg aes.png
[?] 994 bytes of extra data after image end (IEND), offset = 0xb712
extradata:0 .. file: PNG image data, 100 x 100, 8-bit/color RGBA, non-interlaced
00000000: 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 |.PNG........IHDR|
00000010: 00 00 00 64 00 00 00 64 08 06 00 00 00 70 e2 95 |...d...d.....p..|
... 省略 ...
报告说在图片结尾处有另一张 PNG 图片在文件偏移 0xb712 (46866)
处。于是将「To Hexdump」过程禁用,添加新的「Drop bytes(删除字节)」过程,将前 46866
个字节剔除;再根据提示分别添加「Render Image(渲染图片)」、「Parse QR Code(解析 QR 二维码)」,最终得到过关密码 flag{Happy_New_Year_Wuaipojie2023}
。
你也可以直接打开这个解密流程,粘贴 aes.png
内容得到同样的结果。
尝试了一下这玩意,但是只能解出前 160 个字节。不清楚是谜语人 SO 只解密前 160 字节还是哪里调用出毛病了。
复制代码 隐藏代码package com.bytedance.frameworks.core.encrypt;
import com.alibaba.fastjson.util.IOUtils;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.Symbol;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.memory.MemoryBlock;
import com.github.unidbg.utils.Inspector;
import org.apache.commons.io.FileUtils;import java.io.File;
import java.nio.charset.StandardCharsets;public class lcg_2023_spring {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;private final boolean logging;
private final Memory memory;lcg_2023_spring(boolean logging) {
this.logging = logging;emulator = AndroidEmulatorBuilder.for32Bit()
.setProcessName("com.zj.wuaipojie2023_2")
.addBackendFactory(new Unicorn2Factory(true))
.build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
memory = emulator.getMemory(); // 模拟器的内存操作接口
memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析vm = emulator.createDalvikVM(); // 创建Android虚拟机
vm.setVerbose(logging); // 设置是否打印Jni调用细节
DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/example_binaries/armeabi-v7a/lib52pj.so"), false); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数
module = dm.getModule(); // 加载好的 so 对应一个模块
}void destroy() {
IOUtils.close(emulator);
}void DoWork() throws Exception {
Symbol AES_ECB_PKCS7_DecryptSym = module.findSymbolByName("AES_ECB_PKCS7_Decrypt");File file = new File("unidbg-android/src/test/resources/aes.png.txt");
byte[] payload_utf8 = FileUtils.readFileToString(file, "UTF-8").trim().getBytes(StandardCharsets.UTF_8);MemoryBlock payload = memory.malloc(payload_utf8.length + 1, false);
payload.getPointer().write(payload_utf8);MemoryBlock key = memory.malloc(17, false);
key.getPointer().write("wuaipojie2023114".getBytes(StandardCharsets.UTF_8));Number ret = AES_ECB_PKCS7_DecryptSym.call(emulator, payload.getPointer(), key.getPointer());
byte[] result = emulator.getBackend().mem_read(ret.longValue(), 0x100);
Inspector.inspect(result, "result (ECB)");payload.free();
key.free();
}public static void main(String[] args) throws Exception {
lcg_2023_spring test = new lcg_2023_spring(true);
test.DoWork();
test.destroy();
}
}
混淆太厉害了,不太懂如何对抗。过两天偷学一手别人的 Writeup 把混淆部分去掉后再分析吧。当初整了几个钟头后摆烂了。
试了下跟踪生成 log 来自动填充调用 + 去花 + 修正逻辑跳转,但是去花脚本写得太菜了,不少正常的代码也被干掉了。
去花脚本:
复制代码 隐藏代码import idc
import ida_bytesdef patch_bytes(ea, data):
for i in range(len(data)):
ida_bytes.patch_byte(ea + i, data[i])INST_NOP = [0x1F, 0x20, 0x03, 0xD5]
def nop_factory(count):
nop = INST_NOP * count
return lambda ea: patch_bytes(ea, nop)def remove_junk_all_inst(action, pattern):
text_beg = 0x0000_D5C0
text_end = 0x0004_D21Ccur_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 += 1remove_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, #[email protected]
# .text:00000000000112C8 08 11 46 F9 LDR X8, [X8,#[email protected]]
# .text:00000000000112CC 08 01 40 B9 LDR W8, [X8]
# .text:00000000000112D0 09 05 00 51 SUB W9, W8, #1
# .text:00000000000112D4 28 7D 08 1B MUL W8, W9, W8
# .text:00000000000112D8 48 00 00 36 TBZ W8, #0, loc_112E0
# .text:00000000000112DC
# .text:00000000000112DC loc_112DC ; CODE XREF: sub_112B0:loc_112DC↓j
# .text:00000000000112DC 00 00 00 14 B loc_112DCremove_junk_all_inst(
nop_factory(9),
'1F 29 00 71 0B 01 00 54 ?? ?? ?? ?? ?? ?? ?? ?? 08 01 40 B9 09 05 00 51 28 7D 08 1B 48 00 00 36 00 00 00 14'
)# .text:0000000000011B18 3F 29 00 71 CMP W9, #0xA
# .text:0000000000011B1C 8B 00 00 54 B.LT loc_11B2C
# .text:0000000000011B20 0B 05 00 51 SUB W11, W8, #1
# .text:0000000000011B24 6B 7D 08 1B MUL W11, W11, W8
# .text:0000000000011B28 8B 03 00 37 TBNZ W11, #0, loc_11B98# .text:0000000000011AF8 3F 29 00 71 CMP W9, #0xA
# .text:0000000000011AFC 8B 00 00 54 B.LT loc_11B0C
# .text:0000000000011B00 0B 05 00 51 SUB W11, W8, #1
# .text:0000000000011B04 6B 7D 08 1B MUL W11, W11, W8
# .text:0000000000011B08 0B FE 07 37 TBNZ W11, #0, loc_11AC8
remove_junk_all_inst(
nop_factory(5),
'3F 29 00 71 8B 00 00 54 0B 05 00 51 6B 7D 08 1B ?? ?? ?? 37')# .text:0000000000012C78 E9 A7 9F 1A CSET W9, LT
# .text:0000000000012C7C 28 01 08 2A ORR W8, W9, W8
# .text:0000000000012C80
# .text:0000000000012C80 loc_12C80 ; CODE XREF: sub_12C38:loc_12C80↓j
# .text:0000000000012C80 08 00 00 34 CBZ W8, loc_12C80
remove_junk_all_inst(nop_factory(3), 'E9 A7 9F 1A 28 01 08 2A 08 00 00 34')# .text:0000000000010140 3F 29 00 71 CMP W9, #0xA
# .text:0000000000010144 CA 04 00 54 B.GE loc_101DC
# .text:0000000000010148 9F FF FF 17 B loc_FFC4
remove_junk_all_inst(nop_factory(2), '3F 29 00 71 ?? ?? 00 54 ?? ?? ?? 17')###
def blt_to_bal_factory(offset):
def blt_to_bal(ea):
cond_flag = ida_bytes.get_byte(ea + offset)
cond_flag &= 0b1110_0000
cond_flag |= 0b0000_1110
ida_bytes.patch_byte(ea + offset, cond_flag)
# print(f'patching {hex(ea + offset)}: {hex(cond_flag)}')return blt_to_bal
# .text:0000000000012ABC 88 02 40 B9 LDR W8, [X20]
# .text:0000000000012AC0 1F 29 00 71 CMP W8, #0xA
# .text:0000000000012AC4 4B 03 00 54 B.LT loc_12B2C
# 4B -> 4E# .text:0000000000012898 C8 02 40 B9 LDR W8, [X22]
# .text:000000000001289C 1F 29 00 71 CMP W8, #0xA
# .text:00000000000128A0 AB 00 00 54 B.LT loc_128B4# .text:0000000000012E20 A8 02 40 B9 LDR W8, [X21]
# .text:0000000000012E24 1F 29 00 71 CMP W8, #0xA
# .text:0000000000012E28 AB 00 00 54 B.LT loc_12E3C# .text:0000000000010080 08 01 40 B9 LDR W8, [X8]
# .text:0000000000010084 1F 29 00 71 CMP W8, #0xA
# .text:0000000000010088 EB 00 00 54 B.LT loc_100A4remove_junk_all_inst(blt_to_bal_factory(8),
'?? ?? 40 B9 1F 29 00 71 ?? ?? 00 54')# .text:0000000000010104 3F 29 00 71 CMP W9, #0xA
# .text:0000000000010108 08 01 40 B9 LDR W8, [X8]
# .text:000000000001010C 8B 00 00 54 B.LT loc_1011Cremove_junk_all_inst(blt_to_bal_factory(8),
'3F 29 00 71 08 01 40 B9 8B 00 00 54')# .text:0000000000012B94 88 02 40 B9 LDR W8, [X20]
# .text:0000000000012B98 B5 86 45 F9 LDR X21, [X21,#[email protected]]
# .text:0000000000012B9C 1F 29 00 71 CMP W8, #0xA
# .text:0000000000012BA0 AB 00 00 54 B.LT loc_12BB4remove_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_15720remove_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_158E8remove_junk_all_inst(
blt_to_bal_factory(8),
'?? 29 00 71 ?? ?? ?? ?? ?? ?? ?? 54 ?? 05 00 51 ?? ?? ?? 1B ?? ?? ?? 36')# .text:00000000000159C8 3F 29 00 71 CMP W9, #0xA
# .text:00000000000159CC AB 00 00 54 B.LT loc_159E0
# .text:00000000000159D0 C9 02 40 B9 LDR W9, [X22]
# .text:00000000000159D4 2A 05 00 51 SUB W10, W9, #1
# .text:00000000000159D8 49 7D 09 1B MUL W9, W10, W9
# .text:00000000000159DC C9 FC 07 37 TBNZ W9, #0, loc_15974remove_junk_all_inst(
blt_to_bal_factory(4),
'?? 29 00 71 ?? ?? ?? 54 ?? ?? ?? ?? ?? 05 00 51 ?? ?? ?? 1B ?? ?? ?? 36')
条件跳转修复(半成品,写的很粗糙):
复制代码 隐藏代码from typing import Callable
import re
import idc
import ida_bytes
from struct import packverbose = False
def istn_name(ea):
return print_insn_mnem(ea)def istn_operand(ea, pos):
return print_operand(ea, pos)def patch_bytes(ea, data):
for i in range(len(data)):
ida_bytes.patch_byte(ea + i, data[i])def normalize_addr(addr):
return addr & 0xFFFF_FFFFdef 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 0if value.startswith('0x'):
return int(value[2:], 16)
if value.startswith('0'):
return int(value[1:], 8) # should be rare?
return int(value)class InstParseException(Exception):
passSPECIAL_REGISTER_ZERO = 100
rMatchRegister = re.compile(r'[XW](\d+|ZR)')
rOperandIndirectRegImm = re.compile(r'\[(X\d+|ZR),#?(-?0x[\da-fA-F]+|\d+)\]')
rOperandIndirectRegReg = re.compile(r'\[(X\d+|ZR),(X\d+|ZR)\]')
rOperandIndirectRegX = re.compile(r'\[(X\d+|ZR)\]')
rOperandRegX = re.compile(r'X(\d+|ZR)')
rOperandRegW = re.compile(r'W(\d+|ZR)')
rOperandImm = re.compile(r'#?(-?0x[\da-fA-F]+|\d+)')
rOperandWithLSL16 = re.compile(r'#?(-?0x[\da-fA-F]+|\d+),LSL#16')INST_NOP = [0x1F, 0x20, 0x03, 0xD5]
# https://developer.arm.com/documentation/ddi0406/c/Application-Level-Architecture/ARM-Instruction-Set-Encoding/ARM-instruction-set-encoding
condition_encode = {
'EQ': 0b0000,
'NE': 0b0001,
'CS': 0b0010,
'CC': 0b0011,
'MI': 0b0100,
'PL': 0b0101,
'VS': 0b0110,
'VC': 0b0111,
'HI': 0b1000,
'LS': 0b1001,
'GE': 0b1010,
'LT': 0b1011,
'GT': 0b1100,
'LE': 0b1101,
'AL': 0b1110, # unconditional
}def encode_jump(curr_addr, jump_addr, condition=''):
# .text:000000000000E8FC 60 02 00 54 B.EQ loc_E948
# .text:000000000000E900 04 00 00 14 B loc_E910
# .text:000000000000E904 ; ---------------------------------------------------------------------------
# .text:000000000000E904 1F 20 03 D5 NOP
# .text:000000000000E908 1F 20 03 D5 NOP
# .text:000000000000E90C 1F 20 03 D5 NOP ; loc_E910
delta = jump_addr - curr_addr
if delta < 0:
delta += 0x1_0000_0000_0000_0000if verbose:
print(f'delta = {hex(delta)}: {hex(jump_addr)} - {hex(curr_addr)}')opcode = 0b000000 # 6 bits
if condition == '': # unconditional jump
opcode |= 0b0001_01
delta = delta >> 2
delta &= 0x03_FF_FF_FF
else:
opcode |= 0b0101_01
delta = delta << 3 | condition_encode[condition]
delta &= 0xFF_FF_FF
operand = delta | (opcode << 26)
byte_code = pack('<I', operand)
return byte_codeclass 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 Nonedef resolve_operand(self, ea, op_pos, descend_max=10):
indirect_addr = self.resolve_indirect_operand_addr(
ea, op_pos, descend_max)
if indirect_addr is not None:
return get_dword(indirect_addr)op = istn_operand(ea, op_pos)
if m := rOperandRegX.fullmatch(op):
return normalize_addr(
self.find_prev_assign(ea, m.group(0), descend_max - 1))
elif m := rOperandRegW.fullmatch(op):
v = self.find_prev_assign(ea, m.group(0), descend_max - 1)
return normalize_addr(v)
elif m := rOperandImm.fullmatch(op):
return normalize_addr(parse_ida_int(m.group(1)))
elif m := rOperandWithLSL16.fullmatch(op):
return normalize_addr(parse_ida_int(m.group(1)) << 16)
else:
raise InstParseException(
f'unsupported operand to resolve: {hex(ea)} / op = {op}')def resolve_istn_value(self, ea, descend_max=10, ldp_offset=0):
name = istn_name(ea)
if name == 'LDR':
return self.resolve_operand(ea, 1, descend_max)
if name == 'LDP':
addr = self.resolve_indirect_operand_addr(ea, 2)
if addr == None:
raise InstParseException(
f'could not resolve ptr for LDP instruction at {hex(ea)}')
return get_dword(addr + ldp_offset * 8)
elif name == 'MOV':
return self.resolve_operand(ea, 1, descend_max)
elif name == 'ADRL':
return self.resolve_operand(ea, 1, descend_max)
elif name == 'ADRP':
return self.resolve_operand(ea, 1, descend_max)
elif name == 'ADD':
param1 = self.resolve_operand(ea, 1, descend_max)
param2 = self.resolve_operand(ea, 2, descend_max)
if verbose:
print(
f'{hex(ea)}: ADD {hex(param1)}, {hex(param2)} => {hex(param1 + param2)}'
)
return param1 + param2
elif name == 'SUB':
param1 = self.resolve_operand(ea, 1, descend_max)
param2 = self.resolve_operand(ea, 2, descend_max)
return param1 - param2
elif name == 'LSL':
param1 = self.resolve_operand(ea, 1, descend_max)
param2 = self.resolve_operand(ea, 2, descend_max)
return param1 << param2
elif name == 'MOVK':
low_16_bit = self.find_prev_assign(ea, istn_operand(ea, 0),
descend_max - 1)
low_16_bit &= 0xFFFF
high_16_bit = self.resolve_operand(ea, 1, descend_max)
high_16_bit &= 0xFFFF_0000
return (high_16_bit | low_16_bit)
elif name == 'CSEL':
if ea not in self.csel_mapping:
raise InstParseException(
f'CSEL selection not defined in {hex(ea)}')
return self.resolve_operand(ea, self.csel_mapping[ea], descend_max)
elif name == 'CSET':
if ea not in self.cset_mapping:
raise InstParseException(
f'CSET selection not defined in {hex(ea)}')
# should be 1 (taken) or 0 (not taken)
return self.cset_mapping[ea]
else:
raise InstParseException(
f'unknown opcode when attempting to resolve: {hex(ea)}')def find_prev_assign(self, ea, reg_name, descend_max=10):
if descend_max <= 0:
raise InstParseException('reached max descend limit.')
target_reg = get_register_without_prefix(reg_name)
assert target_reg != -1, f"could not find reg name from {reg_name}"if target_reg == SPECIAL_REGISTER_ZERO:
return 0if reg_name in self.register_override:
value = self.register_override[reg_name]
print(f'{hex(ea)}: REG OVERRIDE {reg_name} => {value}')
return valuecurr_addr = ea
max_look_back = max(curr_addr - 1000 * 4, self.text_start)
while curr_addr >= max_look_back:
curr_addr -= 4
name = istn_name(curr_addr)if name == '' or name[0] == 'B': continue
if name == 'LDP':
curr_reg = get_register_without_prefix(
istn_operand(curr_addr, 1))
if curr_reg == target_reg:
result = self.resolve_istn_value(curr_addr,
descend_max,
ldp_offset=1)
return resultcurr_reg = get_register_without_prefix(istn_operand(curr_addr, 0))
if curr_reg == target_reg:
if verbose:
print(f'found assignment to {reg_name}'
f' in {hex(curr_addr)}')
result = self.resolve_istn_value(curr_addr, descend_max)
if verbose:
print(f'{hex(curr_addr)}: '
f'{reg_name} resolved to {hex(result)}')
return result
raise InstParseException(
f'could not find assignment to {reg_name}: {hex(ea)}')def find_prev_cmp(self, ea):
curr_addr = ea
max_look_back = max(curr_addr - 1000 * 4, self.text_start)
while curr_addr >= max_look_back:
curr_addr -= 4
if istn_name(curr_addr) == 'CMP':
return [
curr_addr,
istn_operand(curr_addr, 0),
istn_operand(curr_addr, 1),
]
print(f'CMP inst not found :/')def find_next_matching_istn(self,
ea,
filter: Callable[[int], bool],
max_itsn_distance: int = 1000):
max_look_ahead = min(ea + max_itsn_distance * 4, self.text_end)
for addr in range(ea + 4, max_look_ahead, 4):
# print(f'checking {hex(addr)}: {istn_name(addr)}')
if filter(addr):
return addr
raise InstParseException(f'could not find expected instruction')def analysis_csel_br(self, ea):
# Search for BR instruction
next_br = self.find_next_matching_istn(
ea, lambda addr: istn_name(addr) == 'BR', 40)name = istn_name(ea)
if name == 'CSET':
reg_cond_name = istn_operand(ea, 1)self.cset_mapping[ea] = 1
addr_when_take = normalize_addr(self.resolve_operand(next_br, 0))self.cset_mapping[ea] = 0
addr_when_miss = normalize_addr(self.resolve_operand(next_br, 0))
elif name == 'CSEL':
reg_cond_name = istn_operand(ea, 3)self.csel_mapping[ea] = 1
addr_when_take = normalize_addr(self.resolve_operand(next_br, 0))self.csel_mapping[ea] = 2
addr_when_miss = normalize_addr(self.resolve_operand(next_br, 0))
else:
raise InstParseException('unsupported instruction')print('-' * 30)
print(f'{hex(ea + 0)} B.{reg_cond_name} {hex(addr_when_take)}'
f' -- {encode_jump(ea + 0, addr_when_take, reg_cond_name)}')
print(f'{hex(ea + 4)} B {hex(addr_when_miss)}'
f' -- {encode_jump(ea + 4, addr_when_miss)}')return [reg_cond_name, addr_when_take, addr_when_miss]
def patch_csel_br(self, ea):
result = self.analysis_csel_br(ea)
[reg_cond_name, addr_when_take, addr_when_miss] = resultencoded_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 resultdef 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 = 0x000000000004D21Cok = 0
fail = 0ea = text_start
while ea < text_end:
ea += 4
if istn_name(ea) == 'CSEL':
try:
fixer = ConditionFixer(0x000000000000D5C0, 0x000000000004D21C)
if analysis_only:
fixer.analysis_csel_br(ea)
else:
fixer.patch_csel_br(ea)
print(f'OK: {hex(ea)}')
ok += 1
except InstParseException as ex:
fail += 1
# print(f'could not handle {hex(ea)}: {ex}')
except Exception as ex:
print(f'crashed at {hex(ea)}: {ex}')
return# if fail > 10:
# print('terminated: too many failure.')
# break
print(f'ok({ok}) fail({fail})')# main(False)
# ConditionFixer(0x000000000000D5C0, 0x000000000004D21C).analysis_csel_br(0xE8FC)
上面这个脚本在处理数据的时候有毛病,要手动到 CSEL ...
这样的语句处手动执行,看分析的地址对不对。
例如这一段:
复制代码 隐藏代码.text:000000000001ADE0 49 72 41 F9 LDR X9, [X18,#0x2E0]
.text:000000000001ADE4 1F 01 0F 6B CMP W8, W15
.text:000000000001ADE8 2B B2 90 9A CSEL X11, X17, X16, LT
.text:000000000001ADEC 29 01 00 8B ADD X9, X9, X0
.text:000000000001ADF0 2B 69 6B F8 LDR X11, [X9,X11]
.text:000000000001ADF4 6B 01 01 8B ADD X11, X11, X1
.text:000000000001ADF8 60 01 1F D6 BR X11
选中 1ADE8
后在 IDAPython 控制台输入 analysis_csel_at()
进行分析:
复制代码 隐藏代码Python>analysis_csel_at()
------------------------------
0x1ade8 B.LT 0x1adfc -- b'\xab\x00\x00T'
0x1adec B 0x1aea0 -- b'-\x00\x00\x14'
['LT', 0x1adfc, 0x1aea0]
确认分析正确,加上 patch=True
参数进行自动补丁:
复制代码 隐藏代码Python>analysis_csel_at(patch=True)
自动补丁后:
复制代码 隐藏代码text:000000000001ADE0 49 72 41 F9 LDR X9, [X18,#0x2E0]
.text:000000000001ADE4 1F 01 0F 6B CMP W8, W15
.text:000000000001ADE8 AB 00 00 54 B.LT loc_1ADFC
.text:000000000001ADEC 2D 00 00 14 B loc_1AEA0
.text:000000000001ADF0 2B 69 6B F8 LDR X11, [X9,X11]
.text:000000000001ADF4 6B 01 01 8B ADD X11, X11, X1
.text:000000000001ADF8 60 01 1F D6 BR X11
然后按下 alt-p
让 IDA 重新分析该函数。
缺点就是,效率很低;遇到非常数形式的指令也不懂(如 .text:1AE34 ADRP X8, #[email protected]
)。也不懂得做优化/分析,只能单纯的向上找。
之前顶着混淆分析,结果发现分析的是一堆类似 Vector 数据类型相关的函数… 吐血。
checkSN
函数的大概逻辑:
复制代码 隐藏代码BOOL __fastcall checkSN(JNIEnv *env, __int64 a2, jstring jstr_uid, void *jstr_flag)
{
bool check_ok; // w19
const char *str_uid; // x21
const char *str_flag; // x23
uint64_t flag_len; // x0
__int64 v12; // x0
uint8_t *DataPointer_0; // x19
uint64_t Length_0; // x0
uint8_t *p_flag_data; // x19
__int64 p_flag_len; // x0
uint8_t *ptr_s1; // x20
uint8_t *ptr_s2; // x0
unsigned __int64 flag_len_1; // x0
int v20; // w0
int v21; // w20
uint8_t *expected_hash; // x19
unsigned __int64 Length_1; // x0
char v24[8]; // [xsp+8h] [xbp-578h] BYREF
Vector s2; // [xsp+10h] [xbp-570h] BYREF
Vector s1; // [xsp+28h] [xbp-558h] BYREF
Vector vec_uid_dup; // [xsp+40h] [xbp-540h] BYREF
Vector v28; // [xsp+58h] [xbp-528h] BYREF
Vector vec_flag; // [xsp+70h] [xbp-510h] BYREF
Vector vec_uid; // [xsp+88h] [xbp-4F8h] BYREF
int a1; // [xsp+A4h] [xbp-4DCh] BYREF
Vector actual_hash; // [xsp+A8h] [xbp-4D8h] BYREF
char v33[88]; // [xsp+4C8h] [xbp-B8h] BYREF
__int64 v34; // [xsp+520h] [xbp-60h]v34 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
if ( JNI::GetStringUTFLength(env, jstr_flag) == 44 && JNI::GetStringUTFLength(env, jstr_uid) == 8 )
{
str_uid = JNI::GetStringUTFChars(env, jstr_uid, 0LL);
Vector::InitFromString(&vec_uid, str_uid);
str_flag = (*env)->GetStringUTFChars(env, jstr_flag, 0LL);
Vector::Reset2(&vec_flag);
flag_len = strlen_0(str_flag);
Vector::AddData(&vec_flag, (uint8_t *)str_flag, flag_len);(*env)->ReleaseStringUTFChars(env, jstr_flag, str_flag);
(*env)->ReleaseStringUTFChars(env, jstr_uid, str_uid);Vector::Copy(&vec_uid_dup, &vec_uid);
sub_1B32C(&vec_uid_dup);
Vector::SafeFree(&vec_uid_dup);// Initialize with data from a table?
Vector::Reset2(&s1);
Vector::AddData(&s1, byte_4D220, 10uLL);
Vector::Reset2(&s2);
Vector::AddData(&s2, &byte_4D220[11], 0x10uLL);a1 = 0x69AB81DE;
v12 = sub_10214(&a1); // 时间相关
sub_1F588(v12 / 20000000, (__int64)&actual_hash);
DataPointer_0 = Vector::GetDataPointer_0(&actual_hash);
Length_0 = Vector::GetLength_0(&actual_hash);
sub_13318(&s1, DataPointer_0, Length_0);
Vector::SafeFree(&actual_hash);
p_flag_data = Vector::GetDataPointer_1(&vec_flag);
p_flag_len = Vector::GetLength_1(&vec_flag);
if ( sub_1B788((__int64)p_flag_data, p_flag_len) )
{
ptr_s1 = Vector::GetDataPointer_1(&s1);
ptr_s2 = Vector::GetDataPointer_1(&s2);
if ( sub_165F0((uint64_t)v33, (__int64)ptr_s1, (__int64)ptr_s2) != 1 )
{
check_ok = 1;
LB_CHECK_COMPLETE:
Vector::SafeFree(&s2);
Vector::SafeFree(&s1);
Vector::SafeFree(&v28);
Vector::SafeFree(&vec_flag);
Vector::SafeFree(&vec_uid);
return check_ok;
}
memset(&actual_hash, 0, 0x420u);
flag_len_1 = strlen((const char *)p_flag_data);
sub_168E4((__int64)v33, p_flag_data, flag_len_1, (__int64)&actual_hash, v24);
v21 = v20;
free(p_flag_data);
if ( v21 == 1 )
{
expected_hash = Vector::GetDataPointer_1(&v28);
Length_1 = Vector::GetLength_1(&v28);
a1 = 0x37FA57CD;
check_ok = sub_104C8(&a1, (uint8_t *)&actual_hash, expected_hash, Length_1) == 0;
if ( sub_173B8() == 1 )
goto LB_CHECK_COMPLETE;
}
}
check_ok = 0;
goto LB_CHECK_COMPLETE;
}
return 0;
}
最坑的是里面还有状态机打乱执行流程。去掉混淆后这个倒还也能看,加点注释就好。
导航:
Windows 题(2、5)
安卓题(3、4、6、7) - 其中 7 挑战失败
Web 题 - 缺少 4、8、9、12。← 你在这里
感想:
谜语人滚啊
第 4、8、9、12 问藏的 flag 没找到。
视频里写了,flag1{52pojiehappynewyear}
。
视频里 20 秒左右的二维码,截图后放到 PS 内加个黑白通道过滤来加强画质,方便扫描。
得到一串地址,地址结尾是 flag2{878a48f2}
。
25 秒左右右下角名字变了,显示的是 iodj3{06i95dig}
。
看起来数字和符号没变但是字母变了。
推测 iodj
代表 flag
,得到密码表:
复制代码 隐藏代码abcdefghijklmnopqrstuvwxyz
xyzabcdefghijklmnopqrstuvw
最后得到密码 flag3{06f95afd}
。
30 秒左右处有音频形式的摩斯码,把视频用 Audacity 之类的音频分析工具打开后分析比较容易听。
复制代码 隐藏代码..-. F
.-.. L
.- A
--. G
..... 5
{
. E
.- A
.. I
- T
}
得到 flag5{eait}
。
视频开始时给了提示,是电话号码的声音。
这个我不懂,给 Audacity 装了个插件自动识别的。
插件 rjh-dtmfdec.ny
,地址: https://forum.audacityteam.org/viewtopic.php?t=79168#p245364
选中区域后点击 Analyze → DTMF Decoder,确认使用默认设定,得到结果 590124
,即 flag6{590124}
。
从这个问题开始(应该),对抗到了 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}
。
这个是瞎蒙出来的。
将 FLAG_LINE_A_PARTS
的值转换,可以得到一堆长度是 1 和 2 的数组内容。
因此将长度 - 1 后同第 7 问的方法处理,即:
复制代码 隐藏代码FLAG_LINE_A_PARTS.map(x => x[0]).slice(1, -11).map(x => x.length - 1).join('')
// 得到 011001100110110001100001011001110011000100110000011110110011010001100001001101110011010100110010011000100111110100000000
解码后得到 flag10{4a752b}
。
将 FLAG_LINE_B
的竖杠去掉后随便找个 BrainFuck 执行器跑一下就好,得到 flag11{63418de7}
。
观察首页请求头,发现提示 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
首页查看源代码,得到提示: <!-- 提示:你可以去 DNS 中寻找一些信息 -->
已知直接访问不能进入网站,因此不可能是 A/CNAME。网上随便找了个 DNS TXT Record 查询工具填入域名,得到提示:
复制代码 隐藏代码_52pojie_2023_happy_new_year=flagB{substr(md5(uid+"_happy_new_year_"+floor(timestamp/600)),0,8)}
改写到 php,在网上随便找了个解释器跑一下:
复制代码 隐藏代码<?php
$ts = floor(time() / 600);
$sig = substr(md5("176017_happy_new_year_{$ts}"),0,8);
$flagB = "flagB{{$sig}}";echo($flagB); // flagB{30ade15d}
访问登陆页面 /login
,用开发者工具删除掉 disabled
属性,填入 UID 继续。
提示 您不是 admin,你没有权限获取 flag
,观察 Cookie,发现一个 JWT 一样的东西:
复制代码 隐藏代码2023_challenge_jwt_token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiIxNzYwMTciLCJyb2xlIjoidXNlciJ9.7O8lMotB3DsaL4ndYA5OMU1rYBRxPXaId4LlqkBSOYQ
其实就是 base64 编码后的信息,前两段是认证相关,第三段是签名。
第一段解码后是 {"alg":"HS256","typ":"JWT"}
,将 HS256
改为 none
禁用签名即可。
第二段解码后是 {"uid":"176017","role":"user"}
,将 user
替换为 admin
。
第三段不管,因为没有密钥来生成这个值。
修改后重新 base64 编码,得到新的 Cookie 值来替换:
复制代码 隐藏代码eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1aWQiOiIxNzYwMTciLCJyb2xlIjoiYWRtaW4ifQ.7O8lMotB3DsaL4ndYA5OMU1rYBRxPXaId4LlqkBSOYQ
替换 Cookie 后刷新,得到:
复制代码 隐藏代码欢迎,admin。您的 flag 是 flagC{75c6338f},过期时间是 2023-02-06T05:20:00+08:00
活动已结束,题目打包放到爱盘供大家下载学习(web也一起打包,可以重新下载):
https://down.52pojie.cn/Challenge/Happy_New_Year_2023_Challenge.rar
-官方论坛
www.52pojie.cn
--推荐给朋友
公众微信号:吾爱破解论坛
或搜微信号:pojie_52