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】类有点奇怪

 

网上搜一下即可: 

 

KeepSafe/ReLinker:一个强大的Android原生库加载器。 (github.com)

看到这个介绍,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用到的类

 

posted @ 2023-04-27 16:32  Eeyhan  阅读(841)  评论(0编辑  收藏  举报