简介:面向安全研究与逆向工程场景的AES密钥查找工具,基于C++实现,支持在运行进程内存中定位128位、192位与256位AES密钥。工具本身为开源项目,适合安全分析师、二进制审计人员或有逆向基础的CTF选手学习研究。资源包共包含10个文件,其中4个头文件、1个cpp源码、Visual Studio工程配置(sln与vcxproj)、README及跨平台头文件等,整体约20KB,结构精简,可用Visual Studio 2013或gcc/clang直接编译。目前已有1632人学习下载,参考价值获得一定认可。通过阅读源码,可以理解AES密钥在进程内存中的特征与扫描匹配思路;在逆向分析中,此类工具也常被用来从内存中快速定位加密参数,为后续解密通信数据、提取关键线索提供支持。借助Linux、Windows、macOS相关的头文件,还可灵活适配不同系统环境,便于继续改造或集成到自研工具中。 做恶意样本分析或者CTF逆向的时候,我经常遇到一个非常微妙的场景:程序跑起来了,流量也能抓到,对方用的明明是AES加密,密钥就是不知道在哪。静态分析里翻遍了常量区、资源段、字符串引用,都没有;跟了一遍算法流程,发现密钥是运行时从系统信息、机器码、甚至内存别的进程里拼出来的。这时候最直接的办法,是在程序运行过程中,把正在使用的AES密钥直接从内存里“捞”出来。aes-finder这个实用程序,就是干这个的。
aes-finder的核心定位很明确:它是一个运行期工具,不是静态分析工具。你给它一个目标进程的PID,它会扫描该进程的内存空间,找出其中已经被Key Expansion展开过的AES轮密钥。它不关心你的目标程序是什么语言写的、用没用加密壳、密钥是从哪来的,只要程序在某个时刻用AES加解密,并且轮密钥表已经在内存里展开,就有机会被直接命中。这篇文章我会把它的核心识别原理、工具选型思路、实际使用流程,以及我在实战中踩过的坑完整梳理一遍。适合正在做恶意软件取证、CTF逆向、或者需要在授权环境下分析加密行为的读者参考。
1. AES密钥查找的核心思路
1.1 为什么运行时查找比静态分析高效
静态分析里找AES密钥是个体力活。很多程序不会把密钥原样放在二进制里,常见做法是先存储编码后的密文密钥,运行时再解码;或者通过字符串拼接、XOR、hash派生得到真正的密钥。这种情况下,你从静态角度看到的只是一个不完整的中间值,必须把整个推导逻辑还原出来,才能得到最终密钥。这个过程耗时,而且很容易在某个分支条件上卡住。
但运行时的情况完全不同。密钥在真正用于加密和解密之前,必须经过AES的Key Expansion(密钥扩展)算法,生成完整的轮密钥表(round keys)。这个表会被真实地写入内存,并且在整个加解密会话期间持续驻留。无论密钥本身怎么伪装、怎么派生,最终落地的轮密钥表无法伪装。aes-finder正是抓住了这个“最后一公里”的必然性,直接在内存里找展开后的轮密钥,绕开了静态分析中繁琐的密钥推导过程。
1.2 AES密钥在内存里的存在形态
理解aes-finder的识别机制,关键是要明白AES轮密钥在内存里长什么样。以AES-128为例,原始16字节密钥经过Key Expansion后,会扩展成176字节的轮密钥数据,分成11组(round 0到round 10),每组16字节。加解密每一轮都会使用其中一组。这个过程中,最后一轮轮密钥(round 10)并不是一个随机的数据块,它和前面的轮密钥之间存在着严格的数学校验关系,由S盒替换和轮常量异或两个操作维系。
aes-finder的扫描原理,就是对进程内存中的每个可能偏移位置,应用AES密钥扩展的逆运算进行校验。如果某一处数据能够通过轮密钥结构的完整校验,就极有可能是一份真实的AES轮密钥表。这个过程类似在一堆草稿纸里找一张写了特殊递推公式的纸——纸上内容看起来像随机的十六进制流水账,但前后几行之间必须严格满足数学关系,能做这种校验的只有真正的轮密钥数据。
2. 工具选型与方案拆解
2.1 为什么不做离线内存转储再分析
一提到内存分析,很多人第一反应是先把目标进程的内存dump下来,再用Volatility之类的工具离线分析。这个思路在硬盘取证和事后分析场景当然可行,但放在“程序正在运行、我需要实时拿到密钥”的场景下就不太顺手。第一个问题是你不知道密钥什么时候会被释放,等dump完再慢慢分析,可能密钥已经被覆盖了。第二个问题是进程内存可能极大,尤其是一些业务系统动辄几个GB,离线扫描一遍的成本不低。
aes-finder用的是注入式实时扫描的思路:它直接附加到目标进程,在进程自己的工作集和虚拟内存范围内执行扫描。这种方式能精确地在目标进程还活着、轮密钥表还在内存的时候完成识别,不需要整块内存的原始副本,扫描速度和命中率都更可控。另外,从执行模型上看,它走的是Windows调试API和内存读取接口,而不是把自己的DLL强行塞进目标进程,这样触发目标进程反调试逻辑的概率会低一些。
2.2 核心算法:轮密钥特征识别细节
aes-finder在识别轮密钥时,不是简单地找16字节看起来“随机”的数据,而是把整个轮密钥组的生成顺序作为筛子。AES密钥扩展的逻辑是:每一轮密钥的0-3字节,是从上一轮密钥的对应位置通过S盒替换、轮常量异或、循环移位等操作逐字生成的。所以,只要内存中的一段数据满足这种连续递推关系,就能确认它是轮密钥表,而不是恰好长得很像随机数据。
这样设计有一个明显的好处:即使程序不是直接保存原始密钥,只要它使用正常的AES实现(包括绝大多数的软件实现和硬件加速库),在调用加解密函数时,必然会在内存中展开轮密钥表。aes-finder需要校验的特征来自展开后的结果,与原始密钥的存放方式无关。这意味着,不管你是从配置文件中读取密钥、从API接口动态获取密钥、还是通过PBKDF2或哈希算法派生密钥,最终只要进入了加解密流程,轮密钥表就会暴露在内存里,存在被aes-finder命中的可能。
3. 实操过程与核心环节实现
3.1 环境的准备与权限要求
aes-finder的编译和使用主要面向Windows平台。我在实践中用的是Visual Studio的开发者命令行来编译源码,整个过程不复杂,按仓库里的构建说明走一遍就能得到可执行文件。如果你更习惯Linux环境,也可以走交叉编译路线,但运行时对目标进程的调试权限模型、内存布局扫描方式都是按Windows设计的,Windows下使用体验最稳定。
运行aes-finder必须要管理员权限,这是它能否真正工作的关键。原因在于,它需要调用系统调试接口来打开目标进程并读取内存,而这个操作在默认权限下是被拒绝的。我第一次运行时就踩了这个坑:在普通权限的命令行窗口里执行,程序报“无法打开进程”错误,换成管理员身份的命令行后问题立刻消失。在实战环境中,如果目标是以管理员权限或者系统服务方式运行,你还需要确保自己的分析环境具备足够权限。
3.2 基本用法与一次实战复现
假设你已经拿到了目标进程的PID,命令非常简单,直接把它作为参数传给aes-finder即可。命令行形式大致如下:
aes-finder.exe 1234这里的1234是目标进程PID。执行后,程序会扫描该进程的内存空间,如果发现符合AES轮密钥结构的候选数据,就会在控制台输出命中的内存地址和对应的密钥值。我常用的一个实战演练对象是自己写的一个测试程序:程序内用OpenSSL的AES-256-CBC加密一段数据,密钥随机生成后不落盘,只保存在内存变量里。然后用aes-finder扫描它的PID,几秒内就找到了完整的256位密钥,输出结果和源码里的原始密钥完全一致。
如果目标进程包含多个线程、多个加密上下文,你可能会一次性收到多处命中。建议先用校验模式过滤候选,把那些严格满足轮密钥结构的地址筛选出来,再结合目标的业务逻辑判断哪一个才是真正活跃的密钥。这里还有一个实用的细节:把命中地址附近的内存再dump出来看一看,通常能发现密钥之外的有用信息,比如IV值或者加密数据块的起始位置。
3.3 常见参数与关键时刻的判断
大多数版本的aes-finder会提供少量参数,用来应对不同的密钥长度和字节序情况。AES支持128、192、256三种密钥长度,对应的轮密钥表长度也不同。默认设置通常能覆盖128位,但如果目标程序用的是256位AES,建议确认工具的扫描范围包含了256位轮密钥表的校验逻辑;部分实现需要显式指定密钥长度,比如通过--key-size参数。实战里我建议直接开启“校验模式”或者“严格模式”,防止把内存中的随机碎片误判为轮密钥,虽然扫描速度会慢一些,但准确性高很多。
字节序是另一个必须留意的点。AES轮密钥表中的数据在内存中的排列方式,取决于具体实现是按32位字(DWORD)还是按字节数组保存。x86架构默认小端序,很多软件实现会以字节顺序存储,这正好能和aes-finder默认的识别逻辑对上。但有些用硬件指令优化的库,内部会把轮密钥以32位字为单位存储,这时字段顺序会反过来,如果扫描结果为空,可以试试交换字节序的扫描模式,通常能立刻看到大量命中。
4. 常见问题与排查技巧实录
4.1 为什么扫了半天什么都没有
这是我收到过最多的反馈:对着PID执行了aes-finder,结果啥也没发现。我排查过很多次,绝大多数情况是下面几个原因之一。首先是目标进程确实没用AES,可能用的是ChaCha20、SM4或者干脆是异或加密,那aes-finder当然没有用武之地。其次,目标程序虽然调用了AES,但密钥是在某个临时缓冲区里展开的,加解密完毕立刻清零,你没有赶上那个时间窗口。
权限问题也容易造成“空手而归”。如果目标进程是以更高权限运行的,而你的扫描进程权限不够,OpenProcess虽然会失败,但部分版本的aes-finder不会把错误处理得很细,可能只是静默跳过读取,导致结果为空。遇到这种情况,建议先确认当前命令行是否有管理员权限,再确认能否用其他工具正常读取目标进程内存,做个基础连通性验证。最后,位数不一致也是坑:32位的aes-finder扫描64位进程,或者反过来,都可能出现读取异常。
4.2 命中了一堆候选,怎么确认哪个是真的
aes-finder的识别机制是特征校验,理论上严格模式下的命中已经很可靠,但实际运行中目标进程可能非常复杂,同时存在多个AES上下文,命中候选不止一个。这种情况下不要急着随便挑一个用。我的经验是,把命中地址附近的几百字节内存都dump下来,观察是否有哈希常量表(如SHA系列算法的初始常量)或其他加密算法特征。如果一个地址附近密集分布着各种密码学算法的常量表,那通常是加密库的静态数据区域,不是实际使用中的密钥。
更可靠的方法是对候选密钥做实弹验证:拿它解密一段已知的密文,或者加密一段测试数据比对结果。如果你已经通过流量抓包或文件数据拿到了密文,并且知道是AES的哪种模式(CBC、CTR、GCM等),可以直接用候选密钥去解,能解出有意义内容的那个就是真实密钥。这个方法听起来笨,但在实战里是最能一锤定音的判断方式。
4.3 不同平台和编译版本的差异
Linux和macOS环境下,AES轮密钥一样会在内存中展开,理论上同样的扫描思路可行。但aes-finder的官方支持重点在Windows,跨平台使用需要自己处理进程内存读取接口的差异。在Linux上,你可能会用到/proc/<pid>/mem文件或者process_vm_readv系统调用来读取目标进程内存,这和Windows的ReadProcessMemory机制完全不同。如果只是临时在Linux上分析一个ELF样本,我建议先用gdb挂上去手动dump内存,再用独立脚本校验轮密钥,效率反而更高。
另外要注意,不同编译器优化级别可能影响轮密钥表在内存中的存活时间。开启O2优化后,某些高性能AES实现会把部分轮密钥放进寄存器而非内存,这时aes-finder只能扫到栈上保存的那部分。如果目标程序使用了类似mbedtls_aes_setkey_enc这类短生命周期API,密钥展开和加解密在同一个函数内完成,轮密钥表存在栈上且很快被覆盖,扫描时机就非常讲究,最好在加解密循环触发的最频繁阶段去扫描,命中率会高很多。
4.4 容易被忽视的反调试与自保护机制
不少恶意软件或者商业保护程序会对进程内存做完整性校验,一旦发现被调试或被外部读取,轻则清空密钥,重则直接崩溃退出。我在分析一个带反调试的样本时,aes-finder一直没有命中的原因,就是目标程序每隔几秒就对关键内存区域做CRC校验,发现被读取后立刻把密钥缓冲区重新加密存放。这种情况下,调低扫描频率、拉长监控时间窗口,比反复快速扫描更有效。
还有一个很容易被忽略的点:某些样本使用了“无效化轮密钥表”的技巧,也就是密钥扩展完成后,参与实际运算的是经过再次变换的伪轮密钥表,标准AES的校验关系被刻意打破,所以aes-finder无法识别。遇到这种对抗,单纯的密钥查找工具已经不够用了,需要结合CPU指令级动态插桩,比如用Intel Pin或者Frida hook的AES加密函数,在函数执行时截获传入的轮密钥参数。这种手段看的是实参,而不是扫描内存特征,能绕过大部分轮密钥表混淆。
最后分享一点个人实操体会:aes-finder这类工具,用好了是取证和逆向的加速器,但别把它当作万能钥匙。它在分析目标加密通信、定位恶意样本的C2密钥、验证自己写的代码是否把密钥安全存放这些场景下非常顺手;反过来,如果目标程序根本不在当前机器上运行,或者你只有一份加密后的二进制文件,此时需要的是带符号执行能力的静态分析框架,而不是运行期工具。我自己现在的习惯是,拿到一个样本先跑起来观察行为,同时用aes-finder扫一遍内存,如果没有命中,再切换到静态分析去追密钥逻辑——两个方向配合着来,分析效率比单靠任何一边都要高得多。
本文还有配套的精品资源,点击获取