상세 컨텐츠

본문 제목

[Mobile/IOS] IOS Anti-debugging : 3Ways

기술보안/Mobile:IOS

by about_SC 2020. 6. 1. 10:40

본문

[Mobile/IOS] IOS Anti-debugging : 3Ways

 

 

___Intro.

앱 실행 시 디버거가 사용이 가능하게 되면, 민감한 데이터가 포함된 변수를 추적할 수 있고, 이를 통해 응용 프로그램의 흐름을 제어할 수 있습니다.. 그 뿐만 아니라 메모리와 레지스터를 읽고 수정할 수 있습니다.

*사실상 디버깅으로부터 완전히 보호하는 것을 불가능하다. 앱을 사용할 수 있는 경우, 신뢰할 수 없는 장치에서 앱이 실행될 수 있으며 이를 통해 공격자는 다양한 방법으로 앱의 흐름을 제어할 수 있습니다.

예를 들어 앱의 바이너리를 패치하거나 런타임에 Frida와 같은 도구를 사용하여 앱의 동작을 동적으로 수정하여 모든 앱의 디버깅 방지 컨트롤을 우회할 수 있습니다.

하지만, 아무것도 안하기보다는 여러 개의 방지로직을 추가하여 피해를 최소화하는 것을 권장합니다. 또한, 앱의 다양한 포인트에 탐지로직을 숨겨 디버깅 탐지로직이 쉽게 우회되지 않도록 조치해야 합니다.


 

___How to.


1. Ptrace

iOS XNU 커널은 프로세스를 디버깅하기 위해 ptrace 시스템 호출을 이용해 구현하게 된다.
하지만, ptrace는 디버깅을 하기 위한 대부분의 기능이 미흡하다.

ex) it allows attaching/stepping but not read/write of memory and registers
//메모리 및 레지스터의 첨부/스텝핑은 허용하지만 읽기/쓰기는 허용하지 않습니다.

그럼에도 불구하고, ptrace 의 기능은 유용한 부분이 많은데, 대표적으로 디버깅 방지 기능을 제공한다(PT_DENY_ATTACH). 이 기능을 구현하게 되면 다른 디버거가 호출된 프로세스에 연결할 수 없도록 하며, 연결을 시도하게 되면 프로세스 강제종료가 이루어진다.
메뉴얼 참고

사전에 ptrace가 iOS 공식 API가 아니라는 것을 알아야한다. 비공개 API는 금지되어 있으며, 앱스토어는 이를 포함하는 앱을 거부할 수 있다. 이 때문에, ptrace는 코드에서 직접 호출되지 않고, ptrace 함수 포인터가 dlsym을 통해 획득 될 때 호출된다.

// Example

# import <dlfcn.h>
# import <sys / types.h>
# import <stdio.h>

typedef int (*ptrace_ptr_t)(int _request, pid_t _pid, caddr_t _addr, int _data);

void anti_debug() {

ptrace_ptr = (ptrace_ptr_t)dlsym(RTLD_SELF, "ptrace");

ptrace_ptr(31, 0, 0, 0); // PTRACE_DENY_ATTACH = 31

}

 

//Bypass Anti-Debugging(Ptrace)

위의 코드는 해당 방법으로 우회가 가능하다. dlsym은 두번째 인수(ptrace)와 함께 레지스터 R1에 저장된다. dlsym의 반환값(ptrace 포인터 주소)은 R0에 저장되는데, R0의 결과값은 offset 0x1908A에서 R6으로 이동된다. offset 19098에서 R6의 포인터 값은 BLX R6을 통해 호출되는데, ptrace 함수 호출을 비활성화 하기 위해서 해당 명령어를 NOP으로 대체하게되면 이를 우회할 수 있게된다(명령어 BLX R6(Little Endian에서 0xB0 0x47) → NOP(Little Endian에서 0x00 0xBF) 명령어로 대체)

 

 


2. Sysctl 사용

호출된 디버거를 탐지하는 또다른 방식에는 sysctl이 있다. 애플 문서에 따르면, sysctl은 시스템 정보를 설정하거나(적절한 권한이 있는 경우), 단순히 시스템 정보를 검색할 수 있다(프로세스가 디버깅되고 있는지 여부). 그러나, 앱이 sysctl을 사용하는 것만으로는 완전한 디버깅 방지가 이루어지지 않음을 항상 유의해야한다.

