ks/sig3分析

分析字段:__NS_sig3

初步分析

查壳/查反调等

利用工具,对apk进行一个初步判断。

似乎没有壳,那就不用脱壳了。

image-20250623150600246

即便不做什么处理,我root后的手机,还是可以正常打开快手。

image-20250623151926210

定位__NS_sig3的生成函数

先在jadx中搜索,看看能否直接找到__NS_sig3的相关代码;如果不行,再试试hook HaspMap的put。

直接搜到了,那就直接再jadx中追踪了。

image-20250623153109921

从这段代码中,可以知道__NS_sig3的长度是32字节。

image-20250623184110143

hook函数m160592a,获得request。

image-20250623193413808

下面这些请求路径不需要字段__NS_sig3。

1
"/rest/system/startup", "/rest/n/system/abtest/config", "/rest/system/keyconfig", "/rest/n/system/realtime/startup", "/rest/n/live/config/startup", "/rest/n/feed/hotFast", "/rest/n/feed/selectionFast", "/rest/n/encourage/startup", "/rest/zt/share/system/startup", "/rest/system/startup", "/rest/im/wd/user/startup/push";

在分析__NS_sig3的加密流程之前,注意到,加密函数m160594c的输入是encodePath + str,encodePath不必多说,而这里的str是一个长度为32的字符串(相当于16字节,大概率是md5),接下来分析它是从哪里来的。

sig

下列图表示我的追踪“轨迹”。

image-20250623200734680

image-20250623200903209 image-20250623200927145

image-20250623200949608

假如要使用Sig3,需要清空bodyParam。

image-20250623201121861

用IDA打开getClock对应的libcore。

函数sub_1908出现了md5魔数。——md5_init。

image-20250624133213139 image-20250624133427607

函数sub_1934是md5_update,函数sub_1A4C是md5_transform。

image-20250624134812613

函数sub_19C0是md5_final。

image-20250624135258494

所以,sig就是urlParams、bodyParams、sdk_version等数据经过md5处理的结果,这里就不去验证是不是标准md5了。

函数m160599c

下列图为追踪顺序。

image-20250624142448019

image-20250624142722093

image-20250624142754170

image-20250624142908780

追踪函数m33365b。

image-20250624143324879 image-20250624143338678

image-20250624143355293

image-20250624152425956

image-20250624152503255

image-20250624152818814

image-20250624152838913

image-20250624152902265

image-20250624152938725

image-20250624152954664

在函数doCommandNative处hook,打印调用栈。

image-20250624154205541

hook一下函数doCommandNative的参数,看看是什么。

1
2
3
4
5
6
7
8
9
10
String str2 = (String) this.f46796k.mo33427a().mo33476a(
10418,
new String[]{new String(abstractC16338n.mo33447e()).trim()}, // strEncodePath + str
C16278b.m33236i().m33242j().mo33327a(),
-1,
Boolean.FALSE,
C16278b.m33236i().m33242j().mo33328c(),
null,
Boolean.valueOf(abstractC16338n.mo33448f()),
abstractC16338n.mo33455m()); // 空字符串

image-20250624170504631

定位生成函数位于的so(&& 去除花指令)

跑一下脚本,看看doCommandNative在哪个so里。

image-20250624175134710

