文章总结: 这篇文章详细介绍了如何使用AI辅助技术分析一个使用了白盒AES加密的APP。作者首先通过抓包和hook技术定位加密位置,然后搭建unidbg环境模拟APP的加密过程。面对被ollvm混淆的代码,作者利用AI辅助分析,成功识别出AES加密的各个步骤(SubBytes、ShiftRows、MixColumns、AddRoundKey)。最后,作者通过DFA(差分故障分析)攻击获取了AES密钥,成功解密了密文。文章展示了AI在逆向工程中的实用价值,特别是在分析混淆代码时的辅助作用。
综合评分: 91
文章分类: 逆向分析,移动安全,漏洞分析,AI安全,二进制安全
AI辅助逆向APP白盒AES分析
nianchu
看雪学苑
2025年10月29日 18:06
上海
一、分析定位
#
首先APP为x60企业壳 有frida检测 我采用的是内核模块过的 好了 开始rush b
抓个包先看看参数 如下阔以发现有个q参数 sign参数
#
sign=7453071242111271530988405881185832853&q=nRMSmZftbACgM2slHBK7oInWjkf2hkOgn-nXF2vFgMZ8TzBce9nuQn90up6rVtw7r6oouj6xaI5gWVt189jMGI58dLeWGKCt2hpcAWebpVcSiGrQcNkemn-0-bQ9ev7wiZ8nDc3B0WMo1h3_39UXBdoUk5OR9JD-PdP8zVgG28YUEd6mLGBnQeAC2Pmz1AeFCSulOGMgDgooLkHQBU_zkTiBWThMufvE7NIWvLxtiD10lOlRFMHoFinzGY9dG-jA00X9bwWB3_sdMKqQr3Hnsz4TWvNFcM7Yqs6SDyvmnYFTAWmSqd1NhRbhgDy1uOfGBhnu7MDZYZ6pZtdUaML5_pDycR-aJouuXxjO_Rs3KUGMcEmuo4Jkf32mGq5bpaXzTxBoSpRAK0UiasbdKFJE35n72fQsleoOt2Tl9Tpwg1MWBLTbcTyuy9uQF9pIsP1Fy8CCQ_ycUI1ppbPPxfqdZZuHQ9ujLk2dzECY5N4dQMvxrnhN7uDL-rEnhKIF-HB_Rb7U_rfVd0LNoO02KTdzhG4VodWG8_rdXStD_KnF9TL0iRqgwqD4SCTZxQmf0NHSAb1oXRTFpXtrxKwDGafthreGaCf6SAbMKfqyR_QAXrpYhm-xwK0HVf1-xxLhbHvWJ8Zirwy1207ZAa67qxJ0DWHHVCsrMXf_Ug47im6LpW0%3D&uid=bfcf236c-1f0b-4517-942a-93d846ef739c1747667865472&t=1759404531524&cid=210101
#
那么先确认下加密位置 通过hook java.lang.String 下的一些getBytes方法 得到一些信息 比如登录会提交手机号 所以 追踪我们的手机号看看有没有信息打印 阔以发现 str.getBytesCharset : {“latitude”:”29.207122″,”mobile”:”13232323232″,”countryNo”:”86″,”validateCode”:”123456″,”appversion”:”5397″后面省略
#
这个看起来像是登录提交参数 那很可能是加密明文 于是对信息过滤有手机号 打印堆栈得到以下信息。
at java.lang.String.getBytes(Native Method)
at java.lang.String.getBytes(String.java:978)
at com.******.safeboxlib.CryptoHelper.c(SourceFile:7)
at com.lucky.lib.http2.HttpAesCrypto.d(SourceFile:1)
at com.lucky.lib.http2.AbstractLcRequest.getRequestParams(SourceFile:7)
at com.lucky.lib.http2.HttpPostRequestBuilder.k(SourceFile:15)
at com.lucky.lib.http2.HttpPostRequestBuilder.getRequest(Unknown Source:0)
at com.lucky.lib.http2.AbstractLcRequest.async(SourceFile:5)
at com.lucky.lib.http2.AbstractLcRequest.enqueue(SourceFile:3)
at com.lucky.lib.http2.HttpPostRequestBuilder.enqueue(Unknown Source:0)
at com.lucky.lib.lchttp2rx.result.ReactiveResult$a.a(SourceFile:4)
at io.reactivex.internal.operators.observable.ObservableCreate.F5(SourceFile:3)
at io.reactivex.Observable.subscribe(SourceFile:4)
at io.reactivex.internal.operators.observable.ObservableFlatMap.F5(SourceFile:2)
at io.reactivex.Observable.subscribe(SourceFile:4)
at io.reactivex.internal.operators.observable.ObservableObserveOn.F5(SourceFile:4)
at io.reactivex.Observable.subscribe(SourceFile:4)
at com.luckin.luckyclient.account.login.phoneno.LoginPhoneNoPresenter.n2(SourceFile:31)
at com.luckin.luckyclient.account.login.phoneno.LoginPhoneNoFragment.e2(SourceFile:4)
at com.luckin.luckyclient.account.login.phoneno.LoginPhoneNoFragment.onConfirm(SourceFile:7)
at com.luckin.luckyclient.account.login.phoneno.LoginPhoneNoFragment_ViewBinding$b.b(Unknown Source:2)
at butterknife.internal.DebouncingOnClickListener.onClick(SourceFile:4)
at android.view.View.performClick(View.java:7140)
at android.view.View.performClickInternal(View.java:7117)
at android.view.View.access$3500(View.java:801)
at android.view.View$PerformClick.run(View.java:27351)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
#
通过堆栈 at com.******.safeboxlib.CryptoHelper.c(SourceFile:7) 查找确定加密点位于以下代码 然后jadx 一键生成frida hook 脚本验证 发现此处hook得到与加密结果一致。
#
public synchronized String c(String str) {
c cVar = this.f198397a;
if (cVar != null) {
String str2 = "";
String a10 = cVar.a();
if (TextUtils.isEmpty(a10)) {
return "";
}
try {
str2 = new String(Base64.encode(localAESWork(str.getBytes(), 2, Base64.decode(a10.replace('-', '+').replace('_', '/').getBytes(), 2)), 2));
} catch (Exception e10) {
e10.printStackTrace();
}
return str2.replace('+', '-').replace('/', '_');
}
throw new RuntimeException(StubApp.getString2("40814"));
}
#
继续追踪代码阔以来到最终加密点 函数localAESWork 然后hook_RegisterNatives 得到so层加密详情如下:
#
com.******.safeboxlib.CryptoHelper localAESWork ([BI[B)[B 0x7ce5c4aed8 libcryptoDD.so 0x41ed8
public static native byte[] localAESWork(byte[] bArr, int i10, byte[] bArr2);
#
#
二、搭建unidbg
#
加密函数找到后 就开始我们的unidbg之旅,首先搭建下架子
#
package demo.coffe;
import com.alibaba.fastjson.util.IOUtils;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.Symbol;
import com.github.unidbg.file.FileResult;
import com.github.unidbg.arm.HookStatus;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.arm.context.Arm32RegisterContext;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.debugger.Debugger;
import com.github.unidbg.debugger.DebuggerType;
import com.github.unidbg.file.FileResult;
import com.github.unidbg.file.IOResolver;
import com.github.unidbg.hook.HookContext;
import com.github.unidbg.hook.ReplaceCallback;
import com.github.unidbg.hook.hookzz.Dobby;
import com.github.unidbg.hook.hookzz.HookEntryInfo;
import com.github.unidbg.hook.hookzz.HookZz;
import com.github.unidbg.hook.hookzz.IHookZz;
import com.github.unidbg.hook.hookzz.InstrumentCallback;
import com.github.unidbg.hook.hookzz.WrapCallback;
import com.github.unidbg.hook.xhook.IxHook;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.XHookImpl;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.utils.Inspector;
import com.github.unidbg.virtualmodule.android.AndroidModule;
import com.sun.jna.Pointer;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Base64;
public class rxkf extends AbstractJni implements IOResolver {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
private final boolean logging;
rxkf(boolean logging){
this.logging = logging;
emulator = AndroidEmulatorBuilder.for64Bit()
.setProcessName("com.lucky.luckyclient")
.addBackendFactory(new Unicorn2Factory(true))
.build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
final Memorymemory = emulator.getMemory(); // 模拟器的内存操作接口
memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析
vm = emulator.createDalvikVM(new File("unidbg-android/src/test/java/demo/coffe/kf.apk")); // 创建Android虚拟机
vm.setVerbose(logging); // 设置是否打印Jni调用细节
vm.setJni(this);
emulator.getSyscallHandler().addIOResolver(this);
new AndroidModule(emulator,vm).register(memory);
DalvikModuledm = vm.loadLibrary("cryptoDD", true); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
module = dm.getModule(); // 加载好的libttEncrypt.so对应为一个模块
dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数
// List<String> funclist = AesKeyFinder.readFuncFromIDA("unidbg-android/src/test/java/demo/one/1.txt");
// AesKeyFinder aesKeyFinder = new AesKeyFinder(emulator);
// aesKeyFinder.searchEveryFunction(module.base, funclist);
}
void destroy() {
IOUtils.close(emulator);
if (logging) {
System.out.println("destroy");
}
}
public static byte[] hexStringToBytes(String hexString) {
if (hexString.isEmpty()) {
return null;
}
hexString = hexString.toLowerCase();
final byte[] byteArray = new byte[hexString.length() >> 1];
intindex =0;
for (inti =0; i < hexString.length(); i++) {
if (index > hexString.length() - 1) {
return byteArray;
}
bytehighDit = (byte) (Character.digit(hexString.charAt(index), 16) & 0xFF);
bytelowDit = (byte) (Character.digit(hexString.charAt(index + 1), 16) & 0xFF);
byteArray[i] = (byte) (highDit << 4 | lowDit);
index += 2;
}
return byteArray;
}
public static String bytesTohexString(byte[] bytes) {
StringBuffersb =new StringBuffer();
for (inti =0; i < bytes.length; i++) {
Stringhex = Integer.toHexString(bytes[i] & 0xFF);
if (hex.length() < 2) {
sb.append(0);
}
sb.append(hex);
}
return sb.toString();
}
public static String b64TohexString(String base64String){
byte[] decodedBytes = org.apache.commons.codec.binary.Base64.decodeBase64(base64String);
BigIntegerbigInteger =new BigInteger(1, decodedBytes);
StringhexString = bigInteger.toString(16);
return hexString;
}
public void trace(){
StringtraceFile ="unidbg-android/src/test/java/demo/coffe/trace.txt";
PrintStreamtraceStream =null;
try{
traceStream = new PrintStream(new FileOutputStream(traceFile), true);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
//核心 trace 开启代码,也可以自己指定函数地址和偏移量
emulator.traceCode(module.base,module.base+module.size).setRedirect(traceStream);
}
public static void main(String[] args) {
rxkfdemo =new rxkf(true);
demo.s();
}
public void s(){
DvmClassCryptoHelper = vm.resolveClass("com/******/safeboxlib/CryptoHelper");
//DvmObject<?> Context = vm.resolveClass("android/content/Context").newObject(null);
byte[] bytes = "hello".getBytes();
Stringa2 ="u7Su25kSE9PxcTgQZkRgL0kJ+lDaV2IQcqdsfGGuNDs=" ;
byte[] bytes2 = Base64.getDecoder().decode(a2.replace('-', '+').replace('_', '/').getBytes());
// emulator.traceCode(module.base,module.base+module.size);
ByteArrayreval = CryptoHelper.callStaticJniMethodObject(emulator, "localAESWork([BI[B)[B", bytes,2,bytes2);
System.out.println("结果:" + bytesTohexString(reval.getValue()));
}
@Override
public FileResult resolve(Emulator emulator, String pathname, int oflags) {
System.out.println("pathname:"+ pathname);
// if(pathname.equals("/proc/"+emulator.getPid()+"/cmdline")){
// return FileResult.success(new SimpleFileIO(oflags, new File("unidbg-android/src/test/java/demo/wb/cmdline"), pathname));
//
// }
return null;
}
}
先跑一下再说 发现报错了 去到0x43b08 处 是一个free函数 那么可能是 Unidbg在内存管理方面存在的一些问题,此处是在内存的释放上 free出错。
JNIEnv->SetByteArrayRegion([B@6aaa5eb0, 0, 16, RW@0x40399010) was called from RX@0x40043b08[libcryptoDD.so]0x43b08
[19:53:15 655] WARN [com.github.unidbg.arm.AbstractARM64Emulator] (AbstractARM64Emulator$1:66) - Write memory failed: address=0x0, size=8, value=0x40057000
[19:53:15 655] WARN [com.github.unidbg.AbstractEmulator] (AbstractEmulator:420) - emulate RX@0x40041ed8[libcryptoDD.so]0x41ed8 exception sp=unidbg@0xbffff3b0, msg=unicorn.UnicornException: Invalid memory write (UC_ERR_WRITE_UNMAPPED), offset=10ms
针对此处报错 我们先对free处理看看能否成功跑起来,hook free函数,替换它的实现,让它什么都不做就直接返回。
处理后 unidbg已经跑出了结果 frida hook 传参 并且传入 与抓包对比一致 no problem 下一步开始分析算法,这个案例老版本有很多文章了,阔以知道 这个家伙用的是 白盒aes ida打开so 去到localAESWork 函数处先瞅一眼。
public void patchFree(){
IWhalewhale = Whale.getInstance(emulator);
Symbolfree = emulator.getMemory().findModule("libc.so").findSymbolByName("free");
whale.inlineHookFunction(free, new ReplaceCallback() {
@Override
public HookStatus onCall(Emulator<?> emulator, long originFunction) {
System.out.println("WInlineHookFunction free=" + emulator.getContext().getPointerArg(0));
return HookStatus.LR(emulator, 0);
}
});
}
不看不知道 一看全是线条 ollvm 玩个锤子 奈子的 trace 它吧
先固定传参 方便后续处理我们的aes 传个hello 加密,结果是 fa63a50fd234b8c15a8f4ff9761ea69a 然后开启我们的trace 会再文件夹生成trace文本。
public static void main(String[] args) {
rxkf demo = new rxkf(true);
demo.patchFree();
demo.trace();
demo.s();
}
然后在我们的trace结果中搜索我们的结果 fa63a50fd234b8c15a8f4ff9761ea69a 搜下 0xfa 发现结果有点多 不好看 继续往后找 发现搜素 0x8f 结果比较少 比较好观察 于是 前后观察下 发现 我们生成结果都在 于是找个地址 跳过去看看 这里找的0x199e0
阔以发现 ollvm 严重影响我们分析 那么不还原ollvm 我们就只能硬着头皮看看 主要看函数 跟进去看看 瞧瞧 瞅瞅 他们都干啥了 那么此处 你需要先去了解AES的算法 然后还有白盒AES。
由于论坛已经有很多写的好的文章了,这里就不提哪么详细,那么有人问 我没了解AES算法怎么办 别怕 我们有小帮手AI 毕竟现在是AI辅助逆向时代 我们往下继续找函数。
我们进 219CC 里面去看看 发现是这个鸟样子 看不懂怎么办 不怕 问下ai ai猜猜猜他在干啥
经过AI的一顿分析 喀喀喀的就出来了我们的结果 它说是AES加密中的轮密钥加(AddRoundKey)操作我们继续往下看几个 比如21BEC
这个函数 我们阔以看到有个byte_455E8 它频繁的从里面取了值 我们去看看byte_455E8 这不是s盒吗 看这个操作从s盒取值 然后后面又赋值给result 符合我们AES里面的SubBytes操作 那不问ai了 继续往下看 223E0
223E0 函数 有个很明显的换位操作 符合AES里面的ShiftRows继续看看22810 直接 丢给ai看
说这是列混淆 去trace里面看看这个函数 调用几次
阔以看到是9次 AES-128 中 10轮运算 只有9轮有列混淆 经过AI辅助(不一定正确),我们已经找出了AES的这些步骤 接下来就可以进行DFA攻击了 故障点应该让发生在倒数两个列混淆之间
◆✅SubBytes– 字节替换
◆✅ShiftRows– 行移位
◆✅MixColumns– 列混淆
◆✅AddRoundKey– 轮密钥加
攻击之后 得到的故障密文 与 正确密文 对比如下 可以发现四个字节不一样 说明故障注入成功
public void dfaAttack(){
emulator.attach().addBreakPoint(module.base + 0x22810, new BreakPointCallback() {
intcount =0;
UnidbgPointer pointer;
@Override
public booleanonHit(Emulator<?> emulator, long address) {
count += 1;
RegisterContextregisterContext = emulator.getContext();
pointer = registerContext.getPointerArg(0);
emulator.attach().addBreakPoint(registerContext.getLRPointer().peer, new BreakPointCallback() {
@Override
public booleanonHit(Emulator<?> emulator, long address) {
if(count % 8 == 0){
pointer.setByte(randInt(0, 15), (byte) randInt(0, 0xff));
}
return true;
}
});
return true;
}
});
}
============================
正确:fa63a5 0f d234 b8 c15a 8f 4ff9 76 1ea69a
故障:fa63a5 36 d234 f4 c15a dd 4ff9 43 1ea69a
可以发现4处不同
然后循环注入得到更多的故障密文,使用 phoenixAES 把第十轮的密钥给计算出来
得到
然后使用aes_keyschedule去计算出原始密钥
有了key之后 我们去解密一下 首先密文需要replace(‘-‘, ‘+’).replace(‘_’, ‘/’)); 替换下 然后去解密可以正常解密。
#
看雪ID:nianchu
https://bbs.kanxue.com/user-home-949800.htm
*本文为看雪论坛优秀文章,由 nianchu 原创,转载请注明来自看雪社区
往期推荐
静态程序分析之数据流分析(Foundations + LiveVar Analysis Code)续
基于Minifilter实现目录保护软件,自定义保护目录,用户可选择是否允许文件行为
球分享
球点赞
球在看
点击阅读原文查看更多
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:看雪学苑 nianchu《AI辅助逆向APP白盒AES分析》