sRDI批量转换脚本实战:自动化DLL转Shellcode的原理与实现 1. 项目概述为什么我们需要批量转换DLL为Shellcode在安全研究、红队评估甚至某些特定的软件调试场景里我们常常需要将DLL文件加载到目标进程的内存中执行。传统的LoadLibraryAPI调用虽然简单但它在磁盘上留下了DLL文件的痕迹并且会被绝大多数安全软件EDR/AV的API钩子Hook所监控一抓一个准。反射型DLL注入Reflective DLL Injection, RDI技术应运而生它绕过了LoadLibrary直接在内存中解析和执行DLL实现了“无文件”落地隐蔽性大大提升。而sRDIShellcode Reflective DLL Injection则将这一技术推向了更实用的层面。它把整个反射加载器Reflective Loader和DLL本身打包成一段位置无关的Shellcode。这意味着你得到的不是一个需要复杂注入逻辑的DLL而是一段可以直接用各种经典Shellcode注入技术如VirtualAllocEx WriteProcessMemory CreateRemoteThread投递的二进制数据块。这对于武器化、自动化以及集成到现有工具链中来说是质的飞跃。但是sRDI官方提供的Python脚本ConvertToShellcode.py一次只能处理一个DLL。在实际工作中我们面对的往往是成批的DLL可能是多个功能模块、不同架构x86/x64的版本或者是在迭代开发中需要反复测试的多个变体。手动一个个转换不仅效率低下还容易出错。因此编写一个Python脚本来自动化、批量化地完成这个转换过程就成了一个非常实际且迫切的需求。这个脚本能让我们专注于核心的逻辑和测试而不是重复的转换操作。2. 核心原理深度拆解sRDI是如何工作的要写好批量转换脚本不能只当个“调包侠”必须深入理解sRDI的核心原理。这样在脚本出错或需要定制功能时你才知道从哪里下手。2.1 反射型DLL注入RDI的精髓传统DLL注入的核心是让目标进程调用LoadLibraryA/W系统加载器会做一系列事情找到文件、映射到内存、解析导入表、重定位、调用DllMain。这个过程在用户态和内核态都有清晰的日志和事件可被监控。RDI的思路是“既然系统能做我自己在内存里做一遍不行吗” 它要求DLL内部包含一个特殊的导出函数通常叫ReflectiveLoader。这个函数就是一个迷你的PE加载器它的工作流程可以概括为自举定位首先它需要找到自己DLL数据在内存中的起始位置。这通常通过一些汇编技巧获取当前指令指针如call $5; pop ebx/rax来实现。解析自身PE头把自己当成一个PE文件来解析获取各种目录表如导入表、重定位表的地址。分配最终内存按照PE头中的SizeOfImage在进程内申请具有执行权限的内存块。复制节区并重定位将DLL的各个节.text, .data等复制到新申请的内存中并应用基址重定位如果最终加载地址与DLL编译时预设的基址不同。解析导入表遍历导入表手动调用GetProcAddress或更底层的方法来获取所有依赖函数的地址并填充导入地址表IAT。调用DllMain最后跳转到DLL的入口点DllMain执行完成初始化。整个过程完全在内存中完成不依赖系统的加载器因此避开了许多监控点。2.2 sRDI的巧妙设计分离加载器与载荷原始的RDI需要修改DLL源码加入ReflectiveLoader函数并编译。sRDI做了一个非常聪明的解耦通用Shellcode加载器sRDI项目中的ShellcodeRDI组件编译后生成一段位置无关的ShellcodeShellcodeRDI_x64.bin等。这段Shellcode就是一个通用的反射加载器。DLL作为纯数据待注入的DLL不需要任何特殊编译它就是一个普通的、合法的PE文件。拼接与引导转换过程即ConvertToShellcode函数的核心工作是将三部分在内存中拼接Shellcode加载器通用的反射加载逻辑。目标DLL原始的PE字节流。加载参数一个数据结构包含DLL在拼接块中的偏移量、导出函数哈希等信息。引导逻辑在拼接后的Shellcode块开头是一小段引导代码。它的唯一职责是计算DLL数据在内存中的当前位置然后跳转到通用Shellcode加载器的入口并将DLL信息作为参数传递给它。这样设计的优势极其明显无需源码任何DLL无论是开源项目、第三方库还是自己用msfvenom生成的都可以直接转换。加载器统一且可优化通用的加载器可以集中精力做优化、对抗检测如抹除PE头而不用每个DLL都带一份。输出为纯Shellcode最终产品是一段完整的、自包含的Shellcode兼容性极强可以无缝集成到任何支持Shellcode注入的框架中如Cobalt Strike、Metasploit、自定义的加载器等。2.3 批量转换脚本的核心任务理解了上述原理我们的批量脚本任务就清晰了遍历与筛选找到指定目录下所有需要转换的DLL文件。架构识别自动判断DLL是32位x86还是64位x64因为需要匹配对应架构的sRDI加载器Shellcode。调用核心转换函数对于每个DLL调用sRDI提供的ConvertToShellcode(dll_bytes)函数传入DLL的二进制内容。输出管理将生成的Shellcode保存为文件并合理命名如原文件名.bin或原文件名_x64.bin同时可能需要生成一个记录转换结果的清单。错误处理与日志处理转换过程中可能出现的错误如文件损坏、非PE文件、架构不支持等并提供清晰的日志输出。3. 环境准备与工具链搭建工欲善其事必先利其器。在开始编写脚本前我们需要一个可工作的sRDI Python环境。3.1 获取sRDI项目首先从GitHub克隆官方仓库git clone https://github.com/monoxgas/sRDI.git cd sRDI注意sRDI项目包含C#和Native C组件但对于我们使用Python脚本进行转换来说最关键的是Python目录下的文件。我们主要使用ShellcodeRDI.py这个模块。3.2 Python环境配置项目建议使用Python 3。确保你的环境已安装。关键是要安装pefile这个Python库sRDI的脚本依赖它来解析PE文件。pip install pefile检查Python/ShellcodeRDI.py文件确保它存在。这个文件包含了我们需要的核心转换函数ConvertToShellcode。3.3 理解项目结构对于批量脚本编写我们需要关注以下文件Python/ShellcodeRDI.py: 核心模块包含转换逻辑。Python/ConvertToShellcode.py: 官方提供的单文件转换示例脚本。这是我们学习和参考的蓝本。bin/目录: 这里存放着编译好的、不同架构的通用加载器ShellcodeShellcodeRDI_x64.bin,ShellcodeRDI_x86.bin。ShellcodeRDI.py模块会自动加载它们。3.4 验证基础功能在写批量脚本之前先用官方脚本测试一个DLL确保环境一切正常。# 切换到Python目录 cd Python # 使用项目自带的测试DLL进行转换 python ConvertToShellcode.py ../bin/TestDLL_x64.dll如果成功会生成一个TestDLL_x64.bin文件。你可以用hexdump或xxd简单查看一下它的开头应该是一段机器码Shellcode后面跟着DLL的数据。4. 批量转换脚本的完整实现现在我们开始构建自己的批量转换工具。我将分步拆解并解释每个环节的考量。4.1 脚本框架与参数解析一个健壮的脚本应该支持命令行参数方便集成到自动化流程中。我们使用Python的argparse库。#!/usr/bin/env python3 sRDI Batch DLL Converter - 批量将DLL转换为sRDI Shellcode Author: Your Name import argparse import os import sys import logging from pathlib import Path # 将sRDI的Python目录加入模块搜索路径确保能导入ShellcodeRDI # 假设脚本放在sRDI项目的根目录或者同级目录下有一个srdi文件夹 sys.path.insert(0, os.path.join(os.path.dirname(__file__), Python)) try: from ShellcodeRDI import * except ImportError as e: logging.error(f无法导入ShellcodeRDI模块: {e}) logging.error(请确保脚本在sRDI项目目录下运行或已正确设置Python路径。) sys.exit(1) def main(): parser argparse.ArgumentParser(description批量转换DLL文件为sRDI Shellcode) parser.add_argument(input, help输入路径可以是一个DLL文件或一个包含DLL的目录) parser.add_argument(-o, --output-dir, default./output, help输出目录转换后的.bin文件将保存在此 (默认: ./output)) parser.add_argument(-r, --recursive, actionstore_true, help递归搜索子目录中的DLL文件) parser.add_argument(-f, --filter, default*.dll, help文件过滤模式 (默认: *.dll)) parser.add_argument(-p, --prefix, default, help为输出的文件名添加前缀) parser.add_argument(-s, --suffix, default, help为输出的文件名添加后缀) parser.add_argument(-v, --verbose, actionstore_true, help输出详细信息) parser.add_argument(-l, --log, help将日志保存到指定文件) args parser.parse_args() # 配置日志 log_level logging.DEBUG if args.verbose else logging.INFO logging.basicConfig( levellog_level, format%(asctime)s - %(levelname)s - %(message)s, handlers[] ) if args.log: logging.getLogger().addHandler(logging.FileHandler(args.log)) # 始终输出到控制台 console_handler logging.StreamHandler() console_handler.setLevel(log_level) logging.getLogger().addHandler(console_handler) # 创建输出目录 output_dir Path(args.output_dir) output_dir.mkdir(parentsTrue, exist_okTrue) # 收集要转换的文件列表 input_path Path(args.input) dll_files [] if input_path.is_file(): if input_path.suffix.lower() .dll: dll_files.append(input_path) else: logging.error(f输入文件 {input_path} 不是DLL文件。) sys.exit(1) elif input_path.is_dir(): pattern **/ args.filter if args.recursive else args.filter dll_files list(input_path.glob(pattern)) if not dll_files: logging.warning(f在目录 {input_path} 中未找到匹配 {args.filter} 的DLL文件。) sys.exit(0) else: logging.error(f输入路径 {input_path} 不存在。) sys.exit(1) logging.info(f找到 {len(dll_files)} 个待转换的DLL文件。) # 执行批量转换 success_count batch_convert_srdi(dll_files, output_dir, args.prefix, args.suffix) logging.info(f批量转换完成。成功: {success_count}, 失败: {len(dll_files) - success_count}) if __name__ __main__: main()关键点解析路径处理使用pathlib.Path它是现代Python处理文件路径的推荐方式比os.path更直观、更面向对象。模块导入通过修改sys.path来确保能正确导入sRDI项目内的ShellcodeRDI模块。这是脚本能否运行的关键。灵活的输入支持单个文件和整个目录的输入并通过--recursive和--filter参数提供强大的文件筛选能力。日志系统使用Python内置的logging模块可以方便地控制输出详细程度并支持同时输出到控制台和文件便于调试和审计。4.2 核心转换函数batch_convert_srdi这是脚本的心脏负责遍历文件列表并执行转换。def batch_convert_srdi(dll_path_list, output_dir, prefix, suffix): 批量转换DLL为sRDI Shellcode。 Args: dll_path_list: Path对象的列表每个代表一个DLL文件。 output_dir: Path对象输出目录。 prefix: 输出文件名的前缀。 suffix: 输出文件名的后缀在扩展名之前。 Returns: 成功转换的文件数量。 success_count 0 failed_files [] for dll_path in dll_path_list: dll_name dll_path.stem # 不带扩展名的文件名 dll_arch None logging.info(f正在处理: {dll_path}) try: # 1. 读取DLL文件 with open(dll_path, rb) as f: dll_buffer f.read() if not dll_buffer: raise ValueError(DLL文件为空) # 2. 调用sRDI核心函数进行转换 # ConvertToShellcode 函数会自动识别DLL架构并选用正确的加载器 shellcode ConvertToShellcode(dll_buffer) # 3. 构造输出文件名和路径 # 可以添加架构信息到文件名便于区分 # 我们先尝试获取架构信息可选但很有用 try: # 使用pefile快速解析以获取架构注意这里仅用于命名sRDI内部已做判断 import pefile pe pefile.PE(datadll_buffer, fast_loadTrue) if pe.FILE_HEADER.Machine pefile.MACHINE_TYPE[IMAGE_FILE_MACHINE_I386]: dll_arch x86 elif pe.FILE_HEADER.Machine pefile.MACHINE_TYPE[IMAGE_FILE_MACHINE_AMD64]: dll_arch x64 else: dll_arch unknown pe.close() except Exception as pe_err: logging.debug(f无法解析 {dll_name} 的PE头以获取架构: {pe_err}) dll_arch unknown arch_suffix f_{dll_arch} if dll_arch ! unknown else output_filename f{prefix}{dll_name}{suffix}{arch_suffix}.bin output_path output_dir / output_filename # 4. 保存Shellcode到文件 with open(output_path, wb) as f: f.write(shellcode) file_size_kb len(shellcode) / 1024.0 logging.info(f - 成功转换: {output_path} ({file_size_kb:.2f} KB)) success_count 1 except Exception as e: logging.error(f - 转换失败: {dll_path}. 错误: {e}) failed_files.append((dll_path, str(e))) continue # 打印失败摘要 if failed_files: logging.warning(\n 转换失败的文件摘要 ) for f_path, err in failed_files: logging.warning(f {f_path}: {err}) return success_count关键点与避坑指南二进制读取必须使用rb模式打开DLL文件确保读取的是原始字节避免编码问题。错误处理每个文件的转换都用try...except包裹防止一个文件的失败导致整个批处理任务中止。记录所有失败的文件和原因便于后续排查。架构信息虽然sRDI的ConvertToShellcode函数内部会自动处理架构但我们在输出文件名里加上架构信息如_x64是非常好的实践。这能让你一眼看出哪个Shellcode对应哪个架构的DLL避免在注入时搞混将x64的Shellcode注入到x32进程会导致崩溃。使用pefile我们临时导入pefile来快速读取PE头的Machine字段以判断架构。注意使用fast_loadTrue以提高速度并且要在try块内因为不是所有二进制文件都是有效的PE。文件大小记录输出文件的大小有助于你了解转换后的Shellcode体积对于某些有空间限制的注入场景如通过特定漏洞利用很有参考价值。4.3 增强功能校验与报告一个专业的脚本还应该提供一些额外的信息。def batch_convert_srdi(dll_path_list, output_dir, prefix, suffix): # ... [前面的代码保持不变] ... success_count 0 failed_files [] conversion_report [] # 用于生成报告 for dll_path in dll_path_list: # ... [读取文件调用ConvertToShellcode] ... # 在成功转换后收集报告信息 report_info { input: str(dll_path), output: str(output_path), input_size: len(dll_buffer), output_size: len(shellcode), architecture: dll_arch, status: SUCCESS } conversion_report.append(report_info) except Exception as e: logging.error(f - 转换失败: {dll_path}. 错误: {e}) failed_files.append((dll_path, str(e))) conversion_report.append({ input: str(dll_path), output: N/A, error: str(e), status: FAILED }) continue # ... [打印失败摘要] ... # 生成简易的CSV格式报告 report_path output_dir / conversion_report.csv try: with open(report_path, w, encodingutf-8) as f: f.write(Input File,Output File,Input Size (bytes),Output Size (bytes),Architecture,Status,Error\n) for item in conversion_report: if item[status] SUCCESS: f.write(f{item[input]},{item[output]},{item[input_size]},{item[output_size]},{item[architecture]},SUCCESS,\n) else: f.write(f{item[input]},N/A,N/A,N/A,N/A,FAILED,\{item.get(error, )}\\n) logging.info(f详细转换报告已保存至: {report_path}) except Exception as report_err: logging.error(f生成报告文件失败: {report_err}) return success_count这个报告文件对于批量处理后的结果分析非常有用你可以快速知道哪些成功了输入输出体积变化如何便于管理和归档。5. 实战应用与进阶技巧脚本写好了我们来聊聊怎么用它以及一些更深层次的技巧。5.1 基础使用示例假设你的DLL文件存放在C:\payloads\dlls目录下包含mimikatz_x64.dll,seatbelt_x86.dll等。# 切换到脚本所在目录sRDI项目根目录 python batch_srdi.py C:\payloads\dlls -o C:\payloads\shellcodes -r -v-r: 递归搜索子目录。-v: 输出详细信息。转换后的.bin文件将保存在C:\payloads\shellcodes目录并附带架构后缀。5.2 集成到武器化框架批量转换的产出物.bin文件可以很方便地集成到其他工具中。例如在Cobalt Strike的Aggressor Script中你可以编写一个函数来读取这些.bin文件并用于注入。// 这是一个Aggressor Script的简化示例 sub srdi_inject { local($bin_path $pid $arch); $bin_path path/to/your/converted_payload_x64.bin; $pid $1; // 目标进程ID $arch x64; // 必须与bin文件架构匹配 $data readb($bin_path); // 读取shellcode $handle openf(SCRIPT); writeb($handle, $data); closef($handle); // 使用Cobalt Strike内置的注入命令这里需要根据实际调整 binject($pid, $data, $arch); }5.3 进阶技巧内存中直接转换与传递有时你可能不想生成中间文件而是希望在Python脚本中直接转换并传递给下一个阶段如Socket发送、进程注入等。我们的批量脚本可以很容易地修改为返回一个包含文件名和Shellcode字节流的字典列表。def batch_convert_to_memory(dll_path_list): 批量转换DLL为Shellcode但只保存在内存中返回。 适用于管道式或API化的集成场景。 results [] for dll_path in dll_path_list: try: with open(dll_path, rb) as f: dll_buffer f.read() shellcode ConvertToShellcode(dll_buffer) results.append({ name: dll_path.name, path: str(dll_path), shellcode: shellcode, # 注意这里保存的是完整的bytes对象 size: len(shellcode) }) except Exception as e: logging.error(f转换 {dll_path} 失败: {e}) results.append({name: dll_path.name, error: str(e)}) return results5.4 注意事项与排错架构匹配是生命线这是最容易出错的地方。x64的Shellcode只能注入到x64进程x86的Shellcode只能注入到x86进程。用我们的脚本生成带架构后缀的文件名并在注入前仔细核对目标进程的架构可以用Process Explorer或tasklist /fo csv /v查看。注入错架构会导致目标进程立刻崩溃。DLL的依赖性sRDI只负责将DLL本身加载到内存。如果这个DLL依赖其他系统DLL如user32.dll,gdi32.dll反射加载器会尝试解析这些导入。但是如果DLL依赖非系统路径的、特定的第三方DLL这些DLL必须已经存在于目标进程的内存空间或系统的DLL搜索路径中否则加载会失败。对于武器化DLL应尽量静态编译或仅依赖核心系统库。杀软与EDR的对抗虽然sRDI提高了隐蔽性但现代EDR终端检测与响应会监控进程内存中可疑的PE映像、未签名的代码段执行等行为。单纯的sRDI转换可能不足以绕过高级防护。通常需要结合Shellcode加密/混淆在转换后对生成的.bin文件进行加密或编码。加载器Loader的对抗使用更隐蔽的注入技术如进程镂空、APC注入、线程劫持、进行直接系统调用Syscall以绕过用户态钩子、抹除PE头等。DLL本身的免杀处理对原始DLL进行代码混淆、加壳或使用定制化的编译选项。调试与排查如果转换后的Shellcode注入后没有反应或导致崩溃首先检查架构重复第1点。使用测试DLL先用sRDI项目自带的TestDLL_x64.dll进行转换和注入测试。它能弹出一个简单的消息框。如果测试DLL工作说明你的sRDI环境和注入器是好的问题出在你的DLL上。查看DLL的导出函数使用dumpbin /exports your.dll命令确保DLL有导出函数特别是如果你打算通过sRDI调用特定导出函数。有些DLL可能只是运行时库没有显式导出。在DllMain中简化逻辑你的DLL可能在DllMain中执行了复杂的、依赖特定环境的初始化代码这在反射加载时可能失败。尝试注释掉DllMain中的所有代码只留一个return TRUE;看是否能成功加载。6. 脚本优化与扩展思路基础的批量转换已经完成但我们可以让它更强大。6.1 并行处理加速当需要转换的DLL数量非常多时可以使用Python的concurrent.futures模块进行并行处理充分利用多核CPU。from concurrent.futures import ThreadPoolExecutor, as_completed def convert_single_dll(dll_path, output_dir, prefix, suffix): 包装单个DLL的转换逻辑供线程池调用 # 这里包含之前try块内的所有转换和保存逻辑 # 需要将日志记录改为线程安全的方式或直接返回结果 # ... return (success, dll_path, output_path, error_msg) def batch_convert_parallel(dll_path_list, output_dir, prefix, suffix, max_workers4): success_count 0 with ThreadPoolExecutor(max_workersmax_workers) as executor: future_to_dll {executor.submit(convert_single_dll, dll, output_dir, prefix, suffix): dll for dll in dll_path_list} for future in as_completed(future_to_dll): dll future_to_dll[future] try: success, dll_path, output_path, error future.result() if success: success_count 1 logging.info(f成功: {dll_path} - {output_path}) else: logging.error(f失败: {dll_path} - {error}) except Exception as exc: logging.error(f{dll} 生成异常: {exc}) return success_count注意并行I/O读写文件可能受磁盘速度限制提升有限。但对于CPU密集的PE解析和Shellcode生成部分并行化会有明显收益。同时要注意线程安全避免多个线程同时写同一个日志文件导致内容混乱。6.2 集成Shellcode编码/加密可以在转换流程中直接加入编码环节实现“转换即加密”。def simple_xor_encode(data, key0xAA): 一个简单的XOR编码示例仅用于演示强度很低 return bytes([b ^ key for b in data]) def batch_convert_and_encode(dll_path_list, output_dir, encode_funcNone, encode_suffix_enc): for dll_path in dll_path_list: # ... 读取DLL并转换得到shellcode ... if encode_func: shellcode encode_func(shellcode) # 对shellcode进行编码 output_filename f{dll_path.stem}{encode_suffix}.bin # ... 保存 ...在实际红队操作中你会使用更复杂的加密算法如AES、RC4和动态密钥。6.3 生成C语言数组或.NET字节数组为了方便将Shellcode嵌入到其他语言的源码中如C/C加载器、C#的byte[]可以增加一个输出格式选项。def shellcode_to_c_array(shellcode_bytes, variable_nameshellcode): 将Shellcode字节流转换为C语言数组定义 hex_str , .join(f0x{b:02x} for b in shellcode_bytes) c_code funsigned char {variable_name}[] {{\n {hex_str}\n}};\n c_code funsigned int {variable_name}_len {len(shellcode_bytes)}; return c_code def shellcode_to_csharp_array(shellcode_bytes, variable_nameshellcode): 将Shellcode字节流转换为C#字节数组定义 hex_str , .join(f0x{b:02x} for b in shellcode_bytes) csharp_code fbyte[] {variable_name} new byte[] {{\n {hex_str}\n}}; return csharp_code然后在批量转换循环中除了保存.bin文件也可以选择生成对应的.c或.cs代码片段文件。通过以上步骤我们不仅实现了一个功能完整的sRDI批量转换脚本还深入理解了其背后的原理、实战中的注意事项以及可能的扩展方向。这个脚本能显著提升你在涉及反射式DLL注入场景下的工作效率让你从重复劳动中解放出来更专注于战术和技术的创新。记住工具是死的人是活的理解原理才能灵活运用和排错。