ptrace에 대하여
공부하면서 정리하는 글입니다.
What is ptrace?
ptrace : process trace는 unix 계열 운영체제에서 제공하는 system call의 일종이다.
하나의 tracer process가 다른 tracee 프로세스의 실행을 관찰하고 제어하는 기능을 제공한다.
주로, 디버깅 시스템을 구현하는데 사용되며 자주 사용하는 gdb또한 내부적으로 ptrace를 사용한다.
프로세스가 fork()를 호출하고 execve() 전에 PTRACE_ME 를 하게 해서, 추적을 개시할 수 있다.
또는 PTRACE_ATTACH나 PTRACE_SEIZE로도 가능하다.
추적되고 있는 동안 tracee는 시그널이 전달 될 때마다 멈춘다. (SIGKILL 예외).
tracer는 waitpid 관련 호출에서 알림을 받게 되고 해당 wait호출에서 tracee가 멈춘 이유를 담은 status를 반환한다. (받는다)
PTRACE_O_TRACEEXEC으로 따로 실행중인게 아니라면, tracee가 execve 성공 호출 시 SIGTRAP 시그널을 받게 되며, 새 프로그램이 시작되기 전에, parent process에게 제어권을 준다.
How to use ptrace?
ptrace 함수의 synopsis는 다음과 같다.
#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
참고로, pid_t는 #defind pid_t int 되어있다.
각 인자별로,
request에는 ptrace를 통해 사용할 기능을 지정한다. 이는 ptrace.h에 enum 자료형으로 정의되어 있다.
pid는 linux 스레드의 스레드 id가 들어간다.
addr, data는 request마다 다르게 해석된다.
몇 가지 request들에 대해서 알아보자.
PTRACE_TRACEME
tracee(피 추적자)에서 사용한다. PTRACE_TRACEME를 사용한 프로세스에서는 부모 프로세서에 의해 추적된다.
이 request에서 pid, addr, data는 무시된다. 따라서, ptrace(PTRACE_TREACEME, 0, NULL, NULL) 하면 된다.
PTRACE_TRACEME가 설정되면, 부모 프로세스에 의해 재개될 때까지 실행을 중지한다. 위에 SIGTRAP 관련 내용
PTRACE_SETREGS
tracee의 범용 레지스터나, 부동소수점 레지스터를 tracer 내의 주소 data로부터 수정한다. addr은 무시한다. PTRACE_POKEUSER처럼 일부 범용 레지스터의 변경이 허용되지 않을 수도 있다.
PTRACE_GETREGS
tracee의 범용 레지스터나, 부동소수점 레지스터를 tracer의 data 주소로 복사한다. 데이터 형식에 대한 정보는 <sys/user.h>에 user_regs_struct 라는 구조체를 참고하라.
GETREGS, SETREGS 예시 && wait에 대한 설명
#include <stdio.h>
#include <sys/ptrace.h>
#include <sys/user.h>
#include <unistd.h>
int main() {
pid_t child;
child = fork();
if(child == 0) {
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
execl("/bin/ls", "ls", NULL);
}
else {
wait(NULL);
struct user_regs_struct regs;
ptrace(PTRACE_GETREGS, child, NULL, ®s);
printf("Original RIP: %llx\n", regs.rip);
regs.rip += 2; // Change RIP register value.
ptrace(PTRACE_SETREGS, child, NULL, ®s);
ptrace(PTRACE_CONT, child, NULL, NULL);
wait(NULL); // Wait for child to finish.
}
return 0;
}
chatGPT에게 예시를 작성해달라고 했다. (코드가 맞는지는 검증 안함)
main 함수에서 fork 이후, child process에서 TRACEME를 하고 execl로 ls 명령어를 실행시키고자 한다.
위 상황에서 parent process는 wait를 통해 상태 변경을 기다리고 상태를 받아올 필요는 없다. (다른 블로그들에는 종료를 기다린다고 쓰여있다;;)
PTRACE_REGS를 통해 child로부터 regs에 레지스터 값을 읽어온다.
PTREACE_SETREGS를 통해 레지스터를 세팅한다. GETREGS, SETREGS 모두 인자로 addr은 무시하고 data를 사용한다.
PTRACE_CONT 를 사용하여 CONTINUE 한다.
wait 함수에 대해,
wait 관련 함수는 다음의 시놉시스를 가진다.
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);
wait는 waitpid(-1, &wstatus, 0); 과 같다. return value는 child process의 pid이다.
waitpid에서 첫번째 인자가 -1이면 임의의 child process를 기다린다.
두 번째 인자는 상태를 반환한다. 상태 정보는 WIFSTOPPED, WIFEXITED 등의 매크로로 확인 가능하다.
세 번째 인자는 옵션으로 어느 범위까지 child process를 기다려야 하는가를 말하며, 0을 사용하면 wait와 동일하게 종료한 child process가 없으면 즉시 반환한다.
PTRACE_PEEKDATA, PTRACE_PEEKTEXT
PTRACE_PEEKDATA는 tracee의 메모리 addr에서 word를 읽고 그 word를 반환한다. 리눅스에서 TEXT와 DATA 주소 공간이 따로 있지 않아 둘은 동등하다. (data는 무시된다.)
ptrace(PTRACE_PEEKTEXT, pid, regs.rip, 0); 을 사용하면, data가 0이고, addr이 rip이다. rip의 값을 word 단위로 읽어서 반환한다. 따라서 기계어 코드가 반환될 수 있다.
PTRACE_POKEDATA, PTRACE_POKETEXT
PEEK와 비슷하다, 이 require은 addr과 data를 모두 사용한다. addr에 data를 대입한다. 프로그램 중단점(breakpoint)를 설정하기 위해서 int 3 (0xcc)를 넣는데 사용될 수 있다.
instruction을 삽입할 때, data의 자료형이나 값에 주의하자.(unsigned long) 이거때문에 삽질했다. code 영역에 null이 들어가 SIGSEGV 가 발생한다.
PTRACE_CONT
continue이다. addr은 무시, data가 0이 아니면 tracee에게 보낼 시그널 번호로 해석한다.
PTRACE_SINGLESTEP
PTRACE_CONT 처럼 tracee를 재시작하되, 한 인스트럭션 후에 tracee가 멈추도록 한다. tracer 관점에서는 tracee가 SIGTRAP을 수신하여 멈춘 것처럼 보인다. (SIGTRAP은 TRACEME에서도 설명함)
gdb의 si, ni 역할이다.