5

羽夏逆向指引——注入 - 寂静的羽夏

 2 years ago
source link: https://www.cnblogs.com/wingsummer/p/16095153.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

羽夏逆向指引——注入

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

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

  在安全领域,你或多或少听过注入这个名词,并了解高中注入手段:远程线程注入、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;
}

  如下是实验效果图:

2520882-20220402211852806-628401237.png

  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;
}

  效果图如下:

2520882-20220402221356926-1513744361.png

  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);

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

  羽夏逆向指引——符号


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK