봄날은 갔다. 이제 그 정신으로 공부하자

IOS 앱 디버깅 방지 본문

iOS Tip

IOS 앱 디버깅 방지

길재의 그 정신으로 공부하자 2022. 4. 20. 22:22

 

첫번째 방법은 앱을 실행한 ppid를 확인하는 방법으로

일반적으로 앱은 iOS의 launchd Process에 의해 실행 됩니다.

launchd 프로세스는 user mode에서 첫번째로 실행되는 프로세스로 pid는 1입니다.

즉 앱이 실행될 때 ppid를 확인해서 ppid가 1이 아니면 디버거에 의해 실행 될 수 있다고 판단할 수 있습니다.

사용 방법은 아래와 같습니다.

func hasDebugger() -> Bool {     
    return getppid() != 1
}

 

두번째 방법은 sysctl 커널 명령어를 사용한 방법으로

sysctl 명령어는 커널 runtime 시에 커널의 파라미터를 변경할 때 시스템의 /proc/sys 디렉토리밑에 있는 커널 매개변수를 제어하는데 사용됩니다. sysctl 명령어를 사용해 kinfo_proc의 값에 디버깅 플래그 값(P_TRACED)이 포함되어 있는지 체크하는 방식으로 사용 방법은 아래와 같습니다.

func hasDebugger()-> Bool {
    var name: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
    var info: kinfo_proc = kinfo_proc()
    var info_size = MemoryLayout<kinfo_proc>.size

    let success = name.withUnsafeMutableBytes { (nameBytePtr: UnsafeMutableRawBufferPointer) -> Bool in
        guard let nameBytesBlindMemory = nameBytePtr.bindMemory(to: Int32.self).baseAddress else { return false }
        return -1 != sysctl(nameBytesBlindMemory, 4, &info, &info_size, nil, 0)
    }

    return (success && (info.kp_proc.p_flag & P_TRACED) != 0) ? true : false
}

 

디버거를 사용해 앱을 실행하면 제 PC에서는 info.kp_proc.p_flag의 값이 18436가 나오는데 이를 2진수로 변환하면 100100000000100이 나오는데 P_TRACED의 값이 2048(2진수: 100000000000)이므로 info.kp_proc.p_flag에 P_TRACED가 포함되어 있어 디버거를 통해 앱이 실행되었음을 파악할 수 있습니다.

 

탈옥 감지

이렇게 개발자가 디버깅을 방지했어도 크래커가 탈옥폰을 사용하고 있으면 다양한 우회 방법을 통해 위 두가지 방법을 무력화하고 앱 디버깅이 가능해집니다.

이를 방지하기 위해 앱이 실행 될 때 실행되는 앱이 탈옥폰인지 아닌지를 감지하는 기능이 반드시 추가되어야 합니다.

iOS에서 탈옥 감지는 해당 단말에 탈옥을 도와주는 앱이 설치되었는지, 그에 해당하는 파일들이 존재하는지를 확인하는 방식으로 단말기에 탈옥을 도와주는 앱이 설치되어 있거나 그에 해당하는 파일들이 존재하면 해당 단말기는 탈옥되었다고 판단합니다.

 

탈옥 관련 자세한 정보는 아래 글을 참고하시면 유익합니다. ^^

https://namu.wiki/w/%ED%83%88%EC%98%A5(iOS)

 

아이폰의 탈옥을 도와주는 앱은 대표적인 Cydia를 비롯해 RockApp 등 다수의 앱이 있는 것을 알 수 있지만, 애플의 지속적인 탈옥방지 대응으로 인해 점점 탈옥이 어려워지고 있습니다.

위 글에 따르면 탈옥이 가능한 iOS 버전은 iOS 14 버전까지만 제한적으로 가능하며 15 버전 부터는 현재는 탈옥이 불가능하다고 합니다.

 

탈옥을 감지하는 소스는 아래와 같습니다.

func hasJailbreak() -> Bool {
    guard let cydiaUrlScheme = NSURL(string: "cydia://package/com.example.package") else { return false }
    if UIApplication.shared.canOpenURL(cydiaUrlScheme as URL) {
        return true
    }
    #if arch(i386) || arch(x86_64)
    return false
    #endif
    let fileManager = FileManager.default
    if fileManager.fileExists(atPath: "/Applications/Cydia.app") ||
        fileManager.fileExists(atPath: "/Library/MobileSubstrate/MobileSubstrate.dylib") ||
        fileManager.fileExists(atPath: "/bin/bash") ||
        fileManager.fileExists(atPath: "/usr/sbin/sshd") ||
        fileManager.fileExists(atPath: "/etc/apt") ||
        fileManager.fileExists(atPath: "/usr/bin/ssh") ||
        fileManager.fileExists(atPath: "/private/var/lib/apt") {
        return true
    }

    if canOpen(path: "/Applications/Cydia.app") ||
        canOpen(path: "/Library/MobileSubstrate/MobileSubstrate.dylib") ||
        canOpen(path: "/bin/bash") ||
        canOpen(path: "/usr/sbin/sshd") ||
        canOpen(path: "/etc/apt") ||
        canOpen(path: "/usr/bin/ssh") {
        return true
    }

    let path = "/private/" + NSUUID().uuidString
    do {
        try "anyString".write(toFile: path, atomically: true, encoding: String.Encoding.utf8)
        try fileManager.removeItem(atPath: path)
        return true
    } catch {
        return false
    }        
}
    
func canOpen(path: String) -> Bool {
    let file = fopen(path, "r")
    guard file != nil else { return false }
    fclose(file)
    return true
}
Comments