首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
6
1
[原创]Enigma Virtual Box 分析(KCTF2025 第九题 智斗邪首)
发表于: 2025-9-18 13:11
11062
[原创]Enigma Virtual Box 分析(KCTF2025 第九题 智斗邪首)
易之生生
4
2025-9-18 13:11
11062
# dnSpy脱壳 首先运行htg_Crackme.exe后 附加到进程  然后调试->窗口->模块 列出内存加载的模块  最后保存模块  # 脱壳分析 查看脱壳后的文件,发现.NET.VirtualAddress和.NET.Size都不为0  说明Enigma在运行时修改了0x178和0x17c的值  可以先手动修改.NET.VirtualAddress值为0x400和.NET.Size为0x48,.NET的反编译代码就出来了  虽然可以看到.NET的代码,但是程序无法运行  如何恢复所有的被Enigma修改的PE头,可以在.NET.VirtualAddress的0x00000178处下内存的写断点; 程序断在了sub_1407E5560这个恢复PE头的函数; ## sub_1407E5560分析 首先是恢复PE NumberOfSections = 2 的代码 ``` mov rax, [rbp+var_28] sub word ptr [rax+6], 2 ; 0x0000000140000086 = 0x2 (PE NumberOfSections) mov rax, [rbp+var_28] movzx eax, word ptr [rax+6] sub eax, 1 test eax, eax jl short loc_1407E5630 ``` 然后是.text PointerToRawData对齐.text VirtualAddress 和 .rsrc PointerToRawData对齐.rsrc VirtualAddress ``` loc_1407E5600: add [rbp+var_10], 1 mov r9, [rbp+var_28] mov edx, [rbp+var_10] imul r8, rdx, 28h ; '(' mov rcx, [rbp+var_28] mov edx, [rbp+var_10] imul rdx, 28h ; '(' mov edx, [rcx+rdx+114h] ; 0x0000000140000194(.text VirtualAddress) ; 0x00000001400001BC(.rsrc VirtualAddress) mov [r9+r8+11Ch], edx ; 0X000000014000019C = 0X2000(.text PointerToRawData) ; 0X00000001400001C4 = 0X7A6000(.rsrc PointerToRawData) cmp eax, [rbp+var_10] jg short loc_1407E5600 ``` 最后将.NET.VirtualAddress设置为0x2000和.NET.Size设置为0x48 ,其他的值设置为0; ``` loc_1407E5630: mov r8, [rbp+var_28] mov rax, [rbp+var_28] movzx eax, word ptr [rax+6] imul rdx, rax, 28h ; '(' mov rcx, [rbp+var_28] mov rax, [rbp+var_28] movzx eax, word ptr [rax+6] imul rax, 28h ; '(' mov edx, [r8+rdx+0ECh] mov eax, [rcx+rax+0E8h] lea ecx, [edx+eax] mov rax, [rbp+var_28] mov edx, [rax+38h] call sub_1407E5510 ; 计算文件大小 mov rdx, [rbp+var_28] ; 0X0000000140000080 mov [rdx+50h], eax ; 0X00000001400000D0 = 0X7A8000(PE SizeOfImage) mov rdx, [rbp+var_28] mov rax, cs:qword_14084D050 mov eax, [rax+78h] mov [rdx+90h], eax ; 0X0000000140000110 = 0X0(PE Import.VirtualAddress) mov rdx, [rbp+var_28] mov rax, cs:qword_14084D050 mov eax, [rax+7Ch] mov [rdx+94h], eax ; 0X0000000140000114 = 0X0(PE Import.Size) mov rax, [rbp+var_28] mov dword ptr [rax+28h], 0 ; 0X00000001400000A8 = 0X0(PE AddressOfEntryPoint) mov rax, [rbp+var_28] mov dword ptr [rax+0A0h], 0 ; 0X0000000140000120 = 0X0(PE Exception.VirtualAddress) mov rax, [rbp+var_28] mov dword ptr [rax+0A4h], 0 ; 0X0000000140000124 = 0X0(PE Exception.Size) mov rdx, [rbp+var_28] mov rax, cs:qword_14084D050 mov eax, [rax+80h] mov [rdx+0B0h], eax ; 0X0000000140000130 = 0X0(PE Base Reloc.VirtualAddress) mov rdx, [rbp+var_28] mov rax, cs:qword_14084D050 mov eax, [rax+84h] mov [rdx+0B4h], eax ; 0X0000000140000134 = 0X0(PE Base Reloc.Size) mov rdx, [rbp+var_28] mov rax, cs:qword_14084D050 mov eax, [rax+88h] mov [rdx+0F8h], eax ; 0X0000000140000178 = 0X2000(PE .NET.VirtualAddress) mov rdx, [rbp+var_28] mov rax, cs:qword_14084D050 mov eax, [rax+8Ch] mov [rdx+0FCh], eax ; 0X000000014000017C = 0X48(PE .NET.Size) mov rax, [rbp+var_28] mov dword ptr [rax+0D0h], 0 ; 0x0000000140000150 = 0x0(PE TLS.VirtualAddress) mov rax, [rbp+var_28] mov dword ptr [rax+0D4h], 0 ; 0x0000000140000154 = 0x0(PE TLS.Size) mov rax, [rbp+var_28] mov edx, [rax+54h] ; dwSize lea r9, [rbp+flOldProtect] ; lpflOldProtect mov r8d, [rbp+flOldProtect] ; flNewProtect mov rcx, cs:lpAddress ; lpAddress call VirtualProtect ``` 通过分析可知,AddressOfEntryPoint Exception.VirtualAddress Exception.Size TLS.VirtualAddress TLS.Size 这五个的值为硬编码0;其余的值是通过cs:qword_14084D050+偏移地址动态获取的; 继续下内存的写断点,程序断在了loc_1407AD150处; ## loc_1407AD150分析 loc_1407AD150是一系列的读值赋值操作 ``` loc_1407AD150: mov rdx, cs:qword_14084D050 mov rax, [rbp+var_30] mov eax, [rax+68h] ; 0x1407A8068 mov [rdx+68h], eax mov rdx, cs:qword_14084D050 mov rax, [rbp+var_30] mov rax, [rax+70h] ; 0x1407A8070 mov [rdx+70h], rax mov rdx, cs:qword_14084D050 mov rax, [rbp+var_30] mov eax, [rax+78h] ; 0x1407A8078 mov [rdx+78h], eax mov rdx, cs:qword_14084D050 mov rax, [rbp+var_30] mov eax, [rax+7Ch] ; 0x1407A807C mov [rdx+7Ch], eax mov rdx, cs:qword_14084D050 mov rax, [rbp+var_30] mov eax, [rax+80h] ; 0x1407A8080 mov [rdx+80h], eax mov rdx, cs:qword_14084D050 mov rax, [rbp+var_30] mov eax, [rax+84h] ; 0x1407A8084 mov [rdx+84h], eax mov rdx, cs:qword_14084D050 mov rax, [rbp+var_30] mov eax, [rax+88h] ; 0x1407A8088 mov [rdx+88h], eax mov rdx, cs:qword_14084D050 mov rax, [rbp+var_30] mov eax, [rax+8Ch] ; 0x1407A808C mov [rdx+8Ch], eax mov rdx, cs:qword_14084D050 mov rax, [rbp+var_30] mov eax, [rax+0B0h] ; 0x1407A80B0 mov [rdx+90h], eax mov rdx, cs:qword_14084D050 mov rax, cs:qword_14084D050 cmp dword ptr [rax+88h], 0 setnz byte ptr [rdx+94h] mov rcx, cs:qword_14084CFC0 call sub_1407E5560 ``` 动态分析其值的来源发现其来自0x00000001407A8068开始的地址,位于enigma1的TlsCallbacks处;  # 映射表 * 修复字段 脱壳文件偏移 值来源/计算方式 描述 * NumberOfSections 0x86 固定值: 0x2 节区数量设置为2 * .text PointerToRawData 0x19C 原始文件偏移: 0x194 .text节区的文件偏移 * .rsrc PointerToRawData 0x1C4 原始文件偏移: 0x1BC .rsrc节区的文件偏移 * SizeOfImage 0xD0 计算值: (原始文件0x7A4690 + 原始文件0x7A4694) 按0x2000对齐 映像大小 * AddressOfEntryPoint 0xA8 固定值: 0x0 入口点地址 * Exception.VirtualAddress 0x120 固定值: 0x0 异常表虚拟地址 * Exception.Size 0x124 固定值: 0x0 异常表大小 * TLS.VirtualAddress 0x150 固定值: 0x0 TLS表虚拟地址 * TLS.Size 0x154 固定值: 0x0 TLS表大小 * Import.VirtualAddress 0x110 原始文件偏移: 0x7A4678 导入表虚拟地址 * Import.Size 0x114 原始文件偏移: 0x7A467C 导入表大小 * BaseReloc.VirtualAddress 0x130 原始文件偏移: 0x7A4680 重定位表虚拟地址 * BaseReloc.Size 0x134 原始文件偏移: 0x7A4684 重定位表大小 * .NET.VirtualAddress 0x178 原始文件偏移: 0x7A4688 .NET头虚拟地址 * .NET.Size 0x17C 原始文件偏移: 0x7A468C .NET头大小 # 脱壳代码 根据以上的分析可以编写静态脱壳代码: ``` import struct import os def mmap(): # 定义输入和输出文件名 input_file = "./htg_Crackme.exe" output_file = "./htg_Crackme.exeunpacked.exe" try: # 打开原始文件 with open(input_file, "rb") as f_in: # 创建输出文件 with open(output_file, "wb") as f_out: # 1. 读取前0x2000字节数据到新文件 first_part = f_in.read(0x2000) f_out.write(first_part) # 2. 定位到原始文件的0x400位置 f_in.seek(0x400) # 3. 计算需要读取的字节数 read_size = 0x7a4000 - 0x400 second_part = f_in.read(read_size) f_out.write(second_part) # 4. 在新文件的0x7A5C00处填充0x00 padding_size = 0x400 padding = b'\x00' * padding_size f_out.write(padding) # 5. 定位到原始文件的0x7a4000位置 f_in.seek(0x7a4000) read_size = 0x7a4600 - 0x7a4000 third_part = f_in.read(read_size) f_out.write(third_part) # # 6. 在新文件的0x7A6600处填充0x00 padding_size = 0xA00 padding = b'\x00' * padding_size f_out.write(padding) print("\n建立内存映射完成!") except FileNotFoundError: print(f"错误: 找不到文件 {input_file}") except Exception as e: print(f"发生错误: {str(e)}") def read_byte_at_offset(file_path, offset): """从文件指定偏移读取1字节值""" with open(file_path, 'rb') as f: f.seek(offset) data = f.read(1) return struct.unpack('<B', data)[0] if data else 0 def read_word_at_offset(file_path, offset): """从文件指定偏移读取2字节值""" with open(file_path, 'rb') as f: f.seek(offset) data = f.read(2) return struct.unpack('<H', data)[0] if len(data) == 2 else 0 def read_dword_at_offset(file_path, offset): """从文件指定偏移读取4字节值""" with open(file_path, 'rb') as f: f.seek(offset) data = f.read(4) return struct.unpack('<I', data)[0] if len(data) == 4 else 0 def write_byte_at_offset(file_path, offset, value): """向文件指定偏移写入1字节值""" with open(file_path, 'r+b') as f: f.seek(offset) f.write(struct.pack('<B', value)) def write_word_at_offset(file_path, offset, value): """向文件指定偏移写入2字节值""" with open(file_path, 'r+b') as f: f.seek(offset) f.write(struct.pack('<H', value)) def write_dword_at_offset(file_path, offset, value): """向文件指定偏移写入4字节值""" with open(file_path, 'r+b') as f: f.seek(offset) f.write(struct.pack('<I', value)) def fix_pe_header(original_file, unpacked_file): """ 修复脱壳后文件的PE头 从原始带壳文件读取值,写入脱壳后文件的PE头相应位置 """ print("开始修复PE头...") # PE NumberOfSections 修复为2 write_word_at_offset(unpacked_file, 0x86, 2) print(f"修复 NumberOfSections: 0x2 (写入脱壳文件 0x86)") # PE .text PointerToRawData 和 .text VirtualAddress 对齐 # PE .rsrc PointerToRawData 和 .rsrc VirtualAddress 对齐 field_mappings = [ (0x194, 0x19C, ".text PointerToRawData"), (0x1BC, 0x1C4, ".rsrc PointerToRawData") ] # 处理每个字段映射 for original_file_offset, pe_header_offset, description in field_mappings: # 从原始文件读取值 value = read_dword_at_offset(original_file, original_file_offset) # 写入脱壳后文件 write_dword_at_offset(unpacked_file, pe_header_offset, value) print(f"修复 {description}: 0x{value:08X} (从原始文件 0x{original_file_offset:08X} 写入脱壳文件 0x{pe_header_offset:08X})") struct_rva = 0x7A4600 size_image_value = read_dword_at_offset(original_file, struct_rva + 0x90) size_image_value_ = read_dword_at_offset(original_file, struct_rva + 0x94) size_image = size_image_value + size_image_value_ mod_value = size_image % 0x2000 size_image = size_image + 0x2000 - mod_value field_mappings = [ (size_image, 0xD0, "SizeOfImage"), (0x0, 0xA8, "AddressOfEntryPoint"), (0x0, 0x120, "Exception.VirtualAddress"), (0x0, 0x124, "Exception.Size"), (0x0, 0x150, "TLS.VirtualAddress"), (0x0, 0x154, "TLS.Size") ] # 处理每个字段映射 for value, pe_header_offset, description in field_mappings: # 写入脱壳后文件 write_dword_at_offset(unpacked_file, pe_header_offset, value) print(f"修复 {description}: 0x{value:08X} (写入脱壳文件 0x{pe_header_offset:08X})") struct_rva = 0x7A4600 # 根据实际文件偏移调整 # 定义需要修复的字段映射 # 格式: (原始文件结构体偏移, 脱壳文件PE头偏移, 描述) field_mappings = [ (0x78, 0x110, "Import.VirtualAddress"), (0x7C, 0x114, "Import.Size"), (0x80, 0x130, "BaseReloc.VirtualAddress"), (0x84, 0x134, "BaseReloc.Size"), (0x88, 0x178, ".NET.VirtualAddress"), (0x8C, 0x17C, ".NET.Size") ] # 处理每个字段映射 for struct_offset, pe_header_offset, description in field_mappings: # 计算原始文件中的绝对偏移 original_file_offset = struct_rva + struct_offset # 从原始文件读取值 value = read_dword_at_offset(original_file, original_file_offset) # 写入脱壳后文件 write_dword_at_offset(unpacked_file, pe_header_offset, value) print(f"修复 {description}: 0x{value:08X} (从原始文件 0x{original_file_offset:08X} 写入脱壳文件 0x{pe_header_offset:08X})") print("PE头修复完成!") def fix(): # 文件路径 original_file = "./dnSpy-net-win64/Crackme.exe" # 原始带壳文件 unpacked_file = "./dnSpy-net-win64/Crackme_unpacked.exe" # 脱壳后的文件 # 检查文件是否存在 if not os.path.exists(original_file): print(f"错误: 找不到原始文件 {original_file}") exit(1) if not os.path.exists(unpacked_file): print(f"错误: 找不到脱壳文件 {unpacked_file}") exit(1) # 修复PE头 fix_pe_header(original_file, unpacked_file) if __name__ == "__main__": mmap() fix() ``` 运行结果 ``` 建立内存映射完成! 开始修复PE头... 修复 NumberOfSections: 0x2 (写入脱壳文件 0x86) 修复 .text PointerToRawData: 0x00002000 (从原始文件 0x00000194 写入脱壳文件 0x0000019C) 修复 .rsrc PointerToRawData: 0x007A6000 (从原始文件 0x000001BC 写入脱壳文件 0x000001C4) 修复 SizeOfImage: 0x007A8000 (写入脱壳文件 0x000000D0) 修复 AddressOfEntryPoint: 0x00000000 (写入脱壳文件 0x000000A8) 修复 Exception.VirtualAddress: 0x00000000 (写入脱壳文件 0x00000120) 修复 Exception.Size: 0x00000000 (写入脱壳文件 0x00000124) 修复 TLS.VirtualAddress: 0x00000000 (写入脱壳文件 0x00000150) 修复 TLS.Size: 0x00000000 (写入脱壳文件 0x00000154) 修复 Import.VirtualAddress: 0x00000000 (从原始文件 0x007A4678 写入脱壳文件 0x00000110) 修复 Import.Size: 0x00000000 (从原始文件 0x007A467C 写入脱壳文件 0x00000114) 修复 BaseReloc.VirtualAddress: 0x00000000 (从原始文件 0x007A4680 写入脱壳文件 0x00000130) 修复 BaseReloc.Size: 0x00000000 (从原始文件 0x007A4684 写入脱壳文件 0x00000134) 修复 .NET.VirtualAddress: 0x00002000 (从原始文件 0x007A4688 写入脱壳文件 0x00000178) 修复 .NET.Size: 0x00000048 (从原始文件 0x007A468C 写入脱壳文件 0x0000017C) PE头修复完成! ```
登录后可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
最后于
2025-9-27 22:08 被kanxue编辑 ,原因:
收藏
・
6
点赞
・
1
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
wx_晨梦
期待更多优质内容的分享,论坛有你更精彩!
2026-7-16 08:49
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
1
)
QQ765746002
雪 币:
32
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
5
粉丝
0
关注
私信
QQ765746002
2
楼
谢谢分享
2025-9-29 13:40
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
易之生生
4
19
发帖
11
回帖
257
RANK
关注
私信
他的文章
[原创]挑战 Android 墓碑机制:揭秘某头部社交 APP 永生背后的保活术
9274
[原创]ANDROID 黑科技 : 保活机制深度逆向
35203
[原创] ANDROID 黑科技 : HIDE API 调用
7016
[原创]UNIDBG 环境模拟X音签名算法
20450
[原创]Tenda堆栈缓冲区溢出漏洞 (CVE-2024-2986)
20087
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部