• 前言
    最近看到手里的某短视频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系统调用接口

今天就记录到这里,剩下的算法还原我们留到第二篇在写。今天就记录到这里,剩下的算法还原我们留到第二篇在写。

警告:本文章相关代码与分析流程仅用于技术学习与提升,切勿用于非法用途,否则后果自负。警告:本文章相关代码与分析流程仅用于技术学习与提升,切勿用于非法用途,否则后果自负。