某短视频 sig3 逆向纯算分析
本文通过Unidbg模拟器分析某短视频应用的sig3加密参数生成逻辑。使用AndroidEmulator构建64位环境,加载目标so库并调用JNI方法获取加密结果。重点展示了doCommandNative方法的调用过程,包括参数传递和返回值处理,实现了对com.kuaishou.android.security.internal.dispatch.JNICLibrary类的10418指令调用。代码
前言
今天来分析一下某短视频系列的 sig3 加密参数,主要目的是探索学习app接口的加解密机制。仅供学习,禁止用作其他用途。未经允许禁止任何形式的转载,保留追究法律责任的权利。
逻辑分析
app样本:5Zac55Wq5YWN6LS555+t5Ymn
一. unidbg运行
package com.xx;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.debugger.BreakPointCallback;
import com.github.unidbg.debugger.Debugger;
import com.github.unidbg.file.FileResult;
import com.github.unidbg.file.IOResolver;
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.array.ByteArray;
import com.github.unidbg.linux.android.dvm.api.AssetManager;
import com.github.unidbg.linux.android.dvm.wrapper.DvmBoolean;
import com.github.unidbg.linux.android.dvm.wrapper.DvmInteger;
import com.github.unidbg.linux.file.SimpleFileIO;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.pointer.UnidbgPointer;
import com.github.unidbg.virtualmodule.android.AndroidModule;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import com.github.unidbg.virtualmodule.android.JniGraphics;
public class sign extends AbstractJni implements IOResolver{
@Override
public FileResult resolve(Emulator emulator, String pathname, int oflags) {
return null;
}
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
public sign() {
// 创建模拟器实例,进程名建议依照实际进程名填写,可以规避针对进程名的校验
emulator = AndroidEmulatorBuilder.for64Bit()
.addBackendFactory(new Unicorn2Factory(true))
.setProcessName("com.xx")// 模拟手机文件的根路径
.build();
// 获取模拟器的内存操作接口
final Memory memory = emulator.getMemory();
// 设置系统类库解析
memory.setLibraryResolver(new AndroidResolver(23));
// 创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作
vm = emulator.createDalvikVM(new File("xx"));
// 如果提示缺失依赖so
vm.setJni(this);
new AndroidModule(emulator, vm).register(memory);
new JniGraphics(emulator, vm).register(memory);
emulator.getSyscallHandler().addIOResolver(this); //重定向io
// 设置是否打印Jni调用细节
vm.setVerbose(true);
DalvikModule dm1 = vm.loadLibrary("kwsgmain", true);
module = dm1.getModule();
dm1.callJNI_OnLoad(emulator);
public static void main(String[] args) throws FileNotFoundException {
sign test = new sign();
AndroidEmulator emulator = test.emulator;
VM vm = test.vm;
test.call_doCommandNative_init();
test.get_NS_sig3();
}
public String get_NS_sig3(){
DvmClass dvmClass = vm.resolveClass("com.kuaishou.android.security.internal.dispatch.JNICLibrary");
String methodSign="doCommandNative(I[Ljava/lang/Object;)Ljava/lang/Object;";
DvmObject<?> context = vm.resolveClass("com.kwai.theater.KSApplication").newObject(null);
String aa = "coder7777";
DvmObject<?> ret1 = dvmClass.callStaticJniMethodObject(
emulator, methodSign, 10418,
new ArrayObject(
new ArrayObject(new StringObject(vm, aa)),
new StringObject(vm, "d74f8f6d-951f-4ba0-bace-e5666ea0e323"),
DvmInteger.valueOf(vm, -1),
DvmBoolean.valueOf(vm, false),
context,
null,
DvmBoolean.valueOf(vm, false),
new StringObject(vm, "")
));
System.out.println("QKING, initMain.ret-10418: " + ret1);
return ret1.toString();
}
private void call_doCommandNative_init() {
DvmClass dvmClass = vm.resolveClass("com.kuaishou.android.security.internal.dispatch.JNICLibrary");
String methodSign="doCommandNative(I[Ljava/lang/Object;)Ljava/lang/Object;";
DvmObject<?> context = vm.resolveClass("com.kwai.theater.KSApplication").newObject(null); //
DvmObject<?> ret11 = dvmClass.callStaticJniMethodObject(
emulator, methodSign, 10412,
new ArrayObject(
null,
new StringObject(vm, "d74f8f6d-951f-4ba0-bace-e5666ea0e323"),
null,
null,
context,
null,
null
));
System.out.println("QKING, call_doCommandNative_init: " + ret11);
}
@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;
}
super.callStaticVoidMethodV(vm, dvmClass, signature, vaList);
}
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
switch (signature) {
case "com/kwai/theater/KSApplication->getPackageCodePath()Ljava/lang/String;": {
return new StringObject(vm, "/data/app/~~DsTsX9Czu7IXbyf7Kgqq9w==/com.kwai.theater-Vd-G-su_DD7b_HtgBaAt5w==/base.apk");
}
case "com/kwai/theater/KSApplication->getPackageName()Ljava/lang/String;": {
return new StringObject(vm, "com.kwai.theater");
}
case "com/kwai/theater/KSApplication->getAssets()Landroid/content/res/AssetManager;": {
return new AssetManager(vm, signature);
}
case "com/kwai/theater/KSApplication->getPackageManager()Landroid/content/pm/PackageManager;": {
return vm.resolveClass("android/content/pm/PackageManager").newObject(signature);
}
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}
@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.kwai.theater");
case "com/kuaishou/android/security/internal/common/ExceptionProxy->getThreadByName(Ljava/lang/String;)Ljava/lang/String;":
String threadName = String.valueOf(vaList.getIntArg(0));
return new StringObject(vm, threadName);
}
return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
}
-
正常出结果,这块就没什么好讲的了,网上有很多份,唯一要注意的就是要先初始化一下
-
出结果后,固定入参 ”coder7777“,结果每次都不一样,猜测是有时间戳或者随机数,我们来测试一下,首先固定时间戳:
此时发现入参固定,时间戳固定,结果一致,结果为42530000d2f6651b0a0a09085baf1cd100b40776171b1503,更改参数和时间戳也看不出什么了,咱们接着往下走。 -
so去花
跟着大佬学习肯定不会错,我这里也是看着龙哥的文章去的花,具体过程就不分析了,文章里面或者其他资料都很多,跟着照做就可以实现了。
https://www.yuque.com/lilac-2hqvv/zfho3g/issny5?#%20%E3%80%8A%E8%8A%B1%E6%8C%87%E4%BB%A4%E5%A4%84%E7%90%86%EF%BC%88%E4%B8%80%EF%BC%89%E3%80%8B
二. trace分析
try {
emulator.traceCode(module.base, module.base + module.size).setRedirect(new PrintStream(filePath +"traceCodeCar_0x20694.log"));
}catch (IOException e) {
throw new IllegalStateException(e);
}
-
先trace一份代码出来,首先 根据结果”42530000d2f6651b0a0a09085baf1cd100b40776171b1503“查找,看看是哪里生成的,咱们在trace里面搜索一下 0x1b(不要搜索0x03这些结果比较多)。搜索发现这里会比较像:
unidbg在这里断一下,看看x21
看样子结果是我们想要的,然后打开ida看下流程,快捷键”x“和打断点配合,最终确认如下
上面就是v460 生成的地方,我们在这个地方打上断点:
我们python实现一下异或操作 注意共执行23轮,然后第二十四位不变:
也没问题,结果正是 42530000d2f6651b0a0a09085baf1cd100b40776171b1503 -
现在我们就要分析 41510100d5f0601f0100000054a111dd13a61666000d0003 的来源了,这里我们先测试下
只修改明文:
41510100d5f0601f0100000054a111dd13a61666000d0003 原文
41510100d5f0601f0100000061193a8d13a61666000d00a5 修改后
13-16位和最后一位变化
只修改时间戳:
41510100d5f0601f0100000054a111dd13a61666000d0003 原文
415101000e9525310100000054a111dd12a61666000d004f 修改后
5-8处字节发生变化 17-19和最后一个字节发生变化。
两者均修改
41510100d5f0601f0100000054a111dd13a61666000d0003 原文
415101000e9525310100000061193a8d12a61666000d00f1 修改后
5-8处字节发生变化,13~20处字节发生变化,和最后一个字节变化
三.明文分析
crc32算法
-
由以上可知,13-16位和明文”coder7777“有关,13-16位是 dd11a154, 咱们trace里面搜索一下:
在0x1620处,我们在这里打个断点:
mx0是密文,mx1=0x30是长度,根据网上资料可以,这里用的是crc32算法,我们来测试一下:
得到结果 ”dd11a154’ 且没有魔改aes-cbc加密
这里再看下“b7332938d71fbd84260aabaf3ecd0e6e164c729106057925b51710d697f5e06ced13be5d336bf29ad77fbad8cb814cad”是怎么来的。还是根据网上资料,初步认为是aes-cbc,咱们只需要验证是不是即可。
上文的b73329xxxxx所在的位置是 0x404e44c0,咱们这里trace追踪一下
emulator.traceWrite(0x404e44c0,0x404e44c0+0x30);
这里看到调用的地方是在ilbc里面,0x1d62c是结束调用的地方,咱们在 0x1d62c 前面的位置打个断点
可以看到是在 0x404d31b0 这个位置,我们在trace一下:
emulator.traceWrite(0x404d31b0,0x404d31b0+0x30);
可以看到是在0x25b30,ida分析一下,这里大概就是aes算法的地方了,再具体的就没必要去分析了,网上的资料很多,例如如何知道是aes,且是cbc模式,这里就不细说了。
咱们hook一下入参:
这里一直入参为 33c4d4f0f3316a554af23888c78af4b44050d6c952572c743e047af26c1d8c3e
结果为 b7332938d71fbd84260aabaf3ecd0e6e164c729106057925b51710d697f5e06ced13be5d336bf29ad77fbad8cb814cad
加密方式为 aes-cbc.
咱们直接上dfa:
debugger.addBreakPoint(1073741824 + 0x24B1C, new BreakPointCallback() {
int count = 1;
int count2 = 0;
@Override
public boolean onHit(Emulator<?> emulator, long address) {
byte[] data= {0x1};
if (count % 9 == 0) {
data[0] = (byte) randint(0,0xff);
emulator.getBackend().mem_write(0x404d3150 + count2 * 0x10 + randint(0, 15), data);
}
count++;
if (count == 11) {
count = 1;
count2++;
}
return true;
}
});
运行10次以上,得到一个原文和多个错误密文,然后使用phoenixAES算出第10轮秘钥
算出后再用 Stark 算出主密钥
这里算出密钥后,验证一下:
结果正确,说明咱们aes密钥找对了
hmac-sha256
最后一步了,找出 33c4d4f0f3316a554af23888c78af4b44050d6c952572c743e047af26c1d8c3e 生成的位置,由hook入参可知,33c4xxx存在 0x404d8340,咱们trace一下
emulator.traceWrite(0x404d8340,0x404d8340+0x20);
是在0x1ce50,此时已经生成结果,对上一个函数进行hook
hmac的特征值就是0x36和0x5c,经过验证
所以这个就是密钥,python验证一下
ok,这里也没问题,至此明文分析介绍
四.时间戳分析
这个就比较简单了,网上也有很多说明,就是时间戳的16进制,验证一下
也没问题,然后5-8字节发生的变化,这个是随机数,使用了时间戳作为种子的随机数,这里就不多分析了,知道结果即可。
五.最后一位分析
前面所有字节均已分析完毕,还有最后一位,回到咱们最开始分析的地方
v370 = 0LL;
DWORD1(v460[1]) = (qword_644D0 >> 57) & 2 | ((qword_644D0 & 0x2000000000000000LL) != 0) | (qword_644D0 >> 58) & 4 | (qword_644D0 >> 53) & 0x10 | (qword_644D0 >> 54) & 0x20 | (qword_644D0 >> 44) & 0x40 | 0xD00 | (v368 << 24);
do
{
*(v460 + v370) ^= v368 ^ v370;
++v370;
}
这里循环了23轮,异或得到最终结果。我们在这里hook一下,并根据代码分析发现
最后一位就是(qword_644D0 >> 57) & 2 | ((qword_644D0 & 0x2000000000000000LL) != 0) | (qword_644D0 >> 58) & 4 | (qword_644D0 >> 53) & 0x10 | (qword_644D0 >> 54) & 0x20 | (qword_644D0 >> 44) & 0x40 | 0xD00 | (v368 << 24)计算得到的,python验证一下
至此,所以数据均已分析完成
总结
1.前四位固定值
2.5-8处字节又时间戳为种子的随机数生成
3.9-12固定值
4.13-16由明文生成,采用 CRC32 + AES-CBC + HMAC-SHA256 生成
5.17-20 时间戳生成
6.21-23 固定值
6.第24位由前23位求和取反算出
7.前六步生成的20位密文异或得到最终加密参数
更多推荐
所有评论(0)