Finding Antivirus Detection Virus Signatures (nuitka package)

最近在遇到 Nuitka 打包 會被eset 防毒軟體檢測的方式大概試了下面幾個方法 第一加殼,透過把 binary 壓縮 再套在另外一層的 c shell 來逃避 防毒軟體在 靜態分析掃描到相關特徵碼 第一種方法的話大部分防毒都會檢測到UPX 很早期的加殼,或著叫 chargpt 也可以產生一下簡單 aes 加殼 脫殼的方法 反正就是 將一些section 加密起來,因為是加密的,當 shell 啟動後會 decode 相關的 section 加載正確的 binary data 這些東西當然就不存在於 病毒的資料庫裡面所以當然找不到。 第二移除特徵碼,之前對這個蠻感興趣,剛好有實際案例可以試試看,看了一些查找病毒特徵碼的tool,發現大概可以這樣來實現,大概就是保留原本的 exe pe 結構,然後分別對各個 section 去做分割的動作,每次分割出來的 分塊的section 都有可能涵蓋到一小片段特徵碼被防毒軟體偵測到,也有複合的特徵碼片段的情況,所以那些tool 做的事情就是 拆個片段的特徵碼,然後再掃描看防毒砍掉哪些片段,逐一縮小範圍 目前猜想是這樣, 所以第一個步驟是尋找有可能產生這些片段section 的排列組合至少存在一組以上 那我要反向查找特徵碼最小集合是,例如我要在local 不斷的呼叫 ESET 裡面的ecls.exe(掃描分割出來的檔案來檢測那些塊組合會導致被檢測到病毒) 比如說掃描一個 nuitka 的 編譯pyhton pe結構 大概會得出下列幾個section, 首先你要用 nuitka 編譯一個 python 並印出hello,這時候不管內容是什麼你會被 Eset 監測為

code
Log
 a variant of Python/Packed.Nuitka.Y suspicious application - cleaned by deleting [1]

下面是窮舉nuitka編譯出來的 binary 所有section 並找到 最小集合的 section 會被eset 檢測到是 Python/Packed.Nuitka 組合得程式碼片段

code
import os
import pefile
import subprocess
from itertools import combinations
from concurrent.futures import ThreadPoolExecutor, as_completed

def scan_with_eset(file_path, eset_path="C:\\Program Files\\ESET\\ESET Security\\ecls.exe"):
    """
    使用 ESET 的命令行工具扫描文件
    """
    try:
        command = [eset_path, file_path]
        result = subprocess.run(command, capture_output=True, text=True)
        if  result.stdout.find("a variant of")>=0:
            return True
        return False
    except FileNotFoundError:
        print(f"ESET Command Line Scanner not found at {eset_path}.")
        return False

def generate_section_combination(pe, binary_data, section_names, fill_byte=0x00):
    """
    根据目标 sections,生成包含它们组合的新二进制文件
    """
    new_binary = bytearray(binary_data)

    for section in pe.sections:
        name = section.Name.decode().strip('\x00')
        start = section.PointerToRawData
        size = section.SizeOfRawData

        # 填充非目标 Sections
        if name not in section_names:
            new_binary[start:start + size] = bytes([fill_byte] * size)

    return new_binary

def scan_section_combinations(pe, binary_data, output_dir, eset_path, max_combination_size=3, max_workers=4):
    """
    扫描单独 sections 和它们的组合
    """
    section_names = [section.Name.decode().strip('\x00') for section in pe.sections]
    print(f"Available sections: {section_names}")

    trigger_combinations = []

    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = {}
        # 扫描所有单独的 sections 和它们的组合
        for size in range(1, max_combination_size + 1):
            for combination in combinations(section_names, size):
                temp_file = os.path.join(output_dir, f"combination_{'_'.join(combination)}.bin")
                modified_binary = generate_section_combination(pe, binary_data, combination)

                # 保存临时文件
                with open(temp_file, "wb") as f:
                    f.write(modified_binary)

                # 提交扫描任务并存储 Future 和组合映射
                futures[executor.submit(scan_with_eset, temp_file, eset_path)] = combination

                print(f"Scanning combination: {combination}")

        # 收集结果
        for future in as_completed(futures):
            combination = futures[future]
            try:
                if future.result():
                    print(f"Threat detected in combination: {combination}")
                    trigger_combinations.append(combination)
            except Exception as e:
                print(f"Error scanning combination {combination}: {e}")

    return trigger_combinations

def main(input_file, output_dir, eset_path, max_workers=4):
    """
    主函数
    """
    # 读取二进制文件
    with open(input_file, "rb") as f:
        binary_data = f.read()

    # 初始化 PE 文件解析器
    pe = pefile.PE(data=binary_data)

    # 初始化输出目录
    os.makedirs(output_dir, exist_ok=True)

    # 扫描单独 sections 和它们的组合
    print("Starting section and combination scanning...")
    trigger_combinations = scan_section_combinations(pe, binary_data, output_dir, eset_path, max_combination_size=4, max_workers=max_workers)

    # 输出结果
    if trigger_combinations:
        print("\n=== Threat Combinations Found ===")
        for combination in trigger_combinations:
            print(f"Combination: {combination}")
    else:
        print("No threats detected in any combination.")

