Based on ptrace.
该调试器主要参考Eli Bendersky
的博客(见参考资料)完成。
根据linux
系统调用ptrace
的man-page
,tracee thread
的信号都会被tracer
拦截。
While being traced, the tracee will stop each time a signal is delivered, even if the signal is being ignored. (An exception is SIGKILL, which has its usual effect.) The tracer will be notified at its next call to waitpid(2) (or one of the related "wait" system calls); that call will return a status value containing information that indicates the cause of the stop in the tracee. While the tracee is stopped, the tracer can use various ptrace requests to inspect and modify the tracee. The tracer then causes the tracee to continue, optionally ignoring the delivered signal (or even delivering a different signal instead).
信号拦截功能是trace
后自带的,故下文不再讨论这个功能。
- 启动一个进程并
trace
它。 - 设置一个新的断点,查看断点信息。
- 在断点处继续运行程序。
- 被调试进程进入中断后能够查看被调试进程任意CPU寄存器的内容。
- 被调试进程进入中断后能够查看被调试进程任意内存区域的内容。
仿照GDB
的CLI
。
功能1,./main program_name
启动一个需要调试的进程。
功能2,b addr
e.g. b 0x40128e
设置一个新的断点,以i b
的形式查看当前所有断点的信息。
功能3,c
继续运行程序。
功能4,i r
查看RIP, RBP, RSP
等寄存器的内容。
功能5,x addr
查看[addr, addr+8)
8字节内存区域的内容。
借助ptrace
系统调用,进程可以通过调用fork
,让生成的子进程执行PTRACE_TRACEME
,然后执行execve
。
A process can initiate a trace by calling fork(2) and having the resulting child do a PTRACE_TRACEME, followed (typically) by an execve(2). Alternatively, one process may commence tracing another process using PTRACE_ATTACH or PTRACE_SEIZE.
子进程通过exec执行一个程序时,会在执行之前产生一个trap信号,使得父进程能够在新程序执行前获得控制权,这相当于在程序的开始自动打了一个断点。
If the PTRACE_O_TRACEEXEC option is not in effect, all successful calls to execve(2) by the traced process will cause it to be sent a SIGTRAP signal, giving the parent a chance to gain control before the new program begins execution.
使用PTRACE_PEEKTEXT
和PTRACE_POKETEXT
将断点代码备份并修改为int3
中断。
分为5步:
- 使用
PTRACE_SETREGS
将RIP
寄存器的内容修改为断点的地址。(程序起始的临时断点直接跳到第5步) - 使用
PTRACE_PEEKTEXT
和PTRACE_POKETEXT
复原断点代码。 - 使用
PTRACE_SINGLESTEP
单步执行断点处的指令。 - 使用
PTRACE_PEEKTEXT
和PTRACE_POKETEXT
重新将断点代码备份并修改为int3
中断。 - 使用
PTRACE_CONT
继续执行代码。
使用PTRACE_GETREGS
可以查看被调试进程任意寄存器的内容。
使用PTRACE_PEEKDATA
可以查看被调试进程任意内存区域的内容。
main.c
负责CLI
接口实现,交互逻辑和信息输出。
debuglib.h
和debuglib.c
将复杂的功能封装为函数供主模块调用。
- 设置被调试子程序的追踪状态。
- 断点的生成、启用和禁用。
- 寄存器内容的获取。
- 内存数据的获取。
- 恢复运行状态。
test.c
调试测试程序,有一个全局变量cnt
,一个修改此变量的函数advance
,主函数中循环调用此函数4次,测试调试功能的正确性。
-
可以使用
libbfd
或者libdwarf
解析源程序和汇编代码之间的映射,但是学习成本比较高。 -
readelf -s
可以得到函数地址和全局变量地址,可以在函数处设置断点、查看全局变量内存区的内容来验证调试器功能的正确性。
上图为readelf -s test
得到的结果,其中有main
函数、advance
函数和全局变量cnt
的地址。
每次调试,测试程序在相同代码处的RIP
都不一样。
上图为使用mygdb
对test
程序连续进行四次调试的结果,可以发现每次起始的RIP
都不一样。
ASLR
,全称为 Address Space Layout Randomization,地址空间布局随机化。该技术在 kernel 2.6.12 中被引入到 Linux 系统,它将进程的某些内存空间地址进行随机化来增大入侵者预测目的地址的难度,从而降低进程被成功入侵的风险。
Linux 平台上 ASLR 分为 0,1,2 三级,用户可以通过内核参数 randomize_va_space 进行等级控制,不同级别的含义如下:
- 0 = 关
- 1 = 半随机;共享库、栈、mmap() 以及 VDSO 将被随机化
- 2 = 全随机;除了 1 中所述,还会随机化 heap
注:系统默认开启 2 全随机模式,PIE 会影响 heap 的随机化。
通过读写 /proc/sys/kernel/randomize_va_space 内核文件可以查看或者修改 ASLR 等级:
// 查看ASLR。
cat /proc/sys/kernel/randomize_va_space
// 关闭ASLR。
sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"
开启 ASLR
,在每次程序运行时的时候,装载的可执行文件和共享库都会被映射到虚拟地址空间的不同地址处;而关掉 ASLR
,则可以保证每次运行时都会被映射到虚拟地址空间的相同地址处。
如上图所示,关闭ASLR
后,每次运行都会映射到虚拟地址空间的相同地址处。
解决了ASLR
的问题之后,调试程序发现不符合预期,test
并没有在0x40128e
处(即advance
函数地址)中断,如下图所示。
使用GDB
进行调试,发现advance
处的RIP
和readelf -s test
得到的地址不一样,如下图所示。
程序运行前b advance
在advance
函数处打断点,输出信息说断点地址为0x12a1
,和readelf -s test
得到的一致,但是运行到断点后断点地址变为了0x5555555552a1
,RIP
的值也为0x5555555552a1
。
后来查了很多资料,发现是PIE
的问题,编译被调试的程序时要加上-no-pie
选项禁用pie
。
从上图可以发现禁用pie
后,Type
由DYN
变为了EXEC
,入口地址也由0x10c0
变为了0x4010b0
,而正常情况下64位可执行程序的text-segment
就是从0x400000
开始的(见上图最后一条命令)。
再次用GDB
进行调试,发现advance
处的RIP
和readelf -s test
得到的地址一致了。
这样一来,自己的debugger
终于可以正常停在断点处了。
仿照了GDB
的接口,实现了以下功能:
./main program_name
启动一个需要调试的进程。
b addr
e.g. b 0x40128e
设置一个新的断点,以i b
的形式查看当前所有断点的信息。
c
继续运行程序。
i r
查看RIP, RBP, RSP
等寄存器的内容。
x addr
查看[addr, addr+8)
8字节内存区域的内容。
-
b 0x40128e
在
0x40128e
即advance
函数处打下断点。 -
i b
查看断点信息,显示了断点地址和断点处的原始代码。
-
c
程序继续运行,在第1次进入
advance
函数前中断。 -
x 0x404048
查看内存
[0x404048, 0x404050)
即全局变量cnt
的值,由低地址到高地址分别为0x8877665544332211
和cnt
的初值0x1122334455667788
一致,符合预期。 -
c
程序继续运行,执行
++cnt
,cnt
变为0x1122334455667789
,在第2次进入advance
函数前中断。 -
x 0x404048
查看内存
[0x404048, 0x404050)
即全局变量cnt
的值,由低地址到高地址分别为0x8977665544332211
和cnt
的当前值0x1122334455667789
一致,符合预期。 -
c
程序继续运行,执行
++cnt
,cnt
变为0x112233445566778a
,在第3次进入advance
函数前中断。 -
i r
查看寄存器信息,所有寄存器的内容获取方法都是一样的,为了缩小篇幅,
tracer
仅输出RIP
,RBP
,RSP
三个寄存器的内容. -
c
程序继续运行,执行
++cnt
,cnt
变为0x112233445566778b
,在第4次进入advance
函数前中断。 -
c
程序继续运行,执行
++cnt
,cnt
变为0x112233445566778c
,for
循环结束,main
函数return
后进程结束,tracer
进程判断WIFEXITED(wait_status)
满足,输出child exited
后退出命令循环,结束进程,整个过程均符合预期。
https://linux.die.net/man/2/ptrace
How debuggers work: Part 1 - Basics