
iOS 커널캐시 리버스 엔지니어링을 위한 Ghidra 프레임워크
이 프레임워크는 커널캐시 리버스 엔지니어링 경험의 최종 결과물입니다. 저는 일반적으로 커널과 그 확장을 수동으로 감사하여 취약점을 찾고, 리버싱 과정을 가속화하기 위해 Ghidra에서 보고 싶었던 대부분의 작업을 자동화했습니다. 이는 효과적이며 많은 시간을 절약해 주었습니다. 이 프레임워크는 iOS 12/13/14/15 및 macOS 11/12(커널캐시 및 단일 KEXT 모두)에서 작동하며, 사용자가 자체 환경을 준비하는 번거로움 없이 iOS 커널 연구를 시작할 수 있도록 돕기 위해 공개되었습니다. 제 생각에, 이 프레임워크(제공하는 도구 세트와 IOKit에 대한 기본 지식만 있으면)는 커널캐시 해킹을 시작하기에 충분합니다.
프레임워크는 전적으로 Python으로 작성되었으며, 다른 도구를 구축하도록 확장할 수 있습니다. 거의 모든 프로젝트에서 사용할 수 있고 장황한 매뉴얼을 읽는 시간을 절약해 주는 기본 API를 제공합니다. 핵심 기능은 utils/ 디렉토리에서 확인할 수 있습니다.
Ghidra는 커널캐시 분석에 좋지만, 다른 리버스 엔지니어링 도구와 마찬가지로 수동 작업이 필요합니다. ghidra_kernelcache는 시작 시점과 리버스 엔지니어링 중에도 문제를 수정할 수 있는 좋은 진입점을 제공하여 보기 좋은 디컴파일러 출력을 제공합니다.
@_bazad가 IDAPro로 만든 유사한 프로젝트 ida_kernelcache가 있으며, 이는 IDA에서 커널 이미지로 작업하려는 연구자들에게 좋은 진입점을 제공합니다. 제 프레임워크는 Brandon의 작업과 약간 비슷하지만, 커널캐시 작업 과정을 덜 고통스럽게 만들기 위해 훨씬 더 많은 기능을 제공합니다.
::externalMethod() 및 ::getTargetAndMethodForIndex() 모두에 대한 외부 메서드 디스패치 테이블 자동 수정.이러한 기능은 별도의 도구로 만들어졌으며, 키 단축키를 사용하거나 도구 모음의 아이콘을 클릭하여 실행할 수 있습니다.
저장소를 클론합니다:```sh git clone https://github.com/0x36/ghidra_kernelcache.git
**중요 참고**: 이 프로젝트는 Ghidra 10.1_PUBLIC 및 10.2_DEV에서 테스트되었으며, 이전 버전과 호환되지 않습니다.
*`Windows → Script Manager`* 로 이동하여 *`스크립트 디렉터리`* 를 클릭한 다음 *`ghidra_kernelcache`* 를 디렉터리 경로 목록에 추가합니다.
*`Windows → Script Manager`* 로 이동하여 *스크립트* 목록에서 *`iOS→kernel`* 범주로 이동하고 거기에 표시된 플러그인을 체크하면 GHIDRA 툴바에 나타납니다.
[logos/](https://github.com/0x36/ghidra_kernelcache/tree/master/logos) 디렉터리에 각 도구에 대한 자체 로고를 넣을 수 있습니다.
## iOS kernelcache 심볼리케이션
`ghidra_kernelcache`는 첫 번째 단계에서 [iometa](https://github.com/Siguza/iometa/)([@s1guza](https://twitter.com/s1guza) 제작)를 필요로 합니다. iometa는 커널 바이너리에서 C++ 클래스 정보를 제공하는 강력한 도구로, 독립 실행형 바이너리로 작동하여 출력물을 구문 분석만 하면 원하는 RE 프레임워크로 가져올 수 있다는 큰 장점이 있습니다. 제 프레임워크는 iometa의 출력을 가져와 구문 분석하여 심볼리케이션(symbolicate)하고 가상 테이블을 수정합니다.
### 사용법
커널의 압축을 푼 후, 다음 명령어를 실행합니다 :```sh
$ iometa -n -A /tmp/kernel A10-legacy.txt > /tmp/kernel.txt
# if you want also to symbolicate using jtool2
$ jtool2 --analyze /tmp/kernel
Ghidra에서 kernelcache를 로드할 때, 일괄 가져오기를 사용하지 말고 Mach-O 이미지로 로드하세요.
Kernelcache가 로드되고 자동 분석이 완료되면, 도구 모음에 표시된 아이콘을 클릭하거나 Meta-Shift-K를 누른 다음, iometa 출력의 전체 경로(이 경우 /tmp/kernel.txt)를 입력하세요.
jtool2 심볼을 사용하려면, iOS→kernel 카테고리에 있는 jsymbol.py를 사용할 수도 있습니다.
전체 API 예제는 ghidra_kernelcache/kc.py에 있습니다.
→ 다음은 클래스 객체를 조작하는 몇 가지 예제입니다:```py from utils.helpers import * from utils.class import * from utils.iometa import ParseIOMeta
ff = "/Users/mg/ghidra_ios/kernel.txt" iom = ParseIOMeta(ff) Obj = iom.getObjects() kc = kernelCache(Obj)
kc.process_all_classes()
kc.process_classes_for_bundle("com.apple.iokit.IOSurface")
kc.process_classes_for_bundle("kernel")
kc.process_class("IOGraphicsAccelerator2")
kc.clear_class_structures()
kc.update_classes_vtable()
kc.explore_pac()
보시다시피 kernelcache를 전체 또는 부분적으로 심볼리케이션할 수 있습니다. 부분 심볼리케이션을 선택한 경우, `ghidra_kernelcache`는 진행 전에 모든 클래스 의존성을 자동으로 구성합니다. 전체 kernelcache에 대해 스크립트를 실행(전체 심볼리케이션)하면 `ghidra_kernelcache`가 커널 이미지를 분석하는 데 몇 분이 소요됩니다.
완료되면 Ghidra는 다음을 제공합니다:
→ "iOS"라는 새 카테고리가 북마크 필터(Bookmark Filter)에 추가됩니다:
<img src="https://raw.githubusercontent.com/0x36/ghidra_kernelcache/master/screenshots/image1.png" alt="이미지1" width="200"/>
→ IOKit 클래스 가상 테이블이 'iOS' 북마크에 추가되어 더 빠르고 효율적인 가상 테이블 검색이 가능합니다. 검색창에 문자, 단어 또는 kext 번들을 입력하여 kext나 클래스를 찾을 수 있습니다.
<img src="https://raw.githubusercontent.com/0x36/ghidra_kernelcache/master/screenshots/image2.png" alt="이미지2"/>
→ 가상 테이블 수정: 알 수 없는 코드를 디스어셈블/컴파일하고, 네임스페이스를 수정하며, 클래스 메서드를 다시 심볼리케이션하고 각 메서드에 함수 정의를 적용합니다:
<img src="https://raw.githubusercontent.com/0x36/ghidra_kernelcache/master/screenshots/image3.png" alt="이미지3"/>
→ 클래스 네임스페이스를 생성하고 각 메서드를 해당 네임스페이스에 배치합니다:
<img src="https://raw.githubusercontent.com/0x36/ghidra_kernelcache/master/screenshots/image4.png" alt="이미지4"/>
→ 클래스 계층 구조를 고려한 클래스 구조를 생성합니다:
<img src="https://raw.githubusercontent.com/0x36/ghidra_kernelcache/master/screenshots/image5.png" alt="이미지5"/>
→ 클래스 vtable을 생성하고, 각 메서드에는 더 나은 디컴파일 출력을 위한 자체 메서드 정의가 포함됩니다:
<img src="https://raw.githubusercontent.com/0x36/ghidra_kernelcache/master/screenshots/image6.png" alt="이미지6"/>
전체 구현은 [`utils/class.py.`](https://github.com/0x36/ghidra_kernelcache/blob/master/utils/class.py)에서 확인할 수 있습니다.
다음은 `ghidra_kernelcache`를 사용한 심볼리케이션 전후의 스크린샷입니다:
<img src="https://raw.githubusercontent.com/0x36/ghidra_kernelcache/master/screenshots/image7.png" alt="이미지7"/>
<img src="https://raw.githubusercontent.com/0x36/ghidra_kernelcache/master/screenshots/image8.png" alt="이미지8"/>
## macOS Kext 심볼리케이션
---
`ghidra_kernelcache`의 macOS 지원은 ARM64e 및 x86_64 아키텍처에서 kernelcache와 단일 KEXT 심볼리케이션 모두를 위한 것입니다.
**중요:** 현재 시점에서 Ghidra는 전체 macOS kernelcache를 파싱할 수 없지만, IDA에 로드하여 초기 분석을 수행한 후 데이터베이스(idb를 xml로)를 Ghidra로 가져오는 것은 가능합니다. 그러나 이는 범위를 벗어납니다. 직접 처리할 수 있다면 `ghidra_kernelcache`가 나머지를 처리합니다.
macOS 커널 익스텐션을 심볼리케이션하기 전에 몇 가지 단계를 거쳐야 합니다. `ghidra_kernelcache`의 주요 목표는 클래스 계층 구조를 재구성하고 모든 클래스 구조를 단일 데이터베이스로 관리하는 것이기 때문에, 커널 익스텐션만으로는 이러한 요구 사항을 충족할 수 없습니다. 즉, 단일 KEXT의 심볼리케이션은 커널의 심볼리케이션과 해당 KEXT가 의존하는 다른 커널 익스텐션의 심볼리케이션이 필요하므로 추가 작업이 필요합니다.
`ghidra_kernelcache`는 이제 Ghidra의 강력한 *DataType Project Archive*를 통해 클래스 구조와 가상 메서드 정의를 관리하고 공유함으로써 커널을 포함한 커널 익스텐션을 심볼리케이션하는 강력한 방법을 제공합니다.
### 커널 익스텐션 심볼리케이션 단계
- Ghidra 프로젝트에 새 폴더를 만든 다음, ` /System/Library/Kernels/kernel.release.XXXXX`를 해당 폴더에 로드하고 Ghidra가 이를 분석하도록 합니다.
- 새 *Project Archive*를 만듭니다: `DataType Provider`로 이동 → 창의 오른쪽 상단에 있는 화살표 클릭 → `New Project Archive` → 새로 만든 폴더 안에 배치 → 적절한 이름으로 지정합니다(예: macOS_12.1).
- 이제 `ghidra_kernelcache`를 사용하여 커널을 심볼리케이션합니다. 이 과정은 iOS *kernelcache* 심볼리케이션과 매우 유사합니다.```bash
$ iometa -n -A /System/Library/Kernels/kernel.release.t8101 > /tmp/kernel.txt
KM.py 스크립트에서 찾을 수 있습니다:```pyfrom utils.helpers import * from utils.kext import * iom = ParseIOMeta("/tmp/kernel.txt") Obj = iom.getObjects() kc = Kext(Obj,shared_p="macOS_12.1") kc.process_kernel_kext()
- 완료되면, 커널 아카이브와 `macOS_12.1` 아카이브 간의 데이터베이스 연결이 생성됩니다. 이제 `kernel.release.t8001`을 마우스 오른쪽 버튼으로 클릭하고 → `Commit DataTypes To` → `macOS_12.1`을 선택합니다.
- 그런 다음 `마우스 오른쪽 버튼 클릭` →`Select All` → `Commit`.
- 프로젝트 아카이브 저장 : `마우스 오른쪽 버튼 클릭` → `Save Archive`.
방금 모든 커널 확장 간에 공유할 수 있는 프로젝트 아카이브를 생성했습니다.
Apple Silicon용 `IOSurface` Kext의 예를 들어보겠습니다.```bash
$ lipo /System/Library/Extensions/IOSurface.kext/Contents/MacOS/IOSurface -thin arm64e -output /tmp/iosurface.arm64e
$ iometa -n -A /tmp/iosurface.arm64e > /tmp/iosurface.txt
macOS_12.1을 로드합니다: Data Type Manager → Open Project Archive로 이동한 다음 macOS_12.1을 선택합니다KM.py에서 찾을 수 있습니다:```python
from utils.helpers import *
from utils.kext import *kc = Kext(Obj,shared_p="macOS_12.1")
kc.depac()
kc.process_kernel_kext()
**중요 참고사항**: 때때로 `kc.process_kernel_kext()`가 Ghidra가 일부 C++ 심볼을 디맨글링하지 못해 실패합니다. 이 문제를 해결하려면 스크립트 관리자로 이동하여 `DemangleAllScript.java` 스크립트를 실행한 다음 `kc.process_kernel_kext()`를 다시 시작하십시오.
### 사용자 정의 클래스
일부 C++ 클래스가 `ghidra_kernelcache` 및 `iometa`에서 심볼리케이트할 수 없는 경우가 있어, 이를 처리하기 위해 새로운 기능이 추가되었습니다.
`Custom()` 클래스 재구성은 모든 `::vtable` 심볼을 순회하며 해당 클래스가 이미 정의되었는지 확인하고, 정의되지 않은 경우 자동으로 클래스 구조, 식별된 각 클래스 메서드에 대한 함수 정의, 네임스페이스 및 각 클래스에 대한 가상 함수 테이블을 생성합니다.
현재 사용자 정의 클래스 생성은 macOS에서만 지원됩니다.```bash
$ iometa -n -A /System/Library/Kernels/kernel.release.t8101 > /tmp/kernel.txt
$ iometa -n -A <kext_path> >> /tmp/kernel.txt
from utils.helpers import * from utils.custom_kc import *
if name == "main": default = "/tmp/kernel.txt" ff = askString("iometa symbol file","Symbol file: ",default) iom = ParseIOMeta(ff) Obj = iom.getObjects()
kc = Custom(Obj)
kc.process_all_classes()
kc.explore_pac()
## Miscellaneous scripts
---
### KDK의 Dwarf4 가져오기
Ghidra가 해당 `.dsym` 디렉토리를 로드하지 못하는 문제가 있어 이를 해결하기 위한 작은 스크립트를 만들었습니다. [여기](https://github.com/0x36/ghidra_kernelcache/blob/master/dwarf4_fix.py)에서 찾을 수 있습니다.
**사용법** : KDK 경로에서 커널을 로드하고, Ghidra가 분석을 완료할 때까지 기다린 후 `dwarf_fix.py`를 실행하면 심볼을 로드하며, 이 과정은 몇 분 정도 걸릴 수 있습니다.
<img src="https://raw.githubusercontent.com/0x36/ghidra_kernelcache/master/screenshots/image12.png" alt="image12"/>
### 가상 메서드 호출 참조 해결
`ghidra_kernelcache`는 `kernelCache.explore_pac` 또는 `fix_extra_refs`를 통해 가상 호출을 해결하는 두 가지 방법을 제공합니다.
**kernelCache.explore_pac()**
arm64e 바이너리를 작업 중인 경우, `ghidra_kernelcache`는 `Pointer Authentication Code` 값을 찾아 가상 메서드 호출을 인식할 수 있습니다. 이 과정은 간단하며, `fix_extra_refs()`와 달리 `kernelCache.explore_pac`은 `Pcode` 또는 `varnode` 식별에 의존하지 않고, 프로그램의 모든 명령어를 반복하며 `MOVK` 명령어를 검색하고 두 번째 피연산자를 가져와 데이터베이스에서 해당 값을 찾습니다.
사용법 : **kernelCache**, **Kext** 또는 **Custom**을 통해 *KernelCache* 인스턴스를 생성한 후 `explore_pac()` 메서드를 호출하세요.```py
from utils.helpers import *
from utils.kext import *
if __name__ == "__main__":
default = "/tmp/kernel.txt"
ff = askString("iometa symbol file","Symbol file: ",default)
iom = ParseIOMeta(ff)
Obj = iom.getObjects()
kc = Kext(Obj)
kc.explore_pac()
fix_extra_refs()
이 함수는 기본 데이터 흐름 분석을 기반으로 모든 가상 호출 메서드를 찾고 그 구현을 자동으로 해결합니다. 모든 아키텍처에서 작동하며 디컴파일러 출력에서 소스 데이터 타입을 인식하고 함수 내의 모든 가상 호출 참조를 해결할 수 있습니다. 따라서 사용자는 수동으로 찾지 않고도 구현으로 바로 앞/뒤로 이동할 수 있습니다.
fix_extra_refs가 제공하는 가장 유용한 기능은 실행할 때마다 참조를 동기화된 상태로 유지한다는 것입니다. 예를 들어, 변수 데이터 타입을 클래스 데이터 타입으로 변경하면 fix_extra_refs가 자동으로 변경을 인식하고 모든 호출 사이트를 재귀적으로 탐색하여 참조를 해결하며, 호출 사이트 큐가 빌 때만 중지됩니다.
fix_extra_refs가 제공하는 다른 기능들도 있습니다:
_ptmf2ptf() 호출을 자동으로 감지하고 오프셋과 전체 함수 주소 모두에 대한 호출 메서드를 해결합니다.구현은 utils/references.py에서 확인할 수 있습니다. fix_extra_refs는 pcode 연산을 파싱하고 CALLIND 및 CALL opcode를 찾은 다음, 연산에 관련된 모든 varnodes를 가져옵니다. Varnode 정의가 식별되면 해당 HighVariable을 검색하여 클래스 객체 유형을 식별합니다. 유형이 알 수 없는 경우(즉, 클래스 구조로 보이지 않는 경우) 무시하고, 그렇지 않으면 클래스 이름을 가져와 가상 호출 테이블을 조회한 후 Varnode가 제공한 오프셋을 사용하여 올바른 가상 메서드 호출을 가져와 호출 명령어에 참조를 추가합니다.```py
fix_extra_refs(toAddr(address))
Here is an output example of using `fix_extra_refs` :
<img src="https://raw.githubusercontent.com/0x36/ghidra_kernelcache/master/screenshots/image9.png" alt="image9"/>
수동 수정 없이 **IOService::isOpen()**, **OSArray:getNextIndexOfObject()** 및 **IOStream::removeBuffer()** 가상 호출을 성공적으로 해결했음을 주목하세요.
다음으로, `fix_extra_refs`는 **IOStream::removeBuffer()**를 디컴파일하고, 이 메서드의 모든 HighVariables를 가져온 다음 이전 메서드와 마찬가지로 그들의 참조를 해결합니다... 그리고 계속됩니다.
<img src="https://raw.githubusercontent.com/0x36/ghidra_kernelcache/master/screenshots/image10.png" alt="image10"/>
### 외부 메서드 테이블 자동 수정
모든 연구자가 이 부분을 처리하기 위한 스크립트를 가지고 있다고 생각합니다. IOKit의 주요 공격 표면이기 때문에 수동으로 처리하는 것은 부담이며, 연구자가 여러 외부 메서드 테이블을 파고들고 싶어하는 방식으로 자동화되어야 합니다.
`ghidra_kernelcache`에 의해 제공되는 두 가지 스크립트가 있습니다: **fix_methodForIndex.py** 및 **fix_extMethod.py.** 위에 표시된 것처럼 다른 스크립트와 마찬가지로 활성화할 수 있습니다.
***사용법***: 커서를 외부 디스패치 테이블의 시작 부분에 놓고 스크립트를 실행합니다: 대상과 선택기 수를 제공합니다.
예: `IOStreamUserClient::getTargetAndMethodForIndex()` :
<img src="https://raw.githubusercontent.com/0x36/ghidra_kernelcache/master/screenshots/image11.png" alt="image11"/>
### namespace.py : 메서드 네임스페이스 수정…
이 스크립트는 발견된 모든 메서드에 클래스 유형을 채우는 유용한 스크립트이며, 호출된 함수를 재귀적으로 탐색하고 참조를 해결하기 위해 `extra_refs.py` 스크립트의 종속성입니다.
***사용법***: 원하는 함수의 디컴파일러 출력에 커서를 놓고 도구 모음에서 스크립트를 실행하거나 **Meta-Shift-N**을 누릅니다.
### 심볼 이름 및 유형 전파
`ghidra_kernelcache`는 기본 Pcode 연산에 대한 유형 전파 지원을 제공하지만, 복잡한 캐스팅을 사용하는 일부 변수에서는 실패할 가능성이 있습니다.
누군가 도움을 주고 싶거나 Ghidra에서 저수준 작업을 시작하고 싶다면, 이것이 그 기회입니다.
구현은 [ghidra_kernelcache/propagate.py](https://github.com/0x36/ghidra_kernelcache/blob/master/propagate.py)에서 확인할 수 있습니다.
### 함수 시그니처 로드
Ghidra에서 C++ 헤더 파일을 구문 분석하는 것은 불가능하며, kernelcache에 커널 함수 시그니처를 갖는 것은 디컴파일러 출력에서 많은 것을 개선할 수 있습니다.
예를 들어, `virtual IOMemoryMap * map(IOOptionBits options = 0 );`를 추가했다고 가정하면, Ghidra는 함수 정의와 함수 시그니처 모두에 대해 반환 값을 `IOMemoryMap` 포인터로 자동으로 다시 유형 지정합니다.
구문을 준수하여 **signatures/** 디렉토리에 C++ 심볼을 추가할 수 있으며, 이 디렉토리에서 정의된 함수 시그니처를 찾을 수 있습니다.```c++
// Defining an instance class method
IOMemoryDescriptor * withPersistentMemoryDescriptor(IOMemoryDescriptor *originalMD);
// Defining a virtual method, it must start with "virtual" keyword
virtual IOMemoryMap * createMappingInTask(task_t intoTask, mach_vm_address_t atAddress, IOOptionBits options, mach_vm_size_t offset = 0, mach_vm_size_t length = 0);
// Defining a structure
struct task_t;
// typedef'ing a type
typedef typedef uint IOOptionBits;
// Lines begining with '//' are ignored
사용법: 커널을 심볼화한 후에는 load_sigatnatures.py 스크립트를 실행하여 사용 가능한 모든 함수 서명을 로드하는 것이 좋습니다. 대부분의 이전 도구와 마찬가지로, 이 스크립트는 도구 모음에 추가하거나 플러그인 관리자에서 실행하거나 Meta-Shift-S를 눌러 실행할 수 있습니다.
이 스크립트는 간단합니다. SourceType.USER_DEFINED로 표시된 모든 구조체, 클래스, typedef, 함수 정의 등을 이전 프로젝트에서 새 프로젝트로 가져옵니다.
사용법: 동일한 도구에서 이전 및 새 Ghidra 프로젝트를 열고, load_structs.py 스크립트로 이동하여 src_prog_string 변수에 이전 프로그램 이름을, dst_prog_string 변수에 새 프로그램 이름을 입력한 후 스크립트를 실행합니다.
이 프로젝트가 흥미롭다고 생각하고 기여하고 싶다면 PR을 보내주세요. 검토하겠습니다. 한편, 다음 영역에서 기여를 기대합니다: