1.5 編寫自定位ShellCode彈窗

2023-07-03 09:00:08

在筆者上一篇文章中簡單的介紹瞭如何運用組合語言編寫一段彈窗程式碼,雖然簡易ShellCode可以被正常執行,但卻存在很多問題,由於採用了硬編址的方式來呼叫相應API函數的,那麼就會存在一個很大的缺陷,如果作業系統的版本不統或系統重啟過,那麼基址將會發生變化,此時如果再次呼叫基址引數則會呼叫失敗,本章將解決這個棘手的問題,通過ShellCode動態定位的方式解決這個缺陷,並以此設計出真正符合規範的ShellCode程式碼片段。

自定位程式碼是一種常見的Shellcode技術,它使Shellcode能夠在任何系統上執行,而無需考慮系統記憶體佈局和程式碼地址等問題。以下是Shellcode自定位程式碼的流程:

  • 1.查詢Kernel32.dll基址並在其中尋找LoadLibraryA
  • 2.計算函數名hash摘要並通過hash摘要判斷函數
  • 3.解析Kernel32.dll匯出表
  • 4.最終動態呼叫系列函數

1.5.1 動態查詢Kernel32基址

首先我們需要通過組合的方式來實現動態定位Kernel32.dll中的基址,你或許會有個疑問? 為什麼要查詢Kernel32.dll的地址而不是User32.dll,這是因為我們最終的目的是呼叫MessageBoxA這個函數,而該函數位於 User32.dll這個動態連結庫裡,在某些程式中User32模組並不一定會被載入,而Kernel32則必然會被載入,為了能夠呼叫MessageBoxA函數,我們就需要呼叫LoadLibraryA函數來載入User32.dll這個模組,而LoadLibraryA恰巧又位於kernel32.dll中,因此我們只需要找到LoadLibraryA函數,即可實現載入任意的動態連結庫,並呼叫任意的函數的目的。

由於我們需要動態獲取LoadLibraryA()以及ExitProcess()這兩個函數的地址,而這兩個函數又是存在於kernel32.dll中的,因此這裡需要先找到kernel32.dll的基址,然後通過對其進行解析,從而查詢兩個函數的動態地址。動態的查詢Kernel32.dll的地址可總結為如下:

  • 1.首先通過段選擇子FS在記憶體中找到當前程序內的執行緒環境塊結構體指標TEB。
  • 2.執行緒環境塊偏移位置為fs:[0x30]的位置處存放著指向程序環境塊PEB結構的指標。
  • 3.程序環境塊PEB偏移為0x0c的地址處存放著指向PEB_LDR_DATA的結構體指標。
  • 4.而在PEB_LDR_DATA偏移0x1c的地址處存放著指向Ldr.InMemoryOrderModuleList模組初始化連結串列的頭指標。
  • 5.在初始化連結串列中存放的就是所有程序的模組資訊,通過將偏移值加0x08讀者即可獲取到kernel32.dll的基地址。

既然有了固定的查詢定位公式,接下我們就使用WinDBG偵錯程式來手工完成對Kernel32.dll地址的定位:

小提示:Windbg是Windows Debugger的縮寫,是一種微軟提供的免費偵錯程式工具,用於分析和偵錯Windows作業系統和應用程式。Windbg可以在不重啟系統的情況下,通過連線到正在執行的程序或者作業系統核心,獲取並分析程式的執行資訊、記憶體狀態、暫存器狀態、執行緒狀態、呼叫堆疊等資料,並可以使用符號檔案來解析程式中的符號名,從而幫助開發者定位問題和進行深入偵錯。

讀者可通過附件獲取到WinDBG程式,當用戶開啟WinDBG時讀者可通過Ctrl+E快捷鍵任意開啟一個可執行程式,接著我們開始尋找吧;

1.通過段選擇子FS在記憶體中找到當前的執行緒環境塊TEB。這裡可以利用本地偵錯,並輸入!teb指令,讀者可看到如下輸出:

小提示:TEB(Thread Environment Block)是Windows作業系統中的一個重要資料結構,每個程序都有一個對應的TEB。它主要用於儲存執行緒的環境資訊和狀態,包括執行緒區域性儲存(TLS)指標、例外處理鏈、堆疊資訊、Fiber資訊等。TEB由Windows核心自動建立和管理,可以通過系統呼叫和偵錯程式工具來存取和修改其內容。

