はじめに

2014.12.07~08にわたって開催されたSECCON CTF 2014の予選(英語版)にて,ROP: Impossibleというpwn問題が出題された.タイトルの通り,この問題ではROPが制限されている.
ここでは,その実現手法として用いられていたIntel Pin(pintool)について,またこの問題にあった欠陥について述べる.
したがって,このエントリはROP: Impossibleのネタバレを兼ねている.注意されたし.

問題の概要

問題文は以下の通り.Pinによって保護された脆弱なバイナリからフラグを読み出せというものだ.

ropi.pwn.seccon.jp:10000
read /flag and write the content to stdout, such as the following pseudo code.
open("/flag", 0);
read(3, buf, 32);
write(1, buf, 32);
Notice that the vuln executable is protected by an Intel Pin tool, the source code of which is norop.cpp.

パッと見,バイナリは良心的な構成であるように思われる.

1
vuln: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, for GNU/Linux 2.6.26, BuildID[sha1]=0xcb671b1dc0409082c3f3962818d366fcb8771ead, not stripped

有効になっているのはNXだけだ.

1
2
RELRO STACK CANARY NX PIE RPATH RUNPATH FILE
No RELRO No canary found NX enabled No PIE No RPATH No RUNPATH vuln

だからといって,解法が自明であるわけではない.この問題の肝は,いかにしてPinによる保護をかい潜るかにある.

Pin

Pinは,Intelによって開発されたDBI(dynamic binary instrumentation)フレームワークである.
DBIとは,プログラムにコードを挿入することで実行時の情報を取得・操作する技術であり,プログラムのパフォーマンス測定やエラー検出,CPUキャッシュの分析や未定義命令のエミュレーションなど,多方面で応用されている.
さて,ROP: Impossibleは以下のコードによって保護されている.

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include <stdlib.h>
#include "pin.H"
ADDRINT shadow_stack[4096];
int shadow_sp = -1;
VOID push_retaddr(ADDRINT esp, ADDRINT eip)
{
if(shadow_sp >= (int)sizeof(shadow_stack) - 1){
// cannot push retaddr to shadow stack
exit(-1);
}
PIN_SafeCopy(&shadow_stack[++shadow_sp], (VOID*)esp, sizeof(ADDRINT));
}
VOID pop_retaddr(ADDRINT esp, ADDRINT eip)
{
ADDRINT retaddr;
PIN_SafeCopy(&retaddr, (VOID*)esp, sizeof(ADDRINT));
while(shadow_sp >= 0 && shadow_stack[shadow_sp--] != retaddr);
if(shadow_sp < 0){
exit(-1);
}
}
VOID check_syscall(ADDRINT eax)
{
switch(eax){
// syscalls for exploit
case 3: // sys_read
case 4: // sys_write
case 5: // sys_open
case 6: // sys_close
// syscalls executed until entry point
case 45: // sys_brk
case 122: // sys_newuname
case 192: // sys_mmap2
case 197: // sys_fstatfs64
case 243: // sys_set_thread_area
break;
// invalid syscalls
default:
exit(-1);
}
}
VOID insert_hooks(INS ins, VOID *val)
{
if(INS_IsCall(ins)){
// push retaddr to shadow stack
if(XED_ICLASS_CALL_FAR == INS_Opcode(ins)){
exit(-1);
}
INS_InsertCall(ins, IPOINT_TAKEN_BRANCH,(AFUNPTR)push_retaddr,
IARG_REG_VALUE, REG_ESP, IARG_INST_PTR, IARG_END);
}else if(INS_IsRet(ins)){
// pop retaddr from shadow stack, and then check it
if(XED_ICLASS_RET_FAR == INS_Opcode(ins)){
exit(-1);
}else{
INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)pop_retaddr,
IARG_REG_VALUE, REG_ESP, IARG_INST_PTR, IARG_END);
}
}else if(INS_IsSyscall(ins)){
// check syscall
INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)check_syscall,
IARG_REG_VALUE, REG_EAX, IARG_END);
}
}
int main(int argc, char *argv[])
{
PIN_Init(argc, argv);
INS_AddInstrumentFunction(insert_hooks, NULL);
PIN_StartProgram();
return 0;
}

このプログラムが,vulnの共有ライブラリとして噛まされている.INS_*は命令単位のinstumentationのために提供されているPinのAPIである.
要するに,リターンアドレスの検証によって,ROPが制限されているのだ.

JOP

ROPができない環境ならば,どうすればよいのか.
そもそも,ROPに用いられるretは,スタックの最上位アドレスに対するpopjmpと等価であると見做せる.ゆえにjmpによってchainを構築すれば,retを用いること無くROPと同様なコードを作成することができる.これをJOP(Jump-oriented programming)という.
さらに,スタックに対するアドレスのpushjmpは,callと等価であると見做せる.ゆえにcallpopによってROPを代替することができる.
つまり,ROP: Impossibleはret制限下の環境を前提にjmpcallでchainを構築しろというストイックな問題だった.
Pinに起因する欠陥がなければ.

writeup

この問題を最初に解いたのは,TOEFL BEGINNERという謎のチームだった.続いてbinja, PPPと続いている.
優勝チームであるPPPメンバーのRicky Zhouが公開しているwriteupを見てみよう.

明らかにおかしい.bssセグメントにシェルコードを置いて実行しているだけではないか.
だが,このバイナリはNXが有効だったはずだ.ローカルでこのコードを実行しても,SIGSEGVが発生する.
これは一体どうしたことだろう.

JITコンパイルの弊害

結論から言うと,Pinの制御下にあるバイナリのNXは無効化されるようになっている.
/proc/pid/mapsを確認すると,一見nonexecであるように見える.

1
bf984000-bf9a5000 rw-p 00000000 00:00 0 [stack]

だが,実際はそうではない.
PinはJITコンパイルによってinstrumentationを実現している.そのため,バイナリはPinによってmmapされ,execされる.このとき,Pinは元来バイナリに付与されていた実行権限を無視してしまう.
つまり,ROPを制限するためのPinが,NXを無効化してしまっていたのだ.
そもそも,LinuxにおいてNXの状態をmaps以外から取得するのは難しい.強いて挙げるならば,checksecのようにreadelf -W -l file | grep 'GNU_STACK'を叩くといったところだろうか.だが,これだけではmmapやmprotectに追随することができない.WindowsのVirtualQueryに相当する機能はないのだろうか.

おわりに

XSS Bonsaiも同様だが,CTFについてもテストは重要であるということを気付かされた.
運営の穴を突くのもCTFの醍醐味のひとつなのだろうが,しかし厄介な問題である.
最近はPinやDynamoRioによるマルウェア解析が流行っているように見受けられるが,やがてはDBIツールのデメリットについても検討を加えなければならなくなるだろう.