Anti-Debugging
Anti-Debugging คือเทคนิคที่โปรแกรม (มัก malware หรือโจทย์ reversing ขั้นสูง) ใช้ตรวจจับว่ากำลังถูก debug แล้วเปลี่ยนพฤติกรรม/ขัดขวางการวิเคราะห์ บทนี้ลงลึกเทคนิค anti-debug ทุกแบบบน Linux (ptrace/proc/timing/breakpoint detection) พร้อมวิธี bypass แต่ละแบบด้วย patch/LD_PRELOAD/gdb (เนื้อหาเพื่อการศึกษา/วิเคราะห์ที่ได้รับอนุญาต)
1. หลักการ
โปรแกรมตรวจว่าถูก debug อยู่ไหม ถ้าใช่ก็เปลี่ยนพฤติกรรม (crash, ให้ flag ปลอม, ออกเงียบๆ, เปลี่ยน logic) เพื่อกันการวิเคราะห์ พบใน malware (กันนักวิเคราะห์) และโจทย์ reversing ที่ตั้งใจทำให้ยาก เป้าหมายของเราคือตรวจเจอเทคนิค anti-debug แล้ว bypass เพื่อวิเคราะห์/debug ต่อได้
2. เทคนิค anti-debug บน Linux
| เทคนิค | ทำงานอย่างไร | ตรวจเจอใน |
|---|---|---|
| ptrace(PTRACE_TRACEME) | เรียก ptrace ตัวเอง — ถ้าถูก debug จะ fail (debugger ใช้ ptrace อยู่แล้ว ผูกได้ตัวเดียว) | call ptrace ใน Ghidra |
| /proc/self/status TracerPid | อ่าน TracerPid — ถ้า != 0 = ถูก debug | open("/proc/self/status") |
| timing check | วัดเวลา execution (rdtsc/time) — debugger ทำให้ช้าผิดปกติ | rdtsc, clock_gettime |
| breakpoint detection | scan โค้ดตัวเองหา 0xCC (int3 ของ sw breakpoint) | loop อ่าน .text เทียบ 0xCC |
| parent process check | ดูว่า parent เป็น gdb/strace ไหม | getppid + อ่าน /proc/ppid |
| signal-based | ใช้ SIGTRAP handler เอง — debugger แย่ง signal | sigaction SIGTRAP |
3. หาจุดตรวจ (static)
# imports — เห็น ptrace = มี anti-debug แน่
rabin2 -i binary | grep -iE 'ptrace|getppid'
# ltrace เห็นการเรียก ptrace ตอน run
ltrace ./binary 2>&1 | grep -i ptrace
# เช่น: ptrace(PTRACE_TRACEME, 0, 0, 0) = -1 ← ตรวจ debug
# ใน Ghidra/IDA: หา call ptrace, open("/proc/self/status"), rdtsc
# X xref ไปดูว่าผลถูกใช้ตัดสินใจอย่างไร (if แล้ว exit/crash)4. Bypass ptrace (พบบ่อยสุด)
# วิธี 1: LD_PRELOAD override ptrace ให้คืน 0 เสมอ
cat > fake.c << 'C'
long ptrace(int r, int p, void* a, void* d) { return 0; }
C
gcc -shared -fPIC fake.c -o fake.so
LD_PRELOAD=./fake.so ./binary
# หรือใน gdb:
gdb ./binary
gef> set environment LD_PRELOAD ./fake.so
# วิธี 2: gdb catch syscall ptrace แล้วแก้ return
gef> catch syscall ptrace
gef> run
gef> set $rax = 0 # บังคับให้ผลเป็น "ไม่ถูก debug"
gef> continue
# วิธี 3: patch binary (NOP call ptrace หรือแก้ jump)
# ใน Ghidra: Patch Instruction เปลี่ยน JNZ→JMP/NOP ข้าม check5. Bypass เทคนิคอื่น
- /proc/self/status TracerPid: patch จุดที่อ่าน/เทียบ TracerPid (แก้ให้เป็น 0 เสมอ) หรือ hook open ด้วย LD_PRELOAD ให้คืนไฟล์ปลอม
- timing check: patch จุดเทียบเวลา (ให้ผ่านเสมอ) หรือใน gdb อย่า step ทีละ instruction ตรงนั้น (ใช้ bp ข้าม) — เพราะ step ทำให้ช้า
- breakpoint detection (0xCC): ใช้ hardware breakpoint แทน software (
hbreakใน gdb — ไม่ใส่ 0xCC ในโค้ด) - parent process check: patch จุดเทียบ หรือรันผ่าน wrapper ที่ปลอม parent
- หลักรวม: หาจุด 'ตัดสินใจ' (if debug then ...) ใน Ghidra แล้ว patch ให้ไปทาง 'ไม่ถูก debug' เสมอ — แก้ที่ผลการ check ครอบคลุมทุกเทคนิค
if (is_debugged) exit();) แล้ว patch instruction นั้น (NOP หรือกลับ jump) — แก้ทีเดียวผ่านทุก check ที่นำมาที่ branch นั้น6. Quick Reference
- โปรแกรมตรวจว่าถูก debug แล้วเปลี่ยนพฤติกรรม
- Linux: ptrace(TRACEME), /proc/self/status TracerPid, timing(rdtsc), 0xCC scan, getppid
- หาจุดตรวจ: rabin2 -i | grep ptrace; ltrace; Ghidra xref
- bypass ptrace: LD_PRELOAD override / gdb catch syscall + set $rax=0 / patch
- timing → hbreak (ไม่ step); 0xCC → hardware breakpoint
- ครอบจักรวาล: patch จุด branch ที่ตัดสินจากผล anti-debug
🧭 จับมือทำทีละขั้น (มีแค่ Kali) + ถ้าติดไปไหนต่อ
สมมติกำลังสู้กับ binary ที่มี anti-debug — เปิด gdb ปุ๊บพฤติกรรมเปลี่ยนทันที (crash/ข้ามคำตอบ/ค้าง) มีแค่ Kali เปล่าๆ ทำตามนี้ทีละขั้นเพื่อยืนยันว่ามันเช็คอะไรแล้ว bypass ให้ debug ต่อได้
- 1รันเทียบกันก่อน: ./binary ปกติ vs gdb ./binary แล้ว run — ถ้าพฤติกรรมต่างกันชัดเจน (crash/ข้าม/ค้าง) ให้สงสัย anti-debug ทันที
- 2เช็ค imports: rabin2 -i ./binary | grep -iE 'ptrace|getppid' — ถ้าเจอ ptrace แปลว่ามี anti-debug แน่นอน
- 3ยืนยันด้วย runtime: ltrace ./binary 2>&1 | grep -i ptrace — ดูว่ามีการเรียก ptrace(PTRACE_TRACEME,...) จริงไหม
- 4เปิด ghidra หา xref ของ call ptrace หรือ open(\"/proc/self/status\") แล้วดูว่าผลถูกใช้ตัดสินใจตรงไหน (มัก if แล้ว exit)
- 5ถ้าเป็น ptrace check: ลอง LD_PRELOAD ปลอมฟังก์ชัน ptrace ให้คืน 0 เสมอ แล้วรันใหม่
- 6ถ้าอยากคุมเองใน gdb: gef> catch syscall ptrace แล้ว set $rax = 0 ตอนหยุด
- 7ถ้าเป็น timing check (rdtsc): ใช้ hbreak (hardware breakpoint) แทน stepi/si ทีละคำสั่ง เพราะ step ทำให้ช้าจนโดนจับ
- 8ถ้าเป็น breakpoint scan (หา byte 0xCC): ใช้ hardware breakpoint เท่านั้น อย่าใช้ software breakpoint ธรรมดา
- 9ลองรันใหม่ผ่าน gdb/ltrace อีกครั้ง — ถ้า debug ได้ปกติแล้ว (bp ทำงาน ไม่ crash) ไปทำ dynamic analysis ต่อได้เลย
- 10ถ้ายังติด/มีหลายชั้นซ้อนกัน ให้ patch จุด branch ที่ตัดสินใจถาวรใน ghidra หรือ radare2 แทนที่จะ bypass ทุกครั้งที่รัน
| ขั้นตอน/งาน | เครื่องมือใน Kali | ติดตั้งเพิ่ม (ถ้าไม่มี) | เครื่องมือออนไลน์ |
|---|---|---|---|
| เช็ค imports หา ptrace | rabin2 (radare2) | apt install radare2 | - |
| ยืนยัน syscall ตอนรันจริง | ltrace, strace | apt install ltrace strace | - |
| debug พร้อม UI ช่วยดู register/stack | gdb + gef/pwndbg | git clone gef.git แล้วรัน gdbinit.py | - |
| อ่าน logic/หา xref จุดตรวจ | ghidra | apt install ghidra | dogbolt.org |
| patch จุดตัดสินใจให้ผ่านถาวร | radare2 (r2 -w) | - | - |
| hook/override ฟังก์ชันโดยไม่แก้ไฟล์ | frida | pipx install frida-tools | - |
| เทียบ assembly ระหว่าง compiler | - | - | godbolt.org |
| ตรวจโครงสร้างไฟล์ ELF เพิ่มเติม | readelf, objdump | - | - |
หัวข้อที่เชื่อมโยง
โน้ตของฉัน
ยังไม่มีโน้ตสำหรับหัวข้อนี้