Unidbg的介绍

Allows you to emulate an Android native library, and an experimental iOS emulation.

This is an educational project to learn more about the ELF/MachO file format and ARM assembly.

Use it at your own risk !

官方Github: https://github.com/zhkl0228/unidbg

下载Unidbg

下载有两种方式进行下载

  1. git clone https://github.com/zhkl0228/unidbg.git 通过git命令clone到本地

  1. 通过Github右上角的Code按钮下载zip文件

配置Unidbg

下载好之后我们使用Idea将其打开,发现是一个maven工程我们。进行一下简单的设置

首先新建一个Module用于编写我们自己的代码也可以不新建在原有的Module上新建java文件即可。这里因为方便区分自己的和模板的选择新建一个Module

  1. New => Module 新建模块,记得不要勾上那个复选框

2. 修改模块的依赖选项File => Project structure => module 找到刚刚新建的模块把scope全部改成Compile默认是Test然后把需要用的依赖包添加进来保存

逆向分析Lua

逆向分析Lua

首先我们使用jeb打开apk在androidmanifest文件中找到启动的activity,不难发现启动Act下有一个intent-filter标签在此注明了applicaiton启动的第一个Act “com.androlua.Welcome”

来到Welcome的onCreate函数经过分析this.checkInfo()就是检查是否是第一次打开App如果是第一次打开就执行程序的初始化把需要运行Lua脚本复制到对应的/data/data/包名/files里面

往下滑可以看到调用了startActivity跳转到了另一个Act “Main” 调用的时候传送了一些版本信息

来到Main的onCreate函数发现并没有什么可以的地方,只是获取了对应的Welcome传过来的数据。调用了this.runFunc经过分析不难发现没有我们想要的关键信息

猜测一下是不是继承了其他的Act,然后看一下顶部发现确实继承了其他的Act “LuaActivity”

来到LuaActivity的onCreate发现它前面只是执行了一些窗口的设置

往下滑发现了关键的信息

双击this.k这个函数跳转到对应函数实现的地方发现这个函数只是初始化Lua解释器

在这里发现也执行了this.doFile传入了两个参数,第一个是Lua脚本的路径,第二个是Intent参数。假设一下如果要运行一个脚本是不是得知道Lua的路径推断一下这个可能是关键地方

双击this.doFile打开交叉引用选择LuaActivity的doFile函数,因为他调用这个函数的时候传入了一个LuaPath所以我我们只关心哪里使用了个参数。不难发现蓝色框框的部分调用了this.j.LloadFile加载文件

双击之后发现跳转到了LuaState 这个是Lua解释器接口,发现这个函数又调用了另一个函数双击进去发现调用了native的_LoadFile

往上滑动来到顶部可以看到加载了一个Library “luajava”

下面是我写的一个JNI函数不难看出要实现一个JNI函数必须要的参数是JNIEnv *, jclass, jobject

使用对应的IDA打开对应架构的libluajava.so文件,在左边的Function names搜索loadfile发现只有一个JNI函数。点击一下int a1按键盘上的 Y 键把变量类型设置成JNIEnv *然后代码就一目了然了。因为JNI函数前面三个参数一般都是一些固定的信息,再从刚刚Java的分析不难发现a4,a5是Java层传过来的参数a4是一个LuaState接口。a5就是LuaPath。

从图中发现调用了GetStringUTFChars这个函数把a5的Java字符串(jstring)转换成C的字符串(cstring),调用了j_luaL_loadfilex。传入了一个LuaState和LuaPath的cstring

双击j__luaL_loadfilex跳转到了另一个函数再双击来到了,函数实现过程的地方。其中a2是LuaPath

点击参数中的a2,往下滑可以看到高亮的地方使用了a2。首先通过fopen打开Lua文件脚本文件把文件句柄赋值给v7=stream

首先根据文件句柄获取Lua脚本的第一个字节判断该本属于哪个版本的。而我这个是最新版本的。最新版本的是“=”所以直接看判断"=’的地方

进入到Sub子函数打开看一下判断是读取lua脚本,a3是一个指针在这个函数修改了a3=v7,而v7是lua脚本的长度,根据判断可以知道调用这个函数就是读取lua脚本然后返回,给v20,v36存放的就是文件的大小

然后调用了这个函数加载lua脚本j_luaL_loadbufferx,发现在这个函数进行解密,最后面返回了lua_load

根据Github开源的可以发现调用了这个函数进行执行lua二进制文件,而解密后的二进制文件就是传过来的第三个参数

