새소식

모의해킹/iOS

iOS 안티디버깅 및 안티후킹 기법 및 우회

  • -
반응형

iOS 앱을 진단하다보면 디버깅 및 후킹 탐지 로직을 종종 만나게 되는데

왠지 탈옥 탐지만 걸려있을 때보다 더 까다로운 느낌이다.

심지어 탐지 방법도 다양해서 생소한 함수가 나오면 이게 탐지 로직인지도 모를때가 많다.

방법은 더 많겠지만 일단 최대한 정리해두고 어느 정도는 대비해두도록 하자. 

 


Index

1. ptrace + dlsym
2. ptrace + syscall
3. ptrace + SVC(inline assembly)
4. sysctl
5. isatty
6. ioctl
7. getppid

8. fileExistsAtPath

9. _dyld_get_image_name

10. connect


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
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.