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文件的加密,其中的字符串未被解密。