1
[RegisterNatives] java_class: com.kuaishou.android.security.internal.dispatch.JNICLibrary name: doCommandNative sig: (I[Ljava/lang/Object;)Ljava/lang/Object; fnPtr: 0xa3cd0435  fnOffset: 0xa3cd0435 libkwsgmain.so!0x46435  callee: 0xa3cd77fb libkwsgmain.so!0x4d7fb

位于libkwsgmain.so,偏移0x46435。

好像存在一些加密代码?试一下从内存中dump下来,然后修补。

image-20250625111639967

dump。

image-20250625115313177

修复。

image-20250625115717885

突然想起来,偏移0x46435,说明是thumb指令,应该从0x46434开始算起。

上来就是BL跳转。

image-20250625144418429

在sub_B764中,LR = 0x46448,而R0是未知的,需要回到0x46434查找。

image-20250625161434306

R0 = 0x1fa。

image-20250625162034294

R1 = [0x46c30]

image-20250625162534946

image-20250625162834178

而0x46448 + 0x5C60 == 0x4C0A8,PC = 0x4C0A8。

然而,0x4C0A8处也没有函数定义,这样不好分析。

image-20250625170120530

然而,在最新版本的快手中,不是这个样子的方式跳转的。

重新跑了1次hook RegisterNatives的脚本。

1
[RegisterNatives] java_class: com.kuaishou.android.security.internal.dispatch.JNICLibrary name: doCommandNative sig: (I[Ljava/lang/Object;)Ljava/lang/Object; fnPtr: 0x7bb8184cd4  fnOffset: 0x7bb8184cd4 libkwsgmain.so!0x40cd4  callee: 0x7bb818a164 libkwsgmain.so!0x46164

BR混淆。

image-20250627112711210

image-20250627112737959

上述的计算,可以整理成:

1
2
3
4
5
6
7
8
9
10
11
12
13
X0, X1放入栈中

; 下面计算X9的值
STP X2, X30, [SP + 0x10] ;[SP+0x18] = X30
ADR X1, dword_40cfc ;X1 = 0x40cfc
X1 = X1 - 0x4
X0 = X1
X0 = X0 + 0x14
[SP+0x18] = X0
LDP X2, X9, [SP + 0x10]

X0, X1 = 原来的内容
BR X9

只要我们知道X30的值,就可以利用keypatch将BR X9改成B xxxx。

JNI_OnLoad也是这样的混淆。

image-20250627114224817

直接用一个IDA脚本,遍历so文件。——获得3个操作数,然后在BR X9处进行修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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()

执行完后是这样的。

image-20250627152421719

Unidbg补环境

先搭最基本的架子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.ks;

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 com.github.unidbg.virtualmodule.android.JniGraphics;

import java.io.File;


public class sig3 extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
private final Memory memory;

sig3(){
emulator = AndroidEmulatorBuilder.for64Bit()
.setProcessName("com.smile.gifmaker")
.addBackendFactory(new Unicorn2Factory(true)) // 处理器类型
.build();

// 设置执行多少条指令切换一次线程
emulator.getBackend().registerEmuCountHook(10000);
// 开启日志
emulator.getSyscallHandler().setVerbose(true);
// 开启线程调度器
emulator.getSyscallHandler().setEnableThreadDispatcher(true);
memory = emulator.getMemory(); // 模拟器的内存操作接口
memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解
//创建虚拟机并指定APK文件
vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/ks_pack/ks.apk"));
// 设置是否打印Jni调用细节
vm.setVerbose(true);
// 补jni方法
vm.setJni(this);

// 加载so
DalvikModule dm = vm.loadLibrary("kwsgmain", true);
// 加载好的libkwsgmain.so对应为一个模块
module = dm.getModule();
// 手动执行JNI_OnLoad函数
dm.callJNI_OnLoad(emulator);
}

public static void main(String[] args) {
sig3 _sig3 = new sig3();
}
}

对照着之前的调用参数,添加调用doCommandNative的代码。

image-20250627184717187

1
2
3
4
5
6
7
8
9
10
11
12
void get_Sig3(){
DvmClass JNICLibrary = vm.resolveClass("com.kuaishou.android.security.internal.dispatch.JNICLibrary");
StringObject urlObj = new StringObject(vm, "xianyu");
StringObject appkey = new StringObject(vm, "d7b7d042-d4f2-4012-be60-d97ff2429c17");
DvmInteger intergetobj = DvmInteger.valueOf(vm, -1);
DvmBoolean boolobj = DvmBoolean.valueOf(vm, false);
DvmObject<?> context = vm.resolveClass("com/yxcorp/gifshow/App").newObject(null); // context
StringObject appkey2 = new StringObject(vm, "");
ArrayObject arg2 = new ArrayObject(new ArrayObject(urlObj), appkey, intergetobj, boolobj, context, null, boolobj, appkey2);
String result = JNICLibrary.callStaticJniMethodObject(emulator,"doCommandNative(I[Ljava/lang/Object;)Ljava/lang/Object;",10418,arg2).getValue().toString();
System.out.println(result);
}

报错如下,提示返回值是null。

一般这种情况,都是调用so接口的时候,没有初始化导致的。

1
2
3
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "com.github.unidbg.linux.android.dvm.DvmObject.getValue()" because the return value of "com.github.unidbg.linux.android.dvm.DvmClass.callStaticJniMethodObject(com.github.unidbg.Emulator, String, Object[])" is null
at com.ks.sig3.get_Sig3(sig3.java:63)
at com.ks.sig3.main(sig3.java:69)

libkwsgmain.so里面,一共注册了5个函数,把它们hook起来,不过似乎并没有什么关联。

1
2
3
4
5
[RegisterNatives] java_class: com.kuaishou.android.security.internal.dispatch.JNICLibrary name: doCommandNative sig: (I[Ljava/lang/Object;)Ljava/lang/Object; fnPtr: 0x7bb8184cd4  fnOffset: 0x7bb8184cd4 libkwsgmain.so!0x40cd4  callee: 0x7bb818a164 libkwsgmain.so!0x46164
[RegisterNatives] java_class: com.kuaishou.android.security.internal.dispatch.JNICLibrary name: gDBF sig: ()J fnPtr: 0x7bb81848a4 fnOffset: 0x7bb81848a4 libkwsgmain.so!0x408a4 callee: 0x7bb818a164 libkwsgmain.so!0x46164
[RegisterNatives] java_class: com.kuaishou.android.security.internal.dispatch.JNICLibrary name: dCaBk sig: (ILjava/lang/String;[BI)V fnPtr: 0x7bb8184948 fnOffset: 0x7bb8184948 libkwsgmain.so!0x40948 callee: 0x7bb818a164 libkwsgmain.so!0x46164
[RegisterNatives] java_class: com.kuaishou.android.security.internal.dispatch.JNICLibrary name: gDGI sig: ()[Ljava/lang/String; fnPtr: 0x7bb81843bc fnOffset: 0x7bb81843bc libkwsgmain.so!0x403bc callee: 0x7bb818a164 libkwsgmain.so!0x46164
[RegisterNatives] java_class: com.kuaishou.android.security.internal.dispatch.JNICLibrary name: gKSF sig: ()J fnPtr: 0x7bb81847f0 fnOffset: 0x7bb81847f0 libkwsgmain.so!0x407f0 callee: 0x7bb818a164 libkwsgmain.so!0x46164

同时,注意到Command ID也是有不同的,打印一下args[2]。

image-20250627215223529

可以发现,在第一时间内只会调用1次command ID = 0x28ac,这大概率是初始化的一部分。

image-20250627215156751

所以,加一个执行10412的函数。

1
2
3
4
5
6
7
void moduleInit(){
DvmClass JNICLibrary = vm.resolveClass("com.kuaishou.android.security.internal.dispatch.JNICLibrary");
StringObject appkey = new StringObject(vm, "d7b7d042-d4f2-4012-be60-d97ff2429c17");
DvmObject<?> context = vm.resolveClass("com/yxcorp/gifshow/App").newObject(null); // context
ArrayObject arg2 = new ArrayObject(null, appkey, null, null, context, null, null);
JNICLibrary.callStaticJniMethodObject(emulator,"doCommandNative(I[Ljava/lang/Object;)Ljava/lang/Object;",10412,arg2);
}

之后遇到缺少类的错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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(signature);

}

}

return super.callObjectMethodV(vm, dvmObject, signature, vaList);

}

仍然报错空指针,对比了一些和其它人博客的区别。

补了一句↓,这样就可以正常跑出结果了。

1
new AndroidModule(emulator, vm).register(memory);

image-20250628144948500

每跑一次,都会有一个新结果,为了固定结果,需要将时间戳、随机数导致的,修改下面这个文件的currentTimeMillis()。

1
src/main/java/com/github/unidbg/unix/UnixSyscallHandler.java

固定一下,时间固定成1751095318144。

image-20250628152206220

结果固定为c4d5a086d88864b88c8c8f8ef7c6668e830cc8fe919d9385。

image-20250628152444173

trace指令流。

1
2
3
4
5
6
7
8
9
10
11
void traceCode(){
String traceFile = "unidbg-android/src/test/java/com/ks/traceCode.log"; // 输出的路径
PrintStream traceStream = null; // 打印流
try {
traceStream = new PrintStream(new FileOutputStream(traceFile), true);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
// traceCode 对代码进行监控
emulator.traceCode(module.base, module.base + module.size).setRedirect(traceStream);
}

分析

在文件traceCode.log中,追踪结果“c4d5a086d88864b88c8c8f8ef7c6668e830cc8fe919d9385”。——这里的字符串是之前分析小红书的时候,修改Unidbg得到的。

第一次出现的地址在:0x13b9c,位于函数sub_13b54内。

image-20250628155455570

函数sub_13B54将result中的结果提取到a2中,a2是一个string类型(从sso优化看出来的)。

image-20250628160428702

观察trace日志,结果位于[x0+0x28]的位置,而x0是调用sub_13b54传入的入参。

image-20250629153431870

在函数sub_3CC28内调用了sub_13B54,对这个函数进行分析,发现a1是v9的来源,因此hook函数sub_3cc28。

image-20250629154704114

image-20250629155642090

查看sub_3cc28的引用。

image-20250629184620681

根据日志,发现是从0x44c8c跳转过来的。

image-20250629162305374

0x44c8c不在定义的函数内,于是我在日志中,划分了一个函数。

image-20250629191029580

这个函数的栈不平衡,但勉强可以用来分析,总比纯看汇编方便。

image-20250629192132769

观察v98,注意到,循环次数是23次,但最后的结果是24字节。

image-20250629225110845

对着地址0x44c04下断点。

image-20250629225249476

下图是第一次执行到0x44c04的结果,会发现,最后一个字节已经赋了正确的值。

image-20250629231042590

最后一个字节是单独赋值的,可以关注一下这里的v101。

v101的高8位是:v48的低8位 | v49的高8位,v48源于前23个字节累加然后取负。

image-20250629234640841

image-20250630000912063

而v49来自一个略微复杂的计算。

1
v49 = ((unsigned __int64)qword_70910 >> 53) & 0x10 | ((unsigned __int64)qword_70910 >> 54) & 0x20 | ((unsigned __int64)qword_70910 >> 44) & 0x40 | v46 | 0xD00;

对红色框里的v98进行hook,查看24个字节在异或之前是什么样的。

image-20250630085152982

1
41 51 27 00 59 08 e7 3a 01 00 00 00 7e 4e ed 04 16 98 5f 68 00 0d 00 85

image-20250630085317372

改变输入

有3个可以改变的输入,url、appkey、时间戳,观察改变输入后,内容有什么不同。

urlObj

输入:”xianyu” -> “x14nuy”。

v49的值未变,0xd00 -> 0xd00。

v98的12-15字节发生了变化,最后1个字节之所以变了,是因为前23字节存在变化。

1
2
3
41 51 27 00 59 08 e7 3a 01 00 00 00 |7e 4e ed 04| 16 98 5f 68 00 0d 00 85
↓↓↓
41 51 27 00 59 08 e7 3a 01 00 00 00 |7f 7c 00 53| 16 98 5f 68 00 0d 00 f4

appkey

输入:”d7b7d042-d4f2-4012-be60-d97ff2429c17” -> “d7b7d042-d4f2-4012-60be-d97ff2429c17”。

不能改,改了就报错。

时间戳

输入:”1751095318144L” -> “1751095318145L”。

v49的值未变,0xd00 -> 0xd00。

v98的值未变,可能是时间戳改动太小了?

1
2
3
41 51 27 00 59 08 e7 3a 01 00 00 00 7e 4e ed 04 16 98 5f 68 00 0d 00 85
↓↓↓
41 51 27 00 59 08 e7 3a 01 00 00 00 7e 4e ed 04 16 98 5f 68 00 0d 00 85

输入:”1751095318144L” -> “1751105319144L”。

v49的值未变,0xd00 -> 0xd00。

v98的4-7和16-17字节发生了变化,最后1个字节之所以变了,是因为前23字节存在变化。

1
2
3
41 51 27 00 |59 08 e7 3a| 01 00 00 00 7e 4e ed 04 |16 98| 5f 68 00 0d 00 85
↓↓↓
41 51 27 00 |b6 ca a0 0e| 01 00 00 00 7e 4e ed 04 |27 bf| 5f 68 00 0d 00 a1

如果输入改成”1111111111111L”。

v49的值未变,0xd00 -> 0xd00。

v98的4-7和16-19字节发生了变化,最后1个字节之所以变了,是因为前23字节存在变化。

1
2
3
41 51 27 00 |59 08 e7 3a| 01 00 00 00 7e 4e ed 04 |16 98 5f 68| 00 0d 00 85

41 51 27 00 |7c 75 c0 79| 01 00 00 00 7e 4e ed 04 |c7 35 3a 42| 00 0d 00 da

urlObj和时间戳

输入:”xianyu” -> “x14nuy”。

输入:”1751095318144L” -> “1111111111111L”。

v49的值未变,0xd00 -> 0xd00。

v98的4-7、12-15、16-19字节发生了变化

1
2
3
41 51 27 00 |59 08 e7 3a| 01 00 00 00 |7e 4e ed 04| |16 98 5f 68| 00 0d 00 85
↓↓↓
41 51 27 00 |7c 75 c0 79| 01 00 00 00 |7f 7c 00 53| |c7 35 3a 42| 00 0d 00 49

16-19字节

注意到,参与计算的时间戳是以s为单位。

image-20250630105048439

1751095318144L是微秒,换算成秒的话,值为1751095318L,即0x68 5f 98 16,对应于v98的第16-19字节。

image-20250630105301686

20-23字节

前面分析了,v49的值恒定为0xd00,不管输入的urlObjc和时间戳如何变化,都为这个值。

1
v49 = ((unsigned __int64)qword_70910 >> 53) & 0x10 | ((unsigned __int64)qword_70910 >> 54) & 0x20 | ((unsigned __int64)qword_70910 >> 44) & 0x40 | v46 | 0xD00;

((unsigned __int64)qword_70910 >> 53) & 0x10实际取了qword_70910第57位,作为v49的第4位(从0开始)。

((unsigned __int64)qword_70910 >> 54) & 0x20实际取了qword_70910第59位,作为v49的第5位。

((unsigned __int64)qword_70910 >> 44) & 0x40实际取了qword_70910第50位,作为v49的第6位。

hook了一下,查看qword_70910和v46的值。

qword_70910 = x8 = 0xc001000000000000,所以20-22字节固定为0x000D00。

而最后8位(第23字节)为前面数的总和(超过255取负)。

image-20250630115638274

我怀疑这里的dword_70910是混淆,干扰计算用的。

image-20250630115957984

12-15字节

修改urlObj,可以使得12-15字节发生变化。

image-20250630143629732

在函数sub_120D8处进行hook,

image-20250630144152920

x0应该是我们传入的数据,但不知道被做了什么处理。

image-20250630144807369

x2指向的内容也不是明文。

image-20250630144825417

CRC32

经过搜索,x2指向的内容是crc32的魔数,B71DC104是标准crc32算法的查找表中预计算好的一个魔数,说明函数sub_120d8跟crc32算法相关。——并且是标准的crc32算法。

image-20250630152039424

AES

那么,x0的48字节是什么呢?往地址0x124ff000下写断点。

image-20250630154309863

再hook,打印源地址。

image-20250630154344863

再下写断点。

image-20250630154454278 image-20250630164254200

存在花指令,导致地址0x1E7FC不在定义的函数内。

image-20250630182324242

IDA识别后,把0x1E7FC也算入函数sub_1E32C的范围内了。

追踪src。

image-20250630165349886

hook函数sub_1E07C。

image-20250630165825546

等函数sub_1E07C执行完,再打印x3、x4的内容。

sub_1E07C执行完后,出现了目标48字节。

大概率是传入的第0、1个参数,它们与这48字节的生成相关。

image-20250630172928250

看看第0、1个参数,x1的内容有点像加密后的字符串。

image-20250630201746906

进入函数sub_1E07C,关注函数sub_26024。

image-20250630192948412

根据hook的结果,sub_26024只执行了1次。

image-20250630194424809

image-20250630194443002

a4指向v18

image-20250630194505551

v17指向一块堆内存,v18通过赋值,也指向这块内存。

image-20250630194532102

其中,v24是从0开始取,每次自增1,换算过来,v25每次移动16字节。

image-20250630195629327

根据分析,锁定了sub_25980。(sub_251F4没执行,它代表着AES解密,相反,sub_25980代表着加密;函数sub_2640C用来设置执行加密还是解密)

image-20250630195951640

下面是hook函数sub_25980所得到的参数。

x1指向待加密的数据,这里的数据大小是0x30,本来是0x20,多了0x10的填充。

x2指向加密结束的堆内存,起始地址是0x124d3330。

image-20250630202439641

执行完3次sub_25980后,v25指向的区域是这样的,这是我们目标的48个字节。

image-20250630202728004

用了什么加密呢?大概率是白盒AES,因为通过插件FindCrypt,找到了AES。

image-20250630224657562

回到函数sub_26024,将输入改成2个一样的内容。——判断CBC还是EBC。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
debugger.addBreakPoint(module.base + 0x1E224, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
// 2个16字节
String hexString = "xianyuxianyuismexianyuxianyuisme"; // 十六进制字符串
byte[] bytes = hexString.getBytes();
MemoryBlock newInput = emulator.getMemory().malloc(hexString.length(), true);
newInput.getPointer().write(bytes);
// 将x1指向我们的待加密字符串
emulator.getBackend().reg_write(Arm64Const.UC_ARM64_REG_X1, newInput.getPointer().peer);

return false;
}
});

