- 前言
最近看到手里的某短视频APP来了兴致,特意拿来分析记录下,整个系列文章大概分为抓包,java层分析,so定位,so去花,unidbg,算法还原等几个部分,这几篇文章会记录下我的整个调试过程,前面的文章会比较基础,入门级玩家基本可以略过了,因为考虑到文章的连续性我这边还是会记录发表下。最近看到手里的某短视频APP来了兴致,特意拿来分析记录下,整个系列文章大概分为抓包,java层分析,so定位,so去花,unidbg,算法还原等几个部分,这几篇文章会记录下我的整个调试过程,前面的文章会比较基础,入门级玩家基本可以略过了,因为考虑到文章的连续性我这边还是会记录发表下。 - 抓包
因为需要对关键字段的算法进行分析,所以个人习惯还是要先抓下协议看一下,先不管别的用BurpSuite抓个包出来看下
抓到后总体感觉进包速度跟APP的流量不太匹配,感觉大概率是走了其他协议。既然抓到的包里面有sign字段,就先从sign字段入手看下,
用frida hook看下调用栈
var ins_C82880ba = Java.use("com.kuaishou.weapon.ks.ba");
ins_C82880ba.a.overload('java.lang.String', 'java.lang.String').implementation = function(a, b) {
var ret = this.a(a, b)
show_stacks()
console.log(ret)
return ret
}
后面根据调用栈顺藤摸瓜分析就好,最后找到了发送函数发现跟okhttp有关
试着将okhttp的接口发送与接收接口打印一下看看
var ins_okhttp = Java.use("okhttp3.OkHttpClient")
ins_okhttp.newCall.overload('okhttp3.Request').implementation = function(a) {
console.log(a);
//show_stacks()
return this.newCall(a);
}
var ins_RealCall = Java.use("okhttp3.RealCall");
ins_RealCall.execute.overload().implementation = function() {
//show_stacks()
var ret = this.execute();
console.log(ret.toString())
return ret
}
找到了__NS_sig3字段,这个是我们需要分析算法的字段,之前Burp Suite抓不到包的原因也出来了,走的是quic协议。
- 接口定位
直接在代码搜索__NS_sig3进行定位
一直往里面进
调用接口找到了
- SO定位
直接搜索C0526k开始
至此,so与调用so的接口都确定下来了。
SO去花
将定位到的lib文件导入IDA,f5之后发现 JNI_OnLoad出现jumpout了
直接汇编先分析一下.text:0000000000045854 ; jint JNI_OnLoad(JavaVM *vm, void *reserved) .text:0000000000045854 EXPORT JNI_OnLoad .text:0000000000045854 JNI_OnLoad ; DATA XREF: LOAD:0000000000003AD0↑o .text:0000000000045854 .text:0000000000045854 var_20 = -0x20 .text:0000000000045854 var_10 = -0x10 .text:0000000000045854 var_8 = -8 .text:0000000000045854 .text:0000000000045854 STP X0, X1, [SP,#-32]! #sp开辟32个字节的空间,x0,x1入栈 .text:0000000000045858 STP X2, X30, [SP,#16] #x2,x30入栈 栈从下往上依次是x0,x1,x2,x30 .text:000000000004585C ADR X1, dword_4587C # x1 = 0x4587c .text:0000000000045860 SUBS X1, X1, #4 # x1 = 0x4587c-4->x1 = 0x45878 .text:0000000000045864 MOV X0, X1 # x0' = x1 -> x0' = 0x45877c .text:0000000000045868 ADDS X0, X0, #0x34 ; '4' # x0' = x0' + 0x34 -> x0' = 0x458ac .text:000000000004586C STR X0, [SP,#24] # [SP,#24] = x0' -> 栈从下往上依次是x0,x1,x2,x0' .text:0000000000045870 LDP X2, X9, [SP,#16] # x2 = [SP,#16],x9=[SP,#24] -> x2 = x2, x9=x0' .text:0000000000045874 LDP X0, X1, [SP],#0x20 # x0 = [SP],x1=[SP,#8] -> x0 = x0,x1=x1 sp恢复栈平衡 .text:0000000000045878 BR X9 # br x0' -> br 0x4669c
从注释基本都可以看出来,前期开栈到恢复栈,就弄了一堆花里胡哨的算了下绝对跳转地址,x0与x1未变化,也比较符合花指令的特性。只要把前面的垃圾指令nop掉,绝对跳转改成相对跳转即可。
先手动使用IDA插件Keypatch试一下,先nop掉垃圾指令。
在修改跳转指令为相对跳转
修改完需要导入patch到so中
重新用IDA打开按F5即可生效,但是一个个修改比较麻烦,还是需要用脚本根据特征码定位进行修改会比较方便,对比JNI_OnLoad与JNI_UnLoad就会发现特征码比较明显
直接上脚本
import keystone
from keystone import *
import ida_bytes
import idaapi
import idc
def pattern_search(pattern):
match_list = []
addr = 0
while True:
addr = ida_bytes.bin_search(addr, idc.BADADDR, bytes.fromhex(pattern), None,
idaapi.BIN_SEARCH_FORWARD, idaapi.BIN_SEARCH_NOCASE)
if addr == idc.BADADDR:
break
else:
match_list.append(addr)
addr = addr + 1
return match_list
def get_jumpout_addr(addr):
data1 = idc.get_operand_value(addr + 8, 1)
data2 = idc.get_operand_value(addr + 12, 2)
data3 = idc.get_operand_value(addr + 20, 2)
return data1 - data2 + data3
def generate_asm(code, addr):
ks = Ks(keystone.KS_ARCH_ARM64, keystone.KS_MODE_LITTLE_ENDIAN)
encode, count = ks.asm(code, addr)
return encode
def main():
match_list = pattern_search("E0 07 BE A9 E2 7B 01 A9")
print(len(match_list))
for i in range(len(match_list)):
encode_b = generate_asm("B " + str(hex(get_jumpout_addr(match_list[i]))), match_list[i])
encode_nop = generate_asm("nop", 0)
ida_bytes.patch_bytes(match_list[i], bytes(encode_b))
ida_bytes.patch_bytes(match_list[i] + 4, bytes(encode_nop) * 9)
if __name__ == "__main__":
main()
Unidbg
先整个基本框架跑一下JNI_OnLoad函数,没问题后再调用so函数,调用so的参数直接hook java层接口结合java源码就能获取,这里就不多说了package com.ks.run; import com.github.unidbg.AndroidEmulator; import com.github.unidbg.Module; import com.github.unidbg.arm.backend.Unicorn2Factory; import com.github.unidbg.linux.android.AndroidEmulatorBuilder; import com.github.unidbg.linux.android.AndroidResolver; import com.github.unidbg.linux.android.dvm.*; import com.github.unidbg.linux.android.dvm.array.ArrayObject; import com.github.unidbg.linux.android.dvm.wrapper.DvmBoolean; import com.github.unidbg.linux.android.dvm.wrapper.DvmInteger; import com.github.unidbg.memory.Memory; import com.github.unidbg.virtualmodule.android.AndroidModule; import java.io.File; import java.util.ArrayList; import java.util.List; public class KSEmulator extends AbstractJni { private final AndroidEmulator emulator; private final Module module; private final VM vm; public KSEmulator() { emulator = AndroidEmulatorBuilder .for64Bit() .addBackendFactory(new Unicorn2Factory(true)) .setProcessName("com.smile.gifmaker") .build(); Memory memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver(23)); vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/kuaishou/ks_12.10.30.39327.apk")); vm.setJni(this); vm.setVerbose(true); new AndroidModule(emulator, vm).register(memory); DalvikModule dm = vm.loadLibrary("kwsgmain", true); module = dm.getModule(); System.out.print("base :" + module.base + "\n"); System.out.print("size :" + module.size + "\n"); dm.callJNI_OnLoad(emulator); } private void call_doCommandNative_sig3(String text) { List<Object> params = new ArrayList<>(); params.add(vm.getJNIEnv()); params.add(0); params.add(10418); StringObject str = new StringObject(vm, text); vm.addLocalObject(str); ArrayObject strArray = new ArrayObject(str); StringObject key1 = new StringObject(vm, "d7b7d042-d4f2-4012-be60-d97ff2429c17"); vm.addLocalObject(key1); DvmInteger dInt = DvmInteger.valueOf(vm, -1); vm.addLocalObject(dInt); DvmBoolean dBoolean = DvmBoolean.valueOf(vm, false); vm.addLocalObject(dBoolean); DvmObject<?> dClass = vm.resolveClass("com/yxcorp/gifshow/App").newObject(null); vm.addLocalObject(dClass); StringObject key2 = new StringObject(vm, ""); vm.addLocalObject(key2); ArrayObject paramArray = new ArrayObject(strArray, key1, dInt, dBoolean, dClass, null, dBoolean, key2); params.add(vm.addLocalObject(paramArray)); Number number = module.callFunction(emulator, 0x40cd4, params.toArray()); DvmObject<?> object = vm.getObject(number.intValue()); String result = (String)object.getValue(); System.out.println("result:"+ result); } public static void main(String[] args) { KSEmulator emulator = new KSEmulator(); emulator.call_doCommandNative_sig3("HandsomeBro"); } }
运行后会出现如下错误
[23:29:06 547] WARN [com.github.unidbg.AbstractEmulator] (AbstractEmulator:417) - emulate RX@0x40040cd4[libkwsgmain.so]0x40cd4 exception sp=unidbg@0xbffff020, msg=unicorn.UnicornException: Invalid memory read (UC_ERR_READ_UNMAPPED), offset=13ms @ Runnable|Function64 address=0x40040cd4, arguments=[unidbg@0xfffe1640[libandroid.so]0x640, 0, 10418, 846492085]
Exception in thread "main" java.lang.NullPointerException
at com.ks.run.KSEmulator.call_doCommandNative_sig3(KSEmulator.java:64)
at com.ks.run.KSEmulator.main(KSEmulator.java:70)
Process finished with exit code 1
第一反应大概率是访问了无效内存导致崩溃了,这种情形一般是调用so接口前没有初始化导致的,先hook找一下初始化的流程
function hook_so()
{
var base = Module.findBaseAddress("libkwsgmain.so");
if (base) {
var addr_doCommandNative = base.add(0x40cd4);
Interceptor.attach(addr_doCommandNative, {
onEnter: function (args) {
console.log("doCommandNative() args[2] = " + args[2])
}, onLeave: function (retval) {
}
})
var addr_gdbf = base.add(0x408a4);
Interceptor.attach(addr_gdbf, {
onEnter: function (args) {
console.log("gdbf() enter")
}, onLeave: function (retval) {
}
})
var addr_dcabk = base.add(0x40948);
Interceptor.attach(addr_dcabk, {
onEnter: function (args) {
console.log("dcabk() enter")
}, onLeave: function (retval) {
}
})
var addr_gdgi = base.add(0x403bc);
Interceptor.attach(addr_gdgi, {
onEnter: function (args) {
console.log("gdgi() enter")
}, onLeave: function (retval) {
}
})
var addr_gksf = base.add(0x407f0);
Interceptor.attach(addr_gksf, {
onEnter: function (args) {
console.log("gksf() enter")
}, onLeave: function (retval) {
}
})
}
}
发现JNI接口其他函数并没有调用,但是每次都会调用doCommandNative的0x28ac也就是10412
unidbg文件加上10412的函数
private void call_doCommandNative_init() {
List<Object> params = new ArrayList<>();
params.add(vm.getJNIEnv());
params.add(0);
params.add(10412);
StringObject key1 = new StringObject(vm, "d7b7d042-d4f2-4012-be60-d97ff2429c17");
vm.addLocalObject(key1);
DvmInteger dInt = DvmInteger.valueOf(vm, 0);
vm.addLocalObject(dInt);
DvmObject<?> dClass = vm.resolveClass("com/yxcorp/gifshow/App").newObject(null);
vm.addLocalObject(dClass);
ArrayObject paramArray = new ArrayObject(dInt, key1, dInt, dInt, dClass, dInt, dInt);
params.add(vm.addLocalObject(paramArray));
Number number = module.callFunction(emulator, 0x40cd4, params.toArray());
System.out.println("numbers:" + number);
DvmObject<?> object = vm.getObject(number.intValue());
String result = (String)object.getValue();
System.out.println("result:"+ result);
}
运行后发现会报缺少类的错误,挨个解决一下
public boolean callBooleanMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
switch (signature) {
case "java/lang/Boolean->booleanValue()Z":
return ((DvmBoolean)dvmObject).getValue();
}
return super.callBooleanMethodV(vm, dvmObject, signature, vaList);
}
@Override
public void callStaticVoidMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature) {
case "com/kuaishou/android/security/internal/common/ExceptionProxy->nativeReport(ILjava/lang/String;)V":
return;
}
}
@Override
public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature) {
case "com/kuaishou/android/security/internal/common/ExceptionProxy->getProcessName(Landroid/content/Context;)Ljava/lang/String;":
return new StringObject(vm, "com.smile.gifmaker");
}
return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
}
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
switch (signature) {
case "com/yxcorp/gifshow/App->getPackageCodePath()Ljava/lang/String;": {
return new StringObject(vm, "/data/app/com.smile.gifmaker-VZhzinzcefoqqzJZ47EE0A==/base.apk");
}
case "com/yxcorp/gifshow/App->getPackageName()Ljava/lang/String;": {
return new StringObject(vm, "com.smile.gifmaker");
}
case "com/yxcorp/gifshow/App->getAssets()Landroid/content/res/AssetManager;": {
return new AssetManager(vm, signature);
}
case "com/yxcorp/gifshow/App->getPackageManager()Landroid/content/pm/PackageManager;": {
return vm.resolveClass("android.content.pm.PackageManager").newObject(null);
}
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}
再次运行结果能正常显示,但是每次结果都会不一样,这里就需要找一下随机因子了,一般时间戳会用的比较多一点,也有用系统函数获取随机数的,可以到IDA里面搜下关键字,先搜索下random
发现很多调用都没有用到,在搜索下time
这里IDA里面很多函数都有用到,直接到unidbg工程里面固定下参数
固定后运行发现数据每次都一样了
如果不想再ida中搜索或者搜索不到关键字,其实也可以直接在unidbg工程里面加日志或者固定参数进行测试,unidbg工程有实现标准的linux系统调用接口
今天就记录到这里,剩下的算法还原我们留到第二篇在写。今天就记录到这里,剩下的算法还原我们留到第二篇在写。
警告:本文章相关代码与分析流程仅用于技术学习与提升,切勿用于非法用途,否则后果自负。警告:本文章相关代码与分析流程仅用于技术学习与提升,切勿用于非法用途,否则后果自负。