unidbg简介
unidbg是一个Java项目,可以帮助我们去模拟一个安卓或IOS设备,用于去执行so文件中的算法,从而不需要再去逆向他内部的算法。unidbg的主要功能包括模拟执行、内存访问、指令跟踪、函数调用跟踪等。支持多种架构,包括x86、ARM、MIPS等。
关于so的解决方法:
调试java层获取so层函数返回值,调试so动态库。
frida hook
unidbg
环境配置
运行unidbg项目需要Intellij IDEA,具体下载方式可以自行百度,网上也有教破解方法的文章。
安装完成后可以运行com下的几个自带的测试用例来测试环境。
这里运行TTEncrypt,控制台打印了相关信息,说明项目导入成功,环境没有问题。
unidbg基本语句
创建模拟器
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
private MainActivity() {
// 创建模拟器实例,要模拟32位或者64位,在这里区分
emulator = AndroidEmulatorBuilder
// 指定32位CPU
.for32Bit()
// 添加后端, 推荐使用Dynarmic,运行速度快,但是不支持某些新特性
.addBackendFactory(new DynarmicFactory(true))
// 指定进程名,推荐以安卓包名做进程名
// .setProcessName("")
.build();
// 模拟器的内存操作接口
Memory memory = emulator.getMemory();
// 设置SDK版本
LibraryResolver resolver = new AndroidResolver(23);
// 设置系统类库解析
memory.setLibraryResolver(resolver);
// 创建Android虚拟机
vm = emulator.createDalvikVM(new File("D:\\unidbg-0.9.8\\unidbg-0.9.8\\unidbg-android\\src\\test\\java\\com\\ctf\\cma\\easystd.apk"));
// 设置是否打印Jni调用细节
vm.setVerbose(true);
// 加载libnative-lib.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
DalvikModule dm = vm.loadLibrary(new File("D:\\unidbg-0.9.8\\unidbg-0.9.8\\unidbg-android\\src\\test\\java\\com\\ctf\\cma\\libcma.so"), false);
//获取本SO模块的句柄,后续需要用它
module = dm.getModule();
// 调用JNI OnLoad
dm.callJNI_OnLoad(emulator);
}
emulator 的操作
// 获取内存操作接口
Memory memory1 = emulator.getMemory();
// 获取进程id
int pid = emulator.getPid();
//创建虚拟机
VM dalvikVM = emulator.createDalvikVM();
//创建虚拟机并指定文件
VM dalvikVM1 = emulator.createDalvikVM(new File("ss/ss/apk"));
//获取已经创建的虚拟机
VM dalvikVM2 = emulator.getDalvikVM();
//显示当前寄存器的状态 可指定寄存器
emulator.showRegs();
// 获取后端CPU
Backend backend = emulator.getBackend();
//获取进程名
String processName = emulator.getProcessName();
// 获取寄存器
RegisterContext context = emulator.getContext();
//Trace 读取内存
emulator.traceRead(1,0);
// trace 写内存
emulator.traceWrite(1,0);
//trace 汇编
emulator.traceCode(1,0);
// 是否在运行
boolean running = emulator.isRunning();
符号调用和地址调用
public void md5(){
/* 符号调用
创建项目要和包名一致
*/
DvmObject<?> obj = ProxyDvmObject.createObject(vm, this);
String data = "dta";
DvmObject<?> result = obj.callJniMethodObject(emulator,"md5(Ljava/lang/String;)Ljava/lang/String;", data);
String value = (String) result.getValue();
System.out.println("(symbol)md5 ====> result: " + value);
}
private void call_address() {
/* 地址调用(虽然复杂,但是最好用)
这部分基本上固定写法,只需要改地址偏移和参数类型就行
*/
Pointer jniEnv = vm.getJNIEnv();
DvmObject<?> obj = ProxyDvmObject.createObject(vm, this);
// 如果要传入vm.addLocalObject,String只能这么写
StringObject data = new StringObject(vm, "dta");
List<Object> args = new ArrayList<>();
args.add(jniEnv);
// 除了指针类型和Number类型都需要添加到vm(vm.addLocalObject)才能够在vm中操作该对象
args.add(vm.addLocalObject(obj));
args.add(vm.addLocalObject(data));
// 0x849是地址偏移(要加1)
Number numbers = module.callFunction(emulator, 0x849, args.toArray());
DvmObject<?> object = vm.getObject(numbers.intValue());
String value = (String) object.getValue();
System.out.println("(addr)md5 ====> result: " + value);
}
debug option
要对so层进行调试加上这两条语句即可进行控制台调试。调试命令类似gdb,具体可以自己查阅文档。
Debugger MyDbg = emulator.attach(DebuggerType.CONSOLE);
MyDbg.addBreakPoint(module.base + 0xC65); //断点地址
接下来举一个实例。
Plzedebugme
附件可以最下方找到。
java层分析
先jeb打开apk,找到mainactivity
这里导入了libdebugme.so动态库。
package work.pangbai.debugme;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View.OnClickListener;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.snackbar.Snackbar;
import work.pangbai.debugme.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private ActivityMainBinding binding;
public static boolean status = false;
public static String title = "PlzDebugMe";
static {
System.loadLibrary("debugme");
}
public native boolean check(String arg1) {
}
public native String g4tk4y() {
}
private static String getProperty(Class class0, String s) throws Exception {
return (String)class0.getMethod("get", String.class).invoke(class0, s);
}
public static boolean isEmu() {
try {
Class class0 = Class.forName("android.os.SystemProperties");
return MainActivity.getProperty(class0, "ro.kernel.qemu").length() > 0 || (MainActivity.getProperty(class0, "ro.hardware").equals("goldfish")) || (MainActivity.getProperty(class0, "ro.product.model").equals("sdk"));
}
catch(Exception unused_ex) {
return false;
}
}
@Override // android.view.View$OnClickListener
public void onClick(View view0) {
if(view0 == this.binding.contentMain.frist.buttonFirst) {
String s = String.valueOf(this.binding.contentMain.frist.text.getText());
if(MainActivity..ExternalSyntheticBackport0.m(s)) {
new MaterialAlertDialogBuilder(this).setTitle("CheckResult").setPositiveButton("确定", null).setMessage("不准拿空的骗我哟").create().show();
return;
}
if(this.check(new FishEnc(this.g4tk4y()).doEnc(s))) {
new MaterialAlertDialogBuilder(this).setTitle("CheckResult").setPositiveButton("确定", null).setMessage("Congratulations ! ! ! \n你最棒了啦\n").create().show();
return;
}
new MaterialAlertDialogBuilder(this).setTitle("CheckResult").setPositiveButton("确定", null).setMessage("Wrong \n好像哪里有点问题呢\n").create().show();
}
}
@Override // androidx.fragment.app.FragmentActivity
protected void onCreate(Bundle bundle0) {
super.onCreate(bundle0);
ActivityMainBinding activityMainBinding0 = ActivityMainBinding.inflate(this.getLayoutInflater());
this.binding = activityMainBinding0;
this.setContentView(activityMainBinding0.getRoot());
this.setSupportActionBar(this.binding.toolbar);
this.setTitle("PlzDebugMe");
this.binding.fab.setOnClickListener((View view0) -> new MaterialAlertDialogBuilder(this).setTitle(string.ctf).setPositiveButton(string.ok, null).setMessage(string.msg).create().show());
if(!MainActivity.isEmu()) {
this.binding.contentMain.frist.buttonFirst.setOnClickListener(this);
return;
}
this.binding.contentMain.frist.buttonFirst.setOnClickListener((View view0) -> Snackbar.make(this.binding.getRoot(), string.tips, 1000).show());
}
@Override // android.app.Activity
public boolean onCreateOptionsMenu(Menu menu0) {
this.getMenuInflater().inflate(menu.menu_main, menu0);
return true;
}
@Override // android.app.Activity
public boolean onOptionsItemSelected(MenuItem menuItem0) {
if(menuItem0.getItemId() == id.action_settings) {
Snackbar.make(this.binding.getRoot(), "Meow ?meow meow meow", 500).show();
return true;
}
return super.onOptionsItemSelected(menuItem0);
}
}
在onclick事件中获得了输入,并调用this.check(new FishEnc(this.g4tk4y()).doEnc(s))来检验,其中FishEnc在java层定义,定义如下。g4tk4y在libdebugme.so中。
package work.pangbai.debugme;
import android.util.Base64;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
public class FishEnc {
Cipher cipher;
public FishEnc(String s) {
SecretKeySpec secretKeySpec0 = new SecretKeySpec(s.getBytes(), "Blowfish");
try {
Cipher cipher0 = Cipher.getInstance("Blowfish");
this.cipher = cipher0;
cipher0.init(1, secretKeySpec0);
return;
}
catch(InvalidKeyException | NoSuchPaddingException | NoSuchAlgorithmException noSuchAlgorithmException0) {
throw new RuntimeException(noSuchAlgorithmException0);
}
}
public String doEnc(String s) {
try {
return Base64.encodeToString(this.cipher.doFinal(s.getBytes(StandardCharsets.UTF_8)), 0);
}
catch(IllegalBlockSizeException | BadPaddingException badPaddingException0) {
throw new RuntimeException(badPaddingException0);
}
}
}
FishEnc获得一个秘钥来初始化,然后doEnc对输入blowfish加密再进行base64编码。
最后这个密文经过so层的check来检验。
那么主要就是so层的g4tk4y函数和check函数的分析。
so层分析
首先看g4tk4y函数,
int __fastcall Java_work_pangbai_debugme_MainActivity_g4tk4y(int *a1)
{
int v2; // r5
char v3; // r9
char v4; // r3
char v5; // r0
int v6; // r0
char v8[8]; // [sp+0h] [bp-98h] BYREF
__int64 v9; // [sp+8h] [bp-90h]
__int64 v10; // [sp+10h] [bp-88h]
__int64 v11; // [sp+18h] [bp-80h]
__int64 v12; // [sp+20h] [bp-78h]
char dest[84]; // [sp+28h] [bp-70h] BYREF
v2 = (*(int (__fastcall **)(int *, const char *))(*a1 + 24))(a1, "work/pangbai/tool/App");
dword_3064 = (*(int (__fastcall **)(int *, int))(*a1 + 84))(a1, v2);
(*(void (__fastcall **)(int *, int))(*a1 + 92))(a1, v2);
dword_3068 = (*(int (__fastcall **)(int *, int, const char *, const char *))(*a1 + 576))(
a1,
dword_3064,
"status",
"I");
if ( check() )
exit(0);
v9 = 0LL;
v10 = 0LL;
v11 = 0LL;
v12 = 0LL;
strcpy(dest, "4KBbSzwWClkZ2gsr1qA+Qu0FtxOm6/iVcJHPY9GNp7EaRoDf8UvIjnL5MydTX3eh");
v8[0] = dest[str[0] & 0x3F];
v8[3] = dest[(unsigned __int8)str[2] >> 2];
v8[1] = dest[((unsigned __int8)str[0] >> 6) & 0xFFFFFFC3 | (4 * (str[1] & 0xF))];
v3 = dest[((unsigned __int8)str[4] >> 4) & 0xFFFFFFCF | (16 * (str[5] & 3))];
v4 = dest[str[3] & 0x3F];
v8[2] = dest[((unsigned __int8)str[1] >> 4) & 0xFFFFFFCF | (16 * (str[2] & 3))];
v5 = dest[(unsigned __int8)str[5] >> 2];
v8[5] = dest[((unsigned __int8)str[3] >> 6) & 0xFFFFFFC3 | (4 * (str[4] & 0xF))];
v8[7] = v5;
v6 = *a1;
v8[4] = v4;
v8[6] = v3;
return (*(int (__fastcall **)(int *, char *))(v6 + 668))(a1, v8);
}
这里魔改的换表base64加密,在内存中看到str的值为"7h4K4y"
直接在cyberchef里加密后的结果是错误的。这里就可以用unidbg直接获得返回值
package work.pangbai.debugme;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.LibraryResolver;
import com.github.unidbg.arm.backend.DynarmicFactory;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.debugger.Debugger;
import com.github.unidbg.debugger.DebuggerType;
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.DvmObject;
import com.github.unidbg.linux.android.dvm.StringObject;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.Module;
import com.sun.jna.Pointer;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
public class MainActivity {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
private MainActivity() {
// 创建模拟器实例,要模拟32位或者64位,在这里区分
emulator = AndroidEmulatorBuilder
// 指定32位CPU
.for32Bit()
// 添加后端, 推荐使用Dynarmic,运行速度快,但是不支持某些新特性
// .addBackendFactory(new DynarmicFactory(true))
// 指定进程名,推荐以安卓包名做进程名
.setProcessName("work.pangbai.debugme")
.build();
// 模拟器的内存操作接口
Memory memory = emulator.getMemory();
// 设置SDK版本
LibraryResolver resolver = new AndroidResolver(23);
// 设置系统类库解析
memory.setLibraryResolver(resolver);
// 创建Android虚拟机
vm = emulator.createDalvikVM(new File("D:\\unidbg-0.9.8\\unidbg-0.9.8\\unidbg-android\\src\\test\\java\\work\\pangbai\\debugme\\PlzDebugMe.apk"));
// 设置是否打印Jni调用细节
vm.setVerbose(true);
// 加载libnative-lib.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
DalvikModule dm = vm.loadLibrary(new File("D:\\unidbg-0.9.8\\unidbg-0.9.8\\unidbg-android\\src\\test\\java\\work\\pangbai\\debugme\\libdebugme.so"), false);
//获取本SO模块的句柄,后续需要用它
module = dm.getModule();
// 调用JNI OnLoad
dm.callJNI_OnLoad(emulator);
}
public void g4tk4y(){
/* 符号调用
创建项目要和包名一致, 因为unidbg会根据this的包名进行匹配JNI方法,所以this所属类的包名应该与目标函数相同
*/
DvmObject<?> obj = ProxyDvmObject.createObject(vm, this);
// 这个函数没有参数传入
DvmObject<?> result = obj.callJniMethodObject(emulator,"g4tk4y(Ljava/lang/String;)Ljava/lang/String;");
String value = (String) result.getValue();
System.out.println("(symbol)g4tk4y ====> result: " + value);
}
private void call_address() {
/* 地址调用(虽然复杂,但是最好用)
这部分基本上固定写法,只需要改地址偏移和参数类型就行
在so文件里面方法是Java_work_pangbai_debugme_MainActivity_g4tk4y(int *a1)
所以下面要创建对应的参数, 基本为固定写法
*/
Pointer jniEnv = vm.getJNIEnv();
DvmObject<?> obj = ProxyDvmObject.createObject(vm, this);
// 如果要传入vm.addLocalObject,String只能这么写
List<Object> args = new ArrayList<>();
args.add(jniEnv);
// 这个函数没有参数传入
// 除了指针类型和Number类型都需要添加到vm(vm.addLocalObject)才能够在vm中操作该对象
args.add(vm.addLocalObject(obj));
// Debugger MyDbg = emulator.attach(DebuggerType.CONSOLE);
// MyDbg.addBreakPoint(module.base + 0xC65); //断点地址
// 0xAA9是地址偏移(g4tk4y函数地址0xAA8+1)
Number numbers = module.callFunction(emulator, 0xAA9, args.toArray());
DvmObject<?> object = vm.getObject(numbers.intValue());
String value = (String) object.getValue();
System.out.println("(address)g4tk4y ====> result: " + value);
}
public static void main(String[] args) {
MainActivity mainActivity = new MainActivity();
mainActivity.g4tk4y();
mainActivity.call_address();
}
}
运行可以看到符号调用和地址调用都打印出了返回值。
现在知道了blowfish加密的秘钥,只要再求出密文就可以进行解密了。
密文从check函数中获得。
bool __fastcall Java_work_pangbai_debugme_MainActivity_check(int a1, int a2, int a3)
{
int v5; // r6
int v6; // r5
size_t v7; // r1
int v8; // r0
size_t v9; // r12
int v10; // r2
int v11; // r3
unsigned int v12; // r3
unsigned int v13; // r1
int v14; // r6
char v15; // r3
unsigned int v16; // r1
unsigned int v17; // r1
char v18; // r2
unsigned int v19; // r2
unsigned int v20; // r1
int v21; // r1
int v22; // r3
unsigned int v23; // r3
unsigned int v24; // r2
int v25; // r6
int v26; // r5
int input[51]; // [sp+0h] [bp-E0h] BYREF
sub_DB8((int)input, 0xC8u);
v5 = (*(int (__fastcall **)(int, int, _DWORD))(*(_DWORD *)a1 + 676))(a1, a3, 0);
_strcpy_chk(input, v5, 200);
(*(void (__fastcall **)(int, int, int))(*(_DWORD *)a1 + 680))(a1, a3, v5);
v6 = (*(int (__fastcall **)(int, int, int))(*(_DWORD *)a1 + 600))(a1, dword_3064, dword_3068);
v7 = _strlen_chk((const char *)input, 0xC8u) + 3;
v8 = (v7 >> 2) - 1;
v9 = v7 >> 2;
if ( v7 >= 8 )
{
v10 = input[0];
v11 = 0;
do
{
v14 = v11 + 1;
v15 = 31 - v11;
v16 = input[v14] ^ v10;
if ( v6 )
{
v12 = v16 << v15;
v13 = v16 >> v14;
}
else
{
v12 = v16 >> v15;
v13 = v16 << v14;
}
v10 ^= v12 | v13;
v11 = v14;
input[v14] = v10;
}
while ( v14 != v8 );
}
v17 = input[v8] ^ input[0];
v18 = 32 - v9;
if ( v6 )
{
v19 = v17 << v18;
v20 = v17 >> v9;
}
else
{
v19 = v17 >> v18;
v20 = v17 << v9;
}
v21 = v20 | v19;
input[0] = v21;
v22 = input[v8];
input[0] = v21 ^ v22;
if ( (unsigned __int8)cc[0] != (unsigned __int8)(v21 ^ v22) )
return 0;
v23 = 0;
do
{
v24 = v23;
if ( v23 == 47 )
break;
v25 = *((unsigned __int8 *)input + v23 + 1);
v26 = (unsigned __int8)cc[++v23];
}
while ( v26 == v25 );
return v24 > 0x2E;
}
可以看到解密就是加密过程就是xor和循环左移/右移,这里左移还是右移由v6的值决定,由于只有两种情况,可以都尝试一下。加密的过程是:
tmp = input[i+1] ^ input[i]
v10 = input[i] ^ ROR(tmp, i+1)
input[i+1] = v10
据此可以写出解密脚本
from struct import pack
def ROR(n, v):
return ((n >> v) | (n << (32-v))) & 0xffffffff
def ROL(n, v):
return ((n << v) | (n >> (32-v))) & 0xffffffff
enc = [0x9C5F5508, 0x40561970, 0x58676904, 0xC13E5285, 0x75DC2D4C, 0x06F06EAF, 0x6E7B5DA5, 0xE37EAE2A, 0xF1B9FEFD, 0x06966BAC, 0x4A21BF43, 0x47DBF512]
v6 = 0
for i in range(len(enc) - 1, -1, -1):
tmp = enc[(i + 1) % 12] ^ enc[i]
if v6:
tmp1 = ROL(tmp, i + 1)
else:
tmp1 = ROR(tmp, i + 1)
enc[(i + 1) % 12] = tmp1 ^ enc[i]
for i in range(len(enc)):
print(pack('<I', enc[i]).decode(),end='')
然后cyberchef解密就行,解密的recipe ==> Recipe
- PlzDebugMe.zip 下载