如何写一个简单的壳(Stub篇)

zhugeshi 发布于 2025-03-26 94 次阅读


需求:一个可以执行的加壳器、和一个植入宿主文件的stub

这里的项目参考了黑客免杀攻防的如何用c++写一个加壳器的项目
首先给main函数的执行流程,来巩固我们对程序执行流程的熟悉

void start()
{
    // 1. 初始化所有API
    if ( !InitializationAPI() )  return;

    // 2. 解密宿主程序
    Decrypt();

    // 3. 询问是否执行解密后的程序
    if ( g_stcParam.bShowMessage )
    {
        int nRet = g_funMessageBox(NULL,L"解密完成,是否运行原程序?",L"解密完成",MB_OKCANCEL);        
        if (IDCANCEL == nRet)  return;
    }

    // 4. 跳转到OEP
    __asm jmp g_stcParam.dwOEP;
}

接下来我们来一步步分析InitializationAPI()这个函数

bool InitializationAPI()
{
    HMODULE hModule;

    // 1. 初始化基础API
    g_funGetProcAddress = (LPGETPROCADDRESS)GetGPAFunAddr();
    g_funLoadLibraryEx  =
    (LPLOADLIBRARYEX)g_funGetProcAddress((HMODULE)GetKernel32Base(),"LoadLibraryExW");

    // 2. 初始化其他API
    hModule = NULL;
    if ( !(hModule=g_funLoadLibraryEx(L"kernel32.dll",NULL,NULL)) )  return false;
    g_funExitProcess = (LPEXITPROCESS)g_funGetProcAddress(hModule,"ExitProcess");

    hModule = NULL;
    if ( !(hModule=g_funLoadLibraryEx(L"user32.dll",NULL,NULL)) )  return false;
    g_funMessageBox = (LPMESSAGEBOX)g_funGetProcAddress(hModule,"MessageBoxW");

    hModule = NULL;
    if ( !(hModule=g_funLoadLibraryEx(L"kernel32.dll",NULL,NULL)) )  return false;
    g_funGetModuleHandle = (LPGETMODULEHANDLE)g_funGetProcAddress(hModule,"GetModuleHandleW");

    hModule = NULL;
    if ( !(hModule=g_funLoadLibraryEx(L"kernel32.dll",NULL,NULL)) )  return false;
    g_funVirtualProtect = (LPVIRTUALPROTECT)g_funGetProcAddress(hModule,"VirtualProtect");

    return true;
}

首先我们获得Kernel32.dll的地址,因为这个dll在所有的进程中都存在,并且其中的一个函数对获取宿主PE的API函数至关重要。这里用了内联汇编,因为这样比较方便。

DWORD GetKernel32Base()
{
    DWORD dwKernel32Addr = 0;
    __asm
    {
        push eax
        mov eax,dword ptr fs:[0x30] // eax = PEB的地址
        mov eax,[eax+0x0C]          // eax = 指向PEB_LDR_DATA结构的指针
        mov eax,[eax+0x1C]          // eax = 模块初始化链表的头指针InInitializationOrderModuleList
        mov eax,[eax]               // eax = 列表中的第二个条目
        mov eax,[eax+0x08]          // eax = 获取到的Kernel32.dll基址(Win7下获取的是KernelBase.dll的基址)
        mov dwKernel32Addr,eax
        pop eax
    }

    return dwKernel32Addr;
}

TEB(Thread Environment Block ),它记录的相关线程的信息,每一个线程都有自己的TEB,FS:[0]即是当前线程的TEB。

PEB(Process Environment Block,进程环境块)存放进程信息,每个进程都有自己的PEB信息,TEB偏移0x30即当前进程的PEB。

获取Kernel32.dll的基地址后,我们就可以遍历模块中的信息,来找到我们需要的函数了。

g_funGetProcAddress = (LPGETPROCADDRESS)GetGPAFunAddr();

跟踪这个函数

DWORD GetGPAFunAddr()
{
    DWORD dwAddrBase = GetKernel32Base();

    // 1. 获取DOS头、NT头
    PIMAGE_DOS_HEADER pDos_Header;
    PIMAGE_NT_HEADERS pNt_Header;
    pDos_Header = (PIMAGE_DOS_HEADER)dwAddrBase;
    pNt_Header  = (PIMAGE_NT_HEADERS)(dwAddrBase + pDos_Header->e_lfanew);

    // 2. 获取导出表项
    PIMAGE_DATA_DIRECTORY   pDataDir;
    PIMAGE_EXPORT_DIRECTORY pExport;
    pDataDir = pNt_Header->OptionalHeader.DataDirectory+IMAGE_DIRECTORY_ENTRY_EXPORT;
    pExport  = (PIMAGE_EXPORT_DIRECTORY)(dwAddrBase + pDataDir->VirtualAddress);

    // 3. 获取导出表详细信息
    PDWORD pAddrOfFun      = (PDWORD)(pExport->AddressOfFunctions    + dwAddrBase);
    PDWORD pAddrOfNames    = (PDWORD)(pExport->AddressOfNames        + dwAddrBase);
    PWORD  pAddrOfOrdinals = (PWORD) (pExport->AddressOfNameOrdinals + dwAddrBase);

    // 4. 处理以函数名查找函数地址的请求,循环获取ENT中的函数名,并与传入值对比对,如能匹配上
    //    则在EAT中以指定序号作为索引,并取出其地址值。
    DWORD dwFunAddr;
    for (DWORD i=0; i<pExport->NumberOfNames; i++)
    {
        PCHAR lpFunName = (PCHAR)(pAddrOfNames[i]+dwAddrBase);
        if ( !strcmp(lpFunName, "GetProcAddress") )
        {
            dwFunAddr = pAddrOfFun[pAddrOfOrdinals[i]] + dwAddrBase;
            break;
        }
        if ( i == pExport->NumberOfNames-1 )
            return 0; 
    }

    return dwFunAddr;
}

所以现在我们获得了GetProcAdress的地址,通过函数指针,我们就可以随意调用它。因此我们用它来那到几个重要的函数。

// 2. 初始化其他API
hModule = NULL;
if ( !(hModule=g_funLoadLibraryEx(L"kernel32.dll",NULL,NULL)) )  return false;
g_funExitProcess = (LPEXITPROCESS)g_funGetProcAddress(hModule,"ExitProcess");

hModule = NULL;
if ( !(hModule=g_funLoadLibraryEx(L"user32.dll",NULL,NULL)) )  return false;
g_funMessageBox = (LPMESSAGEBOX)g_funGetProcAddress(hModule,"MessageBoxW");

hModule = NULL;
if ( !(hModule=g_funLoadLibraryEx(L"kernel32.dll",NULL,NULL)) )  return false;
g_funGetModuleHandle = (LPGETMODULEHANDLE)g_funGetProcAddress(hModule,"GetModuleHandleW");

hModule = NULL;
if ( !(hModule=g_funLoadLibraryEx(L"kernel32.dll",NULL,NULL)) )  return false;
g_funVirtualProtect = (LPVIRTUALPROTECT)g_funGetProcAddress(hModule,"VirtualProtect");

因为植入的stub劫持了原oep,是用来解密宿主PE的代码段的,接下就可以解密了。因为加的是一个十分简单的加密壳,所以异或就好了

void Decrypt()
{
    // 在导出的全局变量中读取需解密区域的起始于结束VA
    PBYTE lpStart = g_stcParam.lpStartVA;
    PBYTE lpEnd   = g_stcParam.lpEndVA;

    // 循环解密
    while ( lpStart<lpEnd )
    {
        *lpStart -= 0x88;
        *lpStart ^= 0xA1;
        lpStart++;
    }
}

这一下PE解密完了,就可以跳转到原OEP来执行原来的程序逻辑了,这里也用了内联汇编

// 3. 询问是否执行解密后的程序
if ( g_stcParam.bShowMessage )
{
    int nRet = g_funMessageBox(NULL,L"解密完成,是否运行原程序?",L"解密完成",MB_OKCANCEL);
    if (IDCANCEL == nRet)  return;
}

// 4. 跳转到OEP
__asm jmp g_stcParam.dwOEP;

到这里,stub部分就完成了,接下来我们就要一个加壳器,对宿主PE修改和注入我们的stub
补充:stub一般用DLL的形式,据说因为自带重定位的原因,还没弄明白: ( 下次再说。

超级大菜鸡!!!
最后更新于 2025-03-31