app逆向之安卓native层安全逆向分析(六):frida调试跟栈+unidbg补环境大动作
前言
继续跟着龙哥的unidbg学习:SO逆向入门实战教程六:s_白龙~的博客-CSDN博客
还是那句,我会借鉴龙哥的文章,以一个初学者的角度,加上自己的理解,把内容丰富一下,尽量做到不在龙哥的基础上画蛇添足,哈哈。感谢观看的朋友
分析
首先抓个包看看:
这里面这个sign,就是今天的重点了
这个app没有壳,直接jadx打开,然后搜【sign"】或者【sign=】
很快的,就找到这个地方
目前确实还不确定是不是这里,用objection hook下就知道了:
先hook了类,一堆,卧槽
hook tostring方法:
app多划两下:可以看到基本是对的上的
然后再找找堆栈调用,很快定位到这里:
hook下这个类,发现有一堆,
慢慢来,首先这个appkey,就是这里了
根据龙哥说的,就是libBili.s方法,但是hook了之后就加载就一直卡住。换用fridahook :
function showStacks() {
console.log(
Java.use("android.util.Log")
.getStackTraceString(
Java.use("java.lang.Throwable").$new()
)
);
}
function main() {
Java.perform(function () {
var ClassName = "com.bilibili.nativelibrary.LibBili";
var Bilibili = Java.use(ClassName);
Bilibili.s.overload('java.util.SortedMap').implementation = function (arg) {
console.log(JSON.stringify(arg))
Java.openClassFile("/data/local/tmp/r0gson.dex").load()
const gson = Java.use('com.r0ysue.gson.Gson').$new();
const json_x = gson.toJson(arg)
console.log("json_x===>", json_x);
let res = this.s(arg)
console.log('result===>', res)
showStacks()
return res;
}
});
}
setTimeout(main, 2000)
感觉就是这个s了,入参是:
{"ad_extra":"E0DA3BAB712CAB3902D558CFD4FEB865AB1563B7D61A4A57D02854BB9A4512C5E4737CA15664C505F3E63B954BB68837690B81FE77B2E7EB578F3E3D7C528439725348FE86527D7415BA2267FCCD2C95895AE80E69A960812FE60
BC477171DAD34472EC31D010940A2876CADE887EA26FD5FBFB718D09F193D14732E54E42EB329EBF61CA33DE876AF24C03A7B9E89DB9C831AF6D1CB8B3B883AA3856AF08424AD8913BDE1207A5C2E010E23274ECCA7BC44814561E236325F0C4D2FFE069D2F9B
F5B40411A3D26D0BB864EC87713391FAE1197ACFD402D6D386B035BF04995C280989B7305964F179E149067FA864F66FA856EFBD3C8A005F92F1D7C93D82BC8C5B4804DC54154F34E5820B2A481C3F33EEDDB4323C142359920F60686D95E715E30894676AEDE
6933A31092E98F979157D267D580D056C3EDDD31934E762D7354A2E1959AAD44D1DDAF094B6E8F38C5450C6A164BD40AF2C3FB2E44BD8E0F914C7BA0FBA4FA553A2A1291BAF9E87FC528C2D53A6B3D3EFEA88F5C359119A31018AD879124BE62B4A4BCFEF98FA
5EE1BB5675D9828D1B246A10841CF1F6D1CC1C13C3C11C7E37BCEF93E933395A1FA4D7AEA4CB79DC3ED1EC162C926CFE1157366100A3AF788CF3262DC381B620FB68913694FD332D7D555CD60E3AC54E2F0A5FC2C90530A27721664A64B008FAB0A6","appkey
":"1d8b6e7d45233436","autoplay_card":"11","build":"6180500","c_locale":"zh_CN","channel":"shenma069","column":"2","device_name":"MI 6","device_type":"0","flush":"8","fnval":"400","fnver":"0","force_host":"
0","fourk":"1","guidance":"0","https_url_req":"0","idx":"1682561936","inline_danmu":"2","inline_sound":"1","login_event":"0","mobi_app":"android","network":"wifi","open_event":"","platform":"android","play
er_net":"1","pull":"false","qn":"32","recsys_mode":"0","s_locale":"zh_CN","splash_id":"","statistics":"{\"appId\":1,\"platform\":3,\"version\":\"6.18.0\",\"abtest\":\"\"}"}
我上面的流程只是为了完整的展示,拿到app,定位加密,然后开始分析的,看过我前面的文章的应该都知道我在干嘛。
调试
ok,那就开始so层的调试和分析了
1.找so文件
apk我们知道,但是so文件,在这里,乍一看啥都没有:
根据我们之前的定律,加载so文件都是static方法,然后,system.loadlibrary,但是这里就奇怪了,啥都没有
只看到这里有点可疑:
点进去,知道G是bili
c.c会不会是system.loadlibrary呢?追进去:
看到使用str只有f,其他的逻辑我们可以不用管
f追进去,我们知道,后面两个参数是null,因为根本就没传这两个,那就是这个g方法
g追进去,然后一下就看到了这个loadlibrary:
再追进去,感觉这个【com.getkeepsafe.relinker】类有点奇怪
网上搜一下即可:
看到这个介绍,ok,就是这里了。
所以我们的so文件就是这个bili了。
2.搭架子
又报了这个错:
[main]W/libc: pthread_create failed: clone failed: Out of memory
前面我们已经用过了:
在这加一行这个
emulator.getSyscallHandler().setEnableThreadDispatcher(true);
ok了
而且,我们的目标函数地址也打印出来了:
3.开始调用s方法
但是参数是TreeMap,根据龙哥的博客说的,需要搞个继承关系,map->abstractMap->TreeMap
补一下:
继续补:
插一句,如果这里,你没给ts,会报如下错,而且提示都没有,就很骚,我是用frida hook到的某一个流程的参数,刚好就没有ts。但是实际调用的时候是有ts的:
回到刚才的地方,卧槽,他这里要一个 SignedQuery对象的r方法,也就是这个:
新建了一个util类,然后把jadx的copy过来,导入好已有的,然后如下:
把ContainerUtils的这两个属性拿过来:
现在就差b方法了
把a,b,c都copy完,还有这个:
整过来
现在还有cv.m了,
ok,整好了运行,卧槽,还要这个初始化对象:
再看看这个类的构造方法是啥:
也还好,就两个属性
好了终于没有报错了,发现,这个第二个参数就是sign:
再看这个返回结果 ,好像有点不对劲,以前的不都是直接把结果字符串打印出来了吗,这里咋没打印,
根据前面的跟栈我们知道,他会调用下toString,那么,这里我们也把toString补上去看看:
牛逼,终于出来了。
完整代码
package com.danmaku;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
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.ByteArray;
import com.github.unidbg.memory.Memory;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeMap;
public class bili extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
bili() {
// 创建模拟器实例,进程名建议依照实际进程名填写,可以规避针对进程名的校验
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.bilibili.app").build();
// 获取模拟器的内存操作接口
final Memory memory = emulator.getMemory();
emulator.getSyscallHandler().setEnableThreadDispatcher(true);
// 设置系统类库解析
memory.setLibraryResolver(new AndroidResolver(23));
// 创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作
vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\danmaku\\bilibili.apk"));
// 加载目标SO
DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\danmaku\\libbili.so"), true); // 加载so到虚拟内存
//获取本SO模块的句柄,后续需要用它
module = dm.getModule();
vm.setJni(this); // 设置JNI
vm.setVerbose(true); // 打印日志
dm.callJNI_OnLoad(emulator); // 调用JNI OnLoad
};
public static void main(String[] args) {
bili test = new bili();
System.out.println(test.getSign());
}
@Override
public boolean callBooleanMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
if ("java/util/Map->isEmpty()Z".equals(signature)) {
TreeMap<String, String> treeMap = (TreeMap<String, String>)dvmObject.getValue();
return treeMap.isEmpty();
}
return super.callBooleanMethod(vm, dvmObject, signature, varArg);
}
@Override
public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
switch (signature) {
case "java/util/Map->get(Ljava/lang/Object;)Ljava/lang/Object;":
StringObject keyobject = varArg.getObjectArg(0);
String key = keyobject.getValue();
TreeMap<String, String> treeMap = (TreeMap<String, String>)dvmObject.getValue();
String value = treeMap.get(key);
return new StringObject(vm, value);
}
return super.callObjectMethod(vm, dvmObject, signature, varArg);
}
@Override
public DvmObject<?> callStaticObjectMethod(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
switch (signature){
case "com/bilibili/nativelibrary/SignedQuery->r(Ljava/util/Map;)Ljava/lang/String;":{
DvmObject<?> mapObject = varArg.getObjectArg(0);
TreeMap<String, String> mymap = (TreeMap<String, String>) mapObject.getValue();
String result = util.r(mymap);
return new StringObject(vm, result);
}
}
return super.callStaticObjectMethod(vm, dvmClass, signature, varArg);
}
@Override
public DvmObject<?> newObject(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
switch (signature) {
case "com/bilibili/nativelibrary/SignedQuery-><init>(Ljava/lang/String;Ljava/lang/String;)V":
StringObject stringObject1 = varArg.getObjectArg(0);
StringObject stringObject2 = varArg.getObjectArg(1);
String str1 = stringObject1.getValue();
String str2 = stringObject2.getValue();
return vm.resolveClass("com/bilibili/nativelibrary/SignedQuery").newObject(new SignedQuery(str1, str2));
}
return super.newObject(vm, dvmClass, signature, varArg);
}
public class SignedQuery {
public final String a;
public final String b;
public SignedQuery(String str, String str2) {
this.a = str;
this.b = str2;
}
public String toString() {
String str = this.a;
if (str == null) {
return "";
}
if (this.b == null) {
return str;
}
return this.a + "&sign=" + this.b;
}
};
public String getSign(){
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv());
list.add(0);
// 这里传入treemap
TreeMap<String, String> keymap = new TreeMap<String, String>();
keymap.put("ad_extra", "E0DA3BAB712CAB3902D558CFD4FEB865AB1563B7D61A4A57D02854BB9A4512C5E4737CA15664C505F3E63B954BB68837690B81FE77B2E7EB578F3E3D7C528439725348FE86527D7415BA2267FCCD2C95895AE80E69A960812FE60BC477171DAD34472EC31D010940A2876CADE887EA26FD5FBFB718D09F193D14732E54E42EB329EBF61CA33DE876AF24C03A7B9E89DB9C831AF6D1CB8B3B883AA3856AF08424AD8913BDE1207A5C2E010E23274ECCA7BC44814561E236325F0C4D2FFE069D2F9BF5B40411A3D26D0BB864EC87713391FAE1197ACFD402D6D386B035BF04995C280989B7305964F179E149067FA864F66FA856EFBD3C8A005F92F1D7C93D82BC8C5B4804DC54154F34E5820B2A481C3F33EEDDB4323C142359920F60686D95E715E30894676AEDE6933A31092E98F979157D267D580D056C3EDDD31934E762D7354A2E1959AAD44D1DDAF094B6E8F38C5450C6A164BD40AF2C3FB2E44BD8E0F914C7BA0FBA4FA553A2A1291BAF9E87FC528C2D53A6B3D3EFEA88F5C359119A31018AD879124BE62B4A4BCFEF98FA5EE1BB5675D9828D1B246A10841CF1F6D1CC1C13C3C11C7E37BCEF93E933395A1FA4D7AEA4CB79DC3ED1EC162C926CFE1157366100A3AF788CF3262DC381B620FB68913694FD332D7D555CD60E3AC54E2F0A5FC2C90530A27721664A64B008FAB0A6");
keymap.put("appkey", "1d8b6e7d45233436");
keymap.put("autoplay_card", "11");
keymap.put("build", "6180500");
keymap.put("c_locale", "zh_CN");
keymap.put("channel", "shenma069");
keymap.put("column", "2");
keymap.put("device_name", "MI 6");
keymap.put("device_type", "0");
keymap.put("flush", "8");
keymap.put("fnval", "400");
keymap.put("fnver", "0");
keymap.put("force_host", "0");
keymap.put("fourk", "1");
keymap.put("guidance", "0");
keymap.put("https_url_req", "0");
keymap.put("idx", "1682561936");
keymap.put("inline_danmu", "2");
keymap.put("inline_sound", "1");
keymap.put("login_event", "0");
keymap.put("mobi_app", "android");
keymap.put("network", "wifi");
keymap.put("open_event", "");
keymap.put("platform", "android");
keymap.put("player_net", "1");
keymap.put("pull", "false");
keymap.put("qn", "32");
keymap.put("recsys_mode", "0");
keymap.put("s_locale", "zh_CN");
keymap.put("splash_id", "");
keymap.put("statistics", "{\"appId\":1,\"platform\":3,\"version\":\"6.18.0\",\"abtest\":\"\"}");
keymap.put("ts", "1612693177");
DvmClass Map = vm.resolveClass("java/util/Map");
DvmClass AbstractMap = vm.resolveClass("java/util/AbstractMap",Map);
DvmObject<?> input_map = vm.resolveClass("java/util/TreeMap", AbstractMap).newObject(keymap);
list.add(vm.addLocalObject(input_map));
Number number = module.callFunction(emulator,0x1c97,list.toArray());
String result = vm.getObject(number.intValue()).getValue().toString();
return result;
}
}
util:
package com.danmaku;
import java.io.UnsupportedEncodingException;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
public class util {
private static final char[] f14884c = "0123456789ABCDEF".toCharArray();
private static boolean a(char c2, String str) {
return (c2 >= 'A' && c2 <= 'Z') || (c2 >= 'a' && c2 <= 'z') || !((c2 < '0' || c2 > '9') && "-_.~".indexOf(c2) == -1 && (str == null || str.indexOf(c2) == -1));
}
static String b(String str) {
return c(str, null);
}
static String c(String str, String str2) {
StringBuilder sb = null;
if (str == null) {
return null;
}
int length = str.length();
int i2 = 0;
while (i2 < length) {
int i3 = i2;
while (i3 < length && a(str.charAt(i3), str2)) {
i3++;
}
if (i3 != length) {
if (sb == null) {
sb = new StringBuilder();
}
if (i3 > i2) {
sb.append((CharSequence) str, i2, i3);
}
i2 = i3 + 1;
while (i2 < length && !a(str.charAt(i2), str2)) {
i2++;
}
try {
byte[] bytes = str.substring(i3, i2).getBytes("UTF-8");
int length2 = bytes.length;
for (int i4 = 0; i4 < length2; i4++) {
sb.append('%');
sb.append(f14884c[(bytes[i4] & 240) >> 4]);
sb.append(f14884c[bytes[i4] & 15]);
}
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
} else if (i2 == 0) {
return str;
} else {
sb.append((CharSequence) str, i2, length);
return sb.toString();
}
}
return sb == null ? str : sb.toString();
}
static String r(Map<String, String> map) {
String str;
if (!(map instanceof SortedMap)) {
map = new TreeMap<>(map);
}
StringBuilder sb = new StringBuilder(256);
for (Map.Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey();
if (!key.isEmpty()) {
sb.append(b(key));
sb.append("=");
String value = entry.getValue();
if (value == null) {
str = "";
} else {
str = b(value);
}
sb.append(str);
sb.append("&");
}
}
int length = sb.length();
if (length > 0) {
sb.deleteCharAt(length - 1);
}
if (length == 0) {
return null;
}
return sb.toString();
}
}
欧克。
4.直接补类
根据龙哥的博客,说还有另一种补环境的方式,重建类:
不再继承abstrctjni和不再用setjni方法,改用vm.setDvmClassFactory(new ProxyClassFactory());
package com.danmaku;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
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.jni.ProxyClassFactory;
import com.github.unidbg.memory.Memory;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeMap;
public class bili2 {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
bili2() {
// 创建模拟器实例,进程名建议依照实际进程名填写,可以规避针对进程名的校验
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.bilibili.app").build();
// 获取模拟器的内存操作接口
final Memory memory = emulator.getMemory();
emulator.getSyscallHandler().setEnableThreadDispatcher(true);
// 设置系统类库解析
memory.setLibraryResolver(new AndroidResolver(23));
// 创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作
vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\danmaku\\bilibili.apk"));
// 加载目标SO
DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\danmaku\\libbili.so"), true); // 加载so到虚拟内存
//获取本SO模块的句柄,后续需要用它
module = dm.getModule();
// vm.setJni(this); // 设置JNI
vm.setDvmClassFactory(new ProxyClassFactory());
vm.setVerbose(true); // 打印日志
dm.callJNI_OnLoad(emulator); // 调用JNI OnLoad
};
public static void main(String[] args) {
bili2 test = new bili2();
System.out.println(test.getSign());
}
public String getSign(){
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv());
list.add(0);
// 这里传入treemap
TreeMap<String, String> keymap = new TreeMap<String, String>();
keymap.put("ad_extra", "E0DA3BAB712CAB3902D558CFD4FEB865AB1563B7D61A4A57D02854BB9A4512C5E4737CA15664C505F3E63B954BB68837690B81FE77B2E7EB578F3E3D7C528439725348FE86527D7415BA2267FCCD2C95895AE80E69A960812FE60BC477171DAD34472EC31D010940A2876CADE887EA26FD5FBFB718D09F193D14732E54E42EB329EBF61CA33DE876AF24C03A7B9E89DB9C831AF6D1CB8B3B883AA3856AF08424AD8913BDE1207A5C2E010E23274ECCA7BC44814561E236325F0C4D2FFE069D2F9BF5B40411A3D26D0BB864EC87713391FAE1197ACFD402D6D386B035BF04995C280989B7305964F179E149067FA864F66FA856EFBD3C8A005F92F1D7C93D82BC8C5B4804DC54154F34E5820B2A481C3F33EEDDB4323C142359920F60686D95E715E30894676AEDE6933A31092E98F979157D267D580D056C3EDDD31934E762D7354A2E1959AAD44D1DDAF094B6E8F38C5450C6A164BD40AF2C3FB2E44BD8E0F914C7BA0FBA4FA553A2A1291BAF9E87FC528C2D53A6B3D3EFEA88F5C359119A31018AD879124BE62B4A4BCFEF98FA5EE1BB5675D9828D1B246A10841CF1F6D1CC1C13C3C11C7E37BCEF93E933395A1FA4D7AEA4CB79DC3ED1EC162C926CFE1157366100A3AF788CF3262DC381B620FB68913694FD332D7D555CD60E3AC54E2F0A5FC2C90530A27721664A64B008FAB0A6");
keymap.put("appkey", "1d8b6e7d45233436");
keymap.put("autoplay_card", "11");
keymap.put("build", "6180500");
keymap.put("c_locale", "zh_CN");
keymap.put("channel", "shenma069");
keymap.put("column", "2");
keymap.put("device_name", "MI 6");
keymap.put("device_type", "0");
keymap.put("flush", "8");
keymap.put("fnval", "400");
keymap.put("fnver", "0");
keymap.put("force_host", "0");
keymap.put("fourk", "1");
keymap.put("guidance", "0");
keymap.put("https_url_req", "0");
keymap.put("idx", "1682561936");
keymap.put("inline_danmu", "2");
keymap.put("inline_sound", "1");
keymap.put("login_event", "0");
keymap.put("mobi_app", "android");
keymap.put("network", "wifi");
keymap.put("open_event", "");
keymap.put("platform", "android");
keymap.put("player_net", "1");
keymap.put("pull", "false");
keymap.put("qn", "32");
keymap.put("recsys_mode", "0");
keymap.put("s_locale", "zh_CN");
keymap.put("splash_id", "");
keymap.put("statistics", "{\"appId\":1,\"platform\":3,\"version\":\"6.18.0\",\"abtest\":\"\"}");
keymap.put("ts", "1612693177");
DvmClass Map = vm.resolveClass("java/util/Map");
DvmClass AbstractMap = vm.resolveClass("java/util/AbstractMap",Map);
DvmObject<?> input_map = vm.resolveClass("java/util/TreeMap", AbstractMap).newObject(keymap);
list.add(vm.addLocalObject(input_map));
Number number = module.callFunction(emulator,0x1c97,list.toArray());
String result = vm.getObject(number.intValue()).getValue().toString();
return result;
}
}
直接运行:
把需要的类全都放进去,SignedQuery、ContainerUtils、cv
记得把signedQuery放到他该有的类,其他就随意了,他会自己在同级目录查找,ok,直接出结果。这里龙哥把ContainerUtils、cv类都单独创建了该有的类名前缀,我这里就没有了,一样出结果,感觉还是归类在一起好点
这样的效果就是,不需要去手动补需要的环境了(因为需要的环境类整个都有了)
知识点总结
1.treemap需要注意下,在unidbg里需要继承map,abstractmap
2.varArg.getObjectArg(0)可以获取该方法的参数,0,指第一个,1指第二个
StringObject stringObject1 = varArg.getObjectArg(0);
StringObject stringObject2 = varArg.getObjectArg(1);
String str1 = stringObject1.getValue();
String str2 = stringObject2.getValue();
3.unidbg有新的写法,不继承abstractjni,setjni的地方改成vm.setDvmClassFactory(new ProxyClassFactory());
这种方法适合java环境太多的情况,不用一个一个手动补,直接用java用到的类