函数执行前,x1已经指向xianyuxianyuisme的内容。

image-20250630225923022

执行后,发现结果一模一样,说明采用EBC模式。(无IV)

image-20250630230021753

之前hook函数sub_25980时,填充值是0x10,符合PKCS#7的填充方式。

在下面else的代码块内,下了1个断点,发现每当执行1次函数sub_25980,这个代码块会执行36次。

image-20250630230744884

为什么是36次呢?标准的AES-128有10轮运算,前9轮完全一样,每1轮需要对每列进行列变换,而共有4列,因此前9轮共有36轮列变化,刚好对应else内的代码块执行了36次。

为了更方便分析函数sub_259B8,用d810去除一下控制流平坦化。

过于清晰了。

image-20250630233053889

为了得到密钥,需要使用DFA攻击,在第8轮列混淆之后,第9轮列混淆之前,改变1个字节。

状态矩阵在a1。

image-20250630233850293

而sub_24f78是行位移操作,第9次行位移执行时,正是差分攻击的好时机。

编写脚本,进行差分攻击。

image-20250701013043532

获得第10轮密钥。

image-20250701012800757

得到初始密钥。

image-20250701012834080

试验一下,操作没问题,和之前的hook的结果一样。

image-20250701012902860

HMAC-SHA256

进入AES加密之前,“xianyu”作为输入,会生成下面这32字节。

