PTRACE 시작하기
PTRACE를 사용하기 위해서는 <sys/ptrace.h>
헤더 파일을 포함해야 하며 ptrace() 시스템 콜을 사용한다. PTRACE를 사용하기 전에 대상 프로세스를 생성하거나 이미 실행 중인 프로세스를 선택해야 한다.
프로세스 추적 제어
PTRACE_ATTACH
ptrace(PTRACE_ATTACH, pid, NULL, NULL);
waitpid(pid, NULL, 0);
PTRACE_ATTACH 플래그를 사용하여 프로세스를 추적한다. pid는 추적할 프로세스의 PID이다.
waitpid()
함수를 사용하여 프로세스가 멈출 때까지 대기한다.
PTRACE_DETACH
ptrace(PTRACE_DETACH, pid, NULL, NULL);
PTRACE_DETACH 플래그를 사용하여 추적을 중지한다. 이후에는 프로세스가 정상적으로 실행을 이어간다.
레지스터 상태 읽기/수정
PTRACE_GETREGS
struct user_regs_struct regs;
ptrace(PTRACE_GETREGS, pid, NULL, ®s);
PTRACE_GETREGS 플래그를 사용하여 대상 프로세스의 레지스터 상태를 읽어온다. user_regs_struct
구조체에 읽어온 레지스터 값을 저장한다.
PTRACE_SETREGS
regs.ARM_r6 = 42;
ptrace(PTRACE_SETREGS, pid, NULL, ®s);
PTRACE_SETREGS 플래그를 사용하여 대상 프로세스의 레지스터 상태를 수정할 수 있다. regs은 각 아키텍처가 정의한 레지스터를 사용하면 된다. 위의 예시에서는 ARM 아키텍처 r6 레지스터 값에 42를 대입힌다.(참고: https://elixir.bootlin.com/linux/latest/source/arch/arm/include/uapi/asm/ptrace.h#L12)
메모리 읽기/쓰기
PTRACE_PEEKDATA
long data = ptrace(PTRACE_PEEKDATA, pid, address, NULL);
PTRACE_PEEKDATA 플래그를 사용하여 대상 프로세스의 메모리 값을 읽어온다(read). address는 읽어올 메모리 주소이고, 읽어온 값을 long 타입 변수에 저장한다.
PTRACE_POKEDATA
ptrace(PTRACE_POKEDATA, pid, address, data);
PTRACE_POKEDATA 플래그를 사용하여 대상 프로세스의 메모리에 값을 쓴다(write). address는 쓸 메모리 주소이고, data는 쓸 값을 나타낸다.
실행 제어
ptrace(PTRACE_CONT, pid, NULL, NULL);
waitpid(pid, NULL, 0);
PTRACE_CONT 플래그를 사용하여 대상 프로세스의 실행을 재개한다. waitpid
함수를 사용하여 프로세스가 멈출 때까지 대기한다.
Example:
#include <sys/ptrace.h>
#include <sys/user.h>
#include <asm/ptrace.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
int main(int argc, char *argv[])
{
struct user_regs_struct regs;
unsigned int data;
unsigned char buff[4];
int ret, pid, i, j;
pid = atoi(argv[1]);
ret = ptrace(PTRACE_ATTACH, pid, 0, 0);
printf("return : %d\n", ret);
ptrace(PTRACE_GETREGS, pid, 0, ®s);
printf("stack = %p\n", (void *)regs.ARM_sp);
for (i=0; i<10; i++) {
data = ptrace(PTRACE_PEEKDATA, pid, regs.ARM_sp+i*4, 0);
memcpy(buff, &data, 4);
printf("%08x : ", (unsigned int)regs.ARM_sp+i*4);
for (j=0; j<4; j++) {
if (isprint(data2[j]))
printf("%c ", buff[j]);
else
printf(". ");
}
printf("%08x\n", data);
}
ptrace(PTRACE_POKEDATA, pid, 0x7efd97d0, 0xa5a5a5a5);
ptrace(PTRACE_DETACH, pid, 0, 0);
return 0;
}
프로세스 추적 시작하기
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
PTRACE_TRACEME 플래그를 사용하여 현재 프로세스를 추적 대상으로 설정한다. 이 함수 호출 이후에는 현재 프로세스가 디버거에 의해 추적될 수 있게 제어권을 넘겨준 상태가 된다.
디버거 프로세스 시작하기
pid_t pid = fork();
if (pid == 0) {
// 자식 프로세스 코드
execl("/path/to/program", "program", NULL);
} else {
// 부모 프로세스 코드
wait(NULL);
}
디버거 프로세스를 시작하기 위해 자식 프로세스를 생성한다. 자식 프로세스는 execl
함수를 사용하여 디버깅할 대상 프로그램을 실행한다. 부모 프로세스는 wait
함수를 사용하여 자식 프로세스의 종료를 기다린다(wait).
종료시키기
ptrace(PTRACE_KILL, pid, NULL, NULL)
자식 프로세스에게 SIGKILL을 보내 종료되도록 한다.
Example:
#include <sys/ptrace.h>
#include <sys/user.h>
#include <sys/wait.h>
#include <asm/ptrace.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
int main(int argc, char *argv[])
{
struct user_regs_struct regs;
int ret, status;
pid_t pid = fork();
if (pid == 0) {
// child process
ptrace(PTRACE_TRACEME, 0, 0, 0);
execl("/bin/ls", "/bin/ls", NULL);
return 0;
}
wait(&status);
if (WIFSIGNALED(status)) {
fprintf(stderr, "child process %d was abnormal exit.\n", pid);
return -1;
}
ret = ptrace(PTRACE_GETREGS, pid, 0, ®s);
printf("return : %d\n", ret);
printf("stack = %p\n", (void *)regs.ARM_sp);
printf("pc = %p\n", (void *)regs.ARM_pc);
ptrace(PTRACE_KILL, pid, 0, 0);
return 0;
}