가장 전통적인 안티디버깅 방식.
ptrace(PT_DENY_ATTACH, 0, 0, 0);
이처럼 ptrace에 PT_DENY_ATTACH 옵션을 주어 attach를 막는 방법이다.
하지만 ptrace함수는 public iOS API가 아니므로 직접적으로 사용하면 앱스토어 검수를 통과하지 못한다.
따라서 아래 코드처럼 dlsym으로 ptrace 함수의 주소를 알아와서 사용하는 방식으로 사용한다.
#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_t ptrace_ptr = (ptrace_ptr_t)dlsym(RTLD_SELF, "ptrace"); ptrace_ptr(31, 0, 0, 0); // PTRACE_DENY_ATTACH = 31 }
팁 : 종료 메세지가 exited with status 45(0x2d)라면 ptrace로 종료됐다는 힌트.
사례1
가장 기본적인 예시.
// clang -o main main.c #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/ptrace.h> #include <unistd.h> int main(int argc, char *argv[]) { ptrace(PT_DENY_ATTACH, 0, 0, 0); printf("SUCCESS\n"); return 0; }
우회
방법1. bp ptrace로 ptrace부분에 중단점 설정하고 함수 인자 0x1F(PT_DENY_ATTACH)를 0으로 바꿈.
방법2. 함수호출 명령어를 NOP으로 대체.
Armconverter.com : arm 어셈블리 명령어를 opcode로 변환해주는 사이트
사례2
앞서 PT_DENY_ATTACH로 인해 attach에 실패하게 될 경우 exited with status 45(0x2d)라고 했다.
하지만 종료코드가 45인 것을 보고 bp ptrace를 걸었는데도 중단점에서 멈추지 않는 경우가 있다.
이 경우는 시스템 함수를 직접 direct로 호출한 경우이다.
// clang -o main main.c #include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { asm( "pushq %rax\n" "pushq %rdi\n" "movq $0x1f, %rdi\n" "movq $0x200001A, %rax\n" "syscall\n" "popq %rdi\n" "popq %rax\n" ); printf("SUCCESS\n"); return 0; }
직접 시스템 함수 호출 : syscall(0x200001A)
우회
syscall 호출 순간을 잡아서 0x200001A 인자를 0으로 바꾸면 끝.
사례3
잘 알려지지 않은 사례로 아래 두가지를 검사한다.
- ptrace(PT_DENY_ATTACH, 0, 0, 0)이 잘 동작하는 지
- 이와 관련된 코드가 변경됐는지를 검사
ptrace(PT_DENY_ATTACH, 0, 0, 0 )가 적용된 프로세스에 attach하면 segmentation fault가 발생한다는 점을 이용한 방법.
// clang -o main main.c #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/ptrace.h> #include <unistd.h> int deny_attach_successful = 0; void sigsegv_handler(int sig) { printf("sigsegv_handler: %i\n", sig); deny_attach_successful = 1; } int main(int argc, char *argv[]) { pid_t pid = getpid(); //------------------핵심코드----------------- ptrace(PT_DENY_ATTACH, 0, 0, 0); //attach를 방지하는 ptrace 호출 signal(SIGSEGV, sigsegv_handler); //seg fault 발생 시 전역변수 값 바꾸는 커스텀 핸들러 등록 ptrace(PT_ATTACH, pid, 0, 0); //PT_DENY_ATTACH가 제대로 동작하는지 스스로 테스트 //----------------------------------------- if (!deny_attach_successful) { //PT_DENY_ATTACH가 제대로 적용됐다면 두번째 ptrace에서 seg fault가 발생하고, 커스텀 핸들러에 의해 전역변수 값이 바뀌면서 if문 정상 통과. printf("FAILURE\n"); return 1; } printf("SUCCESS\n"); return 0; }
- 전역변수는 default 값으로 0(false)으로 설정한 상태.
- 첫번째 ptrace 호출에서 PT_DENY_ATTACH를 등록하였다.
그러면 이후의 attach 시도 시 segmentation fault가 뜨는 게 정상이다.
- 따라서 seg fault가 뜨면 전역변수를 1로 설정하는 커스텀 핸들러를 별도로 등록하고,
- 자기 자신에게 ptrace attach를 시도한다.
==> 스스로에게 attach 시도한 것이 첫번째 ptrace에 의해 정상적으로 segmentation fault를 유발했다면, 전역변수를 1로 설정하는 커스텀 핸들러가 동작했을테고, 이후의 if문을 정상적으로 통과할 수 있다.
==> 공격자가 ptrace에 bp걸고 PT_DENY_ATTACH 인자를 변경하여 디버깅이 가능하도록 한 경우라면, 두번째 ptrace attach가 제대로 동작하면서 전역변수가 그대로 0(false)로 유지되므로 if문을 통과하지 못하고 공격자의 디버깅 상태를 감지할 수 있게된다.
우회
1. 첫번째 ptrace 우회
공격자가 디버거로 attach를 할 수 있어야하니까 PT_DENY_ATTACH를 먼저 우회함.
2. signal에 중단점 설정하고 핸들러 주소를 알아둠.
3. 두번째 ptrace 우회
4. 수동으로 signal 핸들러를 호출한다.
전역변수 값을 변경하는 커스텀 핸들러를 수동으로 호출하는 것인데, 이 전역변수를 여러 순간에 검사하는 게 아니라면 그냥 if문을 우회하는 것으로도 충분. 물론 전역변수 메모리 값을 직접 바꿔도 됨.