一
背景
这一题很有迷惑性,看似简单的代码逻辑,一眼看到的答案,其实并不是真相,重点在他的反检测。大多数的时候我们通过静态分析(java层还是so层)找到他的加密算法,再逆向还原其算法就能找到最终的答案,但是这道题不是,接下来我们我们会用到frida、AndroidNativeEmu、unidbg、IDA动静态调试。
二
静态分析
从代码可以看出,输入的密码调用方法securityCheck(String str),满足true则成功。
找到Java_com_yaotong_crackme_MainActivity_securityCheck函数
1.初始化检查
byte_6359 和 byte_635A: 可能是静态变量,表示某些初始化是否已经完成。
sub_2494 和 sub_24F4: 两个子函数,分别在第一次执行时被调用,可能用于解密或初始化全局变量。
结果: 如果两个字节标志未设置,会调用这两个函数并设置标志位,确保只初始化一次。
2.获取 Java 字符串的 UTF-8 值 v5 = (*a1)->GetStringUTFChars(a1, a3, 0);
3.字符串比较
比较 Java 层传入的字符串 a3 是否与硬编码字符串 off_628C 匹配。如果匹配,返回 1,否则返回 0。
分析结果:v6 = off_628C; 就是我们需要的值,aWojiushidaan
但是我们输入该值到APP中提示是不正确的,那么由此可以猜测,必然是APP启动后,有程序修改了off_628C的值。
三
AndroidNativeEmu模拟调用
验证下aWojiushidaan在静态下模拟调用,是否可以安全调用。
import logging
import sys
from unicorn import UC_HOOK_CODE, UC_HOOK_MEM_READ, UC_HOOK_MEM_WRITE
from unicorn.arm_const import *
from androidemu.emulator import Emulator# Configure logging
logging.basicConfig(
stream=sys.stdout,
level=logging.DEBUG,
format="%(asctime)s %(levelname)7s %(name)34s | %(message)s"
)
logger = logging.getLogger(__name__)
# Initialize emulator 实例化虚拟机
emulator = Emulator(vfp_inst_set=True)
# 加载Libc库
emulator.load_library("../example_binaries/32/libc.so", do_init=False)
# 加载要模拟的库
lib_module = emulator.load_library("libcrackme.so", do_init=False)
# 定义内存回调函数以监控变量 0x12A0 的变化
target_address = 0x4450+0xa009b000
string_length =100 # 假设最大字符串长度为 32 字节
# Show loaded modules 打印已经加载的模块
logger.info("Loaded modules:")
for module in emulator.modules:
logger.info("[0x%x] %s" % (module.base, module.filename))
def memory_read_hook(uc, access, address, size, value, user_data):
if address == target_address:
# 获取当前值
# print(uc.mem_read(address, string_length).decode('ascii', errors='ignore'))
current_value = uc.mem_read(address, string_length).split(b'\0', 1)[0].decode('ascii', errors='ignore')
print(f"【READ】 Address: 0x{address:X}, Current Value: {current_value}")
def memory_write_hook(uc, access, address, size, value, user_data):
if address == target_address:
# 获取写入的新值
new_value = uc.mem_read(address, string_length).split(b'\0', 1)[0].decode('ascii', errors='ignore')
print(f"【WRITE】 Address: 0x{address:X}, New Value: {new_value}")
# 注册指令和内存访问钩子
emulator.uc.hook_add(
UC_HOOK_MEM_READ, # 捕获内存读取
memory_read_hook,
None,
target_address,
target_address + string_length
)
emulator.uc.hook_add(
UC_HOOK_MEM_WRITE, # 捕获内存写入
memory_write_hook,
None,
target_address,
target_address + string_length
)
# 模拟运行函数
result1 = emulator.call_symbol(
lib_module,
'Java_com_yaotong_crackme_MainActivity_securityCheck',
emulator.java_vm.jni_env.address_ptr,
0,
"wojiushidaan",//123
is_return_jobject=False
)
# 输出结果
print("jnicheck result : {}".format(result1))
当我们分别输入 123 和 wojiushidaan 看返回的结果
结论:我们可以很确定v6 = off_628C值就是我们输入的值。
四
解法(1)frida hook该函数
在不考虑风控的前提下,明确了目标值用frida hook,是最快的方式,那就来吧,验证下。
function hook_so() {
Java.perform(function(){
var addr = Module.findBaseAddress("libcrackme.so");
var v1 = addr.add(0x4450);
console.log(v1.readCString()); });
}
function main() {
hook_so()
}
setTimeout(main,4000)
得到值 : aiyou,bucuoo 输入该值验证成功
五
解法(2)unidbg 模拟执行,打印关键参数
package com.yaotong.crackme;import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.DynarmicFactory;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.AbstractJni;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.DvmObject;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.pointer.UnidbgPointer;
import java.nio.charset.Charset;
import java.io.File;
public class MainActivity extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
private final Memory memory;
MainActivity() {
// 创建模拟器
emulator = AndroidEmulatorBuilder.for32Bit().addBackendFactory(new DynarmicFactory(true)).build();
// 内存
memory = emulator.getMemory();
// 设置SDK
memory.setLibraryResolver(new AndroidResolver(23));
// 创建虚拟机
vm = emulator.createDalvikVM(new File("unidbg-android/src/test/java/com/yaotong/crackme/you.apk"));
//设置jni
vm.setJni(this);
//打印日志
vm.setVerbose(true);
// 运行so文件
DalvikModule dalvikModule = vm.loadLibrary(new File("unidbg-android/src/test/java/com/yaotong/crackme/libcrackme.so"), true);
//module
module = dalvikModule.getModule();
// 调用JNI——onload
// dalvikModule.callJNI_OnLoad(emulator);
vm.callJNI_OnLoad(emulator, module);
HookAddr();
}
/**
* 打印 Hex Dump 格式的数据
*
* @param data 要打印的字节数组
*/
private static void printHexDump(byte[] data) {
int bytesPerLine = 16; // 每行打印16个字节
for (int i = 0; i < data.length; i += bytesPerLine) {
// 打印当前行的地址(偏移量)
System.out.printf("%08X ", i);
// 打印当前行的十六进制数据
for (int j = 0; j < bytesPerLine; j++) {
if (i + j < data.length) {
System.out.printf("%02X ", data[i + j]);
} else {
System.out.print(" "); // 如果剩余字节不足16个,填充空格
}
}
System.out.print(" |");
// 打印当前行的字符内容(ASCII)
for (int j = 0; j < bytesPerLine; j++) {
if (i + j < data.length) {
byte b = data[i + j];
if (b >= 32 && b <= 126) {
System.out.print((char) b); // 打印可打印字符
} else {
System.out.print("."); // 打印不可打印字符
}
} else {
System.out.print(" "); // 填充空格
}
}
System.out.println("|");
}
}
public static void main(String[] args) {
MainActivity test = new MainActivity();
System.out.println(test.getSName());
}
public void HookAddr() {
// 目标地址,这里是示例地址 0x628C 0x4450
long targetAddress = module.base + 0x4450;
// 使用 UnidbgPointer 来获取目标地址的数据
UnidbgPointer pointer = UnidbgPointer.pointer(emulator, targetAddress);
// 读取目标地址的数据,假设它是一个字符串,长度为 16 字节
byte[] data = pointer.getByteArray(0, 16); // 读取16个字节
printHexDump(data);
// 将读取的字节转换为字符串
// 将读取的字节转换为字符串,并指定正确的编码(例如 UTF-8 或 GBK)
String value = new String(data, Charset.forName("GBK")); // 使用 UTF-8 编码
// 打印地址和读取的内容
System.out.println("Address: " + Long.toHexString(targetAddress));
System.out.println("Data at address: " + value);
}
//符号调用
public Boolean getSName() {
// 创建一个vm对象
DvmObject<?> dvmObject = vm.resolveClass("com/yaotong/crackme/MainActivity").newObject(null);
String input = "123";
// byte[] inputByte = input.getBytes(StandardCharsets.UTF_8);
boolean success = dvmObject.callJniMethodBoolean(emulator, "securityCheck(Ljava/lang/string;)Z", input);
System.out.println("[symble] Call the so md5 function result is ==> " + success);
return success;
}
}
执行验证结果如图所示:
通过模拟下调用(符号调用),在hook关键参数地址,运行后,获得的正确flag:aiyou,bucuoo,然后我们在输入flag 则显示true,成功!
六
解法(3)IDA动态调试
如果不喜欢用frida IDA动态调试也是比较通用直接的,可以一步一步跟踪查看代码的运行逻辑。
1.查看getprop ro.debuggable的值:adb shell getprop ro.debuggable
2.Magisk 重置 ro.debuggable (重启失效)
adb shell #adb进入命令行模式
magisk resetprop ro.debuggable 1
在Module list窗口(Debugger->Debugger windows->Module list)中找到libcrackme.so,双击它,f5进入伪代码页面,在目标函数下断点,v6 = off_C0B0E28C;
按下F9,弹出错误警告。
从这一系列的操作我们可以发现,wojiushidaan 这个值,在APP运行后,被重新赋值了。在进行调试的时候报错,说明有反调试存在。
1.端口检测:调试器在进行远程调试时,会占用一些固定的端口号。IDA Pro可以通过读取/proc/net/tcp文件,查找远程调试所用的23946端口,若发现该端口被占用,则说明进程正在被IDA调试
2.关键文件检测:通过修改android_server文件的名称,防止调试器找到并连接该文件
3.进程ID检测:在没有调试时,TracerPid为0;运行调试时,TracerPid会变为调试器的进程ID。通过修改系统调用函数,伪造TracerPid为0,以欺骗调试器
4.Java层反调试:在AndroidManifest.xml中设置android:debuggable="false",并在build.prop中设置ro.debuggable=0,防止应用在调试模式下运行。此外,可以通过检测Debug.isDebuggerConnected()方法的返回值来判断是否被调试
5.自我调试:父进程创建一个子进程,通过子进程调试父进程。这种方式消耗的系统资源较少,且能有效阻止其他进程调试受保护的进程。
我们要在so文件加载之前进行调试。这样就能判断程序大概检测位置,逐渐深入,定位问题的所在。
APK 重新打包成 debuuger 模式
1.apktool d -f app.apk -o app_source
2.修改 AndroidManifest.xml,找到解包后的 AndroidManifest.xml 文件,确认以下内容:
检查 android:debuggable 属性是否设置为 true,如果没有则添加:
<application android:debuggable="true" ... >
如果已经有 android:debuggable 属性,直接改为 true。
3. 重新打包 APK apktool b app_source -o app_debuggable.apk
4.签名新 APK 使用 keytool 生成调试密钥(如果没有现成的密钥):
keytool -genkey -v -keystore debug.keystore -alias debug -keyalg RSA -keysize 2048 -validity 10000
签名 APK:
jarsigner -verbose -keystore debug.keystore -storepass android -keypass android app_debuggable.apk debug
adb shell am start -D -n com.yaotong.crackme/.MainActivity 以调试模式启动app,让程序停在加载so文件之前。
IDA设置如下:
连接jdb后,IDA运行绿色三角按钮,让程序把so文件加载出来。
adb shell ps | findStr com.yaotong.crackme
adb forward tcp:8855 jdwp:18518
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8855
Ctrl+s 搜索进入我们的目标文件
进入JNI_Onload函数下断点进行调试
当前位置程序并没有奔溃,说明检测点还在后面,然后继续,跳到R7的时候报错了 那么检测点 位置找到了。
把反调试的TracerPid 指令37 FF 2F E1 改为 00 00 00 00 就不会再有反调试的防护机制了。
再进行正常调试。畅通无阻,顺利看到了off_D860B28C 的值 aiyou,bucuoo
看雪ID:西贝巴巴
https://bbs.kanxue.com/user-home-961239.htm
# 往期推荐
球分享
球点赞
球在看
点击阅读原文查看更多