本文嘗試以 GPU 漏洞為引介紹圍繞 GPU 驅動這一攻擊面,安全研究人員對內核漏洞利用技術做的一些探索。
背景介紹
目前移動 SOC 平臺上由多個硬件模塊組成,常見的硬件模塊有:CPU、GPU、Modem基帶處理器、ISP(圖像處理器)等,這些硬件模塊通過硬件總線互聯,協同完成任務。
對于 GPU 驅動漏洞研究來說,我們需要關注的一個關鍵特性是 GPU 和 CPU 共用同一塊 RAM。 在 CPU 側操作系統通過管理 CPU MMU 的頁表來實現虛擬地址到物理地址的映射
GPU 也有自己的 MMU,不過 GPU 的頁表由 CPU 內核中的 GPU 驅動管理,從而限制 GPU 能夠訪問的物理地址范圍。
在實際的業務使用中,一般是 CPU 側分配一段物理內存,然后映射給 GPU , GPU 從共享內存中取出數據完成計算、渲染后再將結果寫回共享內存,從而完成 GPU 與 GPU 之間的交互。對于 GPU 驅動安全研究來說,特殊的攻擊面在于由于其需要維護 GPU 頁表,這個過程比較復雜,涉及到內核中的各個模塊的配合,非常容易出現問題,歷史上也出現了多個由于 GPU 頁表管理失誤導致的安全漏洞
以 ARM Mali 驅動為例,這幾年出現的幾個比較有代表性的漏洞如下:
漏洞 | 類型 | 漏洞原語 |
---|
CVE-2021-39793 | 頁權限錯誤 | 篡改 只讀映射到用戶進程的物理頁 |
CVE-2021-28664 | 頁權限錯誤 | 篡改 只讀映射到用戶進程的物理頁 |
CVE-2021-28663 | GPU MMU 操作異常 | 物理頁 UAF |
CVE-2023-4211 | 條件競爭 UAF | SLUB 對象 UAF |
CVE-2023-48409 | 整數溢出 | 堆溢出 |
CVE-2023-26083 | 內核地址泄露 | 內核地址泄露 |
CVE-2022-46395 | 條件競爭 UAF | 物理頁 UAF |
其中前 3 個漏洞是管理 GPU 頁表映射時的漏洞,后面幾個就是常規驅動漏洞類型
?
CVE-2021-28664
分析代碼下載:https://armkeil.blob.core.windows.net/developer/Files/downloads/mali-drivers/kernel/mali-bifrost-gpu/BX304L01B-SW-99002-r19p0-01rel0.tar
先以最簡單的漏洞開始講起,這個漏洞算是 Mali 第一個出名的漏洞了,同期出道的還有 CVE-2021-28664,這個漏洞是由 Project Zero 捕獲的在野利用,該漏洞的 Patch 如下
static struct kbase_va_region *kbase_mem_from_user_buffer(
struct kbase_context *kctx, unsigned long address,
unsigned long size, u64 *va_pages, u64 *flags)
{
[...]
+ int write;
[...]
+ write = reg->flags & (KBASE_REG_CPU_WR | KBASE_REG_GPU_WR);
+
#if KERNEL_VERSION(4, 6, 0) > LINUX_VERSION_CODE
faulted_pages = get_user_pages(current, current->mm, address, *va_pages,
#if KERNEL_VERSION(4, 4, 168) <= LINUX_VERSION_CODE && \
KERNEL_VERSION(4, 5, 0) > LINUX_VERSION_CODE
- reg->flags & KBASE_REG_CPU_WR ? FOLL_WRITE : 0,
- pages, NULL);
+ write ? FOLL_WRITE : 0, pages, NULL);
#else
- reg->flags & KBASE_REG_CPU_WR, 0, pages, NULL);
+ write, 0, pages, NULL);
#endif
#elif KERNEL_VERSION(4, 9, 0) > LINUX_VERSION_CODE
faulted_pages = get_user_pages(address, *va_pages,
- reg->flags & KBASE_REG_CPU_WR, 0, pages, NULL);
+ write, 0, pages, NULL);
#else
faulted_pages = get_user_pages(address, *va_pages,
- reg->flags & KBASE_REG_CPU_WR ? FOLL_WRITE : 0,
- pages, NULL);
+ write ? FOLL_WRITE : 0, pages, NULL);
#endif
Patch 的關鍵點在于將 get_user_pages 參數中的 reg->flags & KBASE_REG_CPU_WR
換成了 reg->flags & (KBASE_REG_CPU_WR | KBASE_REG_GPU_WR)
,這兩個標記的作用如下:
- KBASE_REG_CPU_WR:表示 reg 能夠已寫權限映射到用戶態進程
- KBASE_REG_GPU_WR: 表示 reg 能夠已寫權限映射到 GPU
reg 的類型為 struct kbase_va_region
, MALI 驅動中使用 kbase_va_region 管理物理內存,包括物理內存的申請/釋放、GPU/CPU 頁表映射管理等。
圖中的關鍵要素如下:
- kbase_va_region 中 cpu_alloc 和 gpu_alloc 指向 kbase_mem_phy_alloc ,表示該 reg 擁有的物理頁,且大部分場景下 cpu_alloc = gpu_alloc
- kbase_va_region 的 flags 字段控制驅動映射 reg 時的權限,假如 flags 為 KBASE_REG_CPU_WR 表示該 reg 能夠被 CPU 映射為可寫權限,如果沒有該 flag 則不允許將 reg 以可寫模式映射到 CPU 進程,確保無法進程修改這些物理頁
核心觀點:驅動利用 kbase_va_region 表示一組物理內存,這組物理內存可以被 CPU 上的用戶進程和 GPU 分別映射,映射的權限由 reg->flags 字段控制.
回到漏洞本身,其調用路徑中的關鍵代碼如下:
kbase_api_mem_import
u64 flags = import->in.flags;
kbase_mem_import(kctx, import->in.type, u64_to_user_ptr(import->in.phandle), import->in.padding, &import->out.gpu_va, &import->out.va_pages, &flags);
copy_from_user(&user_buffer, phandle
uptr = u64_to_user_ptr(user_buffer.ptr);
kbase_mem_from_user_buffer(kctx, (unsigned long)uptr, user_buffer.length, va_pages, flags)
- struct kbase_va_region *reg = kbase_alloc_free_region(rbtree, 0, *va_pages, zone);
- kbase_update_region_flags(kctx, reg, *flags) // 根據用戶態提供的 flags 設置 reg->flags
- faulted_pages = get_user_pages(address, *va_pages, reg->flags & KBASE_REG_GPU_WR, 0, pages, NULL);
漏洞在于傳遞 get_user_pages 參數是只考慮了 KBASE_REG_GPU_WR 情況,沒有考慮 KBASE_REG_CPU_WR,當 reg->flags 為 KBASE_REG_CPU_WR 時 get_user_pages 的第三個參數為 0
long get_user_pages(unsigned long start, unsigned long nr_pages,
unsigned int gup_flags, struct page **pages,
struct vm_area_struct **vmas)
{
return __get_user_pages_locked(current, current->mm, start, nr_pages,
pages, vmas, NULL, false,
gup_flags | FOLL_TOUCH);
}
get_user_pages 的作用的是根據用戶進程提供的 va (start)遍歷進程頁表,返回的是 va 對應物理地址對應的 page 結構體指針,結果保存到 pages 數組中。
即根據 task_struct->mm 找到進程頁表,遍歷頁表獲取物理地址
其中如果 gup_flags 為 1,表示獲取 va 對應 page 后會寫入 page 對應的物理頁,然后在 get_user_pages 內部需要對只讀頁面和 COW 頁面做額外處理,避免這些特殊 va 對應的物理頁被修改導致非預期行為。
- 如果 vma 為只讀,API 返回錯誤碼
- VA 的映射為 COW 頁,在 API 內會完成寫時拷貝,并返回新分配的 page
當 gup_flags 為 0 時則直接返回頁表遍歷的結果(P0)
對于這個漏洞而言,我們可以創建一個 reg->flags
為 KBASE_REG_CPU_WR
的 kbase_va_region
,再導入頁面時就可以獲取進程中任意 va 對應 page 到 kbase_va_region
,最后再將其以可寫權限映射到用戶態進程,這樣就可以實現篡改進程中任意只讀映射對應的物理頁。
這一原語要進一步利用需要依賴操作系統的機制,首先介紹最簡單的一種利用方式,Linux 內核在處理磁盤中的文件系統時,會對從磁盤中讀取的物理頁做緩存來加速文件訪問的性能,同時減少重復文件物理頁,減少開銷
如果所示:
- 當進程嘗試讀取物理頁時,比如只讀權限 mmap ,內核會搜索 page cache 如果找到就直接返回,否則就從磁盤中加載物理頁到 Page Cache 中,然后返回
- 如果是寫則會有對應的 flush cache 機制
具體來說,當兩個進程同時以只讀權限 mmap libc.so 文件時,這兩個進程的 VA 會指向同一個物理頁
這樣當我們利用漏洞修改 Page Cache 中的物理頁后,其他進程也會受到影響,因為都是映射的同一塊物理地址,因此攻擊思路就來了:
- 只讀映射 libc.so 利用漏洞篡改其在 Page Cache 中物理頁,在其中注入 shellcode,等高權限進程調用時就能提權
- 類似的手法修改 /etc/passwd 完成提權
除了修改文件系統的 Page Cache 外,在 Android 平臺上還有一個非常好的目標,binder 驅動會往用戶態進程映射只讀 page,里面的數據結構為 flat_binder_object,binder_transaction_buffer_release 里面會使用 flat_binder_object->handle,相關代碼如下:
首先通過 binder_get_node 查找 node,然后會調用 binder_put_node 減少 node 的引用計數,當 node 引用為0時會釋放 node。
由于 flat_binder_object 所在物理頁用戶態無法修改,所以可以保證這個流程的正確性,當我們只讀物理頁寫漏洞篡改 flat_binder_object->handle 指向另一個 node 時,觸發 binder_transaction_buffer_release 就能導致 node 引用計數不平衡
最后可以將漏洞轉換為 binder_node 的UAF,然后采用 CVE-2019-2205 的利用方式進行漏洞利用即可。
此外類似的漏洞在 2016 年就已經出現在高通 GPU 驅動中,CVE-2016-2067:
同樣的業務場景,也意味著同類型的漏洞也可能會產生
?
CVE-2021-28663
該漏洞是 Mali 驅動在管理 GPU 物理頁映射時導致的物理頁 UAF 漏洞,為了能夠理解該漏洞,首先需要對 Mali 驅動的相關代碼有所了解,上節提到 Mali 使用 kbase_va_region 對象表示物理內存資源,然后 CPU 用戶進程 和 GPU 可以按需映射,對物理內存進行訪問。
kbase_va_region 的創建位于 kbase_api_mem_alloc 接口,其關鍵代碼如下:
kbase_api_mem_alloc
kbase_mem_alloc(kctx, alloc->in.va_pages, alloc->in.commit_pages, alloc->in.extent, &flags, &gpu_va);
reg = kbase_alloc_free_region(rbtree, 0, va_pages, zone); // 分配reg
kbase_reg_prepare_native(reg, kctx, base_mem_group_id_get(*flags))
- reg->cpu_alloc = kbase_alloc_create(kctx, reg->nr_pages, KBASE_MEM_TYPE_NATIVE, group_id);
- reg->gpu_alloc = kbase_mem_phy_alloc_get(reg->cpu_alloc);
kbase_alloc_phy_pages(reg, va_pages, commit_pages) // 為 reg 分配物理內存
if *flags & BASE_MEM_SAME_VA
- kctx->pending_regions[cookie_nr] = reg;
- cpu_addr = vm_mmap(kctx->filp, 0, va_map, prot, MAP_SHARED, cookie); // 映射物理內存到 GPU 和 CPU 頁表
else
kbase_gpu_mmap(kctx, reg, 0, va_pages, 1) // 映射物理內存到 GPU 頁表
- 編輯 GPU 頁表插入映射
- atomic_inc(&alloc->gpu_mappings); // 增加 gpu_mappings 記錄其被 GPU 的引用情況
對于 BASE_MEM_SAME_VA
情況驅動會做特殊處理,SAME_VA 的意思是在映射 reg 時,GPU 和 CPU 的虛擬地址是一樣的,這樣可能是為了便于數據傳遞時,之間進行指針傳遞。
如果沒有設置 BASE_MEM_SAME_VA
則會之間將物理內存映射到 GPU,否則就會通過 vm_mmap --> kbase_mmap --> kbasep_reg_mmap 將物理內存以同樣的 VA 映射到 GPU 和 CPU 側。
兩者均是使用 kbase_gpu_mmap 將 reg 對應的物理內存映射到 GPU 的頁表中.
kbase_va_region 的釋放位于 kbase_api_mem_free 接口,其關鍵代碼如下:
kbase_api_mem_free
reg = kbase_region_tracker_find_region_base_address(kctx, gpu_addr);
err = kbase_mem_free_region(kctx, reg);
這個的大體邏輯是先根據 gpu_addr 找到 reg,然后釋放 reg 和 reg->xx_alloc 的引用,對于這種復雜的對象管理,可以先按照正常流程分析下對象之間的關系, kbase_va_region 中與生命周期相關的字段如下:
上圖表示的是 kbase_api_mem_alloc 創建非 SAME_VA 內存的場景,kbase_gpu_mmap 執行后會對 gpu_mappings 加一,然后通過 kbase_api_mem_free 釋放時,會將 kbase_va_region 和 kbase_mem_phy_alloc 的引用計數減成0,從而釋放兩個對象
如果是 SAME_VA 的情況如下,區別在于 SAME_VA 內存在 kbase_api_mem_alloc 中會調用 vm_mmap 把 reg 同時映射到 CPU 和 GPU 側,這就需要增加對應的引用計數(va_refcnt、kref、gpu_mappings),然后在 munmap 進程 VA 時,減少對應的引用計數
對驅動的對象管理有大概的認知后,具體看下漏洞相關的兩個接口 kbase_api_mem_alias 和 kbase_api_mem_flags_change,分別利用的功能:
- kbase_api_mem_alias:創建別名映射,即新分配一個 reg 與其他已有的 reg 共享 kbase_mem_phy_alloc
- kbase_api_mem_flags_change:釋放 kbase_mem_phy_alloc 中的物理頁
kbase_api_mem_alias 的關鍵代碼如下:
kbase_mem_alias
- reg = kbase_alloc_free_region(&kctx->reg_rbtree_same, 0, *num_pages, KBASE_REG_ZONE_SAME_VA);
- reg->gpu_alloc = kbase_alloc_create(kctx, 0, KBASE_MEM_TYPE_ALIAS,
- reg->cpu_alloc = kbase_mem_phy_alloc_get(reg->gpu_alloc);
- aliasing_reg = kbase_region_tracker_find_region_base_address( kctx, (ai[i].handle.basep.handle >> PAGE_SHIFT) << PAGE_SHIFT);
- alloc = aliasing_reg->gpu_alloc;
- reg->gpu_alloc->imported.alias.aliased[i].alloc = kbase_mem_phy_alloc_get(alloc);
- kctx->pending_regions[gpu_va] = reg;
主要是增加了 alloc 的引用計數 (kref),然后將其放入 kctx->pending_regions,之后進程再通過 mmap 完成 CPU 和 GPU 映射 (kbase_context_mmap
)
if (reg->gpu_alloc->type == KBASE_MEM_TYPE_ALIAS) {
u64 const stride = alloc->imported.alias.stride;
for (i = 0; i < alloc->imported.alias.nents; i++) {
if (alloc->imported.alias.aliased[i].alloc) {
err = kbase_mmu_insert_pages(kctx->kbdev,
&kctx->mmu,
reg->start_pfn + (i * stride),
alloc->imported.alias.aliased[i].alloc->pages + alloc->imported.alias.aliased[i].offset,
alloc->imported.alias.aliased[i].length,
reg->flags & gwt_mask,
kctx->as_nr,
group_id);
kbase_mem_phy_alloc_gpu_mapped(alloc->imported.alias.aliased[i].alloc);
}
}
創建別名映射進程調用 mmap 前后, reg 對象相關引用情況如下:
在 kbase_api_mem_alias 里面增加 aliased[i]->kref 確保其在使用過程中不會被釋放,然后 kbase_mmap 映射內存時增加 aliased[i]->gpu_mappings 記錄其被 GPU 映射的次數,同時增加 reg->va_refcnt 記錄其被 CPU 映射的次數,這個流程是沒有問題的,通過引用計數確保 aliased 中的對象不會釋放。
問題就出在 kbase_api_mem_flags_change 能在不釋放 alloc 時釋放其中的物理頁:
kbase_api_mem_flags_change 可以利用 kbase_mem_evictable_make 將 gpu_alloc 放到驅動自己管理的鏈表中(kctx->evict_list
)當內核指向 shrink 操作時驅動會釋放該鏈表上掛的所有 gpu_alloc。
kbase_mem_evictable_make
- kbase_mem_shrink_cpu_mapping(kctx, gpu_alloc->reg, 0, gpu_alloc->nents); // 移除 CPU 映射
- list_add(&gpu_alloc->evict_node, &kctx->evict_list); // 加到鏈表中
shrink 時釋放 kbase_mem_phy_alloc 物理頁的代碼:
kbase_mem_flags_change 在調用 kbase_mem_evictable_make 前會校驗 gpu_mappings ,大概意思是如果這個 reg 被 GPU 多次映射了就不能執行物理內存釋放操作,但是回到 alias 的流程,在 kbase_api_mem_alias 結束后,aliased 數組中的 gpu_mappings 還是 1
此時調用 kbase_mem_flags_change 將 aliased[i] 放到 kctx->evict_list,此時 alloc->pages 里面的值沒有變化
然后再調用 mmap 映射 kbase_mem_alias 創建的 reg 將 aliased[i] 中的物理頁(alloc->pages)映射到 GPU 側,假設為映射的 VA 為 ALIAS_VA
最后觸發 shrink 機制,釋放 aliased[i] 中的物理頁,之后 ALIAS_VA 還指向已經釋放的物理頁,導致物理頁 UAF.
再次回顧漏洞根因,漏洞是驅動在建立 別名映射時對 gpu_mappings 的管理不當,結合 kbase_api_mem_flags_change 釋放物理頁的邏輯,達成物理頁 UAF,這種漏洞的挖掘個人理解需要先分析內存對象(堆、物理內存)的生命周期,然后組合各個 API 的時序看是否會產生非預期行為,重點還是對象的釋放、申請、復制等邏輯。
物理頁 UAF 的漏洞利用技術目前已經比較成熟,這里列幾個常用的方式:
- 篡改進程頁表:通過 fork + mmap 大量分配進程頁表占位釋放的物理頁,然后通過 GPU 修改頁表實現任意物理內存讀寫
- 篡改 GPU 頁表:通過 GPU 驅動接口分配 GPU 頁表占位釋放的物理頁,然后通過 GPU 修改頁表實現任意物理內存讀寫
- 篡改內核對象:通過噴射內核對象(比如 task_struct、cred)占位,然后 GPU 修改對象實現利用
?
CVE-2022-46395
前面兩個漏洞的利用路徑大概是:發現一個新漏洞,挖掘一種新漏洞利用方式完成利用,本節這個漏洞則是將漏洞轉換為 CVE-2021-28663 ,因為 28663 的能力確實太強大了,物理頁 UAF 的利用簡單、直接,目前堆上的漏洞利用也逐步往物理頁UAF 轉換(Dirty Pagetable)
漏洞是一個條件競爭漏洞,kbase_vmap_prot 后其他線程可以釋放 mapped_evt 對應的物理頁
static int kbasep_write_soft_event_status(
struct kbase_context *kctx, u64 evt, unsigned char new_status)
{
...
mapped_evt = kbase_vmap_prot(kctx, evt, sizeof(*mapped_evt),
KBASE_REG_CPU_WR, &map);
if (!mapped_evt)
return -EFAULT;
*mapped_evt = new_status;
kbase_vunmap(kctx, &map);
return 0;
}
為了擴大 race 的時間窗,作者利用 timerfd 時鐘中斷技術
migrate_to_cpu(0);
int tfd = timerfd_create(CLOCK_MONOTONIC, 0);
int epfds[NR_EPFDS];
for (int i=0; i<NR_EPFDS; i++)
epfds[i] = epoll_create1(0);
for (int i=0; i<NR_EPFDS; i++) {
struct epoll_event ev = { .events = EPOLLIN };
epoll_ctl(epfd[i], EPOLL_CTL_ADD, fd, &ev);
}
timerfd_settime(tfd, TFD_TIMER_ABSTIME, ...);
ioctl(mali_fd, KBASE_IOCTL_SOFT_EVENT_UPDATE,...);
大致思路就是在 kbase_vmap_prot 和 *mapped_evt 之間出發時鐘中斷,從而擴大時間窗,在兩步之間釋放 mapped_evt 對應的物理頁,就能夠達到物理頁 UAF 的能力。
mapped_evt 在頁內的偏移可控,寫的內容為 0 或者 1,總結下來漏洞的原語是物理內存 UAF 寫,寫的值只能 0 或者 1
static inline struct kbase_mem_phy_alloc *kbase_alloc_create(
struct kbase_context *kctx, size_t nr_pages,
enum kbase_memory_type type, int group_id)
{
...
size_t alloc_size = sizeof(*alloc) + sizeof(*alloc->pages) * nr_pages;
...
if (alloc_size > KBASE_MEM_PHY_ALLOC_LARGE_THRESHOLD)
alloc = vzalloc(alloc_size);
else
alloc = kzalloc(alloc_size, GFP_KERNEL);
...
}
kbase_alloc_create 分配 kbase_mem_phy_alloc 時會調用 vzalloc 分配內存,vzalloc 的邏輯是根據大小計算分配的物理頁數目,然后逐次調用 alloc_page 分配物理頁,利用這個邏輯可以比較快速的占位剛剛釋放的物理頁(slab cross cache 時間相對較長)
根據之前的漏洞分析,我們知道 gpu_mappings 控制的物理頁的釋放,如果通過 UAF 將其修改為 0 或者 1,就能提前釋放一個被別名映射的 kbase_mem_phy_alloc 中的物理頁,導致物理頁UAF
struct kbase_mem_phy_alloc {
struct kref kref;
atomic_t gpu_mappings;
size_t nents;
struct tagged_addr *pages;
struct list_head mappings;
實現無限制的物理頁 UAF 讀寫后,就是常規的漏洞利用流程了。這個漏洞利用的核心是利用 GPU 驅動的物理內存管理結構,將一個受限的 UAF 寫轉化為 不受限的 物理頁 UAF.
?
前面的案例都是利用 GPU 自身漏洞,這個案例則是將其他驅動、模塊漏洞(攝像頭驅動的 堆溢出漏洞) 的漏洞 轉換為 GPU 漏洞,進而實現物理頁 UAF 漏洞,核心思路與 CVE-2022-46395 一致,就是篡改 kbase_mem_phy_alloc 的 gpu_mappings 為 0,構造物理頁 UAF
static inline struct kbase_mem_phy_alloc *kbase_alloc_create(
struct kbase_context *kctx, size_t nr_pages,
enum kbase_memory_type type, int group_id)
{
...
size_t alloc_size = sizeof(*alloc) + sizeof(*alloc->pages) * nr_pages;
...
alloc = kzalloc(alloc_size, GFP_KERNEL);
...
}
一個比較有意思的點是研究員發現及時安卓內核啟用了 MTE,仍然有 50% 的概率能夠完成溢出而不被檢測,且及時 MTE 檢測到溢出,也不會導致內核 Panic,只是殺掉用戶進程,這樣就給了攻擊者無限嘗試的能力,相當于 Bypass 了 MTE.
總結
從 CVE-2021-28663/CVE-2021-28664 開始研究人員逐漸重視并投入到 GPU 驅動安全領域,從一開始的挖掘 GPU 特有漏洞,到后面逐步將各種通用漏洞往 GPU 漏洞上轉換,核心原因在于 GPU 驅動本身的能力太強大了,只要能夠控制 GPU硬件的頁表,就能實現任意物理頁的讀寫,而且由于是獨立的硬件,可以直接 Bypass 掉 CPU 側的安全特性(比如 KNOX、PAC、MTE),Patch 內核代碼。
GPU 安全研究還帶來了另一個漏洞利用方向,GPU 驅動由于要管理物理內存,所以容易出現物理內存 UAF,物理 UAF 的利用手段被發掘后,大家發現這個原語實在太強大了,后面涌現了很多將不同漏洞轉換為物理頁UAF的技術,比如 Dirty Pagetable、USMA、 pipe_buffer->page 指針劫持等。
從 GPU 攻擊的路徑來看,也可以了解到一點,即漏洞的修復并不代表漏洞生命的結束,如果一個漏洞的原語過于強大、好用,就可以考慮將其他漏洞往這上面轉換,從而復用歷史的漏洞利用經驗。
?
參考鏈接
轉自博客園,作者hac425
該文章在 2024/12/16 9:37:05 編輯過