两天前发了一个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, 白盒也不算.
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是多少
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中取数据呗.
r3 0x18就是偏移,偏移是从0x0到0xff,所以r2中的数据应该就是byte_D914 这些字节都对的上,后面的就有些偏差了,这里我们只要unidbg中的结果 还原算法的话就把byte_D914转成数组就好了,用到的时候从里面取值. 经过上面一系列操作的话,入参和第一个秘钥加密的结果就好了.这个结果很重要
标准aes中的9轮循环里的内容是字节替换,循环左移,列混淆,轮秘钥加.轮秘钥加上面我们说过了,字节替换就是以自身数值为索引取出s盒里的内容,s盒256字节,类似上面的byte_D914.循环左移和列混淆这个魔改的aes不涉及,所以这里不说了,稍微有些麻烦,感兴趣的自行网上检索. 对照着这4个步骤来看这9轮循环 因为是魔改的,字节替换,循环左移,列混淆,轮秘钥加这4个步骤都改了,所以就按照标准aes中的叫法来给下面的4个循环取上对应的名字
这4个步骤看上去挺相似的,但是主要是v4,v20.v53这几个值在操作还有中间的数组取值操作,这个数组的我就不说了,和上面的数组获取的方式一样. v53是11个轮秘钥,步骤4正好也是用的v53中的轮秘钥,v4和v20是主要需要关注的点,中间记录了他们的变化,我截图没截上而已.这两个值的变化说简单也不简单,说难也难不到哪去. 有那么一丝白盒AES查表的意味. 这是python还原的几个数组还有几张表,有点大,折叠了. 接着上面的分析.我只分析第一个字节替换的过程,后面的3个步骤和这个的分析是一模一样的,所以我尽可能写详细点. 主要是看v4和v20是怎么生成的. v36也就是最开始的第一轮轮秘钥加得到的结果,先来看下结果是多少. 鼠标选中v36转到文本汇编 LDRB R3, [R11,#-36] LDRB指令上面介绍过了,这行指令的意思就是从R11偏移-36(-0x24)的地方取下一个字节给R3,结合c代码,R11处地址-0x24中的数据就是v36 0xbffff5ac-0x24=0xbffff588 记住这16个字节,每一轮加密中的4组加密都需要这个值(每一轮不一样),根据这里的索引去查表,一次用4个字节,一共4次刚好16个字节,每次的4个字节从表中一次查4个字节也是16字节,对应的正好就是v4和v20的值,后面会说表怎么拿到. 接下来先去看下经过68-99行,v20的值变成了多少.
取地址处看下汇编
ADD R3, R2, R3 将寄存器 R2 和寄存器 R3 中的值相加,并将结果存储到寄存器 R3中 unidbg中下断看下R2和R3是什么,根据&v4 + i不看断点处大概也能猜到,R3是i(偏移),R2就是v4
结果也正好验证了,此处刚进入循环所有r3是0,,来看下r2的值,也就是v20 ok,结果我们知道了,接下来带着结果去看v20是怎么生成的.6574处 LDR R3, [R3,R2,LSL#2]将寄存器 R2 中的值左移两位(相当于*4)+R3再赋给R3,unidbg中下断看下
r2这个0xc5有没有很眼熟,请看上面这张图 按照LDR R3, [R3,R2,LSL#2]汇编的操作来执行一下,0xc5*4=0x314,R3=0x4000dc14+0x314=0x4000df28,看下内存中的值是多少 再看下v20的前4个字节 00 BC BC 03变成 03 BC BC 00怎么变得相信不需要我多说了,后面的12个字节也是同理. 接下来说一下怎么获取这张大表,unidbg中hook即可. 因为LDR R3, [R3,R2,LSL#2],r2在0x0到0xff之间,*4后就是0x0到1020,以间隔4dump下unidbg中的内存即可
结果就是这样的 尝试搜索一下0x314也就是788 可以看到结果也是对的上的,后面的流程都是这样,自此,几个数和表的获取都说完了,接下来的流程就是细心点就没问题了.
第10轮标准AES有字节替换,循环左移,轮秘钥加.这里它只有一个循环. 这个表的获取方式和上面的略有不同,不过我相信你如果把前9轮加密能搞定的话,最后一轮肯定是没问题的了.
这样下来的话,这个白盒AES中的算法就搞定了,这里还有几个注意事项 1传进去的明文是32位的,刚好是两个分组长度,所以会再填充一个分组,这个填充模式也是魔改的,具体可以看下方的图 因为传进去的md5值32位,所以只需要再填充一组就可以了,也就是9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b,所以总的分组数就是3组. 2因为是cbc模式,每个分组加密的结果会和下一轮的明文进行异或.初始iv是99303a3a32343a3992923a3b3a999292 3python纯算代码太长了,有好几张表,这里我放不下就放到下方知识星球了,感兴趣的可以加一下.
微信公众号 知识星球 如果你觉得这篇文章对你有帮助也可请作者喝一杯咖啡
package
com.zh2;
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.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.ByteArray;
import
com.github.unidbg.linux.file.ByteArrayFileIO;
import
com.github.unidbg.linux.file.SimpleFileIO;
import
com.github.unidbg.memory.Memory;
import
keystone.Keystone;
import
keystone.KeystoneArchitecture;
import
keystone.KeystoneEncoded;
import
keystone.KeystoneMode;
import
java.io.File;
import
java.io.IOException;
import
java.util.ArrayList;
import
java.util.List;
public
class
zh
extends
AbstractJni
implements
IOResolver {
private
final
AndroidEmulator emulator;
private
final
DvmClass CryptoTool;
private
final
VM vm;
private
final
Module module;
public
zh()
throws
IOException {
emulator = AndroidEmulatorBuilder
.for32Bit()
.addBackendFactory(
new
Unicorn2Factory(
true
))
.setProcessName(
"com.zhihu.android"
)
.build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(
new
AndroidResolver(
23
));
vm = emulator.createDalvikVM(
new
File(
"unidbg-android/apks/zh2/zh9.29.0.apk"
));
vm.setJni(
this
);
vm.setVerbose(
true
);
emulator.getSyscallHandler().addIOResolver(
this
);
DalvikModule dm2 = vm.loadLibrary(
"bangcle_crypto_tool"
,
true
);
module = dm2.getModule();
CryptoTool = vm.resolveClass(
"com.bangcle.CryptoTool"
);
dm2.callJNI_OnLoad(emulator);
}
public
static
String bytesToHex(
byte
[] bytes) {
StringBuilder sb =
new
StringBuilder();
for
(
byte
b : bytes) {
int
unsignedInt = b &
0xff
;
String hex = Integer.toHexString(unsignedInt);
if
(hex.length() ==
1
) {
sb.append(
'0'
);
}
sb.append(hex);
}
return
sb.toString();
}
public
void
x_zse_96_encrypt()
throws
IOException {
List<Object> 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
;
}
}
package
com.zh2;
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.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.ByteArray;
import
com.github.unidbg.linux.file.ByteArrayFileIO;
import
com.github.unidbg.linux.file.SimpleFileIO;
import
com.github.unidbg.memory.Memory;
import
keystone.Keystone;
import
keystone.KeystoneArchitecture;
import
keystone.KeystoneEncoded;
import
keystone.KeystoneMode;
import
java.io.File;
import
java.io.IOException;
import
java.util.ArrayList;
import
java.util.List;
public
class
zh
extends
AbstractJni
implements
IOResolver {
private
final
AndroidEmulator emulator;
private
final
DvmClass CryptoTool;
private
final
VM vm;
private
final
Module module;
public
zh()
throws
IOException {
emulator = AndroidEmulatorBuilder
.for32Bit()
.addBackendFactory(
new
Unicorn2Factory(
true
))
.setProcessName(
"com.zhihu.android"
)
.build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(
new
AndroidResolver(
23
));
vm = emulator.createDalvikVM(
new
File(
"unidbg-android/apks/zh2/zh9.29.0.apk"
));
vm.setJni(
this
);
vm.setVerbose(
true
);
emulator.getSyscallHandler().addIOResolver(
this
);
DalvikModule dm2 = vm.loadLibrary(
"bangcle_crypto_tool"
,
true
);
module = dm2.getModule();
CryptoTool = vm.resolveClass(
"com.bangcle.CryptoTool"
);
dm2.callJNI_OnLoad(emulator);
}
public
static
String bytesToHex(
byte
[] bytes) {
StringBuilder sb =
new
StringBuilder();
for
(
byte
b : bytes) {
int
unsignedInt = b &
0xff
;
String hex = Integer.toHexString(unsignedInt);
if
(hex.length() ==
1
) {
sb.append(
'0'
);
}
sb.append(hex);
}
return
sb.toString();
}
public
void
x_zse_96_encrypt()
throws
IOException {
List<Object> 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
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2024-9-15 08:51
被杨如画编辑
,原因: