吾爱破解 - 2023 春节解题领红包
- Windows 题(2、5)
- 安卓题(3、4、6、7) - 其中 7 挑战失败 ← 你在这里
- Web 题 - 缺少 4、8、9、12。
【春节】解题领红包之三 {Android 初级题}
作为 zip 压缩包打开看看,没有发现 so 文件。直接拉到 JEB 分析。
然后用 JEB 一打开就发现解密部分的表达式已经被静态优化了:
if(this$0.check() == 999) {
Toast.makeText(v4, "快去论坛领CB吧!", 1).show();
key.setText("flag{zhudajiaxinniankuaile}");
}
捡了个漏。
如果硬要分析的话,就得看 smali 代码了:
0000005E const/4 p2, 2
00000060 const-string v0, "hnci}|jwfclkczkppkcpmwckng\u007F"
00000064 invoke-virtual MainActivity->decrypt(String, I)String, p0, v0, p2
传参分别是这个字符串和 p2
,也就是固定的常数 2
。
继续分析解密函数,关键点就是这个 for 循环:
for(i = 0; i < txtArray.length; ++i) {
result.append(((char)(txtArray[i] - delta)));
}
每个字符 -2,放到 JS 里也是轻松解密:
"hnci}|jwfclkczkppkcpmwckng\u007F".split('').map(x => String.fromCharCode(x.charCodeAt() - 2)).join('')
得到同样的过关密码:flag{zhudajiaxinniankuaile}
【春节】解题领红包之四 {Android 初级题}
JEB 打开,直接跳到 MainActivity
代码。
可以看到顶部有一个签名验证,但是我们是静态分析,无视即可。
往下翻,找到关键函数:
private static final void onCreate$lambda-0(MainActivity this$0, View arg4) {
// ... 算法无关代码 ...
String uid = this$0.edit_uid.getText().trim();
if( Flag.INSTANCE.check( uid, this$0.edit_flag.getText().trim() ) ) {
Toast.makeText( ((Context)this$0), "恭喜你,flag正确!", 1 ).show();
} else {
Toast.makeText( ((Context)this$0), "flag错误哦,再想想!", 1 ).show();
}
}
上面的代码中,我已经对部分混淆过的类名进行了分析。对应类名重更名如下:
A -> Flag
B -> Encoder
C -> Crypto
分析基本上没怎么做,因为看代码用到了 MD5,浏览器 JS 跑起来要第三方依赖,还是用 Java 抠代码写注册机简单些。
因为原 APK 用的 Kotlin 加了有很多安全检查代码进去,抠出来后再整理下就是下面这样了:
package cn.lcg.flyingcat;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
class Crypto {
private static char cipher(char c, int delta) {
char base = 0;
if (c >= 'A' && c <= 'Z') base = 'A';
else if (c >= 'a' && c <= 'z') base = 'a';
else return c; // 不处理
int code = c - base;
code = code + delta % 26;
return (char) (code + base);
}
public static String cipher(String str, int delta) {
StringBuilder sb = new StringBuilder();
int n = str.length();
for (int i = 0; i < n; ++i) {
sb.append(cipher(str.charAt(i), delta));
}
return sb.toString();
}
public static String encodeBase64(byte[] src) {
return Base64.getEncoder().encodeToString(src);
}
public static String md5(String data) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(data.getBytes());
byte[] digest = md.digest();
StringBuilder sb = new StringBuilder();
sb.ensureCapacity(32);
for (int b : digest) {
int c = b;
if (c < 0) c += 256; // 溢出
if (c <= 0x0F) sb.append('0'); // 补 0
sb.append(Integer.toHexString(c));
}
return sb.toString();
}
public static String encode(String src) {
int n = src.length();
char[] result = new char[n];
int key = 50;
for (int i = n - 1; i >= 0; i--) {
key ^= (50 ^ 53); // 切换密钥
result[i] = (char) (src.charAt(i) ^ key);
}
return new String(result);
}
}
public class Main {
public static void main(String[] args) throws Exception {
String uid = "176017"; // uid
var flag = Crypto.encode(uid + "Wuaipojie2023").getBytes(StandardCharsets.UTF_8);
var result = Crypto.cipher(Crypto.md5(Crypto.encodeBase64(flag)), 5);
// result == "4k65807686gg2k149h4k338211hi8643"
System.out.println("flag{" + result + "}");
}
}
得到过关密码 flag{4k65807686gg2k149h4k338211hi8643}
。
【春节】解题领红包之六 {Android 中级题}
此题感想:
谜语人滚啊!
JEB 打开 APK,没发现什么东西。有三个 Native 函数,但是并没有调用。
然后有一个函数会判断麦克风音量,根据分贝(?)等级做不同的事情,其中一个情况是写出 aes.png
:
private final void Check_Volume(double vol) {
// 无关代码跳过
int showHint = 0;
if(100 <= vol && vol <= 101) showHint = 1;
if(showHint != 0) {
Toast.makeText(((Context)this), "快去找flag吧", 1).show();
this.write_img(); // 写出 aes.png 到安卓储存区的某个地方,反正能从 APK 里找到
}
}
IDA 打开,没有混淆,轻松定位到对应的三个函数 - 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
内容得到同样的结果。
unidbg 模拟
尝试了一下这玩意,但是只能解出前 160 个字节。不清楚是谜语人 SO 只解密前 160 字节还是哪里调用出毛病了。
package com.bytedance.frameworks.core.encrypt;
import com.alibaba.fastjson.util.IOUtils;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.Symbol;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.memory.MemoryBlock;
import com.github.unidbg.utils.Inspector;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.nio.charset.StandardCharsets;
public class lcg_2023_spring {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
private final boolean logging;
private final Memory memory;
lcg_2023_spring(boolean logging) {
this.logging = logging;
emulator = AndroidEmulatorBuilder.for32Bit()
.setProcessName("com.zj.wuaipojie2023_2")
.addBackendFactory(new Unicorn2Factory(true))
.build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
memory = emulator.getMemory(); // 模拟器的内存操作接口
memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析
vm = emulator.createDalvikVM(); // 创建Android虚拟机
vm.setVerbose(logging); // 设置是否打印Jni调用细节
DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/example_binaries/armeabi-v7a/lib52pj.so"), false); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数
module = dm.getModule(); // 加载好的 so 对应一个模块
}
void destroy() {
IOUtils.close(emulator);
}
void DoWork() throws Exception {
Symbol AES_ECB_PKCS7_DecryptSym = module.findSymbolByName("AES_ECB_PKCS7_Decrypt");
File file = new File("unidbg-android/src/test/resources/aes.png.txt");
byte[] payload_utf8 = FileUtils.readFileToString(file, "UTF-8").trim().getBytes(StandardCharsets.UTF_8);
MemoryBlock payload = memory.malloc(payload_utf8.length + 1, false);
payload.getPointer().write(payload_utf8);
MemoryBlock key = memory.malloc(17, false);
key.getPointer().write("wuaipojie2023114".getBytes(StandardCharsets.UTF_8));
Number ret = AES_ECB_PKCS7_DecryptSym.call(emulator, payload.getPointer(), key.getPointer());
byte[] result = emulator.getBackend().mem_read(ret.longValue(), 0x100);
Inspector.inspect(result, "result (ECB)");
payload.free();
key.free();
}
public static void main(String[] args) throws Exception {
lcg_2023_spring test = new lcg_2023_spring(true);
test.DoWork();
test.destroy();
}
}
【春节】解题领红包之七 {Android 高级题}
混淆太厉害了,不太懂如何对抗。过两天偷学一手别人的 Writeup 把混淆部分去掉后再分析吧。当初整了几个钟头后摆烂了。
试了下跟踪生成 log 来自动填充调用 + 去花 + 修正逻辑跳转,但是去花脚本写得太菜了,不少正常的代码也被干掉了。
去花脚本:
import idc
import ida_bytes
def patch_bytes(ea, data):
for i in range(len(data)):
ida_bytes.patch_byte(ea + i, data[i])
INST_NOP = [0x1F, 0x20, 0x03, 0xD5]
def nop_factory(count):
nop = INST_NOP * count
return lambda ea: patch_bytes(ea, nop)
def remove_junk_all_inst(action, pattern):
text_beg = 0x0000_D5C0
text_end = 0x0004_D21C
cur_addr = text_beg
while cur_addr < text_end:
cur_addr = idc.find_binary(cur_addr, idc.SEARCH_DOWN, pattern)
if cur_addr == idc.BADADDR: break
action(cur_addr)
cur_addr += 1
remove_junk_all_inst(
nop_factory(7),
'88 02 40 B9 1F 29 00 71 AB 00 00 54 A8 02 40 B9 09 05 00 51 28 7D 08 1B 48 FE 07 37'
)
remove_junk_all_inst(
nop_factory(7),
'1F 29 00 71 CB 00 00 54 A8 02 40 B9 09 05 00 51 28 7D 08 1B 48 00 00 36 00 00 00 14'
)
# .text:00000000000112BC 1F 29 00 71 CMP W8, #0xA
# .text:00000000000112C0 0B 01 00 54 B.LT loc_112E0
# .text:00000000000112C4 A8 02 00 F0 ADRP X8, #Oo0O.57_ptr@PAGE
# .text:00000000000112C8 08 11 46 F9 LDR X8, [X8,#Oo0O.57_ptr@PAGEOFF]
# .text:00000000000112CC 08 01 40 B9 LDR W8, [X8]
# .text:00000000000112D0 09 05 00 51 SUB W9, W8, #1
# .text:00000000000112D4 28 7D 08 1B MUL W8, W9, W8
# .text:00000000000112D8 48 00 00 36 TBZ W8, #0, loc_112E0
# .text:00000000000112DC
# .text:00000000000112DC loc_112DC ; CODE XREF: sub_112B0:loc_112DC↓j
# .text:00000000000112DC 00 00 00 14 B loc_112DC
remove_junk_all_inst(
nop_factory(9),
'1F 29 00 71 0B 01 00 54 ?? ?? ?? ?? ?? ?? ?? ?? 08 01 40 B9 09 05 00 51 28 7D 08 1B 48 00 00 36 00 00 00 14'
)
# .text:0000000000011B18 3F 29 00 71 CMP W9, #0xA
# .text:0000000000011B1C 8B 00 00 54 B.LT loc_11B2C
# .text:0000000000011B20 0B 05 00 51 SUB W11, W8, #1
# .text:0000000000011B24 6B 7D 08 1B MUL W11, W11, W8
# .text:0000000000011B28 8B 03 00 37 TBNZ W11, #0, loc_11B98
# .text:0000000000011AF8 3F 29 00 71 CMP W9, #0xA
# .text:0000000000011AFC 8B 00 00 54 B.LT loc_11B0C
# .text:0000000000011B00 0B 05 00 51 SUB W11, W8, #1
# .text:0000000000011B04 6B 7D 08 1B MUL W11, W11, W8
# .text:0000000000011B08 0B FE 07 37 TBNZ W11, #0, loc_11AC8
remove_junk_all_inst(
nop_factory(5),
'3F 29 00 71 8B 00 00 54 0B 05 00 51 6B 7D 08 1B ?? ?? ?? 37')
# .text:0000000000012C78 E9 A7 9F 1A CSET W9, LT
# .text:0000000000012C7C 28 01 08 2A ORR W8, W9, W8
# .text:0000000000012C80
# .text:0000000000012C80 loc_12C80 ; CODE XREF: sub_12C38:loc_12C80↓j
# .text:0000000000012C80 08 00 00 34 CBZ W8, loc_12C80
remove_junk_all_inst(nop_factory(3), 'E9 A7 9F 1A 28 01 08 2A 08 00 00 34')
# .text:0000000000010140 3F 29 00 71 CMP W9, #0xA
# .text:0000000000010144 CA 04 00 54 B.GE loc_101DC
# .text:0000000000010148 9F FF FF 17 B loc_FFC4
remove_junk_all_inst(nop_factory(2), '3F 29 00 71 ?? ?? 00 54 ?? ?? ?? 17')
###
def blt_to_bal_factory(offset):
def blt_to_bal(ea):
cond_flag = ida_bytes.get_byte(ea + offset)
cond_flag &= 0b1110_0000
cond_flag |= 0b0000_1110
ida_bytes.patch_byte(ea + offset, cond_flag)
# print(f'patching {hex(ea + offset)}: {hex(cond_flag)}')
return blt_to_bal
# .text:0000000000012ABC 88 02 40 B9 LDR W8, [X20]
# .text:0000000000012AC0 1F 29 00 71 CMP W8, #0xA
# .text:0000000000012AC4 4B 03 00 54 B.LT loc_12B2C
# 4B -> 4E
# .text:0000000000012898 C8 02 40 B9 LDR W8, [X22]
# .text:000000000001289C 1F 29 00 71 CMP W8, #0xA
# .text:00000000000128A0 AB 00 00 54 B.LT loc_128B4
# .text:0000000000012E20 A8 02 40 B9 LDR W8, [X21]
# .text:0000000000012E24 1F 29 00 71 CMP W8, #0xA
# .text:0000000000012E28 AB 00 00 54 B.LT loc_12E3C
# .text:0000000000010080 08 01 40 B9 LDR W8, [X8]
# .text:0000000000010084 1F 29 00 71 CMP W8, #0xA
# .text:0000000000010088 EB 00 00 54 B.LT loc_100A4
remove_junk_all_inst(blt_to_bal_factory(8),
'?? ?? 40 B9 1F 29 00 71 ?? ?? 00 54')
# .text:0000000000010104 3F 29 00 71 CMP W9, #0xA
# .text:0000000000010108 08 01 40 B9 LDR W8, [X8]
# .text:000000000001010C 8B 00 00 54 B.LT loc_1011C
remove_junk_all_inst(blt_to_bal_factory(8),
'3F 29 00 71 08 01 40 B9 8B 00 00 54')
# .text:0000000000012B94 88 02 40 B9 LDR W8, [X20]
# .text:0000000000012B98 B5 86 45 F9 LDR X21, [X21,#Oo0O.123_ptr@PAGEOFF]
# .text:0000000000012B9C 1F 29 00 71 CMP W8, #0xA
# .text:0000000000012BA0 AB 00 00 54 B.LT loc_12BB4
remove_junk_all_inst(blt_to_bal_factory(12),
'?? 02 40 B9 ?? ?? ?? ?? ?? 29 00 71 ?? ?? 00 54')
# .text:00000000000158E8 1F 29 00 71 CMP W8, #0xA
# .text:00000000000158EC AB F1 FF 54 B.LT loc_15720
# .text:00000000000158F0 2B 05 00 51 SUB W11, W9, #1
# .text:00000000000158F4 6B 7D 09 1B MUL W11, W11, W9
# .text:00000000000158F8 4B F1 07 36 TBZ W11, #0, loc_15720
remove_junk_all_inst(
blt_to_bal_factory(4),
'?? 29 00 71 ?? ?? ?? 54 ?? 05 00 51 ?? ?? ?? 1B ?? ?? ?? 36')
# .text:00000000000157F4 1F 29 00 71 CMP W8, #0xA
# .text:00000000000157F8 4A 01 0B 0B ADD W10, W10, W11
# .text:00000000000157FC 6B 07 00 54 B.LT loc_158E8
# .text:0000000000015800 2B 05 00 51 SUB W11, W9, #1
# .text:0000000000015804 6B 7D 09 1B MUL W11, W11, W9
# .text:0000000000015808 0B 07 00 36 TBZ W11, #0, loc_158E8
remove_junk_all_inst(
blt_to_bal_factory(8),
'?? 29 00 71 ?? ?? ?? ?? ?? ?? ?? 54 ?? 05 00 51 ?? ?? ?? 1B ?? ?? ?? 36')
# .text:00000000000159C8 3F 29 00 71 CMP W9, #0xA
# .text:00000000000159CC AB 00 00 54 B.LT loc_159E0
# .text:00000000000159D0 C9 02 40 B9 LDR W9, [X22]
# .text:00000000000159D4 2A 05 00 51 SUB W10, W9, #1
# .text:00000000000159D8 49 7D 09 1B MUL W9, W10, W9
# .text:00000000000159DC C9 FC 07 37 TBNZ W9, #0, loc_15974
remove_junk_all_inst(
blt_to_bal_factory(4),
'?? 29 00 71 ?? ?? ?? 54 ?? ?? ?? ?? ?? 05 00 51 ?? ?? ?? 1B ?? ?? ?? 36')
条件跳转修复(半成品,写的很粗糙):
from typing import Callable
import re
import idc
import ida_bytes
from struct import pack
verbose = False
def istn_name(ea):
return print_insn_mnem(ea)
def istn_operand(ea, pos):
return print_operand(ea, pos)
def patch_bytes(ea, data):
for i in range(len(data)):
ida_bytes.patch_byte(ea + i, data[i])
def normalize_addr(addr):
return addr & 0xFFFF_FFFF
def get_dword(addr):
return normalize_addr(ida_bytes.get_dword(normalize_addr(addr)))
def get_register_without_prefix(reg_name):
m = rMatchRegister.fullmatch(reg_name)
if m == None:
if verbose:
print(f"WARN: failed to match reg: {reg_name}")
return -1
if m.group(1) == 'ZR':
return SPECIAL_REGISTER_ZERO
return int(m.group(1))
def parse_ida_int(value: str):
if value == '0': return 0
if value.startswith('0x'):
return int(value[2:], 16)
if value.startswith('0'):
return int(value[1:], 8) # should be rare?
return int(value)
class InstParseException(Exception):
pass
SPECIAL_REGISTER_ZERO = 100
rMatchRegister = re.compile(r'[XW](\d+|ZR)')
rOperandIndirectRegImm = re.compile(r'\[(X\d+|ZR),#?(-?0x[\da-fA-F]+|\d+)\]')
rOperandIndirectRegReg = re.compile(r'\[(X\d+|ZR),(X\d+|ZR)\]')
rOperandIndirectRegX = re.compile(r'\[(X\d+|ZR)\]')
rOperandRegX = re.compile(r'X(\d+|ZR)')
rOperandRegW = re.compile(r'W(\d+|ZR)')
rOperandImm = re.compile(r'#?(-?0x[\da-fA-F]+|\d+)')
rOperandWithLSL16 = re.compile(r'#?(-?0x[\da-fA-F]+|\d+),LSL#16')
INST_NOP = [0x1F, 0x20, 0x03, 0xD5]
# https://developer.arm.com/documentation/ddi0406/c/Application-Level-Architecture/ARM-Instruction-Set-Encoding/ARM-instruction-set-encoding
condition_encode = {
'EQ': 0b0000,
'NE': 0b0001,
'CS': 0b0010,
'CC': 0b0011,
'MI': 0b0100,
'PL': 0b0101,
'VS': 0b0110,
'VC': 0b0111,
'HI': 0b1000,
'LS': 0b1001,
'GE': 0b1010,
'LT': 0b1011,
'GT': 0b1100,
'LE': 0b1101,
'AL': 0b1110, # unconditional
}
def encode_jump(curr_addr, jump_addr, condition=''):
# .text:000000000000E8FC 60 02 00 54 B.EQ loc_E948
# .text:000000000000E900 04 00 00 14 B loc_E910
# .text:000000000000E904 ; ---------------------------------------------------------------------------
# .text:000000000000E904 1F 20 03 D5 NOP
# .text:000000000000E908 1F 20 03 D5 NOP
# .text:000000000000E90C 1F 20 03 D5 NOP ; loc_E910
delta = jump_addr - curr_addr
if delta < 0:
delta += 0x1_0000_0000_0000_0000
if verbose:
print(f'delta = {hex(delta)}: {hex(jump_addr)} - {hex(curr_addr)}')
opcode = 0b000000 # 6 bits
if condition == '': # unconditional jump
opcode |= 0b0001_01
delta = delta >> 2
delta &= 0x03_FF_FF_FF
else:
opcode |= 0b0101_01
delta = delta << 3 | condition_encode[condition]
delta &= 0xFF_FF_FF
operand = delta | (opcode << 26)
byte_code = pack('<I', operand)
return byte_code
class ConditionFixer:
text_start: int
text_end: int
csel_mapping = {}
cset_mapping = {}
register_override = {}
def __init__(self, text_start, text_end, register_override=None):
self.text_start = text_start
self.text_end = text_end
if register_override is not None:
self.register_override = register_override
print(f'using reg override: {register_override}')
def resolve_indirect_operand_addr(self, ea, op_pos, descend_max=10):
op = istn_operand(ea, op_pos)
if m := rOperandIndirectRegImm.fullmatch(op):
v_first = self.find_prev_assign(ea, m.group(1), descend_max - 1)
v_second = parse_ida_int(m.group(2))
return v_first + v_second
elif m := rOperandIndirectRegReg.fullmatch(op):
v_first = self.find_prev_assign(ea, m.group(1), descend_max - 1)
v_second = self.find_prev_assign(ea, m.group(2), descend_max - 1)
return v_first + v_second
elif m := rOperandIndirectRegX.fullmatch(op):
reg_value = self.find_prev_assign(ea, m.group(1), descend_max - 1)
return reg_value
return None
def resolve_operand(self, ea, op_pos, descend_max=10):
indirect_addr = self.resolve_indirect_operand_addr(
ea, op_pos, descend_max)
if indirect_addr is not None:
return get_dword(indirect_addr)
op = istn_operand(ea, op_pos)
if m := rOperandRegX.fullmatch(op):
return normalize_addr(
self.find_prev_assign(ea, m.group(0), descend_max - 1))
elif m := rOperandRegW.fullmatch(op):
v = self.find_prev_assign(ea, m.group(0), descend_max - 1)
return normalize_addr(v)
elif m := rOperandImm.fullmatch(op):
return normalize_addr(parse_ida_int(m.group(1)))
elif m := rOperandWithLSL16.fullmatch(op):
return normalize_addr(parse_ida_int(m.group(1)) << 16)
else:
raise InstParseException(
f'unsupported operand to resolve: {hex(ea)} / op = {op}')
def resolve_istn_value(self, ea, descend_max=10, ldp_offset=0):
name = istn_name(ea)
if name == 'LDR':
return self.resolve_operand(ea, 1, descend_max)
if name == 'LDP':
addr = self.resolve_indirect_operand_addr(ea, 2)
if addr == None:
raise InstParseException(
f'could not resolve ptr for LDP instruction at {hex(ea)}')
return get_dword(addr + ldp_offset * 8)
elif name == 'MOV':
return self.resolve_operand(ea, 1, descend_max)
elif name == 'ADRL':
return self.resolve_operand(ea, 1, descend_max)
elif name == 'ADRP':
return self.resolve_operand(ea, 1, descend_max)
elif name == 'ADD':
param1 = self.resolve_operand(ea, 1, descend_max)
param2 = self.resolve_operand(ea, 2, descend_max)
if verbose:
print(
f'{hex(ea)}: ADD {hex(param1)}, {hex(param2)} => {hex(param1 + param2)}'
)
return param1 + param2
elif name == 'SUB':
param1 = self.resolve_operand(ea, 1, descend_max)
param2 = self.resolve_operand(ea, 2, descend_max)
return param1 - param2
elif name == 'LSL':
param1 = self.resolve_operand(ea, 1, descend_max)
param2 = self.resolve_operand(ea, 2, descend_max)
return param1 << param2
elif name == 'MOVK':
low_16_bit = self.find_prev_assign(ea, istn_operand(ea, 0),
descend_max - 1)
low_16_bit &= 0xFFFF
high_16_bit = self.resolve_operand(ea, 1, descend_max)
high_16_bit &= 0xFFFF_0000
return (high_16_bit | low_16_bit)
elif name == 'CSEL':
if ea not in self.csel_mapping:
raise InstParseException(
f'CSEL selection not defined in {hex(ea)}')
return self.resolve_operand(ea, self.csel_mapping[ea], descend_max)
elif name == 'CSET':
if ea not in self.cset_mapping:
raise InstParseException(
f'CSET selection not defined in {hex(ea)}')
# should be 1 (taken) or 0 (not taken)
return self.cset_mapping[ea]
else:
raise InstParseException(
f'unknown opcode when attempting to resolve: {hex(ea)}')
def find_prev_assign(self, ea, reg_name, descend_max=10):
if descend_max <= 0:
raise InstParseException('reached max descend limit.')
target_reg = get_register_without_prefix(reg_name)
assert target_reg != -1, f"could not find reg name from {reg_name}"
if target_reg == SPECIAL_REGISTER_ZERO:
return 0
if reg_name in self.register_override:
value = self.register_override[reg_name]
print(f'{hex(ea)}: REG OVERRIDE {reg_name} => {value}')
return value
curr_addr = ea
max_look_back = max(curr_addr - 1000 * 4, self.text_start)
while curr_addr >= max_look_back:
curr_addr -= 4
name = istn_name(curr_addr)
if name == '' or name[0] == 'B': continue
if name == 'LDP':
curr_reg = get_register_without_prefix(
istn_operand(curr_addr, 1))
if curr_reg == target_reg:
result = self.resolve_istn_value(curr_addr,
descend_max,
ldp_offset=1)
return result
curr_reg = get_register_without_prefix(istn_operand(curr_addr, 0))
if curr_reg == target_reg:
if verbose:
print(f'found assignment to {reg_name}'
f' in {hex(curr_addr)}')
result = self.resolve_istn_value(curr_addr, descend_max)
if verbose:
print(f'{hex(curr_addr)}: '
f'{reg_name} resolved to {hex(result)}')
return result
raise InstParseException(
f'could not find assignment to {reg_name}: {hex(ea)}')
def find_prev_cmp(self, ea):
curr_addr = ea
max_look_back = max(curr_addr - 1000 * 4, self.text_start)
while curr_addr >= max_look_back:
curr_addr -= 4
if istn_name(curr_addr) == 'CMP':
return [
curr_addr,
istn_operand(curr_addr, 0),
istn_operand(curr_addr, 1),
]
print(f'CMP inst not found :/')
def find_next_matching_istn(self,
ea,
filter: Callable[[int], bool],
max_itsn_distance: int = 1000):
max_look_ahead = min(ea + max_itsn_distance * 4, self.text_end)
for addr in range(ea + 4, max_look_ahead, 4):
# print(f'checking {hex(addr)}: {istn_name(addr)}')
if filter(addr):
return addr
raise InstParseException(f'could not find expected instruction')
def analysis_csel_br(self, ea):
# Search for BR instruction
next_br = self.find_next_matching_istn(
ea, lambda addr: istn_name(addr) == 'BR', 40)
name = istn_name(ea)
if name == 'CSET':
reg_cond_name = istn_operand(ea, 1)
self.cset_mapping[ea] = 1
addr_when_take = normalize_addr(self.resolve_operand(next_br, 0))
self.cset_mapping[ea] = 0
addr_when_miss = normalize_addr(self.resolve_operand(next_br, 0))
elif name == 'CSEL':
reg_cond_name = istn_operand(ea, 3)
self.csel_mapping[ea] = 1
addr_when_take = normalize_addr(self.resolve_operand(next_br, 0))
self.csel_mapping[ea] = 2
addr_when_miss = normalize_addr(self.resolve_operand(next_br, 0))
else:
raise InstParseException('unsupported instruction')
print('-' * 30)
print(f'{hex(ea + 0)} B.{reg_cond_name} {hex(addr_when_take)}'
f' -- {encode_jump(ea + 0, addr_when_take, reg_cond_name)}')
print(f'{hex(ea + 4)} B {hex(addr_when_miss)}'
f' -- {encode_jump(ea + 4, addr_when_miss)}')
return [reg_cond_name, addr_when_take, addr_when_miss]
def patch_csel_br(self, ea):
result = self.analysis_csel_br(ea)
[reg_cond_name, addr_when_take, addr_when_miss] = result
encoded_jump = encode_jump(ea + 0, addr_when_take, reg_cond_name)
patch_bytes(ea + 0, encoded_jump)
encoded_jump = encode_jump(ea + 4, addr_when_miss)
patch_bytes(ea + 4, encoded_jump)
print(f'... patched')
print('-' * 30)
return result
# .text 000000000000D5C0 000000000004D21C R . X . L dword 06 public CODE 64 00 0F
def analysis_csel_at(ea=None, patch=False, register_override=None):
if ea == None: ea = idc.get_screen_ea()
fixer = ConditionFixer(0x000000000000D5C0,
0x000000000004D21C,
register_override=register_override)
if patch: return fixer.patch_csel_br(ea)
else: return fixer.analysis_csel_br(ea)
def analysis_csel_in_function(ea=None, patch=False):
if ea == None: ea = idc.get_screen_ea()
search_beg = idc.get_func_attr(ea, idc.FUNCATTR_START)
search_end = idc.get_func_attr(ea, idc.FUNCATTR_END)
result = []
for addr in range(search_beg, search_end, 4):
if istn_name(addr) == 'CSEL':
result.append(analysis_csel_at(addr, patch))
return result
def analysis_resolve_call_fn(ea=None):
if ea == None: ea = idc.get_screen_ea()
fixer = ConditionFixer(0x000000000000D5C0, 0x000000000004D21C)
addr = fixer.resolve_operand(ea, 0)
print(f'param1 resolved at: {hex(addr)}')
def main(analysis_only=True):
print(f' {"-" * 30} begin new session {"-" * 30}')
text_start = 0x000000000000D5C0
text_end = 0x000000000004D21C
ok = 0
fail = 0
ea = text_start
while ea < text_end:
ea += 4
if istn_name(ea) == 'CSEL':
try:
fixer = ConditionFixer(0x000000000000D5C0, 0x000000000004D21C)
if analysis_only:
fixer.analysis_csel_br(ea)
else:
fixer.patch_csel_br(ea)
print(f'OK: {hex(ea)}')
ok += 1
except InstParseException as ex:
fail += 1
# print(f'could not handle {hex(ea)}: {ex}')
except Exception as ex:
print(f'crashed at {hex(ea)}: {ex}')
return
# if fail > 10:
# print('terminated: too many failure.')
# break
print(f'ok({ok}) fail({fail})')
# main(False)
# ConditionFixer(0x000000000000D5C0, 0x000000000004D21C).analysis_csel_br(0xE8FC)
上面这个脚本在处理数据的时候有毛病,要手动到 CSEL ...
这样的语句处手动执行,看分析的地址对不对。
例如这一段:
.text:000000000001ADE0 49 72 41 F9 LDR X9, [X18,#0x2E0]
.text:000000000001ADE4 1F 01 0F 6B CMP W8, W15
.text:000000000001ADE8 2B B2 90 9A CSEL X11, X17, X16, LT
.text:000000000001ADEC 29 01 00 8B ADD X9, X9, X0
.text:000000000001ADF0 2B 69 6B F8 LDR X11, [X9,X11]
.text:000000000001ADF4 6B 01 01 8B ADD X11, X11, X1
.text:000000000001ADF8 60 01 1F D6 BR X11
选中 1ADE8
后在 IDAPython 控制台输入 analysis_csel_at()
进行分析:
Python>analysis_csel_at()
------------------------------
0x1ade8 B.LT 0x1adfc -- b'\xab\x00\x00T'
0x1adec B 0x1aea0 -- b'-\x00\x00\x14'
['LT', 0x1adfc, 0x1aea0]
确认分析正确,加上 patch=True
参数进行自动补丁:
Python>analysis_csel_at(patch=True)
自动补丁后:
text:000000000001ADE0 49 72 41 F9 LDR X9, [X18,#0x2E0]
.text:000000000001ADE4 1F 01 0F 6B CMP W8, W15
.text:000000000001ADE8 AB 00 00 54 B.LT loc_1ADFC
.text:000000000001ADEC 2D 00 00 14 B loc_1AEA0
.text:000000000001ADF0 2B 69 6B F8 LDR X11, [X9,X11]
.text:000000000001ADF4 6B 01 01 8B ADD X11, X11, X1
.text:000000000001ADF8 60 01 1F D6 BR X11
然后按下 alt-p
让 IDA 重新分析该函数。
缺点就是,效率很低;遇到非常数形式的指令也不懂(如 .text:1AE34 ADRP X8, #off_6A2F0@PAGE
)。也不懂得做优化/分析,只能单纯的向上找。
之前顶着混淆分析,结果发现分析的是一堆类似 Vector 数据类型相关的函数… 吐血。
checkSN
函数的大概逻辑:
BOOL __fastcall checkSN(JNIEnv *env, __int64 a2, jstring jstr_uid, void *jstr_flag)
{
bool check_ok; // w19
const char *str_uid; // x21
const char *str_flag; // x23
uint64_t flag_len; // x0
__int64 v12; // x0
uint8_t *DataPointer_0; // x19
uint64_t Length_0; // x0
uint8_t *p_flag_data; // x19
__int64 p_flag_len; // x0
uint8_t *ptr_s1; // x20
uint8_t *ptr_s2; // x0
unsigned __int64 flag_len_1; // x0
int v20; // w0
int v21; // w20
uint8_t *expected_hash; // x19
unsigned __int64 Length_1; // x0
char v24[8]; // [xsp+8h] [xbp-578h] BYREF
Vector s2; // [xsp+10h] [xbp-570h] BYREF
Vector s1; // [xsp+28h] [xbp-558h] BYREF
Vector vec_uid_dup; // [xsp+40h] [xbp-540h] BYREF
Vector v28; // [xsp+58h] [xbp-528h] BYREF
Vector vec_flag; // [xsp+70h] [xbp-510h] BYREF
Vector vec_uid; // [xsp+88h] [xbp-4F8h] BYREF
int a1; // [xsp+A4h] [xbp-4DCh] BYREF
Vector actual_hash; // [xsp+A8h] [xbp-4D8h] BYREF
char v33[88]; // [xsp+4C8h] [xbp-B8h] BYREF
__int64 v34; // [xsp+520h] [xbp-60h]
v34 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
if ( JNI::GetStringUTFLength(env, jstr_flag) == 44 && JNI::GetStringUTFLength(env, jstr_uid) == 8 )
{
str_uid = JNI::GetStringUTFChars(env, jstr_uid, 0LL);
Vector::InitFromString(&vec_uid, str_uid);
str_flag = (*env)->GetStringUTFChars(env, jstr_flag, 0LL);
Vector::Reset2(&vec_flag);
flag_len = strlen_0(str_flag);
Vector::AddData(&vec_flag, (uint8_t *)str_flag, flag_len);
(*env)->ReleaseStringUTFChars(env, jstr_flag, str_flag);
(*env)->ReleaseStringUTFChars(env, jstr_uid, str_uid);
Vector::Copy(&vec_uid_dup, &vec_uid);
sub_1B32C(&vec_uid_dup);
Vector::SafeFree(&vec_uid_dup);
// Initialize with data from a table?
Vector::Reset2(&s1);
Vector::AddData(&s1, byte_4D220, 10uLL);
Vector::Reset2(&s2);
Vector::AddData(&s2, &byte_4D220[11], 0x10uLL);
a1 = 0x69AB81DE;
v12 = sub_10214(&a1); // 时间相关
sub_1F588(v12 / 20000000, (__int64)&actual_hash);
DataPointer_0 = Vector::GetDataPointer_0(&actual_hash);
Length_0 = Vector::GetLength_0(&actual_hash);
sub_13318(&s1, DataPointer_0, Length_0);
Vector::SafeFree(&actual_hash);
p_flag_data = Vector::GetDataPointer_1(&vec_flag);
p_flag_len = Vector::GetLength_1(&vec_flag);
if ( sub_1B788((__int64)p_flag_data, p_flag_len) )
{
ptr_s1 = Vector::GetDataPointer_1(&s1);
ptr_s2 = Vector::GetDataPointer_1(&s2);
if ( sub_165F0((uint64_t)v33, (__int64)ptr_s1, (__int64)ptr_s2) != 1 )
{
check_ok = 1;
LB_CHECK_COMPLETE:
Vector::SafeFree(&s2);
Vector::SafeFree(&s1);
Vector::SafeFree(&v28);
Vector::SafeFree(&vec_flag);
Vector::SafeFree(&vec_uid);
return check_ok;
}
memset(&actual_hash, 0, 0x420u);
flag_len_1 = strlen((const char *)p_flag_data);
sub_168E4((__int64)v33, p_flag_data, flag_len_1, (__int64)&actual_hash, v24);
v21 = v20;
free(p_flag_data);
if ( v21 == 1 )
{
expected_hash = Vector::GetDataPointer_1(&v28);
Length_1 = Vector::GetLength_1(&v28);
a1 = 0x37FA57CD;
check_ok = sub_104C8(&a1, (uint8_t *)&actual_hash, expected_hash, Length_1) == 0;
if ( sub_173B8() == 1 )
goto LB_CHECK_COMPLETE;
}
}
check_ok = 0;
goto LB_CHECK_COMPLETE;
}
return 0;
}
最坑的是里面还有状态机打乱执行流程。去掉混淆后这个倒还也能看,加点注释就好。