羽夏逆向指引——注入

写在前面

  此系列是本人一个字一个字码出来的,包括示例和实验截图。可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我

你如果是从中间插过来看的,请仔细阅读 羽夏逆向指引——序 ,方便学习本教程。

简述

  在安全领域,你或多或少听过注入这个名词,并了解高中注入手段:远程线程注入、APC注入、消息注入、输入法注入、修改PE结构注入。这一切的一切的目的就是将自己的Dll注入到目标进程实现自己的目的。但是一旦涉及注入自己的可执行代码,如果注入Dll,这种方式在0环是极易被发现的,并不是隐蔽性很好的攻击方式。如果注入ShellCode执行,执行完后抹除的话,隐蔽性就明显的提高。下面我们以Dll注入来介绍并以最简单的方式实现以下它们的功能。

远程线程注入

实现

  既然注入Dll,我们就得写一个,如下是其代码:

#include "pch.h"

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    {
        MessageBox(NULL, L"注入成功!!!By.WingSummer.", L"CnBlog", MB_ICONINFORMATION);
        break;
    }
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

  这个Dll的作用就是注入成功之后进行弹窗提示,表示注入成功!我们开始进行一下知识铺垫。
  如何加载Dll呢?我们平时加载的时候会调用LoadLibrary这个函数,如下是函数原型:

HMODULE WINAPI LoadLibraryW(
    _In_ LPCWSTR lpLibFileName
    );

  既然是注入,肯定不是我们自己调用。让一个代码执行就需要线程,如果在对方创建线程需要使用如下函数:

HANDLE WINAPI CreateRemoteThread(
    _In_ HANDLE hProcess,
    _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
    _In_ SIZE_T dwStackSize,
    _In_ LPTHREAD_START_ROUTINE lpStartAddress,
    _In_opt_ LPVOID lpParameter,
    _In_ DWORD dwCreationFlags,
    _Out_opt_ LPDWORD lpThreadId
    );

  使用LoadLibrary这个函数,需要传参一个字符串地址,而这个地址正好可以用lpParameter提供,但是,这个地址是被注入的进程,我们需要在被注入的程序写一个字符串。可以在被注入程序申请一块内存,其函数原型如下:

LPVOID WINAPI VirtualAllocEx(
    _In_ HANDLE hProcess,
    _In_opt_ LPVOID lpAddress,
    _In_ SIZE_T dwSize,
    _In_ DWORD flAllocationType,
    _In_ DWORD flProtect
    );

  申请好了地址,就需要写字符串,需要用到的函数如下:

BOOL WINAPI WriteProcessMemory(
    _In_ HANDLE hProcess,
    _In_ LPVOID lpBaseAddress,
    _In_reads_bytes_(nSize) LPCVOID lpBuffer,
    _In_ SIZE_T nSize,
    _Out_opt_ SIZE_T* lpNumberOfBytesWritten
    );

  而在其他程序中申请内存和写内存都需要相应的进程句柄,我们可以打开进程,需要的函数如下:

HANDLE WINAPI OpenProcess(
    _In_ DWORD dwDesiredAccess,
    _In_ BOOL bInheritHandle,
    _In_ DWORD dwProcessId
    );

  OpenProcess函数的参数dwProcessId表示的是进程ID,这个我们通过输入的方式进行。
  有了以上知识铺垫之后,我们就可以写代码了。但是你可能有疑问,LoadLibrary的地址被注入和注入进程是一样的吗?当然是的。获取函数的时候,我们还需要GetProcAddress函数,其函数原型如下:

FARPROC WINAPI GetProcAddress(
    _In_ HMODULE hModule,
    _In_ LPCSTR lpProcName
    );

  具体代码实现如下:

#include <iostream>
#include<Windows.h>
using namespace std;

#define DllPath L"*:\\****\\DllTest.dll" //根据自己的 Dll 路径来定