if __name__ == "__main__":
    # 输入文件和输出目录
    input_file = "test.exe"  # 替换为你的实际文件路径
    output_dir = "./output"
    eset_path = "C:\\Program Files\\ESET\\ESET Security\\ecls.exe"
    max_workers = 4  # 控制线程池最大线程数

    # 启动分析
    main(input_file, output_dir, eset_path, max_workers)

output

image

code
Starting section and combination scanning...
Available sections: ['.text', '.data', '.rdata', '.eh_fram', '.pdata', '.xdata', '.bss', '.idata', '.CRT', '.tls', '.rsrc', '.reloc']
Scanning combination: ('.text',)
Scanning combination: ('.data',)
Scanning combination: ('.rdata',)
Scanning combination: ('.eh_fram',)
Scanning combination: ('.pdata',)
Scanning combination: ('.xdata',)
Scanning combination: ('.bss',)
Scanning combination: ('.idata',)
Scanning combination: ('.CRT',)
Scanning combination: ('.tls',)
Scanning combination: ('.rsrc',)
Scanning combination: ('.reloc',)
Scanning combination: ('.text', '.data')
Scanning combination: ('.text', '.rdata')
Scanning combination: ('.text', '.eh_fram')
Scanning combination: ('.text', '.pdata')
Scanning combination: ('.text', '.xdata')
Scanning combination: ('.text', '.bss')
Scanning combination: ('.text', '.idata')
Scanning combination: ('.text', '.CRT')
Scanning combination: ('.text', '.tls')
Scanning combination: ('.text', '.rsrc')
Scanning combination: ('.text', '.reloc')
Scanning combination: ('.data', '.rdata')
Scanning combination: ('.data', '.eh_fram')
Scanning combination: ('.data', '.pdata')
Scanning combination: ('.data', '.xdata')
Scanning combination: ('.data', '.bss')

這邊可以看到最終的得出這些section,可以看到這些組合裡面都有 .rdata 也就是我只要破壞其中一個 section 就可讓防毒軟體 識別不出來

code
=== Threat Combinations Found ===
Combination: ('.text', '.rdata', '.rsrc')
Combination: ('.text', '.data', '.rdata', '.rsrc')
Combination: ('.text', '.rdata', '.eh_fram', '.rsrc')
Combination: ('.text', '.rdata', '.pdata', '.rsrc')
Combination: ('.text', '.rdata', '.xdata', '.rsrc')
Combination: ('.text', '.rdata', '.bss', '.rsrc')
Combination: ('.text', '.rdata', '.idata', '.CRT')
Combination: ('.text', '.rdata', '.idata', '.rsrc')
Combination: ('.text', '.rdata', '.CRT', '.rsrc')
Combination: ('.text', '.rdata', '.tls', '.rsrc')
Combination: ('.text', '.rdata', '.rsrc', '.reloc')

所以透過這個方法,例如我接下來只要隨機對 .rdata填充 0x0 不斷的破壞數據在恢復,找到最小破壞的 byte 區間在對應到程式碼,假設沒影響就反向恢復數據,直到找到 eset 這個防毒軟體到底是針對哪一些 特徵來檢測到底是不是病毒,

code
import os
import subprocess
import pefile

def scan_with_eset(file_path, eset_path="C:\\Program Files\\ESET\\ESET Security\\ecls.exe"):
    """
    使用 ESET 的命令行工具扫描文件
    """
    try:
        command = [eset_path, file_path]
        result = subprocess.run(command, capture_output=True, text=True)
        if "a variant of Python/Packed.Nuitka.Y suspicious application" in result.stdout:
            return True
        return False
    except FileNotFoundError:
        print(f"ESET Command Line Scanner not found at {eset_path}.")
        return False

def mutate_rdata_exact(pe, binary_data, section_name, start_offset, end_offset, fill_byte=0x00):
    """
    精确填充 .rdata 的指定范围内容为 fill_byte,并返回修改的差异
    """
    differences = []
    modified_binary = bytearray(binary_data)
    for section in pe.sections:
        name = section.Name.decode().strip('\x00')
        if name == section_name:
            start = section.PointerToRawData + start_offset
            end = section.PointerToRawData + end_offset

            # 修改指定范围并记录原始值
            for offset in range(start, end + 1):
                original_value = modified_binary[offset]
                if original_value != fill_byte:  # 仅修改非 fill_byte 的字节
                    modified_binary[offset] = fill_byte
                    differences.append((offset, original_value, fill_byte))
            return modified_binary, differences
    return binary_data, differences

