惡意軟體PE檔案重建指南

wyzsk發表於2020-08-19
作者: 左懶 · 2015/09/28 9:43

http://int0xcc.svbtle.com/a-guide-to-malware-binary-reconstruction

在分析惡意軟體或對惡意軟體進行脫殼的時候,我們經常會遇到重建PE檔案的需求。現在大多數自動化的PE重建工具雖然很棒,但並不能針對每一種情況,有時候需要我們自己手動重建PE檔案。在這篇部落格中,我們將介紹一些重建PE檔案的方法。

0x00 重建“stolen API code”的IAT表


“stolen API code”技術常被惡意軟體用於阻撓逆向人員脫殼之後重建IAT表,從而達到反脫殼的效果。具體修復“stolen API code”後IAT表的方法在後面會介紹到,我們先了解一下IAT在PE(Portable Executable)檔案裡面的具體實現。

0x01 IAT基礎知識


IAT(Import Address Table)是PE檔案裡面的一種結構,它包含了Windows loader載入動態連結庫和匯入API函式地址的資訊。檢視PE檔案的時候你應該注意到IMAGE_OPTIONAL_HEADER結構裡面的兩個指標:一個指向IMAGE_IMPORT_DESCRIPTOR,另一個指向匯入函式地址的陣列。

函式可以透過函式名稱或序號(API號)匯入。

FirstThunk成員指向匯入的API函式陣列(也稱為匯入地址表)。

上圖顯示了一個kernel32.dll的匯入函式GetProcAddress()的例子。

加殼程式通常會破壞IAT表的原始形式,由殼程式碼自己解決函式匯入而不是依靠Windows loader。因此脫殼後需要重建程式的IAT表,下面我們將使用Scylla v0.9.6b這個工具來重建IAT。

0x02 Stolen API code


一些加殼程式會使用“stolen code”技術防止逆向人員重建IAT表。“stolen code”重新把跳轉到API函式的指令在某個記憶體區域被重新模擬。所以使用掃描器掃描這些匯入函式的時候得到的是一些無效的API指標。

使用“stolen API code”技術的情況下,Scylla是無法自動重建IAT表的。我們需要寫一個Scylla外掛方便獲得正確的偏移然後重建IAT表。

0x03 編寫一個Scylla外掛


Scylla外掛的基本上是以DLL檔案形式注入到目標程式。為此它提供構建外掛所需的API介面,還有讓Scylla外掛方便嵌入到目標程式的介面。此外Scylla還提供了一個命名記憶體對映檔案用於Scylla外掛在目標程式中可以獲取一些資訊。

Scylla提供命名的檔案對映用於目標DLL指向特定的記憶體區域。

這個記憶體對映檔名為“ScyllaPluginExchange”。

透過ScyllaPluginExchange可以獲取到以下的資訊:

UNRESOLVED_IMPORT結構體透過SCYLLA_EXCHANGE.offsetUnresolvedImportsArray成員取得。

編寫外掛的第一步是利用Scylla提供的命名記憶體對映檔案拿到SCYLLA_EXCHANGE結構體的基地址:

#!c++
BOOL getMappedView()
{
    hMapFile = OpenFileMappingA(FILE_MAP_ALL_ACCESS, 0, FILE_MAPPING_NAME); //open named file mapping object    

    if (hMapFile == 0)
    {
        writeToLogFile("OpenFileMappingA failed\r\n");
        return FALSE;
    }    

    // lpViewOfFile就是SCYLLA_EXCHANGE結構體的基地址
    lpViewOfFile = MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, 0); //map the view with full access
    if (lpViewOfFile == 0)
    {
        CloseHandle(hMapFile); //close mapping handle
        hMapFile = 0;
        writeToLogFile("MapViewOfFile failed\r\n");
        return FALSE;
    }
    return TRUE;
}

UNRESOLVED_IMPORT包含了一個未解決的匯入函式列表。

#!c++
typedef struct _UNRESOLVED_IMPORT {       // Scylla Plugin exchange format
    DWORD_PTR ImportTableAddressPointer;  //in VA, address in IAT which points to an invalid api address
    DWORD_PTR InvalidApiAddress;          //in VA, invalid api address that needs to be resolved
} UNRESOLVED_IMPORT, *PUNRESOLVED_IMPORT;

ImportTableAddressPointer指標指向有效的API地址。InvalidApiAddress指標指向未決斷的API函式地址,在本文例子中,這是一塊動態分配的記憶體區域,那些被偷的程式碼(stolen code)就是在這裡進行模擬。

可以看到我們需要計算每個ImportTableAddressPointer到jmp指令有多少個位元組,然後取出JMP指令所跳轉的目標地址減去這個位元組數得到原來的API基地址:

#!c++
    while (unresolvedImport->ImportTableAddressPointer != 0) //last element is a nulled struct
    {
        insDelta = 0;
        invalidApiAddress = unresolvedImport->InvalidApiAddress;
        sprintf(buffer, "API Address = 0x%p\t IAT Address = 0x%p\n",  invalidApiAddress, unresolvedImport->ImportTableAddressPointer);    

        writeToLogFile(buffer);    

        IATbase = unresolvedImport->InvalidApiAddress;
        for (j = 0; j <  COUNT_INS; j++)
        {
            memset(&inst, 0x00, sizeof(INSTRUCTION));    

            i = get_instruction(&inst, IATbase, MODE_32);
            memset(buffer, 0x00, sizeof(buffer));
            get_instruction_string(&inst, FORMAT_ATT, 0, buffer, sizeof(buffer));
            if (strstr(buffer, "jmp"))
            {    

                printf(" JUMP Dest = %d" , ( (unsigned int)strtol(strstr(buffer, "jmp") + 4 + 2, NULL, 16)));
                *(DWORD*)(unresolvedImport->ImportTableAddressPointer) =  ( (unsigned int)strtol(strstr(buffer, "jmp") + 4 + 2, NULL, 16) + IATbase ) - insDelta;
                unresolvedImport->InvalidApiAddress = ( (unsigned int)strtol(strstr(buffer, "jmp") + 4 + 2, NULL, 16) + IATbase ) - insDelta;
                break;
            }
            else
            {
                insDelta = insDelta + i;
            }    

            IATbase = IATbase + i;
        }
        unresolvedImport++; //next pointer to struct
    }

這段程式碼將遍歷所有未決斷的匯入函式,並嘗試定位到正確的API地址。

JMP指令的目標地址減去insDelta就可以得到最終的InvalidApiAddress地址:

#!c++
unresolvedImport->InvalidApiAddress = ((unsigned int)strtol(strstr(buffer, “jmp”) + 4 + 2, NULL, 16) + IATbase) - insDelta;

在修復整個IAT表之後可能還會有一些無效的匯入地址,這些無效的匯入地址需要手動把它們刪除掉。

執行上面編寫的外掛之後還有一些殘留的無效地址,現在手動把它們刪掉:

0x04 匯出RunPE加殼後的程式


RunPE的工作原理是建立一個暫停狀態的dummy程式,然後挖空並注入惡意程式碼。這種技術常用於隱藏惡意程式碼。RunPE注入的程式碼可以匯出為一個有效的PE檔案。對於PE+檔案頭需要修改一下,因為64位架構的PE檔案一些欄位使用了QWORD型別。

Windows loader載入程式到記憶體後根據IMAGE_SECTION_HEADER.VirtualSize進行對齊。但是Section表的RawSize可能會小於VirtualSize,這個時候會作業系統需要填充這塊區域。

磁碟上的PE檔案是根據IMAGE_OPTIONAL_HEADER64.FileAlignment進行對齊的,因此從記憶體中匯出PE檔案之後還需要根據IMAGE_OPTIONAL_HEADER64.FileAlignment對PE檔案進行對齊。

用IDA載入匯出的PE檔案時提示無法找到正確的虛擬地址,因為它的PE檔案不對齊。

這個問題很好解決,我們先取出PE+檔案結構:

#!c++
IMAGE_DOS_HEADER DosHdr = {0};
IMAGE_FILE_HEADER FileHdr = {0};
IMAGE_OPTIONAL_HEADER64 OptHdr = {0};    

// Read All Structure as per offset    

fread(&DosHdr, sizeof(IMAGE_DOS_HEADER), 0x01, fp);    

fseek(fp, (unsigned int)DosHdr.e_lfanew + 4,SEEK_SET);    

fread(&FileHdr, sizeof(IMAGE_FILE_HEADER), 1, fp);
fread(&OptHdr, sizeof(IMAGE_OPTIONAL_HEADER64), 1, fp);

遍歷讀取所有section header:

#!c++
    while (iNumSec < FileHdr.NumberOfSections)
    {
        fread(&pTail[iNumSec], sizeof( IMAGE_SECTION_HEADER), 1, fp);
        iNumSec++;
    }

然後讀取第一個section的PointerToRawData:

#!c++
    i = ftell(fp);    

    buffer = (unsigned char*) malloc(sizeof(char) * pTail[0].PointerToRawData + 1);    

    fseek(fp, 0, SEEK_SET);    

    fread(buffer, pTail[0].PointerToRawData, 1, fp); // Read/Write Everything Till the beginning of first section    

    fwrite(buffer, pTail[0].PointerToRawData, 1, out);

最後,將資料以一個對齊的形式重寫:

#!c++
    while ( i < iNumSec)
    {    

        buffer = (unsigned char*) malloc(sizeof(char) * pTail[i].SizeOfRawData + 1);    

        fseek(fp, pTail[i].VirtualAddress, SEEK_SET);
        fread(buffer, pTail[i].SizeOfRawData, 1, fp);    

        fwrite(buffer, pTail[i].SizeOfRawData, 1, out);
        i++;
    }

全部修復完成之後就可以得到一個正確的PE檔案,下面是IDA載入那個修復好的PE檔案:

sample_plugin.rar

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章