int main()
{
    HMODULE lib = LoadLibrary(L"kernel32.dll");
    if (lib)
    {
        FARPROC loadlib = GetProcAddress(lib, "LoadLibraryW");
        if (loadlib)
        {
            cout << "请输入注入 PID:";
            DWORD pid;
            cin >> pid;

            HANDLE hprocess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
            if (hprocess)
            {
                LPVOID addr = VirtualAllocEx(hprocess, NULL, 4000, MEM_COMMIT, PAGE_READWRITE);
                if (addr)
                {
                    if (WriteProcessMemory(hprocess, addr, DllPath, sizeof(DllPath), NULL))
                    {
                        HANDLE hthread = CreateRemoteThread(hprocess, NULL, NULL, (LPTHREAD_START_ROUTINE)loadlib, addr, 0, NULL);
                        if (hthread)
                        {
                            cout << "注入成功!!!" << endl;
                            CloseHandle(hthread);
                        }
                        CloseHandle(hprocess);
                    }
                    else
                    {
                        cout << "WriteProcessMemory 失败!" << endl;
                    }
                }
                else
                {
                    cout << "VirtualAllocEx 失败!" << endl;
                }
            }
            else
            {
                cout << "OpenProcess 失败!" << endl;
            }
        }
        else
        {
            cout << "获取 LoadLibraryW 地址失败!" << endl;
        }
    }
    else
    {
        cout << "获取 kernel32.dll 地址失败!" << endl;
    }
    system("pause");
    return 0;
}

  如下是实验效果图:

注意事项

  1. 注意程序的位数,64位程序注入64位的DLL,32位注入32位的。
  2. 如果注入高权限的程序,请具有相应的权限。
  3. 如果注入系统服务进程的话,需要通过使用未导出的函数ZwCreateThreadEx,在ntdll里面,需要手动获取。由于会话隔离机制,你无法使用弹窗的形式验证注入成功。

APC 注入

实现

  APC中文名称为异步过程调用,它是Windows十分重要的机制,如果想要学习其内部细节,请自行学习 羽夏看Win系统内核APC篇。下面我们重点介绍最小化实现。
  我们利用创建进程的方式来实现,为什么呢?我们来看一下它的函数原型:

BOOL CreateProcessW(
  [in, optional]      LPCWSTR               lpApplicationName,
  [in, out, optional] LPWSTR                lpCommandLine,
  [in, optional]      LPSECURITY_ATTRIBUTES lpProcessAttributes,
  [in, optional]      LPSECURITY_ATTRIBUTES lpThreadAttributes,
  [in]                BOOL                  bInheritHandles,
  [in]                DWORD                 dwCreationFlags,
  [in, optional]      LPVOID                lpEnvironment,
  [in, optional]      LPCWSTR               lpCurrentDirectory,
  [in]                LPSTARTUPINFOW        lpStartupInfo,
  [out]               LPPROCESS_INFORMATION lpProcessInformation
);

  在最后一个函数中,里面包含进程句柄和主线程句柄,我向线程发送APC的时候就十分方便。下面我们继续看QueueUserAPC的函数原型:

DWORD QueueUserAPC(
  [in] PAPCFUNC  pfnAPC,
  [in] HANDLE    hThread,
  [in] ULONG_PTR dwData
);

  下面我们来开始写代码:

#include <iostream>
#include<Windows.h>
using namespace std;

#define DllPath L"*:\\****\\DllTest.dll" //根据自己的 Dll 路径来定

