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
下载有两种方式进行下载
- git clone https://github.com/zhkl0228/unidbg.git 通过git命令clone到本地
- 通过Github右上角的Code按钮下载zip文件
配置Unidbg
下载好之后我们使用Idea将其打开,发现是一个maven工程我们。进行一下简单的设置
首先新建一个Module用于编写我们自己的代码也可以不新建在原有的Module上新建java文件即可。这里因为方便区分自己的和模板的选择新建一个Module
- 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文件的加密,其中的字符串未被解密。