0x1 基础准备
1.先去攻防世界:下载目标easy-so题目的文件https://adworld.xctf.org.cn/media/file/task/456c1dab04b24036ba1d6e32a08dc882.apk
2.工具:雷电模拟器、NP管理器、IDA、frida、deepseek。
3.本文目的,每次小菜鸟隔好久再来弄就忘记怎么操作,这个干脆把常用的工具步骤记得详详细细的,温故而知新。
4.向deepseek提问,从简洁到复杂,我要干什么?-》我在干什么?遇到什么问题?,如果解决方式不是自己想要的,告诉它以XX的方式干这个事情。
5.注意事项:
1)、雷电模拟器要开启root、硬盘可写入、开发者模式,最好不要有第三方root程序,防止因为未知原因干扰root权限调用
2)、ida和frida调试程序时要上传对应版本的服务端,连接不上模拟器可以通过端口转发,ip的方式进行连接。
6.逆向思维:
1)、字符串a经过过程1、2、3变成b,现在已知最终b要等于xxx,如果反推a?倒着看从3个步骤开始,如果前面是加,那我推回去就是减,如果前面是第一个字符交换,我推回去就是第二个字符与第一个字符交换,这里会发现这个过程正推逆推都一样,所以可以直接复用这个算法,异或和这个类似,两次异或就还原了。
2)、还有一种情况a经过n多种算法变成b,直接推回去很难,但是字符串比较短,例如只有4位是变化的,那么可以尝试爆破法,复用程序中的算法,穷举的方式比较判断得到输入的值。
0x2 分析java层代码
1.使用np管理器的安装包提取功能,提取安装包。
2.然后定位到apk文件,点查看
3.打开apk,可以看到有一个输入框,有一个check按钮,随便输入123,点击check按钮提示验证失败。
4.回到np管理器,发起新搜索,搜索“验证失败”字符串
1)、定位到如下的smali代码
复制代码 隐藏代码.line 28
:cond_26
iget-object v2, p0, Lcom/testjava/jack/pingan2/MainActivity$1;->this$0:Lcom/testjava/jack/pingan2/MainActivity;
const-string v3, "验证失败!"
invoke-static {v2, v3, v4}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
move-result-object v2
invoke-virtual {v2}, Landroid/widget/Toast;->show()V
2)、向上翻可以看到关键跳前面的函数CheckString
复制代码 隐藏代码.line 22
.local v1, "strIn":Ljava/lang/String;
invoke-static {v1}, Lcom/testjava/jack/pingan2/cyberpeace;->CheckString(Ljava/lang/String;)I
move-result v2
if-ne v2, v4, :cond_26
3)、再往上翻是onclick函数,也就说我们输入内容,单击check按钮后会进入这个函数,然后通过CheckString校验我们输入的字符串是否正确。
复制代码 隐藏代码# virtual methods
.method public onClick(Landroid/view/View;)V
5.选择这个函数,点三个点的地方就能跳转到函数声明处,然后看到了native关键字,说明要去so层才能看到代码。
复制代码 隐藏代码# classes.dex
.class publicLcom/testjava/jack/pingan2/cyberpeace;
.superLjava/lang/Object;
.source"cyberpeace.java"
# direct methods
.method static constructor <clinit>()V
.registers 1
.prologue
.line 9
const-string v0, "cyberpeace"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
.line 10
return-void
.end method
.method public constructor <init>()V
.registers 1
.prologue
.line 7
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
.method public static native CheckString(Ljava/lang/String;)I
.end method
6.点np管理器右上角的三个点可以将smali代码转换为java代码,因为这里java层代码没什么可分析的,所以java层的分析到此为止了。
复制代码 隐藏代码//
// Decompiled by Jadx (from NP Manager)
//
package com.testjava.jack.pingan2;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.EditText;
import android.widget.Toast;
classMainActivity$1 implementsOnClickListener {
final/* synthetic */ MainActivity this$0;
public void onClick(View view) {
if (cyberpeace.CheckString(((EditText) this.this$0.findViewById(2131165233)).getText().toString()) == 1) {
Toast.makeText(this.this$0, "验证通过!", 1).show();
} else {
Toast.makeText(this.this$0, "验证失败!", 1).show();
}
}
MainActivity$1(MainActivity mainActivity) {
this.this$0 = mainActivity;
}
}
0x3 so函数分析
1.回到np管理器,找到apk文件这次还是点查,进入到lib目录(存放so文件),因为这里用的是雷电模拟器64位,所以选择x86_64位的so文件。ida安装在我们的win系统上,所以选左边文件,长按可以添加到右边,长按右边可以添加到左边,右边的目录实际映射到本机的。
2.使用雷电模拟器自带的文件夹共享功能,打开电脑文件夹就能找到那个so文件了。
3.使用64位 ida加载so文件,找到CheckString函数,按F5就会出现伪代码
复制代码 隐藏代码_BOOL8 __fastcall Java_com_testjava_jack_pingan2_cyberpeace_CheckString(__int64 a1, __int64 a2, __int64 a3)
{
const char *v3; // r14
size_t v4; // rax
int v5; // r15d
unsigned __int64 v6; // r12
char *v7; // rax
char *v8; // r13
bool v9; // cc
size_t v10; // r12
size_t v11; // rbx
char v12; // al
char v13; // al
size_t v14; // rbx
char v15; // al
v3 = (const char *)(*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)a1 + 1352LL))(a1, a3, 0LL);
v4 = strlen(v3);
v5 = v4;
v6 = (__int64)((v4 << 32) + 0x100000000LL) >> 32;
v7 = (char *)malloc(v6);
v8 = v7;
v9 = v6 <= v5;
v10 = v6 - v5;
if ( v9 )
v10 = 0LL;
memset(&v7[v5], 0, v10);
memcpy(v8, v3, v5);
if ( strlen(v8) >= 2 )
{
v11 = 0LL;
do
{
v12 = v8[v11];
v8[v11] = v8[v11 + 16];
v8[v11++ + 16] = v12;
}
while ( strlen(v8) >> 1 > v11 );
}
v13 = *v8;
if ( *v8 )
{
*v8 = v8[1];
v8[1] = v13;
if ( strlen(v8) >= 3 )
{
v14 = 2LL;
do
{
v15 = v8[v14];
v8[v14] = v8[v14 + 1];
v8[v14 + 1] = v15;
v14 += 2LL;
}
while ( strlen(v8) > v14 );
}
}
return strcmp(v8, "f72c5a36569418a20907b55be5bf95ad") == 0;
}
4.可以看到大概就是v8经过一系列运算,最后和f72c5a36569418a20907b55be5bf95ad比较,相等就是验证通过,如果是原来我需要一行行阅读代码,现在借助deepseek吧,试了试分析的结果,我拿去输入验证失败。
复制代码 隐藏代码一、变换流程分析
函数对输入字符串 v3 进行两次变换:
1. 第一阶段变换(位置交换)
操作:交换 v8[i] 和 v8[i+16](0 ≤ i < len/2)。
效果:将字符串的前16个字符与后16个字符交换位置。
示例:
输入字符串 ABCDEFGHIJKLMNOPQRSTUVWXYZ123456(32字符)
交换后变为 QRSTUVWXYZ123456ABCDEFGHIJKLMNOP。
2. 第二阶段变换(字符对交换)
操作:
交换 v8[0] 和 v8[1]。
从索引2开始,每两个字符交换一次(swap(v8[2]↔v8[3], v8[4]↔v8[5], ...)。
效果:将字符串的字符对顺序打乱。
示例:
输入字符串 123456
交换后变为 214365。
最终正确输入
59da5efbb55b70902a81496563a5c27f
5.deepseek的分析看起来很专业,为什么不对呢?先记下deepseek的分析,手动模拟一下变换过程。
1)、将字符串的前16个字符与后16个字符交换位置
复制代码 隐藏代码f72c5a36569418a20907b55be5bf95ad
f72c5a36569418a2
0907b55be5bf95ad
0907b55be5bf95adf72c5a36569418a2
2)、第二阶段变换:字符对交换,每两个字符交换一次
复制代码 隐藏代码09
07
b5
5b
e5
bf
95
ad
f7
2c
5a
36
56
94
18
a2
90705bb55efb59da7fc2a5636549812a
3)、最终得到flag:90705bb55efb59da7fc2a5636549812a,拿去验证通过。
4)、这里说明一下,是输入按照这个步骤变换成输出,为什么我输出作输入可以模拟直接这样换,因为第一排和第二排交换,再次一次交换就换回了,第一个字符和第二个字符交换,再次一次也是换回了,算法都一样。
6.我以为deepseek在手就能通杀简单的CTF,万万没想到ai逻辑分析正确,代码和输出结果不对,但凡我会写代码都不会吃这样的亏,流下来不会算法的泪水,还好算法不复杂,靠大脑就能手动计算出来。
0x4 使用IDA调试so文件
1.打开cmd窗口,adb devices,adb push android_x64_server /sbin,然后报错了,权限不足,这种问题真烦人,使用批量多开器,新建一个模拟器(为了重置模拟器环境排除干扰),打开root和磁盘可读写功能。
2.然后问问deepseek怎么解决。
复制代码 隐藏代码adb root # 获取 Root 权限
adb remount # 自动重新挂载 /system 为可写
adb push android_x64_server /system/bin/
adb shell chmod +x /system/bin/android_x64_server
复制代码 隐藏代码实际执行结果
C:MySofewareIDA_Pro_7.7dbgsrv>adb root
restarting adbd as root
C:MySofewareIDA_Pro_7.7dbgsrv>adb remount
remount succeeded
C:MySofewareIDA_Pro_7.7dbgsrv>adb push android_x64_server /system/bin/
android_x64_server:1 file pushed, 0 s...d. 54.8 MB/s (1230968 bytes in0.021s)
C:MySofewareIDA_Pro_7.7dbgsrv>adb shell chmod +x /system/bin/android_x64_server
3.启动android server,然后输入adb forward tcp:23946 tcp:23946命令,将手机上的23946窗口,转发到我们电脑本地的23946端口,adb shell am start -n com.testjava.jack.pingan2/.MainActivity。
复制代码 隐藏代码adb shell /system/bin/android_x64_server & //& 表示后台运行。
C:MySofewareIDA_Pro_7.7dbgsrv>adb shell /system/bin/android_x64_server &
IDA Android x86 64-bit remote debug server(ST) v7.7.27. Hex-Rays (c) 2004-2022
Listening on 0.0.0.0:23946...
C:UsersLENOVODesktop>adb shell am start -n com.testjava.jack.pingan2/.MainActivity
Starting: Intent { cmp=com.testjava.jack.pingan2/.MainActivity }
4.使用F2在IDA中下上断点,颜色会变红,然后点击菜单栏debugger->Attach to process...,找到对应报名就成功附加调试了。
5.F9运行,到app输入f72c5a36569418a20907b55be5bf95ad, 开始单步调试走,重点观察v8的变换过程。
1)、首先看第一个循环代码
复制代码 隐藏代码memcpy(v8, v3, v5);//这一行v8开始将我输入的值复制过去了
v12 = v8[v11];
v8[v11] = v8[v11 + 16];
v8[v11++ + 16] = v12;
do
{
v12 = v8[v11];
v8[v11] = v8[v11 + 16];
v8[v11++ + 16] = v12;
}
while ( strlen(v8) >> 1 > v11 );
//一轮循环
f72c5a36569418a20907b55be5bf95ad
072c5a36569418a2f907b55be5bf95ad
//二轮循环
092c5a36569418a2f707b55be5bf95ad
//最后一轮交换后
0907b55be5bf95adf72c5a36569418a2
//经过反复调试,可以看出这里就是交换前16位和后16位的字符,逐一交换
2)、在继续往下走
复制代码 隐藏代码v13 = *v8;
*v8 = v8[1];
v8[1] = v13;
//这里可以看出是将第一个字符和第二个字符交换,if语句都是干扰
v14 = 2LL;
do
{
v15 = v8[v14];
v8[v14] = v8[v14 + 1];
v8[v14 + 1] = v15;
v14 += 2LL;
}
while ( strlen(v8) > v14 );
//这里下标从2开始就是从第三个字符开始交换,+1可以看出是第三个字符和后一个字符交换,重复这个过程
//结束循环时v8的值为:90705bb55efb59da7fc2a5636549812a
return strcmp(v8, "f72c5a36569418a20907b55be5bf95ad") == 0;
//最后这里就是判断是否相等了。
3)、这里说明一下,IDA调试命令和OD差不多,F2下断点,F8单步运行,F4运行到鼠标指定的行,鼠标放在v8上面就可以看到具体内容了。
6.告诉deepseek:使用java编写一段代码,计算“f72c5a36569418a20907b55be5bf95ad”每两位交换一次,最后将前16位与后16位交换后的值。
复制代码 隐藏代码package test;
publicclassTest {
publicstaticvoidmain(String[] args) {
String original = "f72c5a36569418a20907b55be5bf95ad";
// 第一步:每两位交换一次
String swapped = swapEveryTwoChars(original);
System.out.println("每两位交换后: " + swapped);
// 第二步:交换前16位和后16位
String result = swapFirstAndSecondHalf(swapped);
System.out.println("最终结果: " + result);
}
// 每两位交换一次
publicstaticStringswapEveryTwoChars(String str) {
char[] chars = str.toCharArray();
for (int i = 0; i < chars.length - 1; i += 2) {
char temp = chars[i];
chars[i] = chars[i + 1];
chars[i + 1] = temp;
}
returnnewString(chars);
}
// 交换前16位和后16位
publicstaticStringswapFirstAndSecondHalf(String str) {
if (str.length() != 32) {
thrownewIllegalArgumentException("输入字符串长度必须为32位");
}
String firstHalf = str.substring(0, 16);
String secondHalf = str.substring(16);
return secondHalf + firstHalf;
}
}
每两位交换后: 7fc2a5636549812a90705bb55efb59da
最终结果: 90705bb55efb59da7fc2a5636549812a
7.我算是发现了,deepseek给的分析过程合情合理,代码也比较准确,但是给不了你正确的值,需要你自己动手跑一遍,deepseek会惩罚不想动手的人。
0x5 使用frida hook so中的函数
1.先看一下我系统里面frida的版本,然后下载对应版本的服务端去https://github.com/frida/frida/releases/tag/16.6.6:frida-server-16.6.6-android-x86_64.xz
复制代码 隐藏代码C:UsersLENOVO>frida --version
16.6.6
2.将下载sever解压,重命名为frida-server(只是为了名称短点,改成别的都行),传到模拟器上运行,提示权限不足就加上su命令。
复制代码 隐藏代码adb push frida-server /data/local/tmp/
adb shell chmod +x /data/local/tmp/frida-server
adb shell /data/local/tmp/frida-server &
3.我告诉deepseek你给我hook v8的返回值,一直提示找不到Java_com_testjava_jack_pingan2_cyberpeace_CheckString函数,自定义函数不能hook吗?最后选择去hook strcmp这个库函数。
复制代码 隐藏代码Java.perform(function() {
// 1. 查找strcmp函数
const strcmp = Module.findExportByName(null, "strcmp");
if (!strcmp) {
console.log("[-] 找不到strcmp函数");
return;
}
console.log("[+] strcmp函数地址: " + strcmp);
// 2. Hook strcmp函数
Interceptor.attach(strcmp, {
onEnter: function(args) {
// 读取两个比较字符串
const str1 = args[0].readUtf8String();
const str2 = args[1].readUtf8String();
// 检查是否是我们要找的比较(与目标字符串比较)
const targetStr = "f72c5a36569418a20907b55be5bf95ad";
if (str2 === targetStr) {
console.log("n[+] 捕获到v8字符串比较:");
console.log("变换后的字符串(v8):", str1);
console.log("目标字符串:", str2);
// 保存到全局变量以便在onLeave中使用
this.isTargetComparison = true;
this.v8str = str1;
}
},
onLeave: function(retval) {
if (this.isTargetComparison) {
console.log("比较结果:", retval.toInt32() === 0 ? "匹配" : "不匹配");
console.log("完整的v8字符串内容:", this.v8str);
// 如果你想强制返回匹配结果,可以取消下面注释
// retval.replace(0); // 0表示匹配
}
}
});
console.log("[+] strcmp hook已安装,等待捕获v8字符串...");
});
4.这里直接 -U一直显示等待连接,然后还是选择老办法adb转发端口,然后使用ip地址连接模拟器。
复制代码 隐藏代码C:UsersLENOVODesktop>frida -H 127.0.0.1:27042 -f com.testjava.jack.pingan2 -l
hook.js
____
/ _ | Frida 16.6.6 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to 127.0.0.1:27042 (id=socket@127.0.0.1:27042)
Spawned `com.testjava.jack.pingan2`. Resuming main thread!
[Remote::com.testjava.jack.pingan2 ]-> [+] strcmp函数地址: 0x7ffff6726020
[+] strcmp hook已安装,等待捕获v8字符串...
[+] 捕获到v8字符串比较:
变换后的字符串(v8): 90705bb55efb59da7fc2a5636549812a
目标字符串: f72c5a36569418a20907b55be5bf95ad
比较结果: 不匹配
完整的v8字符串内容: 90705bb55efb59da7fc2a5636549812a
5.函数是静态注册的,并且从IDA中可以到已导出,为什么不能hook呢,deepseek说可能延迟加载了,所以我得这样问deepseek:使用Frida Hook监控V8值变化(处理SO延迟加载,函数已导出)
6.最终得到如下脚本,其中目标SO文件名、目标导出函数名要根据实际情况修改(so文件名就是apk解压出来的那个,不要改动,目标函数函数要写全,IDA里面一般识别到的java开头的那个就是)。
复制代码 隐藏代码Java.perform(function() {
// 配置参数
const config = {
targetSo: "libcyberpeace.so", // 目标SO文件名
targetFunction: "Java_com_testjava_jack_pingan2_cyberpeace_CheckString", // 目标导出函数名
targetString: "f72c5a36569418a20907b55be5bf95ad", // 用于识别比较的目标字符串
maxRetries: 10, // 最大重试次数
retryInterval: 1000// 重试间隔(ms)
};
console.log(`[+] 开始Hook目标函数: ${config.targetSo}!${config.targetFunction}`);
// 1. 等待SO加载并获取函数地址
let retryCount = 0;
let targetFuncAddress = null;
functionwaitAndHook() {
const mod = Process.findModuleByName(config.targetSo);
if (mod) {
console.log(`[+] 模块已加载: ${config.targetSo} @ ${mod.base}`);
// 获取导出函数地址
targetFuncAddress = Module.getExportByName(config.targetSo, config.targetFunction);
if (targetFuncAddress) {
console.log(`[+] 找到导出函数 ${config.targetFunction} @ ${targetFuncAddress}`);
setupHooks();
} else {
console.log(`[-] 在${config.targetSo}中未找到导出函数${config.targetFunction}`);
}
} elseif (retryCount < config.maxRetries) {
retryCount++;
console.log(`[.] 等待模块加载 (${retryCount}/${config.maxRetries})...`);
setTimeout(waitAndHook, config.retryInterval);
} else {
console.log("[-] 模块加载超时");
}
}
// 2. 设置Hook
functionsetupHooks() {
let v8Ptr = null;
// Hook目标函数
Interceptor.attach(targetFuncAddress, {
onEnter: function(args) {
console.log("n=== 函数调用开始 ===");
// 保存v8指针到上下文
this.v8 = v8Ptr;
v8Ptr = null;
// 获取输入字符串
try {
const inputStr = Java.vm.getEnv().getStringUtfChars(args[2], null).readCString();
console.log(`输入字符串: ${inputStr}`);
if (this.v8) {
console.log(`v8地址: ${this.v8}`);
// 读取初始值(可能尚未初始化)
// console.log(`v8初始值: ${this.v8.readCString()}`);
}
} catch (e) {
console.log(`获取输入字符串错误: ${e}`);
}
},
onLeave: function(retval) {
console.log("n=== 函数调用结束 ===");
if (this.v8) {
try {
const finalValue = this.v8.readCString();
console.log(`v8最终值: ${finalValue}`);
console.log(`返回值: ${retval}`);
// 可选:强制修改返回值
// if (finalValue === config.targetString) {
// retval.replace(1);
// }
} catch (e) {
console.log(`读取v8值错误: ${e}`);
}
}
}
});
// Hook strcmp获取最终比较
const strcmpFunc = Module.findExportByName(null, "strcmp");
if (strcmpFunc) {
Interceptor.attach(strcmpFunc, {
onEnter: function(args) {
try {
const compareStr = args[1].readCString();
if (compareStr === config.targetString) {
this.isTargetCompare = true;
console.log(`[strcmp] 比较值: ${args[0].readCString()}`);
}
} catch (e) {
console.log(`strcmp读取错误: ${e}`);
}
},
onLeave: function(retval) {
if (this.isTargetCompare) {
console.log(`[strcmp] 比较结果: ${retval}`);
}
}
});
}
// Hook memcpy监控数据复制
const memcpyFunc = Module.findExportByName(null, "memcpy");
if (memcpyFunc) {
Interceptor.attach(memcpyFunc, {
onEnter: function(args) {
if (this.v8 && args[0].equals(this.v8)) {
try {
console.log(`[memcpy] 复制到v8: ${args[1].readCString()}`);
} catch (e) {
console.log(`memcpy读取错误: ${e}`);
}
}
}
});
}
console.log("[+] Hook安装完成,等待函数调用...");
}
// 开始执行
waitAndHook();
});
输出:
[.] 等待模块加载 (1/10)...
[.] 等待模块加载 (2/10)...
[.] 等待模块加载 (3/10)...
[+] 模块已加载: libcyberpeace.so @ 0x7fff55510000
[+] 找到导出函数 Java_com_testjava_jack_pingan2_cyberpeace_CheckString @ 0x7fff5
5510800
[+] Hook安装完成,等待函数调用...ace.so!Java_com_testjava_jack_pingan2_cyberpeac
e_CheckString
=== 函数调用开始 ===
输入字符串: f72c5a36569418a20907b55be5bf95ad
[strcmp] 比较值: 90705bb55efb59da7fc2a5636549812a
[strcmp] 比较结果: 0xffffffd3
=== 函数调用结束 ===
7.这里so文件加载好像必须是我点check按钮后才会加载。
0x6 总结
还是得有点基础,用起deepseek才会得心应手,不然给我一个代码我都不知道怎么修改,怎么定制我要的功能。
原文始发于微信公众号(逆向有你):安卓逆向 -- IDA和frida调试回忆
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论