wololo.netでjigsaw氏がPSPのexploitが内部でどのような動作をしているのかについての解説記事を投稿していました。6.60 kxploitはsome1氏が発見し、Pro CFWで使われているカーネルexploitです。その6.60 kxploitに焦点を当てて解説されていますので翻訳してみました。
6.60 kxploit(注: kernel exploitのことをいつからかkxploitと呼ぶようになっています)はmbuf[1]をPSPに移植したifhandle.pfxの中にあります。mbufはネットワークパケットやソケットバッファのメモリ管理をする基本的なユニットです。元々はBSDのものですがBSDライセンスに従って広く商用利用されています。ifhandleはFreeBSD 4のものに若干の修正を加えて移植されたものです。一部のifhandleのルーチンは100%オリジナルのmbufコードそのものです。
kxploitを調べる前に、まずkxploitでどんなことができるのかを確認しておきましょう。その答えは単刀直入に言うとカーネル特権で自分で用意したルーチンを実行することが可能になるということです。カーネル特権があれば非署名自作コードの実行やバージョンチェックスキップなどを実行するためにカーネルへパッチをすることができます。
複雑な話ではありませんが、どうすればカーネル特権を得ることができるのかは簡単な話ではありません。カーネルルーチン[3]を実行するためのsyscallを思い出してみてください。カーネルルーチンをどうにかして自分で用意したルーチンへリダイレクトできれば、その用意したルーチンは即座にカーネルレベルのルーチンになります。カーネルルーチンのリダイレクトはどのようにすれば可能なのかについて考えてみましょう。ケースバイケースですが、殆どの場合カーネルルーチンが配置されるカーネル領域のモディファイが必要になります。
それはまるでジレンマです。カーネル特権を得るためにはカーネル特権が要求されるカーネル領域をモディファイしなければならないということだからです。これこそ
kxploitが必要な理由です。殆どの場合、kxploitはカーネル領域に値を書き込むことができてしまうというカーネルのバグのことです。
通常kxploitにはある種の制限が存在します。例えば6.39 kxploitの場合どこにでも0xFFFFFFFFという値が書き込めますが、6.20 kxploitの場合はその値が0×0です。こういったケースの場合アドレスのコントロールはできますが、そこに書き込む値はコントロールできません。とはいってもそれ以上のことは必要ないので実はそれで十分なのです。カーネル特権をチェックしたりルーチンのオフセットをロードするところを1文字変えるだけでカーネルルーチンのリダイレクトは可能になります。肝心なチェックポイントだけを変えてやることで自分で用意したルーチンのアドレスをロードしてコールできるようカーネルに命令を下せることができるようになります。
通常はカーネル領域にゼロの値を書き込みます。これはMIPSのゼロというのがNOP、つまりNo-operation(何もしない)を意味し、他のMIPS命令をマスクするのに都合が良いからです。
つまり、結論としてはこうです: カーネル領域の任意のアドレスにゼロという値を書き込む方法を探す必要がある。[4]
では、6.60 kxploitを詳しく見て行きましょう。
以下はsceNetMCopydataの1行です:
0x00001DDC: 0x0C000C8C '....' - jal memcpy
memcpyの第1と第2引数に注目します。それぞれ行き先とデータのソースです。
規則[5]ではルーチンの第1引数である$a0を見てみます。
...
...
0x00001D18: 0x00E09021 '!...' - addu $s2, $a3, $zr
...
...
0x00001D98: 0x02402021 '! @.' - addu $a0, $s2, $zr
sceNetMCopydataの第4引数($a3)はmemcpyの行き先にあたります。引数に関してはチェックされません。そのためカーネル領域のアドレスを引数にセットすることが可能であれば…
しかし、sceNetMCopydataはユーザー領域へエクスポートできません。つまり直接コールすることができないということです。ところが運良くsceNetMCopydataはユーザー領域から簡単にアクセスできる他のsyscallからコールされるのです。ここではsceNetMPulldownがターゲットです。
ここでsceNetMCopydataをコールする方法を見つけるためsceNetMPulldownの主要部分を解析してみましょう。sceNetMPulldownでは2ヶ所でsceNetMPulldownをコールしています。ここでは0×00003018でのコールに注目します。その理由は0x00002E9Cでのコールがユーザーでは扱えないsceNetMallocInternalによって割り当てられるmbufへのデータコピーだからです。
既に述べた通り、ifhandleはmubfが実装されたものがベースになっていますので、参考としてオリジナルのFreeBSDのコードを入手することができます。ifhandleのライン間マッピングのことがこれで分かります。m_pulldownについては http://fxr.watson.org/fxr/source/kern/uipc_mbuf2.c?v=FREEBSD4 で分かります。
MIPSアセンブラについては詳しくなっておいてください。簡単に理解できるようコメントを多用してあります。
struct m_hdr {
struct mbuf *mh_next; /* next buffer in chain */
struct mbuf *mh_nextpkt; /* next chain in queue/record */
caddr_t mh_data; /* location of data */
int mh_len; /* amount of data in this mbuf */
short mh_type; /* type of data in this mbuf */
short mh_flags; /* flags; see below */
};
struct mbuf {
struct m_hdr m_hdr;
union {
struct {
struct pkthdr MH_pkthdr; /* M_PKTHDR set */
union {
struct m_ext MH_ext; /* M_EXT set */
char MH_databuf[MHLEN];
} MH_dat;
} MH;
char M_databuf[MLEN]; /* !M_PKTHDR, !M_EXT */
} M_dat;
};
#define mtod(m, t) ((t)((m)->m_data))
#define m_next m_hdr.mh_next // 0
#define m_len m_hdr.mh_len // c
#define m_data m_hdr.mh_data // 8
#define m_type m_hdr.mh_type // 10
#define m_flags m_hdr.mh_flags // 12
struct mbuf *sceNetMPulldown(struct mbuf *m, int off, int len, int *offp)
{
struct mbuf *n, *o;
int hlen, tlen, olen;
// fp = offp
// s6 = len
// s4 = m
// s2 = off
// s1 = n
//0x00002C10
if (m == NULL)
return NULL;
//0x00002C18
if (m 0x800) {
sceNetMFreem(m);
return NULL;
}
//0x00002C4C
n = m; // s1 = n
//0x00002C50
while (n != NULL && off > 0) {
if (n->m_len > off)
break;
off -= n->m_len;
n = n->m_next;
}
//0x00002C84
while (n != NULL && n->m_len == 0) {
n = n->m_next;
if (n < 0) //kernel space pointer?
return NULL;
}
//0x00002CB8
if (n == NULL) {
sceNetMFreem(m);
return NULL;
}
//0x00002CBC
if ((off == 0 || offp) && len m_len - off)
goto out;
//0x00002CE0
if (len m_len - off) {
goto loc_00003050; //not interested
}
//0x00002CEC
hlen = n->m_len - off; // s3 = hlen
tlen = len - hlen; // s7 = tlen
olen = 0; // v1 = olen
// s0 = o
//0x00002D00
for (o = n->m_next; o != NULL; o = o->m_next)
olen += o->m_len;
//0x00002D18
if (hlen + olen m_flags & 1)) {
//0x00002D3C
if (_lw(n + 0x34) != 0) {
// we can't go now
goto loc_00002D50;
}
//0x00002D4C
a2 = (_lw(n + 0x44) == n); // a2 MUST equal to 0
}
//loc_00002D50
if ((off == 0 || offp)) {
//0x00002D60
if ((m->m_flags & 1) == 0) {
// we have to stay here
goto loc_00003040;
}
//0x00002D7C
x = (_lw(n + 0x30) + _lw(n + 0x3c)) - (n->m_data + n->m_len);
// x is the macro M_TRAILINGSPACE
if (x >= tlen && a2 == 0) {
// finally...
//0x00003018
m_copydata(n->m_next, 0, tlen, mtod(n, caddr_t) + n->m_len);
}
}
}
見ての通り、コードはほとんどがmbufそのものです。
0×00003018のsceNetMCopydataに行くまでにいくつものトラップを回避しています。例えばm_flagsやm_len、m_nextがセットしてあるところなどです。一方で2つのmbufが互いに行き来できるようなリンクが必要です。自分で考えて欲しいのと、PRO CFWを見れば分かることなのでこのkxploitを有効にするにはどうすればいいのかについてはここでお話しするつもりはありません。
これはkxploitがどんなものなのかをお見せして、どうすればkxploitを見つけることができるのかをお見せするためのものです。今までに公開されたほとんどのkxploitはメモリーアドレスのチェックを無くすという同じような内容です。
理論的にはユーザー領域からカーネル領域のポインター(メモリーアドレス)はどれも有効になるはずです。しかし経験上PSPが発表されてから10年近く経ちますがまだこういったkxploitが存在しています。もちろんこういったkxploitを見つかることはめったにありませんし、場合によってはsyscallの制限[6]のため必ず有効なものとも限りません。
一方で他の種類のkxploitも存在します。カーネルメモリーのモディファイなしに直接コールすることでユーザーコードを有効にできるスタックオーバーフロー(プログラム中で関数呼び出しが多すぎる時に発生する脆弱性で、バッファオーバーフローの一種)もその一つです。次回はそのことについて触れてみましょう。
ところで、kxploitにはWindowsやLinuxのような主要なOSではありがちなヒープオーバーフロー(OSやアプリケーションが動的に確保するメモリ領域(ヒープ)からデータがあふれてしまう脆弱性で、バッファオーバーフローの一種)というものもあります。しかしPSPでは見つかったことも公開されたこともありません。いつかヒープオーバーフローの美しさもお見せできる機会があるといいですね。
- [1] http://people.freebsd.org/~hmp/documentation/manpages/mbuf.pdf
[2] http://fxr.watson.org/fxr/source/kern/?v=FREEBSD4
[3] http://wololo.net/2012/05/30/syscalls-nids-imports/
[4] これは可能性の一つです。他のケースも今度ご説明します。
[5] http://www.cygwin.com/ml/binutils/2003-06/msg00436.html
[6] http://wololo.net/2012/06/07/syscall-internals/