某短视频平台最新sig3字段算法分析(一)

前言

最近看到手里的某短视频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 0x458ac

从注释基本都可以看出来,前期开栈到恢复栈,就弄了一堆花里胡哨的算了下绝对跳转地址,x0与x1未变化,也比较符合花指令的特性。只要把前面的垃圾指令nop掉,绝对跳转改成相对跳转即可。

先手动使用IDA插件Keypatch试一下,先nop掉垃圾指令

在修改跳转指令为相对跳转

重新用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系统调用接口

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

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

### 关于iOS App Crash Related to `__NS_sig3` 或 NSException 处理 当涉及到 iOS 应用程序崩溃 (`crash`) 的问题时,尤其是与特定签名机制(如 `__NS_sig3`)相关的情况,通常需要深入理解应用内部的安全逻辑以及异常处理流程。 #### 1. **关于 `__NS_sig3` 和其作用** `__NS_sig3` 是某些移动应用程序中用于安全验证的种签名字段。它通常是通过复杂的哈希函数生成的字符串,旨在防止未经授权的数据篡改或伪造请求。例如,在快手应用中,该字段可能由以下方式生成: ```python def gen_tokensig(sig, salt=""): v = sig + salt return hashlib.sha256(v.encode(&#39;utf-8&#39;)).hexdigest() ``` 上述代码片段展示了如何基于输入参数 `sig` 和可选盐值 `salt` 来计算 SHA-256 哈希值[^2]。这种实现方法可以用来保护数据传输过程中的完整性。 #### 2. **Crash 可能的原因分析** 如果个 iOS 应用因 `__NS_sig3` 而发生崩溃,则可能是由于以下几个原因之引起的: - **非法访问内存**: 如果 Hook 操作不当或者修改了原生库的行为,可能会导致未定义行为并触发运行时错误。 - **不匹配的签名校验失败**: 当服务器端接收到客户端发送过来带有错误签名的消息时,服务端会拒绝响应甚至终止连接尝试;而本地调试环境下也可能因为未能正确模拟官方算法而导致断言条件被触发展现出崩溃现象。 - **异常捕获不足**: 在开发阶段如果没有妥善设置全局范围内的 NSError 或者 try-catch 结构来应对潜在风险事件的话,那么旦遇到预料之外状况就很容易造成整个进程停止运作。 以下是针对这些情况的些解决方案建议: #### 解决方案: 使用 Frida 进行动态分析 Frida 提供了种强大的工具集允许开发者轻松挂钩任意 API 并实时监控它们之间的交互细节。对于研究像 KSecurity 类这样的组件非常有用处. 示例脚本如下所示: ```javascript Java.perform(function () { var IKSecurityExCls = Java.use("com.xxx.android.security.KSecurity"); IKSecurityExCls.atlasSign.implementation = function (a) { var result = this.atlasSign(a); console.log(a + " >>> atlasSignB " + result); return result; }; }); ``` 此段 JavaScript 将拦截每次调用 `atlasSign()` 方法的动作,并打印原始输入及其对应的输出结果到控制台以便进步审查.[^1] #### 解决方案二: 改善现有的 Python 实现 为了更精确地模仿目标平台上的认证协议,我们还可以调整之前提到过的 python 函数以适应更多场景需求. 改进后的版本如下: ```python import hashlib class SignatureGenerator(object): @staticmethod def generate_token_signature(base_string, additional_salt=&#39;&#39;): combined_value = base_string + additional_salt digest_algorithm_instance = hashlib.sha256(combined_value.encode()) final_hashed_output = digest_algorithm_instance.hexdigest() return final_hashed_output @staticmethod def create_md5_based_signature(input_parameters_dict={}, fixed_suffix=&#39;ca8e86efb32e&#39;): sorted_keys_list = list(dict(sorted(input_parameters_dict.items())).keys()) concatenated_query_strings = &#39;&#39; for current_key in sorted_keys_list[:-3]: concatenated_query_strings += &#39;{}={}&#39;.format(current_key, input_parameters_dict[current_key]) complete_message_to_be_encoded = concatenated_query_strings + fixed_suffix md5_encoder_object = hashlib.md5(complete_message_to_be_encoded.encode()) resulting_checksum = md5_encoder_object.hexdigest() return resulting_checksum ``` 以上类封装了两个主要功能——个是创建基于SHA-256散列技术的新令牌标记;另个则是构建MD5摘要形式的标准签名串。这样做的好处在于提高了代码复用率的同时还增强了灵活性便于后续扩展维护工作开展顺利进行下去. 最后值得注意的是,在实际操作过程中还需要特别关注各个具体环节是否存在边界约束违反等问题以免引起不必要的麻烦。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值