如上執行緒環境塊偏移位置為0x30的地方存放著指向程序環境塊PEB的指標。結合上圖可見,當前PEB的地址為002bb000

小提示:PEB是Windows作業系統的程序環境塊(Process Environment Block)的縮寫。PEB是一個資料結構,其中包含了關於程序的許多資訊,例如程序的模組、堆、執行緒等等。PEB由作業系統核心在建立程序時分配和初始化,並且只有在程序執行期間才可用。

2.在程序環境塊中偏移位置為0x0c的地方存放著指向PEB_LDR_DATA結構體的指標,其中存放著已經被程序裝載的動態連結庫的資訊,如下圖所示;

3.接著PEB_LDR_DATA結構體偏移位置為0x1c的地方存放著指向模組初始化連結串列的頭指標InInitializationOrderModuleList,如下圖所示;

4.模組初始化連結串列InInitializationOrderModuleList中按順序存放著PE裝入執行時初始化模組的資訊,第一個連結串列節點是ntdll.dll,第二個連結串列結點就是kernel32.dll。我們可以先看看
InInitializationOrderModuleList中的內容:

上圖中的0x005a3ad8儲存的是第一個鏈節點的指標,解析一下這個結點,可發現如下地址:

上圖中的0x77200000ntdll.dll的模組基地址,而0x005a4390則是指向下一個模組的指標,我們繼續跟隨0x005a4390地址,則此處看到的標黃處是下一個模組kernel32.dll的基地址。

最後我們通過輸入!peb命令,輸出當前所有載入模組並驗證一下:

既然有了如上所述的方法,那麼讀者可以很容易的實現這段功能,為了便於讀者理解,筆者先提供一段使用C語言書寫的實現方式,如下程式碼所示;

#include <windows.h>
#include <stdio.h>

int main(int argc, char * argv[])
{
    DWORD *PEB = NULL;
    DWORD *Ldr = NULL;
    DWORD *Init = NULL;
    DWORD *Kernel32 = NULL;

    __asm
    {
        mov eax, fs:[0x30]
        mov PEB,eax
    }
    printf("得到PEB指標 = %x \n", PEB);

    Ldr = *(DWORD **)((unsigned char *)PEB + 0x0c);
    printf("得到LDR結構指標 = %x \n", Ldr);

    Init = *(DWORD **)((unsigned char *)Ldr + 0x1c);
    printf("得到InInitializationOrderModuleList結構指標 = %x \n", Init);

    Kernel32 = *(DWORD **)((unsigned char *)Init + 0x08);
    printf("得到Kernel32的基地址 = %x \n", Kernel32);

    system("pause");
    return 0;
}

執行輸出效果如下圖所示,讀者可自行檢查讀取結果的準確性;

將此段程式碼翻譯為組合模式也很容易,如下是通過組合實現的流程;

    .386p
    .model flat,stdcall
    option casemap:none

include windows.inc
include kernel32.inc
includelib kerbcli.lib
assume fs:nothing

.code
    main PROC
        xor eax,eax
        xor edx,edx
        mov eax,fs:[30h]           ; 得到PEB結構地址
        mov eax,[eax + 0ch]        ; 得到PEB_LDR_DATA結構地址
        mov esi,[eax + 1ch]        ; 得到 InInitializationOrderModuleList
        lodsd                      ; 得到KERNEL32.DLL所在LDR_MODULE結構的
        mov eax,[eax]              ; Windows 7 以上要將這裡開啟
        mov edx,[eax + 8h]         ; 得到BaseAddress,既Kernel32.dll基址
        ret
    main ENDP
END main

1.5.2 動態查詢並列舉程序模組

在讀者閱讀過第一節中的內容時,相信您已經可以熟練的掌握WinDBG偵錯程式的基本使用了,本節我們將擴充套件一個知識點,以讓讀者能更好的理解WinDBG偵錯命令,本次我們實現列舉程序模組的功能,本案例將不在解釋基本功能。

通過PEB/TEB找到自身程序的所有載入模組資料,首先獲取TEB執行緒環境塊。在程式設計的時候,TEB始終儲存在暫存器FS中。

得到LDR結構:Ldr = *( ( DWORD ** )( ( unsigned char * )PEB + 0x0c ) );

然後再找到PEB結構偏移為0x30從該命令的輸出可以看出,PEB結構體的地址位於TEB結構體偏移0x30的位置處。

找到了PEB也就可以找到_PEB_LDR_DATA結構 其位於PEB偏移0c的位置上。

Ldr = *( ( DWORD ** )( ( unsigned char * )PEB + 0x0c ) );

從輸出結果可以看出,LDRPEB結構體偏移的0x0C處,該地址儲存的地址是0x77325d80通過該地址來解析LDR結構體。

Flink = *( ( DWORD ** )( ( unsigned char * )Ldr + 0x14 ) );

位於LDR偏移14的位置就是InLoadOrderModuleList其所指向的就是模組名稱表。

現在來手動遍歷[ 0x5a3bd0 - 0x5aa5b8 ]第一條連結串列,輸入命令dd 0x5a3bd0

連結串列偏移0x18的位置是模組的對映地址 ImageBase,連結串列偏移 0x28 的位置是模組的路徑及名稱的地址,連結串列偏移 0x30 的位置是模組名稱的地址。

如上圖中的輸出結果,地址005a2480儲存有當前模組詳細路徑資訊,而005a24ae則儲存有當前模組名,我們可以通過du命令來進行驗證;

當讀者需要讀入下一個模組連結串列時,則需要存取0x005a3ac8這個記憶體地址,其中儲存著下一個連結串列結構,依次遍歷。

當然這個連結串列結構其實存取InMemoryOrderModuleList同樣可以得到,這兩個都指向同一片區域。

上方介紹的結構,是微軟保留結構,只能從網上找到一個結構定義,根據該結構的定義做進一步解析即可。