//Example

#include <assert.h>
#include <stdbool.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/sysctl.h>

static bool AmIBeingDebugged(void)
    // 현재, 프로세스가 디버깅중인 경우 true를 반환합니다.
    // (디버거가 사전에 실행되고 있거나, 앱 실행 후에 부착된 경우 모두 탐지하여 true를 반환한다)
{
    int                 junk;
    int                 mib[4];
    struct kinfo_proc   info;
    size_t              size;

    // sysctl 함수 호출이 실패한다면, 아래의 플래그를 초기화해야한다.

    info.kp_proc.p_flag = 0;

    // mib 배열을 초기화하면, 그것은 우리에게 sysctl명령과 관련된 정보를 제공해준다.
    // 이 경우, 우리는 mib 배열을 통해 특정 프로세스에 대한 정보를 볼 수 있다.

    mib[0] = CTL_KERN;
    mib[1] = KERN_PROC;
    mib[2] = KERN_PROC_PID;
    mib[3] = getpid();

    // Call sysctl.

    size = sizeof(info);
    junk = sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, NULL, 0);
    assert(junk == 0);

    // P_TRACED 플래그가 설정되어 있으면 True를 반환하게 된다.

    return ( (info.kp_proc.p_flag & P_TRACED) != 0 );
}

 

//Bypass Anti-Debugging(Systcl)

sysctl은 ptrace와 달리 디버깅 탐지 시 프로세스를 자동으로 종료시킬 수 없고, 탐지만 가능하다. 따라서 반환값에 따라 디버깅 여부를 탐지하고 시스템 콜을 통해 앱을 종료시켜야 한다. 예제 코드는 아래와 같다.

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/sysctl.h>
#include <stdlib.h>
  
static int is_debugger_present(void)
{
    int name[4];
    struct kinfo_proc info;
    size_t info_size = sizeof(info);
  
    info.kp_proc.p_flag = 0;
  
    name[0] = CTL_KERN;
    name[1] = KERN_PROC;
    name[2] = KERN_PROC_PID;
    name[3] = getpid();
  
    if (sysctl(name, 4, &info, &info_size, NULL, 0) == -1) {
        perror("sysctl");
        exit(-1);
    }
    return ((info.kp_proc.p_flag & P_TRACED) != 0);
}
  
int main (int argc, const char * argv[])
{
    printf("Looping forever");
    fflush(stdout);
    while (1)
    {
        sleep(1);
        if (is_debugger_present())
        {
            printf("Debugger detected! Terminating...\n");
            exit(-1); 
            return -1;
        }
        printf(".");
        fflush(stdout);
    }
    return 0;
}

위의 코드와 같은 경우, 이를 우회하기 위해서는 is_debugger_present() 함수의 리턴값을 변조하거나, main 함수에 is_debugger_present()와 관련된 분기문을 수정하는 방법이 존재한다.

sysctl의 함수 호출 및 탐지방법에 대해 더 자세한 내용을 원하시는 분들은 하기의 링크를 참조바란다.
클릭

 

 

 


3. getppid 사용

iOS 상에서 실행되는 프로그램들은 부모 PID를 확인하여 디버거에 의해 실행되었는지 여부를 탐지할 수 있다. 일반적으로 프로그램은 사용자모드에서 실행되는 첫번째 프로세스이며 PID=1을 갖는 프로세스에 의해 시작된다.

그러나, 디버거를 attach하게 되면 getppid()가 1과 다른 PID를 반환하는 것을 확인할 수 있다. 이러한 탐지 기술은 Objective-C 또는 Swift를 사용하여 네이티브 코드로 구현할 수 있다.

//Example

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

 

//Bypass Anti-Debugging(getppid)

위와 같은 코드도 동일하게 위에서 서술된 방법으로 우회가 가능하다.
(바이너리 패치, Frida hooking을 통해 우회가 가능하다)


 

 

Reference.

https://mobile-security.gitbook.io/mobile-security-testing-guide/ios-testing-guide/0x06j-testing-resiliency-against-reverse-engineering

 

관련글 더보기