【Linux】基于ptrace系统调用实现一个debugger
基于ptrace的debugger设计
1. 程序的设计思路
1.1 设计思路
本次设计实现的debugger针对被调试进程主要实现了6项功能:
- 可以读取被调试进程CPU所有寄存器的值
- 可以对被调试进程进行单步调试
- 可以恢复被调试进程运行
- 可以查看被调试进程任意内存空间
- 可以计算被调试进程执行完需要多少条指令
- 可以在指定地址插入断点
为了在不同的功能之间进行切换,使用循环轮询手动输入参数的方式来决定使用哪一项功能。
1 | Type "exit" to exit debugger. |
系统调用Ptrace的定义:
1 | long ptrace(enum __ptrace_request request, pid_t pid,void *addr,void *data); |
ptrace的第一个参数可以通过指定request请求来实现不同的功能。使用PTRACE_GETREGS参数来一次性获取所有寄存器的值,使用PTRACE_SINGLESTEP来进行单步调试,PTRACE_CONT来让被暂停的进程恢复运行。
为了读取任意内存空间,需要知道内存空间的起始地址,一次性读取多少个字节,因此默认采用rip寄存器存放的指针作为默认的起始地址,也就是默认从下一条指令的地址开始读,可以指定一次性读多少个字节,这里我默认一次性读取40个字节,为了既能够读到rip指针之后的数据也能读到rip指针之前的数据,引入偏移量offset,这样可以在指定了起始地址的基础上加上偏移量,从而理论上能够读取任意内存区域。当然,如果明确知道要读的内存起始地址,也可以忽略rip指针直接指定起始地址。
计算进程执行完需要多少条指令比较简单,只需要不停单步执行直到退出,每执行一步就计数即可。
给进程打断点的实现最为困难,本次设计仅针对进程特定地址进行插入断点。可以使用Ptrace的PTRACE_PEEKDATA,PTRACE_POKEDATA两个请求,来在进程指定的地址读出指令和注入新的指令。因此可以在指定的地址插入int3(0xcc)中断指令实现断点,为了让插入断点的进程依然能够恢复运行,在插入断点之前对该地址原有指令进行备份,遇到断点之后再将备份的指令还原,并且恢复命中断点时的寄存器值,尤其是rip指针需要减1,回退一个地址。
过程如上图所示,第一步rip先指向byte2对应地址处,利用PTRACE_PEEKDATA将byte2,byte3取出备份,同时保存当前寄存器值,为恢复做备份。第二步插入0xcc,0x00指令,即int3中断指令,执行一步来到第三步rip指向0x00,触发中断,子进程暂停。第四步,为了让子进程继续运行,将备份的原始指令写入rip-1处,并且利用PTRACE_SETREGS将寄存器值恢复成原来的值,此时rip跟着上移。这样子进程可以继续正常运行不会core dump。以上四步构成了在byte2对应地址处打上断点的操作。
要完成插入断点并且运行到断点停止,并且能恢复原有指令继续正常运行的非常关键的一点就是需要知道子进程是否命中断点。因为子进程完全有可能因为接收到其他信号而暂停,同时产生SIGTRAP信号发送给父进程,并不一定就是因为断点而暂停并发送SIGTRAP信号。因此在等待被调试进程的时候,当截获SIGTRAP信号需要取出rip指针,此时如果是断点触发的暂停信号,rip肯定指向0xcc指令的下一条指令,故而只需要判断当初我们输入的打断点的地址addr是否等于rip-1。如果相等那么断点命中,命中之后就可以将原有指令恢复,把寄存器值恢复。
2. 程序的模块划分
主要函数
1 | void getdata(pid_t child, long addr, char* str, int len); |
3. 遇到的问题及解决方法
3.1 Linux地址空间随机化产生的问题
运行代码fork子进程之后,循环单步执行,每执行一步输出一次rip指针,共万步有余。每次执行代码输出的rip都各不相同,从第一次输出的rip到最后一次输出的rip指针并不是固定的几个值,而是每次执行输出的都是一批不同的rip序列。这给后期断点功能的实现造成了很大的麻烦。比如我使用GDB给被调试进程main函数第一行打断点,执行到断点处执行i r rip
命令观察此时rip值,假设此时rip的值为aaa。我以为获得了子进程源代码main函数第一行的地址即aaa,于是将其设为断点地址却发现断点命中不了。
为了确认我自己代码fork出的子进程所有指令的地址里有aaa,我单步执行,每次单步就取一次rip指针的值,与aaa进行比对,发现没有任何地址与aaa相等,这与gdb给出的结果不符。且每次运行,rip输出的序列内容都和上一次运行输出的rip序列不同。经过查找资料确定是Linux地址空间随机化的缘故。ASLR 技术将进程的某些内存空间地址进行随机化来增大入侵者预测目的地址的难度,从而降低进程被成功入侵的风险。
使用命令sudo bash -c "echo 0 > /proc/sys/kernel/randomize_va_space"
关闭了ASLR,之后rip输出的序列不再随机变化,而是固定的序列。由此GDB获取到的rip地址和我自己获取的rip开始保持一致。
3.2 无法确定应该注入断点的地址
解决了被调试进程虚拟地址总是变化的问题之后,就可以指定断点的地址了。使用反汇编命令objdump -d test
获得被调试子进程的汇编代码以及每条汇编代码的偏移地址。我发现gdb断点到相应的行给出来的地址和反汇编的地址不一样,这样如果想要通过反汇编找到main函数入口地址,根据这个入口地址设置断点是无法成功的,通过观察我发现反汇编出来的地址是偏移地址,这个偏移地址总是与被调试进程对应指令的实际虚拟地址相差一个常数,我这里是0x5555_5555_4000。比如反汇编被调试子进程main函数入口地址是0x1129,直接将0x1129作为断点地址会报错,如果将0x5555_5555_4000 + 0x1129作为main函数的虚拟地址就可以断点注入成功,而且这个相加的和与gdb获得的地址一致。
本来直接将这个神秘常数拿来相加就可以利用反汇编得到的地址打断点了,但是我不能保证所有的被调试进程都是相差这个常数,经过查阅资料我知道0x5555_5555_4000是子进程的虚拟地址空间的首地址,通过pmap -x pid
命令可以获取任意进程的内存分布范围。查阅资料得知,Linux将进程的内存分布信息缓存在/proc/进程pid/maps文件中,pmap的原理也是解析这个文件,于是我通过解析这个文件便成功获取到了子进程的虚拟内存起始地址。
如此就可以很方便地通过反汇编objdump -d
指令获取汇编的偏移地址,作为断点地址的参数进行断点注入了,而无需关心子进程的虚拟内存其实地址是多少,因为反汇编得出来的汇编指令的地址是不变的。
3.3 断点命中成功,恢复源代码失败
恢复寄存器的时候忘记调整rip指针了,应该将rip指针减一,回退到断点的地址处。
4. 程序使用说明及运行结果
当前目录下含有5个文件
1 | tree |
Linux 平台上 ASLR 分为 0,1,2 三级,用户可以通过一个内核参数 randomize_va_space 进行等级控制。它们对应的效果如下:
0:没有随机化。即关闭 ASLR。
1:保留的随机化。共享库、栈、mmap() 以及 VDSO 将被随机化。
2:完全的随机化。在 1 的基础上,通过 brk() 分配的内存空间也将被随机化。
ASLR.sh脚本用来设置随机化等级:
ptrace_debugger是main.cpp编译的可执行文件
test是被调试进程test.cpp编译的可执行文件
执行如下命令关闭随机化:
1 | ./ASLR.sh 0 |
运行ptrace_debugger:
1 | ./ptrace_debugger |
查看寄存器:
1 | (PDebugger) >r |
单步调试:
1 | (PDebugger) >r |
恢复运行:
1 | (PDebugger) >s |
查看任意内存空间:
1 | (PDebugger) >m -off -20 -nb 40 |
计算指令数:
1 | (PDebugger) >ic |
断点调试:
先进行反汇编
1 | hy@ubuntu:~/下载/ptrace_debugger$ ls |
可以看到main函数入口地址是0x1129
打断点:
1 | Please input the name of program to be traced: |
5. 代码
完整项目地址GitHub - Kakaluoto/ptraceDebugger: 利用ptrace系统调用实现的debugger
5.1 被调试子进程tracee
test.cpp:
1 | int main() { |
5.2 关闭ASLR脚本
1 | !/bin/bash |
5.3 Debugger代码
1 |
|
参考文章
linux工具pmap原理? - 知乎
linux - 在 Linux 中如何确定 PIE 可执行文件的文本部分的地址? - IT工具网
Linux虚拟地址空间布局以及进程栈和线程栈总结 - Xzzzh - 博客园
Linux虚拟地址空间布局 - clover_toeic - 博客园
针对 Linux 环境下 gdb 动态调试获取的局部变量地址与直接运行程序时不一致问题的解决方案 - yhjoker - 博客园
Writing a Linux Debugger Part 3: Registers and memory
浅析Linux 64位系统虚拟地址和物理地址的映射及验证方法 - Binfun - 博客园
Linux:断点原理与实现 - 云+社区 - 腾讯云
一口气看完45个寄存器,CPU核心技术大揭秘 - 知乎
断点原理与实现 - 知乎
一窥GDB原理
Linux沙箱入门——ptrace从0到1 - 安全客,安全资讯平台
Linux内核学习笔记(4)— wait、waitpid、wait3 和 wait4 - tongye - 博客园
GDB原理之ptrace实现原理 - 云+社区 - 腾讯云
用图文带你彻底弄懂GDB调试原理 - 云+社区 - 腾讯云
Linux Hook 笔记 - evilpan
[原创]一窥GDB原理-Pwn-看雪论坛-安全社区|安全招聘|bbs.pediy.com
Linux沙箱入门——ptrace从0到1 - 安全客,安全资讯平台
Linux信号列表及其详解 - zy010101 - 博客园
Linux 信号(signal) - 简书
PTRACE - Linux手册页-之路教程
Linux的中断和系统调用 & esp、eip等寄存器 - blcblc - 博客园
Linux ptrace 简介
Linux Hook 笔记 - 有价值炮灰 - 博客园
linux ptrace II - mmmmar - 博客园
Ptrace—Linux中一种代码注入技术的应用 | 力托斯特的博客
linux ptrace I - mmmmar - 博客园
linux中fork()函数详解 - 学习记录园 - 博客园
[译] 玩转ptrace (一) - twoon - 博客园
打印进程内存信息 | 100个gdb小技巧
pwn从入门到放弃第三章——gdb的基本使用教程 | PWN? PWN!
c - objdump -d输出汇编的含义 - Thinbug
c - 为什么GDB和Objdump的指令地址相同? - Thinbug
GDB调试 - devbins blog
调试器工作原理之二——实现断点(ptrace) - CodeAntenna
汇编语言基础:寄存器和系统调用 - Yungyu - 博客园
在程序地址上打断点 | 100个gdb小技巧
C语言指针转换为intptr_t类型 - Rabbit_Dale - 博客园
x86_64汇编基础 - ym65536 - 博客园
GDB调试-从入门实践到原理
objdump(Linux)反汇编命令使用指南_wang.wenchao的博客-CSDN博客_linux 反汇编
解决munmap_chunk(): invalid pointer和Segmentation fault的bug_summer的专栏-CSDN博客_munmap_chunk()
调试器工作原理之二——实现断点(ptrace)_weixin_33895016的博客-CSDN博客
gdb查看当前汇编指令_counsellor的专栏-CSDN博客_gdb 查看汇编
objdump命令的使用_北落师门’的专栏-CSDN博客_objdump
进程与进程描述符(task_struct)_Steve_Abelieve-CSDN博客_进程描述符
Linux 查看进程内存分布_谈谈1974-CSDN博客_linux 查看内存布局
Linux下关闭ALSR(地址空间随机化)的方法_counsellor的专栏-CSDN博客_linux关闭地址随机化
Linux平台的ASLR机制_加号减减号的博客-CSDN博客_aslr linux
ELF entry point和装载地址_ayu_ag的专栏-CSDN博客_elf入口地址
linux中如何断点调试程序,开发一个Linux调试器(二):断点_刘为龙的博客-CSDN博客
Linux 命令(1)—— nm 命令_baboon_chen-CSDN博客
Linux X86_64位虚拟地址空间布局与试验_Meows的牧场-CSDN博客_虚拟空间64位
深入理解 Linux 位置无关代码 PIC_内核工匠-CSDN博客
Linux ELF装载过程及64位地址空间布局_@HDS的博客-CSDN博客
linux内存地址断点,开发一个 Linux 调试器(三):寄存器和内存_别总叫我大叔的博客-CSDN博客
链接与加载-NJU-JYY_Adenialzz的博客-CSDN博客
自己写调试器 软断点 [Linux]_氺在飛天-CSDN博客
linux调试器的实现—-断点的实现_darmao的博客-CSDN博客_linux打断点
LINUX 逻辑地址、线性地址、虚拟地址和物理地址_十一月zz的博客-CSDN博客_linux 逻辑地址
详解:物理地址,虚拟地址,内存管理,逻辑地址之间的关系_17岁boy的博客-CSDN博客_逻辑地址与物理地址
调试器工作原理系列三篇_关注 Linux c/c++ 数据存储 网络 算法……-CSDN博客
Linux源码分析之Ptrace_七月冷雨-CSDN博客_linux ptrace
x86_64汇编之六:系统调用(system call)_ponnylv的博客-CSDN博客
Linux Ptrace 详解_七月冷雨-CSDN博客_linux ptrace
玩转ptrace(二)_zhangmiaoping23的专栏-CSDN博客
linux系统:ptrace系统调用浅析_老王不让用的博客-CSDN博客_ptrace 函数调用