使用unidbg执行安卓so层中的方法

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

附件:
0 条评论
某人
表情
可输入 255