int main()
{
    HMODULE lib = LoadLibrary(L"kernel32.dll");
    if (lib)
    {
        FARPROC loadlib = GetProcAddress(lib, "LoadLibraryW");
        if (loadlib)
        {
            cout << "创建记事本进程开始实验,按任意键继续……";
            cin.get();

            WCHAR app[] = L"notepad.exe";
            STARTUPINFO info = { sizeof(STARTUPINFO) };
            PROCESS_INFORMATION pi;
            BOOL ret = CreateProcess(NULL, app, NULL, NULL, NULL, 0, NULL, NULL, &info, &pi);

            if (ret)
            {
                LPVOID addr = VirtualAllocEx(pi.hProcess, NULL, 4000, MEM_COMMIT, PAGE_READWRITE);
                if (addr)
                {
                    if (WriteProcessMemory(pi.hProcess, addr, DllPath, sizeof(DllPath), NULL))
                    {
                        if (QueueUserAPC((PAPCFUNC)loadlib, pi.hThread, (ULONG_PTR)addr))
                        {
                            WaitForSingleObjectEx(pi.hThread, -1, TRUE);    //触发 APC
                            cout << "注入成功!!!" << endl;
                        }
                    }
                    else
                    {
                        cout << "WriteProcessMemory 失败!" << endl;
                    }
                }
                else
                {
                    cout << "VirtualAllocEx 失败!" << endl;
                }
            }
            else
            {
                cout << "创建进程失败!" << endl;
            }

            CloseHandle(pi.hProcess);
            CloseHandle(pi.hThread);
        }
        else
        {
            cout << "获取 LoadLibraryW 地址失败!" << endl;
        }
    }
    else
    {
        cout << "获取 kernel32.dll 地址失败!" << endl;
    }
    system("pause");
    return 0;
}

  效果图如下:

注意事项

  1. 注意程序的位数,64位程序注入64位的DLL,32位注入32位的。
  2. 里面的相关细节请学习我在文中提到的教程,这些东西并不是一言两语就能说明白的。

消息注入

  在Windows中大部分的应用程序都是基于消息机制的,它们都有一个消息过程函数,根据不同的消息完成不同的功能。Windows操作系统提供的钩子机制就是用来截获和监视系统中这些消息的。按照钩子作用的范围不同,它们又可以分为局部钩子和全局钩子。局部钩子是针对某个线程的;而全局钩子则是作用于整个系统的基于消息的应用。全局钩子需要使用DLL文件,在DLL中实现相应的钩子函数。
  至于为什么全局钩子必须是DLL,简单思考就可以得到答案,因为我们需要对任何GUI进程进行挂钩,既然到用户进程只有DLL能做到。
  我们需要使用SetWindowsHookEx函数进行挂钩,如下是其函数原型:

HHOOK WINAPI SetWindowsHookEx(
 _In_ int idHook,
 _In_ HOOKPROC lpfn,
 _In_ HINSTANCE hMod,
 _In_ DWORD dwThreadId)

  第一个参数就是表示要安装的钩子程序的类型,第二个是处理函数,第三个是包含由lpfn参数指向的钩子过程的DLL句柄,最后一个参数是与钩子程序关联的线程标识符,如果此参数为0,则钩子过程与系统中所有线程相关联。
  在操作系统中安装全局钩子后,只要进程接收到可以发出钩子的消息,全局钩子的DLL文件就会由操作系统自动或强行地加载到该进程中。因此,设置全局钩子可以达到DLL注入的目的。创建一个全局钩子后,在对应事件发生的时候,系统就会把DLL加载到发生事件的进程中,这样,便实现了DLL注入。
  为了能够让DLL注入到所有的进程中,程序设置WH_GETMESSAGE消息的全局钩子。下面我们开始实现DLL

#include "pch.h"

// 共享内存
#pragma data_seg("shared")
HHOOK g_hHook = NULL;
#pragma data_seg()
#pragma comment(linker, "/SECTION:shared,RWS")

#define EXPORT extern "C" __declspec(dllexport)

HMODULE ghModule;

// 钩子回调函数
LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam)
{
    return ::CallNextHookEx(g_hHook, code, wParam, lParam);
}

EXPORT BOOL SetGlobalHook()
{
    g_hHook = ::SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, ghModule, 0);
    if (NULL == g_hHook)
    {
        return FALSE;
    }
    return TRUE;
}