typedef struct _LDR_DATA_TABLE_ENTRY {
    PVOID Reserved1[2];
    LIST_ENTRY InMemoryOrderLinks;
    PVOID Reserved2[2];
    PVOID DllBase;
    PVOID EntryPoint;
    PVOID Reserved3;
    UNICODE_STRING FullDllName;
    BYTE Reserved4[8];
    PVOID Reserved5[3];
    union {
    ULONG CheckSum;
    PVOID Reserved6;
    };
    ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

根據如上分析細節,那麼描述列舉模組列表的核心程式碼就可以寫成如下案例;

#include <Windows.h>
#include <stdio.h>

int main(int argc, char* argv[])
{
    DWORD *PEB = NULL, *Ldr = NULL, *Flink = NULL, *p = NULL;
    DWORD *BaseAddress = NULL, *FullDllName = NULL,*Ba = NULL;

    __asm
    {
        mov eax, fs:[0x30]
        mov PEB, eax
    }

    Ldr = *((DWORD **)((unsigned char *)PEB + 0x0c));
    Flink = *((DWORD **)((unsigned char *)Ldr + 0x14));
    p = Flink;

    p = *((DWORD **)p);
    while (Flink != p)
    {
        BaseAddress = *((DWORD **)((unsigned char *)p + 0x10));
        FullDllName = *((DWORD **)((unsigned char *)p + 0x20));

    if (BaseAddress == 0)
        break;

    printf("映象基址 = %08x \n --> 模組路徑 = %S \n", BaseAddress, (unsigned char *)FullDllName);

        p = *((DWORD **)p);
    }
    system("pause");
    return 0;
}

讀者編譯並執行該程式,則預設會列舉出當前模組所匯入的所有模組資訊,其輸出效果如下圖所示;

1.5.3 計算函數Hash摘要值

案例介紹瞭如何使用Win32組合語言和C語言計算字串的hash摘要值。字串的hash摘要值是通過一定的演演算法將字串壓縮為一個固定長度的十六進位制數,用於在程式中進行快速的字串比較。具體而言,該案例使用了迴圈移位hash計演演算法,並最終得到了字串的 hash 值,並以十六進位制數的形式輸出。

讀者一定有疑問為啥需要HASH壓縮處理? 原因是,如果直接將函數名壓棧的話,我們就需要提供更多的空間來儲存ShellCode程式碼,為了能夠讓我們編寫的ShellCode程式碼更加的短小精悍,所以我們將要對字串進行hash處理,將字串壓縮為一個十六進位制數,這樣只需要比較二者hash值就能夠判斷目標函數,儘管這樣會引入額外的hash演演算法,但是卻可以節省出儲存函數名字的空間。

為了能讓讀者理解計算原理,此處我們先使用C語言做摘要計算描述,如下程式碼中的GetHash函數,該函數接受一個指向字元陣列的指標,即一個字串,然後對字串進行雜湊計算,並返回計算結果。

雜湊計算的過程是通過迴圈遍歷字串中的每個字元,對其進行位運算和加法運算,最終得到一個32位的雜湊值。對於字串中的每個字元,程式首先將雜湊值左移25位,然後將結果右移7位,相當於是對雜湊值進行了迴圈右移25位。然後程式將該字元的ASCII值加到雜湊值上。迴圈遍歷完字串中的所有字元后,雜湊值即為最終的計算結果。

#include <stdio.h>
#include <windows.h>

DWORD GetHash(char *fun_name)
{
    DWORD digest = 0;
    while (*fun_name)
    {
        digest = ((digest << 25) | (digest >> 7));
        digest += *fun_name;
        fun_name++;
    }
    return digest;
}

int main(int argc, char *argv[])
{
    DWORD MessageBoxHash;
    DWORD ExitProcessHash;
    DWORD LoadLibraryAHash;

    MessageBoxHash = GetHash("MessageBoxA");
    printf("MessageBoxHash = 0x%.8x\n", MessageBoxHash);

    ExitProcessHash = GetHash("ExitProcess");
    printf("ExitProcessHash = 0x%.8x\n", ExitProcessHash);

    LoadLibraryAHash = GetHash("LoadLibraryA");
    printf("LoadLibraryAHash = 0x%.8x\n", LoadLibraryAHash);

    system("pause");
    return 0;
}

執行上方C語言實現程式碼,則讀者可以此獲取到三個核心函數的Hash值,其輸出效果如下圖所示;

在理解了C語言版本的計算流程後,那麼組合語言版本的也應該很容易理解,如下是使用Win32組合語言的實現過程,並在MASM上正常編譯,組合版字串轉換Hash值。

    .386p
    .model flat,stdcall
    option casemap:none
    
include windows.inc
include kernel32.inc
include msvcrt.inc
includelib kernel32.lib
includelib msvcrt.lib

.data
    data db "MessageBoxA",0h
    Fomat db "0x%x",0
.code
    main PROC
        xor eax,eax               ; 清空eax暫存器
        xor edx,edx               ; 清空edx暫存器
        lea esi,data              ; 取出字串地址
    loops:
        movsx eax,byte ptr[esi]   ; 每次取出一個字元放入eax中
        cmp al,ah                 ; 驗證eax是否為0x0即結束符
        jz nops                   ; 為0則說明計算完畢跳轉到nops
        ror edx,7                 ; 不為零,則進行迴圈右移7位
        add edx,eax               ; 將回圈右移的值不斷累加
        inc esi                   ; esi自增,用於讀取下一個字元
        jmp loops                 ; 迴圈執行
    nops:
        mov eax,edx               ; 結果存在eax裡面
        invoke crt_printf,addr Fomat,eax
        ret
    main ENDP
END main

1.5.4 列舉Kernel32匯出表

在文章開頭部分我們通過WinDBG偵錯程式已經找到了Kernel32.dll這個動態連結庫的基地址,而Dll檔案本質上也是PE檔案,在Dll檔案中同樣存在匯出表,其內部記錄著該Dll的匯出函數。接著我們需要對Dll檔案的匯出表進行遍歷,不斷地搜尋,從而找到我們所需要的API函數,同樣的可以通過如下定式獲取到指定的匯出表。

  • 1.從kernel32.dll載入基址算起,偏移0x3c的地方就是其PE檔案頭。
  • 2.PE檔案頭偏移0x78的地方存放著指向函數匯出表的指標。
  • 3.匯出表偏移0x1c處的指標指向儲存匯出函數偏移地址(RVA)的列表。
  • 4.匯出表偏移0x20處的指標指向儲存匯出函數函數名的列表。

首先我們通過WinDBG來實現讀取匯入表及匯出表試試,我們以讀取ole32.dll為例,首先讀者需要通過lmvm ole32.dll查詢到該模組的入口地址,如圖所示該模組的入口地址為0x75830000

解析DOS頭,DOS頭通過_IMAGE_DOS_HEADER結構被定義,在解析時讀者應傳入模組入口0x75830000地址,其次DOS頭中e_lfanew欄位指向了PE頭,該欄位需要注意;

  • 執行讀入DOS頭:dt ole32!_IMAGE_DOS_HEADER 75830000

解析PE頭,PE頭通過DOS頭部的e_lfanew中儲存的之加上模組基地址獲取到,在本例中則是通過75830000+0n264獲取到;

  • 讀入PE頭:dt ole32!_IMAGE_NT_HEADERS 75830000+0n264

接著需要在_IMAGE_OPTIONAL_HEADER可選頭中找到EXPORT匯出表基地址,通過PE頭基址75830108加上0x018也就是OptionalHeader的偏移,即可定位到DataDirectory[0]也就是匯出表基地址,其地址為75830180

根據上述定義,繼續尋找EXPORT匯出表的實際地址,需要注意的是Evaluate expression中的結果是根據ole32模組的基地址與VirtualAddress當前地址相加後得到的,如下圖所示

當讀者需要列舉特定模組時,則可通過模組基地址加上例如Name欄位偏移值,來讀入模組名稱;

如果讀者需要列舉所有匯出函數,則讀者可通過模組基地址加上AddressOfNames欄位,並通過如下命令實現完整輸出;

  • .foreach(place {dd 758e4088}){r @$t0=${place}+75830000; .if(@$t0<778e4088){da @$t0}}

匯入表的列舉與匯出表類似,為了節約篇幅此處只給出偵錯資料,讀者可根據自己的掌握情況自行分析學習;

# 根據模組基地址獲取模組e_lfanew
0:000> dt ole32!_IMAGE_DOS_HEADER 0x75830000
   +0x000 e_magic          : 0x5a4d
   +0x028 e_res2           : [10] 0
   +0x03c e_lfanew         : 0n264

# 定位到NT頭部
0:000> dt ole32!_IMAGE_NT_HEADERS 0x75830000 + 0n264
   +0x000 Signature        : 0x4550
   +0x004 FileHeader       : _IMAGE_FILE_HEADER
   +0x018 OptionalHeader   : _IMAGE_OPTIONAL_HEADER

# 基地址與e_lfanew相加得到OPTIONAL
0:000> ?0x75830000 + 0n264
Evaluate expression: 1971519752 = 75830108

# 查詢OPTIONAL
0:000> dt ole32!_IMAGE_OPTIONAL_HEADER -v -ny DataDirectory 75830108+0x018
struct _IMAGE_OPTIONAL_HEADER, 31 elements, 0xe0 bytes
   +0x060 DataDirectory : [16] struct _IMAGE_DATA_DIRECTORY, 2 elements, 0x8 bytes

0:000> ? 75830108+0x018+0x60
Evaluate expression: 1971519872 = 75830180

# 得到資料目錄表地址
0:000> dt ole32!_IMAGE_DATA_DIRECTORY 75830180+8
   +0x000 VirtualAddress   : 0xbd9f8
   +0x004 Size             : 0x460

0:000> ? 0x75830000+0xbd9f8
Evaluate expression: 1972296184 = 758ed9f8

# DataDirectory[1]即為匯入表,地址為758ed9f8
0:000> dt ole32!_IMAGE_IMPORT_DESCRIPTOR 758ed9f8
   +0x000 Characteristics  : 0xbe700
   +0x000 OriginalFirstThunk : 0xbe700
   +0x004 TimeDateStamp    : 0
   +0x008 ForwarderChain   : 0
   +0x00c Name             : 0xbe87a
   +0x010 FirstThunk       : 0xbd8a8

0:000> da 0x75830000+0xbe87a
758ee87a  "api-ms-win-crt-string-l1-1-0.dll"

# 每一個_IMAGE_IMPORT_DESCRIPTOR的大小為0x14
0:000> ?? sizeof(_IMAGE_IMPORT_DESCRIPTOR)
unsigned int 0x14

# 也就是說,每次遞增14即可輸出下一個匯入函數名
0:000> dt ole32!_IMAGE_IMPORT_DESCRIPTOR 758ed9f8+14
   +0x000 Characteristics  : 0xbe6f4
   +0x000 OriginalFirstThunk : 0xbe6f4
   +0x004 TimeDateStamp    : 0
   +0x008 ForwarderChain   : 0
   +0x00c Name             : 0xbe89c
   +0x010 FirstThunk       : 0xbd89c

0:000> da 0x75830000+0xbe89c
758ee89c  "api-ms-win-crt-runtime-l1-1-0.dl"

0:000> dt ole32!_IMAGE_IMPORT_DESCRIPTOR 758ed9f8+28
   +0x000 Characteristics  : 0xbe64c
   +0x000 OriginalFirstThunk : 0xbe64c
   +0x004 TimeDateStamp    : 0
   +0x008 ForwarderChain   : 0
   +0x00c Name             : 0xbeb88
   +0x010 FirstThunk       : 0xbd7f4
0:000> da 0x75830000+0xbeb88
758eeb88  "api-ms-win-crt-private-l1-1-0.dl"

# 分析第一個IID的IAT和INT
# 先看INT: IMAGE_THUNK_DATA其實就是一個DWORD,如IID一樣,也是一個接一個,最後一個為NULL

第一個:
0:000> dd 0xbe6f4+0x75830000 L1
758ee6f4  000be86c

# 最高位不為1(為1表示為序號輸入)指向_IMAGE_IMPORT_BY_NAME結構

.foreach(place {dd 758ee6f4}) {r @$t0 = ${place}+75830000+2; .if (@$t0<86d00000){da @$t0;}}
758ee86e  "_initterm_e"
758ee862  "_initterm"
75830002  "."
758eeb80  "memset"
758ee84e  "wcsncmp"
758ee858  "strcspn"

我們將問題迴歸到列舉匯出表上,函數的RVA地址和名字按照順序存放在上述兩個列表中,我們可以在列表定位任意函數的RVA地址,通過與動態連結庫的基地址相加得到其真實的VA,而計算的地址就是我們最終在ShellCode中呼叫時需要的地址,其組合核心列舉程式碼如下所示;

#include <stdio.h>
#include <Windows.h>

int main(int argc, char * argv[])
{
    int a;
    __asm
    {
        mov ebx, dword ptr fs : [0x30]         ; 獲取當前執行緒資訊的地址
        mov ecx, dword ptr[ebx + 0xc]          ; 獲取PEB結構體的地址
        mov ecx, dword ptr[ecx + 0x1c]         ; 獲取PEB結構體中的LDR結構體的地址
        mov ecx, [ecx]                         ; 獲取LDR結構體中的InMemoryOrderModuleList的頭節點地址
        mov edx, [ecx + 0x8]                   ; 獲取第一個模組的基址,即ntdll.dll的基址

        mov eax, [edx+0x3c]                    ; 獲取PE頭偏移地址
        mov ecx, [edx + eax + 0x78]            ; 獲取匯出表VA地址偏移
        add ecx,edx                            ; 將匯出表的VA地址轉換成絕對地址
        mov ebx, [ecx+0x20]                    ; 獲取匯出表中的匯出函數名偏移陣列的地址
        add ebx,edx                            ; 將函數名偏移陣列的VA地址轉換成絕對地址
        xor edi,edi                            ; 將edi清零,用於迴圈計數

    s1:
        inc edi                                ; 計數器自增1
        mov esi, [ebx+edi*4]                   ; 通過偏移獲取匯出函數名的地址
        add esi,edx                            ; 將匯出函數名的VA地址轉換成絕對地址

        cmp esi,edx                            ; 檢查匯出函數名的地址是否合法,如果等於基址則跳過
        je no
        loop s1                                ; 繼續查詢匯出函數名

    no:
        xor eax,eax                            ; 清零eax暫存器,用於返回值
    }
    system("pause");
    return 0;
}

1.5.5 整合自定位ShellCode

完整的組合程式碼如下,下方程式碼是一個定式,這裡就只做了翻譯,使用編譯器編譯如下程式碼。

#include <stdio.h>
#include <windows.h>

int main(int argc, char *argv)
{
    __asm
    {
        // 將索要呼叫的函數hash值入棧儲存
            CLD                      // 清空標誌位DF
            push 0x1E380A6A          // 壓入MessageBoxA-->user32.dll
            push 0x4FD18963          // 壓入ExitProcess-->kernel32.dll
            push 0x0C917432          // 壓入LoadLibraryA-->kernel32.dll
            mov esi, esp             // 指向堆疊中存放LoadLibraryA的地址
            lea edi, [esi - 0xc]     // 後面會利用edi的值來呼叫不同的函數

            // 開闢記憶體空間,這裡是堆疊空間
            xor ebx, ebx
            mov bh, 0x04       // ebx為0x400
            sub esp, ebx       // 開闢0x400大小的空間

            // 將user32.dll入棧
            mov bx, 0x3233
            push ebx           // 壓入字元'32'
            push 0x72657375    // 壓入字元 'user'
            push esp
            xor edx, edx        // edx=0

            // 查詢kernel32.dll的基地址
            mov ebx, fs:[edx + 0x30]     // [TEB+0x30] -> PEB
            mov ecx, [ebx + 0xC]         // [PEB+0xC] -> PEB_LDR_DATA
            mov ecx, [ecx + 0x1C]        // [PEB_LDR_DATA+0x1C] -> InInitializationOrderModuleList
            mov ecx, [ecx]               // 進入連結串列第一個就是ntdll.dll
            mov ebp, [ecx + 0x8]         //ebp = kernel32.dll 的基地址

        // hash 的查詢相關
        find_lib_functions :
                           lodsd                     // eax=[ds*10H+esi],讀出來是LoadLibraryA的Hash
                           cmp eax, 0x1E380A6A       // 與MessageBoxA的Hash進行比較
                           jne find_functions        // 如果不相等則繼續查詢
                           xchg eax, ebp
                           call[edi - 0x8]
                           xchg eax, ebp

        // 在PE檔案中查詢相應的API函數
        find_functions :
        pushad
            mov eax, [ebp + 0x3C]        // 指向PE頭
            mov ecx, [ebp + eax + 0x78]  // 匯出表的指標
            add ecx, ebp                 // ecx=0x78C00000+0x262c
            mov ebx, [ecx + 0x20]        // 匯出函數的名字列表
            add ebx, ebp                 // ebx=0x78C00000+0x353C
            xor edi, edi                 // 清空edi中的內容,用作索引

        // 迴圈讀取匯出表函數
        next_function_loop :
        inc edi                            // edi作為索引,自動遞增
            mov esi, [ebx + edi * 4]       // 從列表陣列中讀取
            add esi, ebp                   // esi儲存的是函數名稱所在的地址
            cdq

        // hash值的運算過程
        hash_loop :
        movsx eax, byte ptr[esi]         // 每次讀取一個位元組放入eax
            cmp al, ah                   // eax和0做比較,即結束符
            jz compare_hash              // hash計算完畢跳轉
            ror edx, 7
            add edx, eax
            inc esi
            jmp hash_loop
        // hash值的比較函數
        compare_hash :
        cmp edx, [esp + 0x1C]
            jnz next_function_loop         // 比較不成功則查詢下一個函數
            mov ebx, [ecx + 0x24]          // ebx=序數表的相對偏移量
            add ebx, ebp                   // ebx=序數表的絕對地址
            mov di, [ebx + 2 * edi]        // di=匹配函數的序數
            mov ebx, [ecx + 0x1C]          // ebx=地址表的相對偏移量
            add ebx, ebp                   // ebx=地址表的絕對地址
            add ebp, [ebx + 4 * edi]       // 新增到EBP(模組地址庫)
            xchg eax, ebp                  // 將func addr移到eax中    
            pop edi                        // edi是pushad中最後一個堆疊
            stosd
            push edi
            popad

            cmp eax, 0x1e380a6a             // 與MessageBox的hash值比較
            jne find_lib_functions

        // 下方的程式碼,就是我們的彈窗
        xor ebx, ebx          // 清空eb暫存器
            push ebx          // 截斷字串0

            push 0x2020206b
            push 0x72616873
            push 0x796c206f
            push 0x6c6c6568
            mov eax, esp

            push ebx          // push 0
            push eax          // push "hello lyshark"
            push eax          // push "hello lyshark"
            push ebx          // push 0
            call[edi - 0x04]  // call MessageBoxA

            push ebx          // push 0
            call[edi - 0x08]  // call ExitProcess
    }
    return 0;
}

執行後會彈出一個提示框hello lyshark說明我們成功了,此列程式碼就是所謂的自定位程式碼,該程式碼可以不依賴於系統環境而獨立執行;