Oversight

TCPサーバの接続情報と、ELFファイル oversight および libc-2.27.so が与えられた。

oversightGhidraで逆コンパイルすると、以下の処理をしていることがわかった。

  1. wait関数において、数値を読み込み、それをprintfの何番目のデータを出力するかの指定に用いてデータを出力する。
  2. get_num_bytes関数において、何バイト読み込むかを入力させる。(最大256バイト)
  3. echo関数において、256バイトのローカル配列を用意する。
  4. echo_inner関数において、ローカル配列に指定されたバイト数をfread関数で読み込み、読み込んだデータの後ろに1バイトの0x00を書き込む。

ローカル配列は256バイトしか無いため、256バイト読み込む指定をすると、ローカル配列の1個次のバイトに0x00を書き込むことになる。
この位置にはecho関数のプロローグで保存した%rbpの値があり、0x00の書き込みによってこの値が小さくなることがある。
すると、get_num_bytes関数のエピローグにおいて、この小さくなった%rbpの値を%rspに書き込むため、スタックポインタが下位側にずれることがある。
その結果、スタックポインタがデータの読み込み先のローカル配列内の要素を指してくれることがあり、ROP (Return-Oriented Programming) に繋がる。

CS50 IDE上のGDBでoversightを実行し、 wait関数でデータを出力するprintf関数を呼び出す直前のスタックの様子を調べると、以下のようになった。

Information to connect to a TCP server and ELF files oversight and libc-2.27.so were given.

Decompiling oversight via Ghidra, I found it doing this process:

  1. In the function wait, read an integer and ouptut some data using the integer to specify which data to output in the function printf.
  2. In the function get_num_bytes, read how many bytes to read. (256 bytes at maximum)
  3. In the function echo, allocate an 256-byte local array.
  4. In the function echo_inner, read the specified number of bytes to the local array, and write one-byte 0x00 after the data read.

The local array has only 256 bytes, so when it is specified to read 256 bytes, it will write 0x00 to the byte next to the local array.
The value of %rbp is stored in the prologue of the function echo to the area. Writing 0x00 to the are may make the value smaller.
After that, the new value of %rbp is written to %rsp in the epilogue of the function get_num_bytes, and the stack pointer may be moved to an lower address.
This may make the stack pointer point at somewhere in the local array to read data and enable ROP (Return-Oriented Programming).

I executed oversight on CS50 IDE with GDB and observed the stack just before calling the printf function in the wait function to output some data. Here is the result:

Breakpoint 2, 0x000055555555541e in wait () (gdb) x/32gx $rsp 0x7fffffffc1d0: 0x0000000000000d68 0x00000a3732e5ead1 0x7fffffffc1e0: 0x67616d2072756f59 0x65626d756e206369 0x7fffffffc1f0: 0x3225203a73692072 0x00000a786c6c2437 0x7fffffffc200: 0x0000555555558070 0x00007ffff7fb84a0 0x7fffffffc210: 0x0000000000000000 0x00007ffff7e5f013 0x7fffffffc220: 0x0000000000000010 0x00007ffff7fb76a0 0x7fffffffc230: 0x0000555555556075 0x00007ffff7e5271a 0x7fffffffc240: 0x0000555555555430 0x00007fffffffc270 0x7fffffffc250: 0x00005555555550e0 0x00005555555550e0 0x7fffffffc260: 0x00007fffffffc270 0x00005555555550d5 0x7fffffffc270: 0x0000000000000000 0x00007ffff7df20b3 0x7fffffffc280: 0x00007ffff7ffc620 0x00007fffffffc368 0x7fffffffc290: 0x0000000100000000 0x00005555555550b0 0x7fffffffc2a0: 0x0000555555555430 0x7043ad119c25f561 0x7fffffffc2b0: 0x00005555555550e0 0x00007fffffffc360 0x7fffffffc2c0: 0x0000000000000000 0x0000000000000000 (gdb) where #0 0x000055555555541e in wait () #1 0x00005555555550d5 in main () (gdb)

0x7fffffffc268main関数へ戻るリターンアドレスが配置されていることが読み取れる。
さらに、main関数では呼び出された後%rbpをプッシュする以外にスタックにデータを確保していないため、 その2要素先の 0x7fffffffc278 にあるのがmain関数から戻るリターンアドレスであることがわかる。
経験上printf関数はスタック上の最初のデータを6番目として扱うはずなので、ここは27番目になる。
よって、wait関数ではprintf関数に27番目のデータを出力させることで、main関数から戻るリターンアドレスがわかり、libcの位置を求めることができる。

また、readelf -s コマンドで調べた結果、今回の libc-2.27.so における system関数のオフセットは 0x4f550 であった。
さらに、libc-2.27.so からバイナリエディタで文字列 /bin/sh を検索したところ、0x1b3e1a バイト目 (0-origin) に見つかった。

さらに、関数呼び出し時のスタックのアライメントとecho関数のコードを考えると、 今回の256バイトの配列は16進数の末尾が0のアドレスに配置されることがわかる。
スタックポインタが16の倍数の時に関数を呼び出した状態にするには、関数を呼び出すとスタックにリターンアドレスが積まれるので、 スタックポインタの16進数の末尾が8の状態で関数の先頭に飛ぶようにすればよい。
したがって、ROPでsystem("/bin/sh")を呼び出す際、system関数のアドレスは、 16進数の末尾が0のアドレス、すなわち最後から偶数番目の要素に配置するとよい。

これらを踏まえ、サーバに接続してシェルを起動する以下のプログラムを用意した。
0x00を書き込む前の保存された%rbpの値によってはスタックが十分移動せず、シェルの起動に失敗することがあるようなので、 シェルの起動に成功するまで接続とデータの送受信をやり直すようにした。

I found that there is a return address to return to the function main at 0x7fffffffc268.
Also, I found that the return address to return from the function main is at 0x7fffffffc278, which is 2 elements ahead, because there are no data allocated on the stack except for pushing %rbp in the function main.
The function printf should treat the first element on the stack as 6th, so this element is 27th.
Therefore, we can obtain the address to return from the function main by having the function printf output the 27th data in the function wait. This is useful to know where the libc is placed on the memory.

Investigating via the readelf -s command, I found that the offset of the function system in libc-2.27.so used in this challenge is 0x4f550.
I also searched for a string /bin/sh from the file libc-2.27.so via a binary editor, and found on the 0x1b3e1a-th byte (the first byte is 0th).

Considering the stack allignment on calling functions and the program for the function echo, the 256-byte array should be placed at an address whose last digit in hexadecimal is 0.
To execute functions as if it is called with a stack pointer which is multiple of 16, the function should be started with the last digit of the stack pointer in hexadecimal being 8,
considering that an return address is pushed on the stack when calling a function.
Therefore, to call system("/bin/sh") via ROP, we should place the address of the function system to an address whose last digit in hexadecimal is 0. In other words, it should be placed to a even-th element from the last.

Based on these findings, I created a program to connect to the server and launch the shell.
The stack may not move well and it may fail to launch the shell, depending on the value of %rbp before writing 0x00, so I had it repeat connection and communication until it succeeds in launching the shell.

shell.pl

シェルでls -alコマンドを実行すると、ファイル flag.txt があることがわかった。
cat flag.txt コマンドを実行すると、flagが得られた。

Executing a command ls -al on the shell, I found that there is a file flag.txt.
I obtained the flag by executing a command cat flag.txt.

DUCTF{1_sm@LL_0ver5ight=0v3rFLOW}

DownUnderCTF 2021