
編譯和運行example/open1_hook.c後,內核日誌的輸出
Xnuspy是一個安裝新系統調用xnuspy_ctl Pongoos模塊,它允許您從用戶空間掛起內核函數。它支持iOS 13.X,iOS 14.X和iOS 15.x在CheckRA1N 0.12.2及以上。不支持4K設備。
該模塊完全中絕化了KTRR/KPP,並可以在EL1內部創建RWX內存。不要在日常駕駛員上使用它。
需要libusb : brew install libusb
在頂級目錄中運行make 。它將構建加載程序和模塊。
在make之前添加這些。
XNUSPY_DEBUG=1kprintf )。XNUSPY_SERIAL=1IOLog 。XNUSPY_LEAKED_PAGE_LIMIT=n64 。在調試內核恐慌下可以找到更多信息。XNUSPY_TRAMP_PAGES=n XNUSPY_DEBUG和XNUSPY_SERIAL不依賴彼此。
建造了所有內容後,請checkra1n將設備啟動到pongo shell: /Applications/checkra1n.app/Contents/MacOS/checkra1n -p
在同一目錄中,您構建了加載程序和模塊,請執行loader/loader module/xnuspy 。執行此操作後,Xnuspy將完成它的事情,並在幾秒鐘內您的設備啟動。如果需要利用SEPROM,則loader將在發出xnuspy-getkernelv之後再等待幾秒鐘。
有時,在Checkra1n的KPF運行後,我的幾個電話會陷入“啟動”。我還沒有弄清楚原因是什麼,但是如果發生這種情況,請重試。另外,如果設備在bootx後懸掛,請重試。最後,在我的iPhone X運行iOS 13.3.1上標記xnuspy_ctl代碼為可執行文件是有點斑點的,但在其他手機上有100%的時間成功。如果您在執行鉤程序時使用內核指令進行暫停,請重試。
Xnuspy將修補一個enosys系統調用,以指向xnuspy_ctl_tramp 。這是一個小型蹦床,將編譯的xnuspy_ctl代碼標記為可執行文件並分支到它。您可以在module/el1/xnuspy_ctl/xnuspy_ctl.c和example目錄中的示例中找到xnuspy_ctl的實現。
內部include/xnuspy/是xnuspy_ctl.h ,該標頭定義了xnuspy_ctl的常數。它旨在將其包含在所有連接內核功能的程序中。
您可以使用sysctlbyname弄清楚修補了哪個系統調用:
size_t oldlen = sizeof(long);
long SYS_xnuspy_ctl = 0;
sysctlbyname("kern.xnuspy_ctl_callnum", &SYS_xnuspy_ctl, &oldlen, NULL, 0);
該系統呼叫需要四個論點, flavor , arg1 , arg2和arg3 。味道可以是XNUSPY_CHECK_IF_PATCHED , XNUSPY_INSTALL_HOOK ,Xnuspy_register_death_callback, XNUSPY_REGISTER_DEATH_CALLBACK , XNUSPY_CALL_HOOKME ,xnuspy_kread,xnuspy_kread,, XNUSPY_CACHE_READ , XNUSPY_KREAD ,或XNUSPY_GET_CURRENT_THREAD XNUSPY_KWRITE接下來的三個論點的含義取決於風味。
XNUSPY_CHECK_IF_PATCHED存在此問題,因此您可以檢查是否存在xnuspy_ctl 。用這種風味調用它會導致它返回999 。其他參數的值被忽略了。
XNUSPY_INSTALL_HOOK我設計了這種風味以匹配MSHookFunction的API。 arg1是您希望掛接的內核函數的不便地址。如果您提供滑動地址,您很可能會恐慌。 arg2是指向您與ABI兼容的替換功能的指針。 arg3是xnuspy_ctl copyout代表原始內核函數的蹦床地址的指針。如果您不打算調用原件,則可以NULL 。
XNUSPY_REGISTER_DEATH_CALLBACK這種口味使您可以註冊可選的“死亡回調”,當您的掛鉤程序退出時,功能Xnuspy會打電話。它使您有機會清理從內核鉤中創建的任何東西。如果您創建了任何內核線程,則會告訴他們在此功能中終止。
您的回調不會異步調用,因此,如果阻止,則可以防止Xnuspy的垃圾收集線程執行。
arg1是指您回調功能的指針。其他參數的值被忽略了。
XNUSPY_CALL_HOOKME hookme是一個小型組件存根,Xnuspy通過Xnuspy Cache出口供您掛鉤。使用這種風味調用xnuspy_ctl會導致hookme被調用,為您提供一種輕鬆獲得內核代碼執行而無需連接實際內核函數的方法。
arg1是一個論點,將在調用Hookme時傳遞給hookme 。這可能是NULL 。
XNUSPY_CACHE_READ這種味道為您提供了一種從Xnuspy Cache中閱讀的方法。它包含許多有用的東西,例如kprintf , current_proc , kernel_thread_start ,一些libc函數和內核幻燈片,因此您不必自己找到它們。有關緩存ID的完整列表,請查看example/xnuspy_ctl.h 。
arg1是xnuspy_ctl.h和arg2中定義的緩存ID之一,是xnuspy_ctl copyout請求的地址或值的指針。其他參數的值被忽略了。
XNUSPY_KREAD這種口味為您提供了一種簡單的方法,可以在沒有TFP0的情況下從用戶空間讀取內核內存。
arg1是一個內核虛擬地址, arg2是用戶空間緩衝區的地址,而arg3是該用戶空間緩衝區的大小。 arg3字節將從arg1到arg2編寫。
XNUSPY_KWRITE這種口味為您提供了一種簡單的方法,可以在沒有TFP0的情況下從用戶空間寫入內核內存。
arg1是一個內核虛擬地址, arg2是用戶空間緩衝區的地址,而arg3是該用戶空間緩衝區的大小。 arg3字節將從arg2到arg1編寫。
XNUSPY_GET_CURRENT_THREAD這種風味為用戶空間提供了調用線程的內核地址。
arg1是xnuspy_ctl copyout current_thread的返回值的指針。其他參數的值被忽略了。
對於除XNUSPY_CHECK_IF_PATCHED以外的所有口味,成功返回0 。錯誤後,返回-1並設置errno 。 XNUSPY_CHECK_IF_PATCHED不會返回任何錯誤。 XNU的mach_to_bsd_errno用於將kern_return_t轉換為適當的errno 。
XNUSPY_INSTALL_HOOK有關的錯誤errno設置為...
EEXIST if:arg1表示的不便內核函數已經存在一個鉤子。ENOMEM if:unified_kalloc返回NULL 。ENOSPC如果:xnuspy_tramp結構,Xnuspy內部的數據結構。除非您同時掛起數百個內核功能,否則這不應該發生。如果您需要更多功能掛鉤,請查看限制。ENOTSUP如果:ENOENT :mh_for_addr無法確定與呼叫者地址空間內arg2相對應的MACH-O標頭。EFAULT如果:EIO if:mach_make_memory_entry_64沒有返回確定的Mach-O標題的__TEXT和__DATA段的整個內存條目。 errno還取決於vm_map_wire_external , mach_vm_map_external , mach_make_memory_entry_64 , copyin , copyout和如果適用的vm_map_wire_external的返回值。
如果此口味返回錯誤,則目標核函數不會掛鉤。如果您通過了arg3的NULL指針,則可能已經初始化了也可能沒有初始化。如果是的話,它是不安全的。
XNUSPY_REGISTER_DEATH_CALLBACK有關的錯誤errno設置為...
ENOENT :如果此口味返回錯誤,則您的死亡回調未註冊。
XNUSPY_CALL_HOOKME有關的錯誤errno設置為...
ENOTSUP如果:hookme離包含xnuspy_tramp結構的內存太遠。這是在Pongoos內部確定的,只有在Xnuspy不得不退回到kernelcache內部的未使用的代碼中,才能發生。在這種情況下,幾乎可以肯定會引起hookme恐慌,您必須找出另一個內核功能要鉤。如果這種口味返回錯誤,則不會調用hookme 。
XNUSPY_CACHE_READ有關的錯誤errno設置為...
EINVAL if:arg1表示的常數並不表示緩存中的任何內容。arg1是IO_LOCK ,但內核是iOS 14.4.2或以下或iOS 15.x。arg1是IPC_OBJECT_LOCK ,但內核是iOS 15.x。arg1是IPC_PORT_RELEASE_SEND ,但內核是iOS 14.5或更高。arg1是IPC_PORT_RELEASE_SEND_AND_UNLOCK ,但內核是iOS 14.4.2或以下。arg1是KALLOC_CANBLOCK ,但內核是iOS 14.x或更高。arg1是KALLOC_EXTERNAL ,但內核是iOS 13.x。arg1是KFREE_ADDR ,但內核是iOS 14.x或更高。arg1是KFREE_EXT ,但內核是iOS 13.x。arg1是PROC_REF ,但內核是iOS 14.8或以下。arg1是PROC_REF_LOCKED ,但內核是iOS 15.x。arg1是PROC_RELE ,但內核是iOS 14.8或以下。arg1已被PROC_RELE_LOCKED ,但內核是iOS 15.x。arg1是VM_MAP_UNWIRE ,但內核是iOS 15.x。arg1為VM_MAP_UNWIRE_NESTED ,但內核是iOS 14.8或以下。 errno還取決於copyout的返回值,如果適用,則是一次性初始化函數的返回值。
如果此口味返回錯誤,則您通過arg2傳遞的指針不會初始化。
XNUSPY_KREAD和XNUSPY_KWRITE有關的錯誤errno設置為...
EFAULT如果:arg1或arg2的地址翻譯失敗。如果您使用XNUSPY_DEBUG=1編譯,則將有關它的消息打印到內核日誌。如果此口味返回錯誤,則不會讀取/編寫內核內存。
XNUSPY_GET_CURRENT_THREAD有關的錯誤如果copyout失敗,則將errno設置為其返回值。
在編寫替換功能時,很容易忘記我正在編寫內核代碼。當您寫鉤子時,這是要記住的幾件事:
__TEXT段之外的用戶空間代碼。例如,如果您不小心致電printf而不是kprintf ,則會感到恐慌。如果該功能尚未通過XNUSPY_CACHE_READ可用,則需要重新實現您要調用的任何LIBC函數。但是,您可以為其他內核函數創建功能指針並將其稱為。PAGE_SIZE擴展到vm_page_size ,而不是常數。在閱讀此變量之前,您需要禁用PAN(在A10+上,我也不建議這樣做),否則您會感到恐慌。-fno-stack-protector編譯代碼,並且-D_FORTIFY_SOURCE=0在某些情況下,該設備必須通過確定另一個用戶空間指針來讀取___stack_chk_guard ,這會在A10+上恐慌。還建議瀏覽https://developer.apple.com/library/archive/documentation/darwin/conceptual/kernelprogramming/style/style/style.html。
編寫代碼時,錯誤是不可避免的,因此最終您將引起內核恐慌。恐慌並不一定意味著Xnuspy有一個錯誤,因此在打開問題之前,請確保您在撥打原始功能並返回其值(如果需要的話)時仍然恐慌。如果您仍然慌張,那麼它可能是一個Xnuspy錯誤(請打開問題),但是如果沒有,則替換有問題。
由於Xnuspy實際上並未將執行重定向到EL0頁,因此調試恐慌並不那麼簡單。打開module/el1/xnuspy_ctl/xnuspy_ctl.c ,然後在xnuspy_install_hook中唯一呼叫kwrite_instr之前,將呼叫添加到IOSleep幾秒鐘。這樣做是為了確保設備恐慌之前有足夠的時間進行日誌傳播。重新編譯Xnuspy用XNUSPY_DEBUG=1 make -B並再次加載模塊。加載模塊後,如果還沒有,請從klog/中編譯klog 。將其上傳到您的設備並執行stdbuf -o0 ./klog | grep shared_mapping_kva 。再次運行您的鉤程序,並觀看klog的一行,看起來像這樣:
shared_mapping_kva: dist 0x7af4 uaddr 0x104797af4 umh 0x104790000 kmh 0xfffffff00c90c000
如果您要安裝多個鉤子,則會發生多個掛鉤。在這種情況下, dist和uaddr會有所不同,但是umh和kmh不會。 kmh指向了內核映射您程序__TEXT段的開始。將您的掛鉤程序扔進您喜歡的拆卸器中並重新置換,以便其Mach-O標頭處於kmh的地址。對於IDA Pro,這是Edit -> Segments -> Rebase program... Image base燃燒。在設備再次重新啟動後,如果有與恐慌日誌中內核映射相對應的地址,則它們將與拆卸匹配。如果沒有,那麼您的替換內部可能會有某種微妙的內存損壞。
Xnuspy也無法知道內核線程是否仍在卸載掛鉤後,在插入程序的__TEXT段的內核映射上是否仍在執行(或將執行)。 Xnuspy所做的一件事是,在您的掛鉤程序死亡之後,不要立即對此映射進行處理。相反,它被添加到隊列的盡頭。一旦Xnuspy的垃圾收集線程注意到在該隊列中保存價值多少頁映射的設定限制,它將開始從隊列的前部進行交易,並將繼續持續到該限制不再超過該限制。默認情況下,此限制為1 MB或64頁。
儘管這確實有幫助,但鉤程序的__TEXT和__DATA段就越大,Xnuspy贏得這場比賽的可能性就越小。如果您定期感到恐慌並且有一個大掛鉤程序,請嘗試通過在make前添加XNUSPY_LEAKED_PAGE_LIMIT=n來增加此限制。這將把此限制設置為n頁,而不是64頁。
Xnuspy保留其xnuspy_tramp structs XNU靴子之前的一頁靜態內存存儲器,使您同時掛在225個內核函數上。如果需要更多,則可以在make前添加XNUSPY_TRAMP_PAGES=n 。這將告訴Xnuspy為xnuspy_tramp結構保留靜態內存的n頁。但是,如果Xnuspy必須退回到內核內部已經未使用的代碼,那麼這將被忽略。當這種情況發生時,它的工作方式將詳細介紹。
由於某種原因,來自os_log_with_args的日誌不會顯示在命令行工具oslog輸出的流中。來自kprintf的日誌也不會這樣做,但是可以用dmesg看到它們。但是, dmesg不是現場供稿,因此我寫了klog ,該工具是實時顯示kprintf登錄的工具。在klog/中找到它。我強烈建議您使用它,而不是將dmesg發送給您的kprintf消息。
如果您獲得open: Resource busy運行klog後的資源忙碌,請運行此命令launchctl unload /System/Library/LaunchDaemons/com.apple.syslogd.plist ,然後重試。
不幸的是,如果在XNU的Bootargs中設置了atm_diagnostic_config=0x20000000 ,則將無法看到任何NSLog 's。 klog取決於存在此引導參數。如果您想要NSLog返回,請從pongo_send_command inide loader.c中刪除該啟動參數。
Xnuspy將為您管理。一旦流程退出,該過程安裝的所有內核掛鉤都將在一秒鐘左右的時間內卸載。
大多數功能掛鉤框架具有一定的最小長度,使給定的功能可鉤。僅當您打算調用原始功能並且掛鉤功能的第一個指令不是B ,Xnuspy才有此限制。在這種情況下,最小長度為八個字節。否則,沒有最小長度。
Xnuspy使用X16和X17作為其蹦床,因此,期望這些函數的內核函數無法鉤住函數呼叫(沒有多少人期望這一點)。如果您要掛鉤的功能以BL開頭,並且您打算調用原始功能,則只有執行原始函數不修改X17 ,才可以這樣做。
xnuspy_ctl將在新啟動後首次進行一次性初始化。這是Xnuspy的唯一一部分,它是可種族的,因為我無法靜態地初始化我使用的讀/寫鎖。在第一個呼叫返回後,任何將來的呼叫都將是線程安全的。
這是簡化的,但它很好地捕捉了主要思想。 Xnuspy中的函數鉤是一個位於可寫的可執行內核內存上的結構。在大多數情況下,這是由Pongoos內部的alloc_static返回的內存。它可以歸結為:
struct {
uint64_t replacement;
uint32_t tramp[2];
uint32_t orig[10];
};
如果replacement是替換功能的內核虛擬地址(在後面詳細說明), tramp是一個小型蹦床,將執行重新引導到replacement中,而orig是代表原始功能的較大,更複雜的蹦床。
Xnuspy所做的第一件事之一是確定EL0替換位於呼叫過程的地址空間內的位置。這樣做是為了使內核函數可以從動態庫中掛鉤。與該替換的地址相對應的MACH-O標頭被保存。
之後,創建了該標題的__TEXT和__DATA段(以及這些之間的任何段(如果有)之間的任何段)的共享用戶內核映射。共享__TEXT ,因此您可以從鉤子中調用其他功能。 __DATA共享,因此EL1和EL0都可以看到全局變量的變化。
由於此映射是__TEXT和__DATA的一對一副本,因此很容易找出用戶替換功能的地址。給定調用過程的MACH -O標頭u的地址,共享映射k的開始的地址以及用戶替換功能r的地址r,我們應用以下公式: replacement = k + (r - u)
之後, replacement是用戶在共享映射上替換功能的內核虛擬地址,並寫入功能鉤結構。 Xnuspy不會將執行重新指導到替換功能的EL0地址,因為這是極其不安全的:這不僅使我們受到調度程序的擺佈,而且無法控制kernel掛鉤dies的過程,而kernel掛鉤dies的過程仍在替換率上仍在執行。
最後,共享映射標記為可執行文件,並組裝無條件的直接分支( B )。它將執行力定向到tramp的開始,是替代現在鉤的內核功能的第一個指令。不幸的是,這限制了我們從分支到鉤結構遠離給定核函數超過128 MB的鉤子。 Xnuspy在啟動之前確實檢查了此方案,並落入了kernelcache中的未使用的代碼,以便如果發現可能發生這種情況,則可以將鉤子結構固定在上面。
我會盡力確保補丁程序工作,因此,如果某件事不起作用,請打開問題。