使用Unidbg HOOk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
package blog.jamiexu.cn;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.arm.HookStatus;
import com.github.unidbg.file.FileResult;
import com.github.unidbg.file.IOResolver;
import com.github.unidbg.file.linux.AndroidFileIO;
import com.github.unidbg.hook.HookContext;
import com.github.unidbg.hook.ReplaceCallback;
import com.github.unidbg.hook.hookzz.HookZz;
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.DvmClass;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.pointer.UnidbgPointer;
import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;

public class Lua extends AbstractJni implements IOResolver<AndroidFileIO> {

private final AndroidEmulator androidEmulator;
private final VM vm;
private final Memory memory;
private final boolean debug;

private final String LUAJAVA_PATH = "MyDebug/src/main/resources/Lua/APItest_1.0_sign/lib/armeabi-v7a/libluajava.so";
private final String LUA_APP = "MyDebug/src/main/resources/Lua/APItest_1.0_sign.apk";


private final String LUA_INPUT = "MyDebug/src/main/resources/Lua/APItest_1.0_sign/assets/main.lua";
private final String LUA_OUT = "MyDebug/src/main/resources/Lua/out.lua";


private Lua(boolean debug) {
this.debug = debug;
this.androidEmulator = AndroidEmulatorBuilder.for32Bit() //创建一个32架构的模拟器
.setProcessName("blog.jamiexu.cn.lua").build();
this.memory = this.androidEmulator.getMemory();
this.memory.setLibraryResolver(new AndroidResolver(23));//设置Android的SDK版本
this.vm = this.androidEmulator.createDalvikVM(new File(this.LUA_APP));//创建虚拟机
this.vm.setVerbose(debug);//设置打印日志
this.vm.setJni(this);//设置JNI环境
this.androidEmulator.getSyscallHandler().addIOResolver(this);//添加IO用于打开文件

// if (this.debug) this.androidEmulator.attach(DebuggerType.ANDROID_SERVER_V7);

}


public static void main(String[] args) {
Lua lua = new Lua(true);//创建一个Android模拟环境
DalvikModule luajava = lua.getVm().loadLibrary(new File(lua.LUAJAVA_PATH), true);//加载Libso
luajava.callJNI_OnLoad(lua.getAndroidEmulator());//初始化调用On_Load

HookZz hookInstance = HookZz.getInstance(lua.getAndroidEmulator());//初始化一个Hook接口
hookInstance.replace(luajava.getModule().findSymbolByName("lua_load"), new ReplaceCallback() {
@Override
public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) {
return HookStatus.LR(emulator, context.getIntArg(2));//反汇第三个参数data的数据
}
});

DvmClass luaState = lua.getVm().resolveClass("com/luajava/LuaState");//实现一个类
long luaInstance = luaState.callStaticJniMethodLong(lua.getAndroidEmulator(), "_newstate()J");//调用静态函数初始化接口
int i = luaState.callStaticJniMethodInt(lua.getAndroidEmulator(), "_LloadFile(JLjava/lang/String;)I",//模拟调用_LloadFile函数
luaInstance, "test.lua");//传入两个参数对应的

UnidbgPointer pointer = UnidbgPointer.pointer(lua.getAndroidEmulator(), i);//获取指针
if (pointer != null) {//判断指针是否是空指针,若不是执行

int[] size = new int[1];
pointer.read(4, size, 0, size.length);//获取解密后文件大小
byte[] file = new byte[size[0]];
pointer.getPointer(0).read(0, file, 0, file.length);//把文件读取到buffer
try {
FileUtils.writeByteArrayToFile(new File(lua.LUA_OUT), file);//保存文件
} catch (IOException e) {
e.printStackTrace();
}
}


lua.close();
}


@Override
public FileResult<AndroidFileIO> resolve(Emulator<AndroidFileIO> emulator, String pathname, int oflags) {
// 实现一个IO接口当模拟运行c的fopen函数的时候返回的就是,这个函数中的文件IO。判断fopen的文件名设置对应的文件
if ("test.lua".equals(pathname)) {
return FileResult.success(emulator.getFileSystem().createSimpleFileIO(new File(LUA_INPUT), oflags, pathname));
}
return null;
}


public AndroidEmulator getAndroidEmulator() {
return androidEmulator;
}

public VM getVm() {
return vm;
}

public Memory getMemory() {
return memory;
}

public void close() {
try {
this.androidEmulator.close();
} catch (IOException e) {
e.printStackTrace();
}
}


}

对比解密前和解密后的数据文件

可以用Unluac进行解密后的Lua文件反编译,只解密了整个Lua文件的加密,其中的字符串未被解密。