def binary_search_rdata(pe, binary_data, output_dir, eset_path, section_name, start, end, fill_byte=0x00):
    """
    使用二分法在 .rdata 中找到最小影响范围
    """
    if start > end:
        return []

    # 当范围缩小到单个字节
    if start == end:
        print(f"Testing single byte at Offset: {start}")
        temp_binary, diff = mutate_rdata_exact(pe, binary_data, section_name, start, end, fill_byte)
        temp_file = os.path.join(output_dir, f"single_byte_{start}.bin")
        with open(temp_file, "wb") as f:
            f.write(temp_binary)

        if not scan_with_eset(temp_file, eset_path):
            print(f"Threat neutralized by modifying single byte at Offset: {start}")
            return diff  # 返回当前唯一的差异作为结果
        return []

    # 中间索引
    mid = (start + end) // 2
    print(f"Testing range {start}-{mid}")
    left_binary, left_diff = mutate_rdata_exact(pe, binary_data, section_name, start, mid, fill_byte)
    left_file = os.path.join(output_dir, f"rdata_left_{start}_{mid}.bin")
    with open(left_file, "wb") as f:
        f.write(left_binary)

    if not scan_with_eset(left_file, eset_path):
        print(f"Threat neutralized by modifying range {start}-{mid}")
        return binary_search_rdata(pe, binary_data, output_dir, eset_path, section_name, start, mid, fill_byte)

    print(f"Testing range {mid + 1}-{end}")
    right_binary, right_diff = mutate_rdata_exact(pe, binary_data, section_name, mid + 1, end, fill_byte)
    right_file = os.path.join(output_dir, f"rdata_right_{mid + 1}_{end}.bin")
    with open(right_file, "wb") as f:
        f.write(right_binary)

    if not scan_with_eset(right_file, eset_path):
        print(f"Threat neutralized by modifying range {mid + 1}-{end}")
        return binary_search_rdata(pe, binary_data, output_dir, eset_path, section_name, mid + 1, end, fill_byte)

    return left_diff + right_diff

def refine_to_byte_level(binary_data, effective_differences, output_dir, eset_path):
    """
    在已确定的范围内逐字节确认有效的修改字节
    """
    minimal_differences = []
    for offset, original, modified in effective_differences:
        temp_binary = bytearray(binary_data)
        temp_binary[offset] = modified

        temp_file = os.path.join(output_dir, f"test_byte_{offset}.bin")
        with open(temp_file, "wb") as f:
            f.write(temp_binary)

        if not scan_with_eset(temp_file, eset_path):
            print(f"Byte at offset {offset} is critical for neutralization.")
            minimal_differences.append((offset, original, modified))
        else:
            print(f"Byte at offset {offset} does not affect neutralization. Reverting.")

    return minimal_differences

def main(input_file, output_dir, eset_path):
    """
    主函数
    """
    # 读取二进制文件
    with open(input_file, "rb") as f:
        binary_data = f.read()

    # 初始化 PE 文件解析器
    pe = pefile.PE(data=binary_data)

    # 定义目标 Sections
    target_sections = ['.text', '.rdata', '.idata', '.CRT']

    # 初始化输出目录
    os.makedirs(output_dir, exist_ok=True)

    print("Checking if the base combination triggers a threat...")
    if not scan_with_eset(input_file, eset_path):
        print("Base combination does not trigger any threat. Exiting.")
        return

    print("Base combination triggers threat. Starting binary search...")

    for section in pe.sections:
        name = section.Name.decode().strip('\x00')
        if name in target_sections:
            print(f"Analyzing section: {name}")
            if name == ".rdata":
                start = 0
                size = section.SizeOfRawData

                # 使用二分法找到初步修改范围
                effective_differences = binary_search_rdata(pe, binary_data, output_dir, eset_path, name, start, size - 1)

                print("\n=== Refining to Byte Level ===")
                minimal_differences = refine_to_byte_level(binary_data, effective_differences, output_dir, eset_path)

                # 输出最终的最小修改结果
                log_file = os.path.join(output_dir, "minimal_modifications.log")
                with open(log_file, "w") as log:
                    log.write("=== Minimal Modifications ===\n")
                    for offset, original, modified in minimal_differences:
                        log.write(f"Offset: {offset}, Original: {hex(original)}, Modified: {hex(modified)}\n")

                print(f"Minimal modification log saved to {log_file}")

if __name__ == "__main__":
    # 输入文件和输出目录
    input_file = "test.exe"  # 替换为你的实际文件路径
    output_dir = "./output"
    eset_path = "C:\\Program Files\\ESET\\ESET Security\\ecls.exe"

    # 启动分析
    main(input_file, output_dir, eset_path)

image

也就是我最小異動這個 115496這個 offset 的 byte就可以導致 eset 判別不出來我這是什麼封裝的 記得條十進位 右上角 才可以匹配偏移量

image

修改為0x0 ,ctrl+s image

image

很好 你的程式就變木馬了xdd

cache.dir 應該對應到的是 nuitka 動態解壓縮的一些行為, 不過其實最開始我也有想過替換掉 mingw gcc 的 cflags -flto 或者一些方法不過還是沒辦法躲過eset檢測。