现在追踪这32字节哪里来的。

image-20250701092041613

早在函数sub_26024的时候,x1就指向了这32字节。

image-20250701092425536

往回溯源,又可以找到,a2源于函数

image-20250701092643110

hook函数sub_1DEC0,会发现encrypt_data还不是目标32字节。

image-20250701094117337

而a4里面,存放着的是明文xianyu。

点入函数sub_1DEC0进行分析,在函数217D8找到了SHA256的字眼。

image-20250701094449969

交给大模型进行分析,分析这个是一个HMAC-SHA256函数,a5、a6是opad、ipad也确认了这一点,下图异或的密钥反了,但结果一致也说明是opad/ipad。

image-20250701095442973

image-20250701095509469

用到了HMAC-SHA256,密钥如下,但不知道有没有魔改。

image-20250701104955510

经过判断是标准的HMAC-SHA256。

image-20250701101620373

总结

做个小总结,输入“xianyu”经过HMAC-SHA256,变成32字节,又经过AES变成48字节(填充也有16字节),最后经过CRC32变成4字节。

4-7字节

第4-7字节和时间戳有关,而赋值的地方在这里。

image-20250701103945379

交叉引用,大概率是在JNI_OnLoad里执行写的。

image-20250701104322347

在JNI_OnLoad可以看到,随机种子是time(0),被固定了,所以随机数也被固定了,至此分析结束。

image-20250701104851928