本分析是四年前的,时过境迁,应该没啥价值了。只当纪念那个曾经的疯魔时光!
APP加固系统也算是一个完整的APP加固保护系统,基本功能应该都具备,基本上也是以对dex的文件和代码保护为立足点,这个也是Android APP加固保护的基本思路。下面从三个方面来讲解他的保护系统架构和功能。
本次使用*翻译APP作为分析的样本,应该是专业版加固系统。
本分析还是基本上以AndroidNativeEmU模拟器分析日志为主。不过为了保证函数可以正常运行,部分数据来自真实环境,并还原到原偏移地址中,保证模拟器正常执行。本次分析还首次在模拟器中实现art模式下的Dex加载过程,不用到真实环境中去dump dex了。
第一、 对自身模块的保护:
APP加固系统都自身模块的保护并没有像其他加固保护系统那么强大和复杂,就是使用了code段加密,然后中so的init_array中进行解密,然后抹除so头,防止内存dump的方式。到JNI_OnLoad的时候代码段已经解密完成,这个时候dump下代码,然后把原始的头还原回去基本上就能用来分析了,如果要用模拟器调试,还得patch掉解密和抹除so头的代码。
下面来看具体的代码实现。
Calling Init for: samples/appjiagu/libappprotect-up.so
Calling Init function: cbff8d85//这个就是init_array中的第一个函数,解密函数。
把so头0x1000字节清零:
Executing syscall mprotect(cbfab000, 00001000, 00000003) at 0xcbffd165
call mprotect:0xcbfab000 ç基地址,即so头地址
======================= Registers =======================
R0=0xcbfab034 R1=0x0 R2=0x1 R3=0x1
R4=0x7ffce8 R5=0x7ffce8 R6=0x7ffe90 R7=0x7ffff8 R8=0x0
R9=0x0 R10=0x0 R11=0x0 R12=0x7ffce0 SP=0x7ffce0
LR=0xcbffd165 PC=0xcbffd60c
======================= Disassembly =====================
0xcbffd60c: 0170 strb r1, [r0] ç开始把so头清零
0xcbffd60e: 6e48 ldr r0, [pc, #0x1b8]
0xcbffd610: 7f49 ldr r1, [pc, #0x1fc]
0xcbffd612: 7944 add r1, pc
0xcbffd614: 4018 adds r0, r0, r1
解密代码段中加密的代码:
Executing syscall mprotect(cbfae6f1, 00042C22, 00000007) at 0xcbffab7f
解密代码段中加密的代码:
Executing syscall mprotect(cbfae6f1, 00042C22, 00000007) at 0xcbffab7f
到这里:
都是被加密的代码。
======================= Registers =======================
R0=0x47 R1=0xcbfae6f1 R2=0x7ffe60 R3=0x7ffe58
R4=0x7ffcf8 R5=0x7ffdd8 R6=0x7ffe90 R7=0x7ffff8 R8=0x0
R9=0x0 R10=0x0 R11=0x0 R12=0xffff1c30 SP=0x7ffce0
LR=0xcbffe2f3 PC=0xcbffca94
======================= Disassembly =====================
0xcbffca94: 0870 strb r0, [r1] < --R1=0xcbfae6f1
0xcbffca96: 1878 ldrb r0, [r3]
0xcbffca98: 1168 ldr r1, [r2]
0xcbffca9a: 0870 strb r0, [r1]
0xcbffca9c: 1020 movs r0, #0x10
这个函数执行完就可以dump so,然后修复代码了:
JNI_OnLoad:代码已经解密出来了:
0xcbfae9c4: f0b5 push {r4, r5, r6, r7, lr}
0xcbfae9c6: 03af add r7, sp, #0xc
0xcbfae9c8: 89b0 sub sp, #0x24
0xcbfae9ca: 6e46 mov r6, sp
0xcbfae9cc: 3162 str r1, [r6, #0x20]
>dump 0xcbfab000
CBFAB000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
CBFAB010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
CBFAB020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
CBFAB030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
可以看到so头被清除了,
修复so方法:
先dump 数据,然后用010Editor 把原始的头贴回去。
由于代码已经被解出来,所以后面不能再被解密了,patch代码,第一个就是把清除so头的代码除掉。然后把解密代码的写入操作代码也nop掉。这样就保证了可以二次加载这个so进行运行分析。
0xcbffd60c: 0170 strb r1, [r0] ç开始把so头清零 nop掉代码
0xcbffca94: 0870 strb r0, [r1] < --R1=0xcbfae6f1 nop掉
0xcbffca96: 1878 ldrb r0, [r3]
0xcbffca98: 1168 ldr r1, [r2]
0xcbffca9a: 0870 strb r0, [r1] nop掉代码
0xcbffca9c: 1020 movs r0, #0x10
从分析来看,APP加固对自身模块的保护力度不是很强,修复起来也比较容易。
JNI_OnLoad 函数主要就是把libc中的这些函数填到函数列表中,然后通过列表的索引进行调用。这样防止分析代码:
Called dlopen(libc.so)
Loading module 'vfs/system/lib/libc.so'.
call malllos size:0x9 at 0xcbfc15e7
malloc addr:0x2022000
Called dlsym(0xcbbdf000, _exit) at 0xcbfb1669
symbol:_exit addr->: 0xcbc2780c
call malllos size:0x9 at 0xcbfc166f
malloc addr:0x2023000
Called dlsym(0xcbbdf000, exit) at 0xcbfb167f
导入的函数有:
#libc_fun_name = ["_exit","exit","pthread_create","pthread_join","memcpy","malloc","calloc","memset","fopen","fclose","fgets","strtoul","strtoull","strstr","ptrace","mprotect","strlen","sscanf","free","strdup","strcmp","strcasecmp","utime","mkdir","open","close","unlink","stat64","time","snprintf","strchr","strncmp","pthread_detach","pthread_self","opendir","readdir","closedir","mmap","munmap","lseek","fstat","read","select","bsd_signal","fork","prctl","setrlimit","getppid","getpid","waitpid","kill","flock","write","execve","execv","execl","sysconf","__system_property_get","ftruncate","gettid","pread64","pwrite64","pread","pwrite"," ","statvfs"]
后面就是注册加固系统com.app.protect.A 主功能函数:
JNIEnv->FindClass(com/app/protect/A) was called
JNIEnv->RegisterNatives(1, 0x007fff58, 4) was called
Register native ('n001', '(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IZ)V',function->'0xcbfaf829') failed on class com_app_protect_A.
Register native ('n002', '(Landroid/content/Context;)V',function->'0xcbfaf999') failed on class com_app_protect_A.
Register native ('n003', '()[Ljava/lang/String;',function->'0xcbfaf9f9') failed on class com_app_protect_A.
Register native ('n004', '()V',function->'0xcbfafad5') failed on class com_app_protect_A.
这个版本的加固中,注册了四个函数。主要的功能在'n001'这个函数中。包括dex解压,解密,加载,附加数据的处理。Dex_VMP数据初始化。
另外Native so中大量使用字符串加密,防止关键字符串暴露,也是APP加固的技术特点:
第一、
第二、 对dex文件的保护:
Android APP加固系统保护的重点就是dex,所以对dex文件的保护是比较重要的。APP加固系统这块也做的比较好,甚至全程没有dex明文文件落地,都是从apk包中在运行时解压解密加载。具体我们来看看流程:
APP加固的JNI_OnLoad中注册了四个Native函数,其中n001函数就是处理dex解压,解密,加载和vmp数据的。
protected void attachBaseContext(Context arg8) {
StubApplication.mContext = arg8;
StubApplication.mBootStrapApplication = this;
AppInfo.APKPATH = arg8.getApplicationInfo().sourceDir;
AppInfo.DATAPATH = StubApplication.getDataFolder(arg8.getApplicationInfo().dataDir);
if(!Debug.isDebuggerConnected()) {
StubApplication.loadLibrary();
A.n001(AppInfo.PKGNAME, AppInfo.APPNAME, AppInfo.APKPATH, AppInfo.DATAPATH, Build.VERSION.SDK_INT, AppInfo.REPORT_CRASH);
}
if(AppInfo.APPNAME != null && AppInfo.APPNAME.length() > 0) {
StubApplication.mRealApplication = MonkeyPatcher.createRealApplication(AppInfo.APPNAME);
}
super.attachBaseContext(arg8);
if(StubApplication.mRealApplication != null) {
MonkeyPatcher.attachBaseContext(arg8, StubApplication.mRealApplication);
}
}
Java层的入口,可以看出来,首先对调试状态进行了检测,如果是调试状态就不会加载so模块,也不会执行dex的解密函数A.n001.防止APP被调试。如果没有被调试则加载完libappprotect.so后进入dex的加载函数,即n001函数。从上面我们知道JNI_OnLoad中注册了这个函数,函数地址是:function->'0xcbfaf829',我们就看看这个Native函数的具体功能:
首先获取APP的包信息,从包信息中获取signatures,从apk包的META-INF目录读取签名文件TRANSLAT.RSA,然后从0x3A开始读两个字节的签名数据长度,根据长度读取后面的签名数据。比如这个APP的签名长度是0x235
读取数据如下:
00000040: 30 82 02 31 30 82 01 9A A0 03 02 01 02 02 04 4E 0..10..........N
00000050: 25 42 6B 30 0D 06 09 2A 86 48 86 F7 0D 01 01 05 %Bk0...*.H......
00000060: 05 00 30 5D 31 0B 30 09 06 03 55 04 06 13 02 43 ..0]1.0...U....C
00000070: 4E 31 10 30 0E 06 03 55 04 08 13 07 62 65 69 6A N1.0...U....beij
然后对这个签名文件取hash:
.text:CBFB71CE 28 4D LDR R5, =(_GLOBAL_OFFSET_TABLE_ - 0xCBFB71D4)
.text:CBFB71D0 7D 44 ADD R5, PC ; _GLOBAL_OFFSET_TABLE_
.text:CBFB71D2 44 19 ADDS R4, R0, R5 ; byte_CC00F664
.text:CBFB71D4 20 46 MOV R0, R4
.text:CBFB71D6 39 46 MOV R1, R7
.text:CBFB71D8 1B F0 E8 FA BL Fun_md5 ; 获取apk 数字签名,然后MD5这个签名,给后面的dex decode 做key
2020-11-11 17:39:40,906 DEBUG androidemu.native.hooks | call memcpy (len:0x10)
2020-11-11 17:39:40,906 DEBUG androidemu.native.hooks | memcpy_data:0xcc00f664-->0x20cdb11
2020-11-11 17:39:40,907 INFO androidemu.native.hooks | addr -->0xcbfb6f9b
2020-11-11 17:39:40,907 DEBUG androidemu.native.hooks |
CC00F664: 05 86 74 2E 88 A2 E6 A1 9E 99 65 98 EC 33 6B 61 ..t.......e..3ka <== md5
这个hash值用于后面dex前0x1000字节的解密key。这样设计的目的是在用防止APP被二次打包,打包后的签名已经改变,改变后的这个签名hash值也会改变,然后就不能再解密出正确的dex文件。对dex的保护起到了一定的作用,如果在不能完整恢复原dex代码的情况下,加固保护系统就能起作用。
后面就是从apk包中解压assets目录下所有的jar文件。这个jar文件就是dex的密文,也就是前0x1000个字节没有被解密的dex文件。
2020-11-16 14:23:04,042 INFO androidemu.native.hooks | string-->'assets/appprotect1.jar'
2020-11-16 14:23:04,045 DEBUG androidemu.native.hooks | call memcmp;data1->0x21435d9,data2->0x7ff8d8,len->24
2020-11-16 14:23:04,045 DEBUG androidemu.native.hooks | mem1_data:
2020-11-16 14:23:04,045 DEBUG androidemu.native.hooks |
021435D9: 61 73 73 65 74 73 2F 62 61 69 64 75 70 72 6F 74 assets/appprot
021435E9: 65 63 74 31 2E 6A 61 72 ect1.jar
2020-11-19 11:23:30,452 DEBUG androidemu.native.memory | call malllos size:0x7e62dc at 0xcbfd291b
2020-11-19 11:23:30,455 INFO androidemu.native.memory | malloc addr:0x21db000 ß申请到的空间,后面存放解压数据。size:0x7e62dc 是appprotect1.jar的大小。
读取这个数据后使用libart模块中的zlib函数进行解压:
if ( !j_inflateInit2_(&v19, 0xFFFFFFF1, "1.2.3", 0x38) )
{
if ( j_inflate(&v19, 4) == 1 )
{
v10 = v23;
j_inflateEnd(&v19);
if ( v10 == v9 )
{
LABEL_13:
v6 = 1;
if ( *(_DWORD *)&v16 >= 0x8001u )
sub_CBFB11A0(v8, 0);
goto LABEL_16;
}
}
else
{
j_inflateEnd(&v19);
}
}
这个时候的dex结构如上图所示,下面开始解这些部分。首先用签名的hash生成的key解密前0x1000字节:
这个解压出来的数据前0x1000个字节是密文的,后面需要用APP签名的hash值来解密:
解密算法:
======================= Registers =======================
R0=0x21db000 R1=0x7ff758 R2=0x20cdc3c R3=0xcbff04f9
R4=0x21db000 R5=0x7ff7b0 R6=0x1000 R7=0xcbff04f9 R8=0x0
R9=0x0 R10=0x0 R11=0x0 R12=0xcc00eef8 SP=0x7ff720
LR=0xcbff0e11 PC=0xcbff0ca8
======================= Disassembly =====================
0xcbff0ca8: b847 blx r7 <=解密函数
0xcbff0caa: 3b46 mov r3, r7
0xcbff0cac: 2868 ldr r0, [r5]
0xcbff0cae: 029a ldr r2, [sp, #8]
0xcbff0cb0: 1168 ldr r1, [r2]
>dump 0x20cdc3c // APP 签名hash生成的解密key
020CDC3C: 4D 23 BC 03 9D 27 9A 95 B2 85 D7 ED 76 C3 3D BA M#...'......v.=.
020CDC4C: 10 D7 1A A0 C0 A0 FE FA 1D E9 14 58 9D C1 CD AE ...........X....
020CDC5C: 52 4B 9B 2D D0 77 E4 5A DD 49 EA A2 80 28 D9 F6 RK.-.w.Z.I...(..
020CDC6C: 7C 0A 2B 97 82 3C 7F 77 0D 3E 0E F8 5D 61 33 54 |.+..<.w.>..]a3T
>dump 0x21db000 // dex 密文数据
021DB000: E3 7A E6 20 86 8C 4B 89 F8 74 A9 AF 65 5B 06 78 .z. ..K..t..e[.x
021DB010: 69 DD 9E C2 C2 68 85 1E A5 A7 35 E0 88 96 E4 4F i....h....5....O
021DB020: 38 16 B1 B7 84 AB 0A E8 7F E1 5D 3C 74 A6 24 FF 8.........]<t.$.
021DB030: 82 CB 1D 61 60 AF 48 8C 9F 50 11 BF C9 72 98 2E ...a`.H..P...r..
到这里全部解出来了:
======================= Registers =======================
R0=0x76e69e89 R1=0x195a3f R2=0x7ff758 R3=0xcbff04f9
R4=0x21dc000 R5=0x7ff7b0 R6=0x0 R7=0xcbff04f9 R8=0x0
R9=0x0 R10=0x0 R11=0x0 R12=0xcc00eef8 SP=0x7ff720
LR=0xcbff0cab PC=0xcbff0ce6
======================= Disassembly =====================
0xcbff0ce6: 0b98 ldr r0, [sp, #0x2c]
0xcbff0ce8: 0a99 ldr r1, [sp, #0x28]
这个时候的dex有部分代码被移走了,需要重新解密填充回来:
下面的函数就是解密填充:
======================= Registers =======================
R0=0x21db000 R1=0x7e62dc R2=0x0 R3=0xcbff04f9
R4=0x0 R5=0x21db000 R6=0x7ff804 R7=0xcbc3064d R8=0x0
R9=0x0 R10=0x0 R11=0x0 R12=0xcc00eef8 SP=0x7ff7d8
LR=0xcbff0cab PC=0xcbfd2954
======================= Disassembly =====================
0xcbfd2954: e8f79efc bl #0xcbfbb294 //解密并填充回被移动走的代码的函数。
2020-11-19 15:40:58,411 DEBUG androidemu.native.hooks | call memcpy (len:0x739dc)
2020-11-19 15:40:58,411 DEBUG androidemu.native.hooks | memcpy_data:0x2944514-->0x28d06ac
2020-11-19 15:40:58,412 INFO androidemu.native.hooks | addr -->0xcbfc1003
2020-11-19 15:40:58,412 DEBUG androidemu.native.hooks |
密文数据:
02944514: 33 A9 E7 6F C3 9B 4C DD F2 82 6E 56 D5 0D 40 91 3..o..L...nV..@.
02944524: C3 FA A6 2C 82 F1 5A 6A 57 24 C8 F3 68 92 4C A0 ...,..ZjW$..h.L.
02944534: 93 0B C4 13 AF DC 0A A0 B0 FE 26 36 89 D2 1E F1 ..........&6....
02944544: C2 D9 97 1D 30 43 95 04 2B 5B B7 8F 0D 56 9A 75 ....0C..+[...V.u
02944554: 7B 63 AC 1B F4 D8 1C 8E A1 D1 78 1B 8B D3 23 CC {c........x...#.
02944564: AA 69 35 BF 11 61 AA 4F 72 01 ED D5 0E 3B E5 09 .i5..a.Or....;..
这个数据的解密也是用到APP签名的hash生成的key来解密。所以如果签名改变dex就会解密失败。
【附加数据解密并填充被移走的dex数据】
到这里dex的解压和解密完成,可以dump下来了。
下面是dex加载过程:
用InMemoryDexClassLoader类调用NewObjectV加载dex,InMemoryDexClassLoader内部会使用mmap分配内存存放dex,分配一个新的DexPathList$Element数组,将原来系统的类加载器和刚才的InMemoryDexClassLoader中的classLoader.pathList.dexElements合并成一个数组,然后替换原来系统中的类加载器的dexElements。
参考资料:https://blog.csdn.net/xingzhong128/article/details/80470796
JNIEnv->NewByteArray(8282844) was called
JNIEnv->SetByteArrayRegion was called
JNIEnv->CallStaticObjectMethodV(java/nio/ByteBuffer, wrap <([B)Ljava/nio/ByteBuffer;>, 0x7ff984) was called
NIEnv->NewObjectV(dalvik/system/InMemoryDexClassLoader, <init>, 0x7ff984) was called
JNIEnv->FindClass(dalvik/system/BaseDexClassLoader) was called
JNIEnv->ExceptionCheck() was called
JNIEnv->FindClass(dalvik/system/DexPathList) was called
JNIEnv->GetFieldId(29 [dalvik/system/BaseDexClassLoader], pathList, Ldalvik/system/DexPathList;) was called
JNIEnv->GetFieldId(30 [dalvik/system/DexPathList], dexElements, [Ldalvik/system/DexPathList$Element;) was called
JNIEnv->FindClass(dalvik/system/DexPathList$Element) was called
JNIEnv->GetObjectField(dalvik/system/BaseDexClassLoader, pathList <Ldalvik/system/DexPathList;>) was called
JNIEnv->GetObjectField(dalvik/system/DexPathList, dexElements <[Ldalvik/system/DexPathList$Element;>) was called
JNIEnv->GetObjectField(dalvik/system/BaseDexClassLoader, pathList <Ldalvik/system/DexPathList;>) was called
JNIEnv->GetObjectField(dalvik/system/DexPathList, dexElements <[Ldalvik/system/DexPathList$Element;>) was called
JNIEnv->GetArrayLength(33) was called
JNIEnv->GetArrayLength(35) was called
JNIEnv->newobjectarray(2, 31) was called
JNIEnv->GetObjectArrayElement(33, 0) was called
JNIEnv->SetObjectArrayElement(0) was called
JNIEnv->GetObjectArrayElement(35, 0) was called
JNIEnv->SetObjectArrayElement(1) was called
这样刚才解压解密还原的dex被加载进内存,而整个过程中没有明文文件落地,避免被copy出来。
.text:CBFB56A6 7C 69 LDR R4, [R7,#0x74+var_60] dex 个数计数器
.text:CBFB56A8 1E F0 DC FB BL sub_CBFD3E64 //获取总个数
.text:CBFB56AC 00 F0 DA F8 BL sub_CBFB5864
.text:CBFB56B0 84 42 CMP R4, R0 //循环解压解密加载所有的dex
.text:CBFB56B2 44 DA BGE loc_CBFB573E
.text:CBFB56B4 FF E7 B loc_CBFB56B6
加载完成后注册dex vmp 函数:
JNIEnv->FindClass(com/app/protect/A) was called
JNIEnv->RegisterNatives(57, 0x007ff984, 10) was called
Register native ('V', '(ILjava/lang/Object;[Ljava/lang/Object;)V',function->'0xcbfd9ed9') failed on class com_app_protect_A.
Register native ('Z', '(ILjava/lang/Object;[Ljava/lang/Object;)Z',function->'0xcbfd9ed9') failed on class com_app_protect_A.
Register native ('B', '(ILjava/lang/Object;[Ljava/lang/Object;)B',function->'0xcbfd9ed9') failed on class com_app_protect_A.
Register native ('C', '(ILjava/lang/Object;[Ljava/lang/Object;)C',function->'0xcbfd9ed9') failed on class com_app_protect_A.
Register native ('S', '(ILjava/lang/Object;[Ljava/lang/Object;)S',function->'0xcbfd9ed9') failed on class com_app_protect_A.
Register native ('I', '(ILjava/lang/Object;[Ljava/lang/Object;)I',function->'0xcbfd9ed9') failed on class com_app_protect_A.
Register native ('J', '(ILjava/lang/Object;[Ljava/lang/Object;)J',function->'0xcbfd9ed9') failed on class com_app_protect_A.
Register native ('F', '(ILjava/lang/Object;[Ljava/lang/Object;)F',function->'0xcbfd9ed9') failed on class com_app_protect_A.
Register native ('D', '(ILjava/lang/Object;[Ljava/lang/Object;)D',function->'0xcbfd9ed9') failed on class com_app_protect_A.
Register native ('L', '(ILjava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;',function->'0xcbfd9ed9') failed on class com_app_protect_A.
这十个vmp 函数根据类型不同而分别调用。
初始化附加段的vmp数据:
======================= Registers =======================
R0=0x2100000 R1=0x29be544 R2=0x2c80 R3=0x2100028
R4=0x7ff928 R5=0x7ff940 R6=0x7ff998 R7=0x7ffa08 R8=0x0
R9=0x0 R10=0x0 R11=0x0 R12=0x0 SP=0x7ff920
LR=0xcbfdac4d PC=0xcbfe62a0
======================= Disassembly =====================
0xcbfe62a0: f0b5 push {r4, r5, r6, r7, lr} //初始化vmp函数
0xcbfe62a2: 03af add r7, sp, #0xc
0xcbfe62a4: 93b0 sub sp, #0x4c
0xcbfe62a6: 6e46 mov r6, sp
0xcbfe62a8: 7262 str r2, [r6, #0x24]
>dump 0x29be544 0x2c80 //vmp 数据偏移 和 长度
029BE544: 42 44 30 35 32 37 26 00 00 00 01 00 00 00 26 00 BD0527&.......&.
029BE554: 00 00 00 00 00 00 00 00 00 00 80 00 00 00 2C 00 ..............,.
029BE564: 00 00 80 2B 00 00 02 00 00 00 4C 00 00 00 00 0A ...+......L.....
.text:CBFE7338 ; ---------------------------------------------------------------------------
.text:CBFE7338
.text:CBFE7338 loc_CBFE7338 ; CODE XREF: sub_CBFE62A0+108C↑j
.text:CBFE7338 ; sub_CBFE62A0+1092↑j ...
.text:CBFE7338 01 20 MOVS R0, #1
.text:CBFE733A 30 64 STR R0, [R6,#0x40]
.text:CBFE733C 24 21 MOVS R1, #0x24 ; '$'
.text:CBFE733E 11 F0 1F F9 BL j_calloc // 申请128个长度为0x24的空间创建索引
解析这段数据,创建索引
>dump 0x2104000
02104000: 00 00 00 0A 26 00 00 00 04 00 00 00 01 00 01 00 ....&...........
02104010: 02 00 02 00 00 00 07 00 86 E5 9B 02 00 00 00 00 ................
02104020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
02104030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ...............
一直到:
>dump 0x219b000
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2024-4-30 18:02
被kanxue编辑
,原因: