两天前发了一个x-zse-96的文章,当时遇到了点问题,只分析到了最后一个白盒AES函数里面,并且当时用dfa攻击还原出了秘钥,IV也确定了,但是加密结果不对,本来打算把下文鸽掉的,因为当时unidbg没跑起来,用frida去hook白盒AES中的每一行汇编有点麻烦,没有unidbg方便.后来小白大佬说unidbg可以跑通,并把还原算法的任务交给了我.我拿着小白提供的unidbg代码,开始了算法还原之旅.还原算法,篇幅有点长,会介绍AES的10轮加密以及如何从汇编代码和伪c代码中还原出魔改的AES,由于so是从内存中dump下来的,反编译出来的伪代码准确性很差,主要看的就是汇编.严格来说,这个AES魔改的已经完全不能算是AES了,但是他c中的函数名称还是叫wb_laes_encrypt,白盒也不算.
package
com.zh2;
import
com.github.unidbg.AndroidEmulator;
com.github.unidbg.Emulator;
com.github.unidbg.Module;
com.github.unidbg.arm.backend.Unicorn2Factory;
com.github.unidbg.debugger.Debugger;
com.github.unidbg.file.FileResult;
com.github.unidbg.file.IOResolver;
com.github.unidbg.linux.android.AndroidEmulatorBuilder;
com.github.unidbg.linux.android.AndroidResolver;
com.github.unidbg.linux.android.dvm.*;
com.github.unidbg.linux.android.dvm.array.ByteArray;
com.github.unidbg.linux.file.ByteArrayFileIO;
com.github.unidbg.linux.file.SimpleFileIO;
com.github.unidbg.memory.Memory;
keystone.Keystone;
keystone.KeystoneArchitecture;
keystone.KeystoneEncoded;
keystone.KeystoneMode;
java.io.File;
java.io.IOException;
java.util.ArrayList;
java.util.List;
public
class
zh
extends
AbstractJni
implements
IOResolver {
private
final
AndroidEmulator emulator;
DvmClass CryptoTool;
VM vm;
Module module;
zh()
throws
IOException {
emulator = AndroidEmulatorBuilder
.for32Bit()
.addBackendFactory(
new
Unicorn2Factory(
true
))
.setProcessName(
"com.zhihu.android"
)
.build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(
AndroidResolver(
23
));
vm = emulator.createDalvikVM(
File(
"unidbg-android/apks/zh2/zh9.29.0.apk"
vm.setJni(
this
);
vm.setVerbose(
emulator.getSyscallHandler().addIOResolver(
DalvikModule dm2 = vm.loadLibrary(
"bangcle_crypto_tool"
,
module = dm2.getModule();
CryptoTool = vm.resolveClass(
"com.bangcle.CryptoTool"
dm2.callJNI_OnLoad(emulator);
}
static
String bytesToHex(
byte
[] bytes) {
StringBuilder sb =
StringBuilder();
for
(
b : bytes) {
int
unsignedInt = b &
0xff
;
String hex = Integer.toHexString(unsignedInt);
if
(hex.length() ==
1
) {
sb.append(
'0'
sb.append(hex);
return
sb.toString();
void
x_zse_96_encrypt()
List list = new ArrayList<>(5); list.add(vm.getJNIEnv()); list.add(0); byte[] byy1 = {-118,-125,-125,41,41,34,-113,-115,42,35,34,42,-125,-128,40,-125,-125,33,-126,41,40,-118,-117,35,-125,-128,-117,35,42,-115,35,-113}; byte[] byy2 = {-103,48,58,58,50,52,58,57,-110,-110,58,59,58,-103,-110,-110}; ByteArray arr1 = new ByteArray(vm,byy1); list.add(vm.addLocalObject(arr1)); list.add(vm.addLocalObject(new StringObject(vm, "541a3a5896fbefd351917c8251328a236a7efbf27d0fad8283ef59ef07aa386dbb2b1fcbba167135d575877ba0205a02f0aac2d31957bc7f028ed5888d4bbe69ed6768efc15ab703dc0f406b301845a0a64cf3c427c82870053bd7ba6721649c3a9aca8c3c31710a6be5ce71e4686842732d9314d6898cc3fdca075db46d1ccf3a7f9b20615f4a303c5235bd02c5cdc791eb123b9d9f7e72e954de3bcbf7d314064a1eced78d13679d040dd4080640d18c37bbde"))); ByteArray arr2 = new ByteArray(vm,byy2); list.add(vm.addLocalObject(arr2)); Number number = module.callFunction(emulator, 0xa708, list.toArray()); ByteArray resultArr = vm.getObject(number.intValue()); System.out.println("result:"+ bytesToHex(resultArr.getValue())); } public static void main(String[] args) throws IOException { zh zh = new zh(); zh.patchInstruction(); zh.x_zse_96_encrypt(); } private void patchInstruction() { try (Keystone keystone = new Keystone(KeystoneArchitecture.Arm, KeystoneMode.ArmThumb)) { KeystoneEncoded assemble = keystone.assemble("nop;nop"); byte[] machineCode = assemble.getMachineCode(); emulator.getMemory().pointer(module.base + 0x8EBC).write(0,machineCode,0,machineCode.length); KeystoneEncoded encoded = keystone.assemble("mov r3, 1"); byte[] patchCode = encoded.getMachineCode(); emulator.getMemory().pointer(module.base + 0x4e70).write(0, patchCode, 0, patchCode.length); KeystoneEncoded encoded1 = keystone.assemble("mov r3, 1"); byte[] patchCode1 = encoded1.getMachineCode(); emulator.getMemory().pointer(module.base + 0x4e84).write(0, patchCode1, 0, patchCode1.length); } catch (Exception e) { e.printStackTrace(); } } @Override public DvmObject getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) { switch (signature) { case "com/secneo/apkwrapper/H->PKGNAME:Ljava/lang/String;": { String packageName = vm.getPackageName(); if (packageName != null) { return new StringObject(vm, packageName); } break; } case "com/secneo/apkwrapper/H->ISMPASS:Ljava/lang/String;": { return new StringObject(vm, "###MPASS###"); } } return super.getStaticObjectField(vm, dvmClass, signature); } @Override public DvmObject callObjectMethodV(BaseVM vm, DvmObject dvmObject, String signature, VaList vaList) { switch (signature) { case "android/app/ActivityThread->getSystemContext()Landroid/app/ContextImpl;": { return vm.resolveClass("android/app/ContextImpl").newObject(signature); } case "android/app/ContextImpl->getPackageManager()Landroid/content/pm/PackageManager;": { return vm.resolveClass("android/content/pm/PackageManager").newObject(signature); } case "java/io/File->getPath()Ljava/lang/String;": { System.out.println("PATH:" + dvmObject.getValue()); if (dvmObject.getValue().equals("android/os/Environment->getExternalStorageDirectory()Ljava/io/File;")) { return new StringObject(vm, "/mnt/sdcard"); } } case "java/lang/String->intern()Ljava/lang/String;": { String string = dvmObject.getValue().toString(); return new StringObject(vm, string.intern()); } case "java/lang/Class->getDeclaredFields()[Ljava/lang/reflect/Field;": { DvmClass c = (DvmClass) dvmObject; System.out.println(c.getClassName()); } } return super.callObjectMethodV(vm, dvmObject, signature, vaList); } @Override public DvmObject getObjectField(BaseVM vm, DvmObject dvmObject, String signature) { switch (signature) { case "android/content/pm/PackageInfo->applicationInfo:Landroid/content/pm/ApplicationInfo;": { return vm.resolveClass("android/content/pm/ApplicationInfo").newObject(signature); } case "android/content/pm/ApplicationInfo->sourceDir:Ljava/lang/String;": { return new StringObject(vm, "/data/app/com.zhihu.android-XnTWbKh_JrM9gUKdEn_Wug==/base.apk"); } case "android/content/pm/ApplicationInfo->dataDir:Ljava/lang/String;": { return new StringObject(vm, "/data/data/com.zhihu.android-XnTWbKh_JrM9gUKdEn_Wug=="); } case "android/content/pm/ApplicationInfo->nativeLibraryDir:Ljava/lang/String;": { return new StringObject(vm, "/data/app/com.zhihu.android-XnTWbKh_JrM9gUKdEn_Wug==/lib/arm"); } } return super.getObjectField(vm, dvmObject, signature); } @Override public DvmObject callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) { switch (signature) { case "android/os/Environment->getExternalStorageDirectory()Ljava/io/File;": { return vm.resolveClass("java/io/File").newObject(signature); } } return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList); } @Override public FileResult resolve(Emulator emulator, String pathname, int oflags) { if (pathname.equals("/proc/self/cmdline")) { return FileResult.success(new ByteArrayFileIO(oflags, pathname, "com.zhihu.android".getBytes())); } if (pathname.equals("/data/app/com.zhihu.android-XnTWbKh_JrM9gUKdEn_Wug==/base.apk")) { return FileResult.success(new SimpleFileIO(oflags, new File("D:\\code\\me\\app\\unidbg-master\\unidbg-android\\src\\test\\resources\\zhihu\\zhihu.apk"), pathname)); } return null; } }
ArrayList<>(
5
list.add(vm.getJNIEnv());
list.add(
0
[] byy1 = {-
118
,-
125
41
34
113
115
42
35
128
40
33
126
117
};
[] byy2 = {-
103
48
58
50
52
57
110
59
ByteArray arr1 =
ByteArray(vm,byy1);
list.add(vm.addLocalObject(arr1));
list.add(vm.addLocalObject(
StringObject(vm,
"541a3a5896fbefd351917c8251328a236a7efbf27d0fad8283ef59ef07aa386dbb2b1fcbba167135d575877ba0205a02f0aac2d31957bc7f028ed5888d4bbe69ed6768efc15ab703dc0f406b301845a0a64cf3c427c82870053bd7ba6721649c3a9aca8c3c31710a6be5ce71e4686842732d9314d6898cc3fdca075db46d1ccf3a7f9b20615f4a303c5235bd02c5cdc791eb123b9d9f7e72e954de3bcbf7d314064a1eced78d13679d040dd4080640d18c37bbde"
)));
ByteArray arr2 =
ByteArray(vm,byy2);
list.add(vm.addLocalObject(arr2));
Number number = module.callFunction(emulator,
0xa708
, list.toArray());
ByteArray resultArr = vm.getObject(number.intValue());
System.out.println(
"result:"
+ bytesToHex(resultArr.getValue()));
main(String[] args)
zh zh =
zh();
zh.patchInstruction();
zh.x_zse_96_encrypt();
patchInstruction() {
try
(Keystone keystone =
Keystone(KeystoneArchitecture.Arm, KeystoneMode.ArmThumb)) {
KeystoneEncoded assemble = keystone.assemble(
"nop;nop"
[] machineCode = assemble.getMachineCode();
emulator.getMemory().pointer(module.base +
0x8EBC
).write(
,machineCode,
,machineCode.length);
KeystoneEncoded encoded = keystone.assemble(
"mov r3, 1"
[] patchCode = encoded.getMachineCode();
0x4e70
, patchCode,
, patchCode.length);
KeystoneEncoded encoded1 = keystone.assemble(
[] patchCode1 = encoded1.getMachineCode();
0x4e84
, patchCode1,
, patchCode1.length);
catch
(Exception e) {
e.printStackTrace();
@Override
DvmObject getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {
switch
(signature) {
case
"com/secneo/apkwrapper/H->PKGNAME:Ljava/lang/String;"
: {
String packageName = vm.getPackageName();
(packageName !=
null
StringObject(vm, packageName);
break
"com/secneo/apkwrapper/H->ISMPASS:Ljava/lang/String;"
"###MPASS###"
super
.getStaticObjectField(vm, dvmClass, signature);
DvmObject callObjectMethodV(BaseVM vm, DvmObject dvmObject, String signature, VaList vaList) {
"android/app/ActivityThread->getSystemContext()Landroid/app/ContextImpl;"
vm.resolveClass(
"android/app/ContextImpl"
).newObject(signature);
"android/app/ContextImpl->getPackageManager()Landroid/content/pm/PackageManager;"
"android/content/pm/PackageManager"
"java/io/File->getPath()Ljava/lang/String;"
"PATH:"
+ dvmObject.getValue());
(dvmObject.getValue().equals(
"android/os/Environment->getExternalStorageDirectory()Ljava/io/File;"
)) {
"/mnt/sdcard"
"java/lang/String->intern()Ljava/lang/String;"
String string = dvmObject.getValue().toString();
StringObject(vm, string.intern());
"java/lang/Class->getDeclaredFields()[Ljava/lang/reflect/Field;"
DvmClass c = (DvmClass) dvmObject;
System.out.println(c.getClassName());
.callObjectMethodV(vm, dvmObject, signature, vaList);
DvmObject getObjectField(BaseVM vm, DvmObject dvmObject, String signature) {
"android/content/pm/PackageInfo->applicationInfo:Landroid/content/pm/ApplicationInfo;"
"android/content/pm/ApplicationInfo"
"android/content/pm/ApplicationInfo->sourceDir:Ljava/lang/String;"
"/data/app/com.zhihu.android-XnTWbKh_JrM9gUKdEn_Wug==/base.apk"
"android/content/pm/ApplicationInfo->dataDir:Ljava/lang/String;"
"/data/data/com.zhihu.android-XnTWbKh_JrM9gUKdEn_Wug=="
"android/content/pm/ApplicationInfo->nativeLibraryDir:Ljava/lang/String;"
"/data/app/com.zhihu.android-XnTWbKh_JrM9gUKdEn_Wug==/lib/arm"
.getObjectField(vm, dvmObject, signature);
DvmObject callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
"java/io/File"
.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
FileResult resolve(Emulator emulator, String pathname,
oflags) {
(pathname.equals(
"/proc/self/cmdline"
FileResult.success(
ByteArrayFileIO(oflags, pathname,
.getBytes()));
SimpleFileIO(oflags,
"D:\\code\\me\\app\\unidbg-master\\unidbg-android\\src\\test\\resources\\zhihu\\zhihu.apk"
), pathname));
unidbg跑不起来的原因是有几个初始化的函数无法执行起来,当时我也是打了几个patch下去,最终没能跑起来,也许是版本有差异,我弄的是最新版的,这个unidbg中的版本是9.29.0.
还原算法
这个就是目标函数了,其实它有5个参数,ida识别出错了,参数一和参数2都是入参,结果通过a2返回回去来看下函数最开始的地方,v3被赋值成了入参,v53 = *a3,v52 = a3[3] / 32 + 6这里ida反汇编出来的有些问题,不过根据aes的算法特征也可以大概猜出这里是在做什么,v52 = a3[3] / 32 + 6,我们知道AES有10轮加密,第10轮没有mixcolumns(列混淆),并且aes的分组长度16字节128位,128/32+6=10,下面的从1开始到10结束正好是9轮.
初始轮秘钥加
这样的话框中的应该就是第一个轮秘钥加了.说是加其实是异或,比如秘钥k0是0x11111111111111111111111111111111,明文是0x22222222222222222222222222222222,异或就是他两直接异或就好了.因为c语言只能一个字节一个字节的异或,所以16个字节他这里有16轮.result是第一个入参,和第二个入参是同一个地址,这里我hook过了result经过9轮加密始终没变,所以上面result赋值的结果不用管,只需要看*(&v36 + i) = (byte_D914[(v53 + i) & 0xF ^ (16 * (v3 + i))] >> 4) ^ (16 * (byte_D914[((v53 + i) >> 4) & 0xF ^ (16 * ((v3 + i) >> 4))] >> 4));这行就可以了,正常来说两个字节异或直接0x11^0x22就可以了,他这里那么长肯定是改了AES的,v3是入参,v53来自上面的*a3,接下来unidbg中看下这个v53是多少,鼠标选中这个星号,按tab转文本汇编STR R3, [R11,#-16] (Store Register),存储指令,结合着c代码,就是把寄存器r3的值存到R11偏移-16的位置,对应着v53=*a3,unidbg中下断看下R3是多少
HookByConsoleDebugger() {
Debugger debugger = emulator.attach();
debugger.addBreakPoint(module.base+
0x6444
需要encrypt函数调用前执行
m某个寄存器可以直接看内存中的数据,默认是112字节,可以在后面加数组指定dump多少字节,好像后面还有
长度是1611,这个11好像是aes的11个轮秘钥,但是他的11个轮秘钥是没有联系的,标准的aes是第一轮可以推出后面的轮秘钥的.(&v36 + i) = (byte_D914[(v53 + i) & 0xF ^ (16 * (v3 + i))] >> 4) ^ (16 * (byte_D914[((v53 + i) >> 4) & 0xF ^ (16 * ((v3 + i) >> 4))] >> 4)); 16轮循环下来,v53只消耗第一个轮秘钥,v3是入参,v36是结果,byte_D914是一个数组.这里看上去写的有点复杂,其实不然,就是byte_D914[(v53 + i) & 0xF ^ (16 * (v3 + i))] >> 4异或(16 * (byte_D914[((v53 + i) >> 4) & 0xF ^ (16 * ((v3 + i) >> 4))] >> 4)),想要获得结果就需要知道byte_D914是多少点过去只有100字节,但是*(v53 + i) & 0xF ^ (16 * (v3 + i))或者((v53 + i) >> 4) & 0xF ^ (16 * (*(v3 + i) >> 4))的结果在0x0到0xff之间有256个,所以一直带下面的256个应该都是byte_D914的.但是当你把这256个字节扣出来并且加密一遍后你会发现结果有几个字节是正确的,有几个字节是错误的,应该是因为dump下来的so有些问题.这里ida中的静态byte_D914是不准确的,真正准确的是内存中的,所以可以从unidbg中把这256个字节dump下来.鼠标选中byte_D914按tab转到文本汇编,后面的一些操作都需要汇编的基础,因为c的代码不准确,想要搞so的算法还原必须要懂汇编LDRB R3, [R2, R3](Load Register Byte)字面意思,从R2+R3的地址加载一个字节到R3中,对应着c代码就是从byte_D914中取数据呗.
0x64c0
r3 0x18就是偏移,偏移是从0x0到0xff,所以r2中的数据应该就是byte_D914这些字节都对的上,后面的就有些偏差了,这里我们只要unidbg中的结果还原算法的话就把byte_D914转成数组就好了,用到的时候从里面取值.经过上面一系列操作的话,入参和第一个秘钥加密的结果就好了.这个结果很重要
9轮循环
标准aes中的9轮循环里的内容是字节替换,循环左移,列混淆,轮秘钥加.轮秘钥加上面我们说过了,字节替换就是以自身数值为索引取出s盒里的内容,s盒256字节,类似上面的byte_D914.循环左移和列混淆这个魔改的aes不涉及,所以这里不说了,稍微有些麻烦,感兴趣的自行网上检索.对照着这4个步骤来看这9轮循环因为是魔改的,字节替换,循环左移,列混淆,轮秘钥加这4个步骤都改了,所以就按照标准aes中的叫法来给下面的4个循环取上对应的名字
#字节替换
登录后可查看完整内容 [注意]看雪招聘,专注安全领域的专业人才平台! 最后于 2024-9-15 08:51 被杨如画编辑 ,原因: #逆向分析 #协议分析 #HOOK注入
[注意]看雪招聘,专注安全领域的专业人才平台!
收藏 ・15 免费 ・3 支持 分享 分享到微信 分享到QQ 分享到微博
赞赏记录 参与人 雪币 留言 时间 沈聽白 你的分享对大家帮助很大,非常感谢! 2025-3-10 17:17 mb_yvuzbfvo 你的帖子非常有用,感谢分享! 2025-1-18 22:20 狄人3 为你点赞~ 2024-3-29 16:15 查看更多 赞赏 × 1 雪花 5 雪花 10 雪花 20 雪花 50 雪花 80 雪花 100 雪花 150 雪花 200 雪花 支付方式: 微信支付 赞赏留言: 快捷留言 感谢分享~ 精品文章~ 原创内容~ 精彩转帖~ 助人为乐~ 感谢分享~ 最新回复 (8) 秋狝 雪 币: 4064 活跃值: (31456) 能力值: ( LV2,RANK:10 ) 在线值: 发帖 0 回帖 1569 粉丝 38 关注 私信 秋狝 2 楼 感谢分享 2024-3-24 16:53 0 肉蚌葱鸡 雪 币: 19 活跃值: (1113) 能力值: ( LV2,RANK:10 ) 在线值: 发帖 1 回帖 18 粉丝 5 关注 私信 肉蚌葱鸡 3 楼 感谢楼主分享 试了一下是可以补跑通的 都我没弄这个 没验证 2024-5-3 15:47 0 杨如画 雪 币: 3188 活跃值: (2274) 能力值: ( LV7,RANK:100 ) 在线值: 发帖 14 回帖 41 粉丝 213 关注 私信 杨如画 4 楼 肉蚌葱鸡 感谢楼主分享 试了一下是可以补跑通的 都我没弄这个 没验证 大部分接口会验证,纯算也有 2024-5-5 18:52 0 mb_nuxwsyal 雪 币: 235 能力值: ( LV1,RANK:0 ) 在线值: 发帖 1 回帖 3 粉丝 1 关注 私信 mb_nuxwsyal 5 楼 肉蚌葱鸡 感谢楼主分享 试了一下是可以补跑通的 都我没弄这个 没验证 可以改成独立运行版本吗? 我想要,可以付费 2024-10-24 22:59 0 杨如画 雪 币: 3188 活跃值: (2274) 能力值: ( LV7,RANK:100 ) 在线值: 发帖 14 回帖 41 粉丝 213 关注 私信 杨如画 6 楼 有python版纯算法 2024-10-25 10:39 0 mb_nuxwsyal 雪 币: 235 能力值: ( LV1,RANK:0 ) 在线值: 发帖 1 回帖 3 粉丝 1 关注 私信 mb_nuxwsyal 7 楼 杨如画 有python版纯算法 在哪? 2024-10-25 10:48 0 杨如画 雪 币: 3188 活跃值: (2274) 能力值: ( LV7,RANK:100 ) 在线值: 发帖 14 回帖 41 粉丝 213 关注 私信 杨如画 8 楼 mb_nuxwsyal 在哪? 在我电脑上? 2024-10-25 12:54 0 mb_nvmtlmsm 雪 币: 17 能力值: ( LV1,RANK:0 ) 在线值: 发帖 1 回帖 4 粉丝 0 关注 私信 mb_nvmtlmsm 9 楼 大神,怎么联系你,有项目找你合作 2025-3-9 23:19 0 游客 登录 | 注册 方可回帖 回帖 表情 雪币赚取及消费 高级回复 返回
感谢楼主分享 试了一下是可以补跑通的 都我没弄这个 没验证
肉蚌葱鸡 感谢楼主分享 试了一下是可以补跑通的 都我没弄这个 没验证
杨如画 有python版纯算法
mb_nuxwsyal 在哪?
杨如画 14 发帖 41 回帖 100 RANK 关注 私信 unidbg代码还原算法初始轮秘钥加9轮循环#字节替换 他的文章 [原创]shopee app算法分析第一篇 31796 关于我们 联系我们 企业服务 看雪公众号 专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图 赞赏 × 雪币: + 留言: 快捷留言 非常支持你的观点!这个讨论对我很有帮助,谢谢!感谢你分享这么好的资源!谢谢你的细致分析,受益匪浅!感谢你的积极参与,期待更多精彩内容!感谢你的贡献,论坛因你而更加精彩!你的分享对大家帮助很大,非常感谢!期待更多优质内容的分享,论坛有你更精彩!你的帖子非常有用,感谢分享!请遵守论坛规则,避免发布广告内容!请注意发帖规范,保持良好的讨论环境! 为你点赞!
返回顶部 × 求助问答申诉 举报此帖 × 申请推荐此帖 × × Close 游客下载提示 × 1.请先关注公众号。 2.点击菜单"更多"。 3.选择获取下载码。