iOS 안티디버깅 및 안티후킹 기법 및 우회
- -
iOS 앱을 진단하다보면 디버깅 및 후킹 탐지 로직을 종종 만나게 되는데
왠지 탈옥 탐지만 걸려있을 때보다 더 까다로운 느낌이다.
심지어 탐지 방법도 다양해서 생소한 함수가 나오면 이게 탐지 로직인지도 모를때가 많다.
방법은 더 많겠지만 일단 최대한 정리해두고 어느 정도는 대비해두도록 하자.
Index
1. ptrace + dlsym
2. ptrace + syscall
3. ptrace + SVC(inline assembly)
4. sysctl
5. isatty
6. ioctl
7. getppid
1. ptrace + dlsym
typedef int (*PTRACE_T)(int request, pid_t pid, caddr_t addr, int data);
static void AntiDebug_ptrace() {
void *handle = dlopen(NULL, RTLD_GLOBAL | RTLD_NOW);
PTRACE_T ptrace_ptr = dlsym(handle, "ptrace");
ptrace_ptr(PT_DENY_ATTACH, 0, 0, 0);
}
ptrace를 직접 호출하면 앱스토어 검수에서 걸리기 때문에 보통 dlsym으로 포인터를 지정하여 호출한다고 한다.
파라미터로 전달되는 상수 PT_DENY_ATTACH(31) 을 0으로 치환하면 우회 가능하다.
우회 방안
// AntiDebug ptrace
Interceptor.attach(Module.findExportByName(null, 'ptrace'), {
onEnter: function(args) {
if(args[0].toInt32() == 31) {
console.log("[D] Bypassed ptrace");
args[0] = ptr(0x0);
}
}
});
2. ptrace + syscall
void AntiDebug_sysPtrace() {
syscall(SYS_ptrace, PT_DENY_ATTACH, 0, 0, 0);
}
syscall을 이용하여 ptrace를 호출하는 방식이다.
system_call_table에 매핑되어 있는 Kernel Mode의 명령어를 직접 호출하는 식인데 테이블은 아래 링크를 참고했다.
링크: https://www.theiphonewiki.com/wiki/Kernel_Syscalls
Kernel Syscalls - The iPhone Wiki
Note on these Args go in their normal registers, like arg1 in R0/X0, as usual. Syscall # goes in IP (that's intra-procedural, not instruction pointer!), a.k.a R12/X16. As in all ARM (i.e. also on Android) the kernel entry is accomplished by the SVC command
www.theiphonewiki.com
ptrace 가 26으로 매핑되어 있기 때문에 syscall 함수의 args[0] 이 SYS_ptrace(26) 일때
args[1] 에 PT_DENY_ATTACH(31)이 오지 못하도록 변조하면 되지 않을까 싶다.
우회 방안
// AntiDebug syscall+ptrace
Interceptor.attach(Module.findExportByName(null, 'syscall'), {
onEnter: function(args) {
if(args[0].toInt32() == 26 && args[1].toInt32() == 31) {
console.log("[D] Bypassed ptrace+syscall");
args[1] = ptr(0x0);
}
}
});
3. ptrace + SVC(inline assembly)
static __attribute__((always_inline)) void AntiDebug_inlineSVC() {
#ifdef __arm64__
__asm__("mov X0, #31\n"
"mov X1, #0\n"
"mov X2, #0\n"
"mov X3, #0\n"
"mov w16, #26\n"
"svc #0x80");
#endif
}
인라인 어셈블리 구문을 이용하여 SVC로 ptrace를 호출하는 방식이다.
SVC 호출 시 x16 레지스터에는 함수(syscall number)가, x0부터 x8 까지의 레지스터에는 파라미터가 전달된다.
이 경우엔 IDA 같은 툴을 이용하여
x0에 31, x16에 26을 넣고 SVC를 호출하는 포인트를 찾고
svc 명령어 시작 시점(offset)에 후킹을 걸어 context를 변조하여 우회하면 된다.
우회 방안
//AntiDebug ptrace+SVC(inline asm)
var m = Module.findBaseAddress('secuworm'); // svc 명령어가 존재하는 바이너리명으로 치환
Interceptor.attach(m.add(0x2054C) , { // svc 명령어 시작 주소(offset)로 치환
onEnter(args) {
if(this.context.x0 == 31) {
console.log('[D] Bypassed ptrace+svc');
this.context.x0=0x0;
}
},
onLeave(retval) { }
});
4. sysctl
static int AntiDebug_sysctl(void)
{
int junk;
int mib[4];
struct kinfo_proc info;
size_t size;
info.kp_proc.p_flag = 0;
mib[0] = CTL_KERN;
mib[1] = KERN_PROC;
mib[2] = KERN_PROC_PID;
mib[3] = getpid();
size = sizeof(info);
junk = sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, NULL, 0);
assert(junk == 0);
return (info.kp_proc.p_flag & P_TRACED) ? 1 : 0;
}
sysctl 을 이용하여 디버깅을 탐지하는 기법이다.
kinfo_proc 구조체의 p_flag 값이 P_TRACED(0x800) 로 설정되어 있는지 체크하여 디버깅 여부를 판별하므로,
해당 p_flag 값을 검증하는 식으로 우회해야 한다.
우회 방안
// AntiDebug sysctl
Interceptor.attach(Module.findExportByName(null, 'sysctl'), {
onEnter(args) {
this.info = args[2];
},
onLeave(retval) {
var p_flag = this.info.add(32).readU16();
if (p_flag !== 0x800) {
Memory.writeByteArray(this.info.add(32), [0x00, 0x00])
console.log("[D] Bypased sysctl");
}
}
});
5. isatty
#include <unistd.h>
void AntiDebug_isatty() {
if (isatty(1)) {
exit(1);
} else {
}
}
isatty 의 파라미터로 file discriptor가 넘겨지는데 1은 표준 출력(standard output)을 나타내며,
즉 표준 출력이 터미널인지를 묻는 것으로 디버깅을 체크하는 방식이다. (isatty = is a tty?)
isatty의 함수 실행 결과가 false가 되도록 변조해야 한다.
우회 방안
//AntiDebug isatty
Interceptor.attach(Module.findExportByName(null, 'isatty'), {
onEnter(args) {
},
onLeave(retval) {
console.log("[D] Bypassed isatty");
return 0;
}
});
6. ioctl
#include <sys/ioctl.h>
void AntiDebug_ioctl() {
if (!ioctl(1, TIOCGWINSZ)) {
exit(1);
} else {
}
}
ioctl 함수를 통해 표준 출력 fd 의 상태를 체크하여 디버깅 여부를 체크하는 방식이다.
해당 함수는 성공 시 0, 실패 시 음수를 리턴하므로 리턴값을 -1로 변조하였다.
우회 방안
// AntiDebug ioctl
Interceptor.attach(Module.findExportByName(null, 'ioctl'), {
onEnter(args) {
},
onLeave(retval) {
console.log("[D] Bypassed ioctl");
return -1;
}
});
7. getppid
func AmIBeingDebugged() -> Bool {
return getppid() != 1
}
일반적으로 iOS에서 사용자 모드로 실행되는 앱들은 PPID(부모 PID)가 1인 것을 이용하여 디버깅 여부를 탐지하는 방식이다.
getppid 함수 호출 시 리턴값을 1로 변조하여 PPID를 속여야 한다.
우회 방안
// AntiDebug getppid
Interceptor.attach(Module.findExportByName(null, 'getppid'), {
onEnter(args) {
},
onLeave(retval) {
console.log("[D] Bypassed getppid");
return 1;
}
});
8. fileExistsAtPath
private static func checkExistenceOfSuspiciousFiles() -> CheckResult {
let paths = [
"/usr/sbin/frida-server"
]
for path in paths {
if FileManager.default.fileExists(atPath: path) {
return (false, "Suspicious file found: \(path)")
}
}
return (true, "")
}
frida 사용 시, 기기 내에 frida-server 바이너리가 존재하게 되므로 해당 파일의 기본 경로인 /usr/sbin/frida-server 의 존재 여부를 체크하는 방식이다.
fileExistsAtPath 함수를 통해 넘겨지는 파라미터를 확인하고, frida-server의 존재 여부를 확인하려 한다면 숨겨주도록 하자.
우회 방안
// fileExistsAtPath
var fileExistsAtPath = ObjC.classes.NSFileManager["- fileExistsAtPath:"];
var flag_feap=0;
Interceptor.attach(fileExistsAtPath.implementation, {
onEnter: function(args) {
var str = ObjC.Object(args[2]);
flag_fea = 0;
if (arr.indexOf(str.toString()) > -1) {
console.log("[H] Bypassed fileExistsAtPath: " + str.toString());
flag_feap = 1;
} else {
//console.log("[H] " + str.toString());
}
},
onLeave: function(retval) {
if (flag_feap) {
retval.replace(0);
flag_feap = 0;
}
}
});
9. _dyld_get_image_name
private static func checkDYLD() -> CheckResult {
let suspiciousLibraries = [
"FridaGadget",
"frida", // Needle injects frida-somerandom.dylib
"cynject",
"libcycript"
]
for libraryIndex in 0..<_dyld_image_count() {
// _dyld_get_image_name returns const char * that needs to be casted to Swift String
guard let loadedLibrary = String(validatingUTF8: _dyld_get_image_name(libraryIndex)) else { continue }
for suspiciousLibrary in suspiciousLibraries {
if loadedLibrary.lowercased().contains(suspiciousLibrary.lowercased()) {
return (false, "Suspicious library loaded: \(loadedLibrary)")
}
}
}
return (true, "")
}
frida 등의 후킹 툴을 사용 시 샌드박스라고 불리는 공간에 dyld 를 주입하여 접근하게 된다.
_dyld_get_image_name 함수를 통해 로드되는 라이브러리를 확인하여 frida 등이 실행되었는지 체크하는 방식이다.
리턴값 변조를 통해 우회 가능할 것으로 보인다.
우회 방안
// _dyld_get_image_name
Interceptor.attach(Module.findExportByName(null, "_dyld_get_image_name"), {
onLeave: function (retval) {
var path = "";
path = retval.readUtf8String();
if (path.indexOf("frida") > -1) {
console.log("[H] Bypassed _dyld_get_image_name Before: " + path);
Memory.protect(retval, Process.pageSize, 'rwx');
Memory.writeUtf8String(retval, path.replace("frida", "secuworm"));
console.log("[H] Bypassed _dyld_get_image_name After: " + retval.readUtf8String());
}
});
10. connect
private static func checkOpenedPorts() -> CheckResult {
let ports = [
27042, // default Frida
4444 // default Needle
]
for port in ports {
if canOpenLocalConnection(port: port) {
return (false, "Port \(port) is open")
}
}
return (true, "")
}
private static func canOpenLocalConnection(port: Int) -> Bool {
func swapBytesIfNeeded(port: in_port_t) -> in_port_t {
let littleEndian = Int(OSHostByteOrder()) == OSLittleEndian
return littleEndian ? _OSSwapInt16(port) : port
}
var serverAddress = sockaddr_in()
serverAddress.sin_family = sa_family_t(AF_INET)
serverAddress.sin_addr.s_addr = inet_addr("127.0.0.1")
serverAddress.sin_port = swapBytesIfNeeded(port: in_port_t(port))
let sock = socket(AF_INET, SOCK_STREAM, 0)
let result = withUnsafePointer(to: &serverAddress) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
connect(sock, $0, socklen_t(MemoryLayout<sockaddr_in>.stride))
}
}
defer {
close(sock)
}
if result != -1 {
return true // Port is opened
}
return false
}
connect 함수를 통해 frida 에서 사용하는 기본 포트인 27042 포트에 연결 가능한지 체크하는 방식이다.
관련 포트로 연결을 시도하면 리턴값을 실패(-1)한 것으로 속여 우회하면 된다.
우회 방안
// Native function connect
var flag_connect=0;
Interceptor.attach(Module.findExportByName(null, 'connect'), {
onEnter: function (args) {
var str = Memory.readUShort(args[1].add(2));
var port = ((str & 0xFF) << 8) | ((str & 0xFF00) >> 8);
flag_connect=0;
if (port == 27042) {
console.log("[N] Bypassed connect frida: ");
flag_connect=1;
}
},
onLeave: function (retval) {
if (flag_connect) {
retval.replace(-1);
}
}
});
Reference
https://github.com/jmpews/HookZzModules/tree/master/AntiDebugBypass
https://mobile-security.gitbook.io/mobile-security-testing-guide/ios-testing-guide/0x06j-testing-resiliency-against-reverse-engineering
https://stackoverflow.com/questions/56985859/ios-arm64-syscalls
https://www.coredump.gr/articles/ios-anti-debugging-protections-part-2/
https://aboutsc.tistory.com/218
https://blog.naver.com/gigs8041/222101950710 https://nightohl.tistory.com/entry/ptrace%EA%B4%80%EB%A0%A8-%EC%95%88%ED%8B%B0%EB%94%94%EB%B2%84%EA%B9%85-%EC%A0%95%EB%A6%AC-%EB%A7%81%ED%81%AC
https://github.com/securing/IOSSecuritySuite
'모의해킹 > iOS' 카테고리의 다른 글
Palera1n 탈옥 후 SSH 패스워드 초기화 (0) | 2024.03.10 |
---|---|
Binary Patch with (IDA, radare2) (2) | 2023.02.09 |
소중한 공감 감사합니다