iOS 디버깅 우회

모바일 2022. 5. 14. 15:05

https://blog.naver.com/gigs8041/222101950710

 

[iOS][FRIDA] Anti Debugging Bypass

지난번에 안드로이드 안티 디버깅 우회에 대해서 포스팅한 적이 있다. https://blog.naver.com/gigs8041/22...

blog.naver.com

 

 

지난번에 안드로이드 안티 디버깅 우회에 대해서 포스팅한 적이 있다.

https://blog.naver.com/gigs8041/222005876208

이번에는 iOS 안티 디버깅 우회에 대해 포스팅해보려 한다. 동적 디버깅에 사용된 툴은 lldb이고, IDA로 소스 코드 정적 분석을 했다.

예제 앱(DVIA) + 실제 금융 앱에서 사용되고 있는 안티 디버깅 로직은 총 3개였다. (내가 모르는 디버깅 탐지 패턴이 더 있을 수도 있다.)

1. sysctl

2. dlsym + ptrace

3. SVC

사실 세 가지 패턴 모두 코드 패치를 통해 우회한 블로그 포스팅은 많이 보였으나, 프리다를 사용해서 우회했다는 블로그 포스팅은 본 적이 없다. 그래서 구글링 열심히 하면서 디버깅 탐지 로직에 대해 이해하고 프리다 스크립트를 작성해서 안티 디버깅 로직을 우회해봤다.


sysctl 의 경우 attach 는 가능하나 디버깅할 수 없고, ptrace 는 이미 프로세스가 선점되어 있어 attach 조차 불가능하다.

sysctl

ptrace

그럼 위 두 가지 방식의 안티 디버깅 로직을 확인하고 우회해보도록 하겠다.


먼저 sysctl을 사용한 안티 디버깅 로직은 다음과 같다.

sysctl 안티 디버깅 로직

sysctl 함수의 결과 값은 성공 시 0, 실패 시 -1을 반환한다. 여기서 중요한 포인트는 sysctl의 세 번째 인자이다.

sysctl의 세 번째 인자에는 디버깅 플래그 값(P_TRACED)을 담고 있는 구조체의 주소 값이 들어가있다.

구조체의 주소 값에서 플래그 값이 들어가 있는 주소 값 만큼 offset 을 더해주고, 16진수 800과 & 연산을 통해 현재 프로세스가 디버깅 중인지 체크한다.

따라서, 프리다를 통해 위 로직을 우회해보겠다. 우회 스크립트는 다음과 같다.

Interceptor.attach(Module.findExportByName(null, 'sysctl'), { onEnter: function(args) { // console.warn('\t[*] sysctl called !'); this.kp_proc = args[2]; this.count = args[1]; }, onLeave: function(retval) { if(retval == 0x0) { if(this.count.toInt32() == 4) { var p_flag = Memory.readInt(this.kp_proc.add(32)); // console.log(Memory.readByteArray(this.kp_proc.add(32), 2)); if((p_flag & 0x800) !== 0) { console.log('\x1b[34m[!] Anti Debugging Bypass ! (sysctl)\x1b[0m'); Memory.writeByteArray(this.kp_proc.add(32), [0x00, 0x00]); } } } } });

위 스크립트를 실행시킨 뒤 attach를 하면 디버깅이 가능하다.


다음으로 ptrace를 사용하는 안티 디버깅 로직을 우회해보자. 안티 디버깅 로직은 다음과 같다.

ptrace 안티 디버깅 로직

dlsym을 통해 ptrace 함수 주소 값을 가져온 뒤, PT_DENY_ATTACH(31) 플래그 값을 설정해주면 해당 프로세스에 attach 할 수 없다.

해당 로직은 ptrace 함수 후킹 후 첫 번째 인자 변조를 통해 쉽게 우회할 수 있다.

Interceptor.attach(Module.findExportByName(null, 'ptrace'), { onEnter: function(args) { // console.warn('\t[*] ptrace called !'); if(args[0].toInt32() == 31) { console.log('\x1b[34m[!] Anti Debugging Bypass !(ptrace)\x1b[0m'); args[0] = ptr(0x0); } } });

위 스크립트를 실행시키면 ptrace를 통해 디버깅을 탐지하는 로직을 우회할 수 있다.


위 두 가지 스크립트를 모두 작성해서 디버깅을 시도했는데, attach가 되지 않는다면 SVC(Supervisor Call) 를 통해 ptrace를 호출했을 가능성이 크다. SVC를 사용한 안티 디버깅 로직은 다음과 같다.

위 어셈블리어는 syscall(ptrace(31), 0, 0, 0) 과 같다.

E0 03 80 D2 (MOV X0, #0x1F)

01 00 80 D2 (MOV X1, #0)

02 00 80 D2 (MOV X2, #0)

03 00 80 D2 (MOV X3, #0)

50 03 80 D2 (MOV X16, #0x1A)

01 10 00 D4 (SVC 0x80)

ptrace 함수를 사용하는 건 동일하지만, dlsym 으로 호출하는 것이 아닌 SVC 를 통해 호출한다는 점에서 다르다. 그래서 프리다로 ptrace 함수를 후킹해도 후킹되지 않는다.

SVC로 호출한 ptrace 함수를 후킹하는 방법은 메모리 상에서 위 HEX 값을 스캔해서 매핑시킨 뒤, 메모리를 덮어 쓰면 된다.

var m = Process.findModuleByName('ModuleName'); // console.log(JSON.stringify(m)); var pattern = '50 03 80 D2 01 10 00 D4'; // Memory.scanSync(m.base, m.size, pattern); Memory.scan(m.base, m.size, pattern, { onMatch: function(address, size) { // console.log('Memory.scan() found match at ' + address + ' with size ' + size); console.log('\x1b[34m[!] Anti Debugging Bypass ! (SVC ptrace)\x1b[0m'); console.log(Memory.readByteArray(address, size)); Memory.protect(address, size, 'rwx'); Memory.writeByteArray(address.add(4), [0x1F, 0x20, 0x03, 0xD5]); console.warn(Memory.readByteArray(address, size)); }, onComplete: function () { console.log('Memory.scan() complete'); } })

위 스크립트는 앱 최초 실행 시 특정 모듈의 메모리를 스캔해서 syscall ptrace가 매핑되면 SVC 0x80 어셈블리어를 NOP 처리한다.

특정 앱에서 위 스크립트가 적용되지 않는 케이스가 있다. 이럴 때 패턴을 새로 정의해주면 된다.

var pattern = 'E0 03 80 D2 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? 50 03 80 D2 01 10 00 D4'; ... Memory.scan(m.base, m.size, pattern, { onMatch: function(address, size) { ... Memory.writeByteArray(address, [0x00, 0x00]); ... }, onComplete: function () { ... } })

앞서 소개한 세 가지 방법 외에 SVC로 sysctl을 사용하는 경우도 있을 것이고, syscall 이라는 함수를 사용 할 수도 있을 것 같은데, 여러 가지 앱을 분석해보면서 접해보지 않았기 때문에 별도의 스크립트는 작성하지 않았다. 나중에 다른 iOS 안티 디버깅 기술을 찾게 된다면 업데이트 해야겠다.

 

블로그 이미지

wtdsoul

,