// 卸载钩子
EXPORT BOOL UnsetGlobalHook()
{
    if (g_hHook)
    {
        ::UnhookWindowsHookEx(g_hHook);
    }
    return TRUE;
}

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    {
        ghModule = hModule;
        break;
    }
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

  上面代码实现了全局钩子的设置、钩子回调函数的实现以及全局钩子的卸载,这些操作都需要用到全局钩子的句柄作为参数。而全局钩子是以DLL形式加载到其他进程空间中的,而且进程都是独立的,所以任意修改其中一个内存里的数据是不会影响另一个进程的。那么,如何将钩子句柄传递给其他进程呢?为了解决这个问题,这里采用的方法是在DLL中创建共享内存。
  共享内存是指突破进程独立性,多个进程共享同一段内存。在DLL中创建共享内存,就是在DLL中创建一个变量,然后将DLL加载到多个进程空间,只要一个进程修改了该变量值,其他进程DLL中的这个值也会改变,就相当于多个进程共享一个内存。
  在上面的代码中,使用#pragma data_seg创建了一个名为shared的数据段,然后使用/section:shared,RWSshared数据段设置为可读、可写、可共享的共享数据段。
  下面我们实现加载全局钩子的程序:

#include <iostream>
#include<Windows.h>
using namespace std;

#define DllPath L"E:\\VsProject\\C++\\DllTest\\x64\\Debug\\DllTest.dll"

typedef BOOL(*SetGlobalHook)();
typedef BOOL (*UnsetGlobalHook)();

int main()
{
    HMODULE lib = LoadLibrary(DllPath);
    if (lib)
    {
        SetGlobalHook sethook = (SetGlobalHook)GetProcAddress(lib, "SetGlobalHook");
        UnsetGlobalHook unsethook = (UnsetGlobalHook)GetProcAddress(lib, "UnsetGlobalHook");
        if (sethook&&unsethook)
        {
            if (sethook())
            {
                cout << "已被 Hook ,按任意键取消 Hook ……" << endl;
                cin.get();
                unsethook();
            }
        }
        else
        {
            cout << "获取函数失败!!!" << endl;
        }
    }
    else
    {
        cout << "加载全局 Hook 失败!!!" << endl;
    }
    system("pause");
    return 0;
}

  然后我们加载钩子之后,启动新的记事本,就可以发现DLL被注入了。

输入法注入

  IME输入法实际就是一个DLL文件,只不过后缀为IME罢了,需要导出必要的接口供系统加载输入法时调用。我们可以在此IME文件的DllMain函数的入口通过调用LoadLibrary函数来加载需要注入的DLL
  对于IME,必须导出如下函数:

ImeConversionList           //将字符串/字符转换成目标字符串/字符 
ImeConfigure                //设置ime参数  
ImeDestroy                  //退出当前使用的IME  
ImeEscape                   //应用软件访问输入法的接口函数  
ImeInquire                  //启动并初始化当前ime输入法  
ImeProcessKey               //ime输入键盘事件管理函数  
ImeSelect                   //启动当前的ime输入法  
ImeSetActiveContext         //设置当前的输入处于活动状态  
ImeSetCompositionString     //由应用程序设置输入法编码  
ImeToAsciiEx                //将输入的键盘事件转换为汉字编码事件  
NotifyIME                   //ime事件管理函数  
ImeRegisterWord             //向输入法字典注册字符串  
ImeUnregisterWord           //删除被注册的字符串  
ImeGetRegisterWordStyle  
ImeEnumRegisterWord  

  其中最重要的就是ImeInquire函数,当切换到此输入法时此函数就会被调用启动并初始化输入法。参数lpIMEInfo用于输入对输入法初始化的内容结构,参数lpszUIClass为输入法的窗口类。lpszUIClass对应的窗口类必须已注册,我们应该在DllMain入口处注册此窗口类,我们来看一下函数原型:

BOOL WINAPI ImeInquire(LPIMEINFO lpIMEInfo,LPTSTR lpszUIClass,LPCTSTR lpszOption);

  由于实现起来还是比较复杂的,其原理就是用输入法弄个壳,安装好,被触发到然后执行目标代码,具体就不实现了。

下一篇

  羽夏逆向指引——符号

posted @ 2022-04-03 09:28  寂静的羽夏  阅读(653)  评论(0编辑  收藏  举报