ptrace使用和调试
1. 文章参考[原创]一窥GDB原理-PwnLinux ptrace系统调用详解:利用 ptrace 设置硬件断点<<软件调试>> 张银奎2. ptrace函数原型enum __ptrace_request{PTRACE_TRACEME = 0,//被调试进程调用PTRACE_PEEKDATA = 2,//查看内存PTRACE_PEEKUSER = 3,//查看struct
1. 文章参考
Linux ptrace系统调用详解:利用 ptrace 设置硬件断点
<<软件调试>> 张银奎
<<程序员的自我修养>> 俞甲子 石凡 潘爱民
2. ptrace函数原型
enum __ptrace_request
{
PTRACE_TRACEME = 0, //被调试进程调用
PTRACE_PEEKDATA = 2, //查看内存
PTRACE_PEEKUSER = 3, //查看struct user 结构体的值
PTRACE_POKEDATA = 5, //修改内存
PTRACE_POKEUSER = 6, //修改struct user 结构体的值
PTRACE_CONT = 7, //让被调试进程继续
PTRACE_SINGLESTEP = 9, //让被调试进程执行一条汇编指令
PTRACE_GETREGS = 12, //获取一组寄存器(struct user_regs_struct)
PTRACE_SETREGS = 13, //修改一组寄存器(struct user_regs_struct)
PTRACE_ATTACH = 16, //附加到一个进程
PTRACE_DETACH = 17, //解除附加的进程
PTRACE_SYSCALL = 24, //让被调试进程在系统调用前和系统调用后暂停
};
long int ptrace (enum __ptrace_request __request, ...)
这个枚举值只列出了一部分,更多的功能可以去查看man手册
3. 基本用法和被调用进程的信息获取
3.1 寄存器信息获取
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/reg.h>
#include <sys/user.h>
#include <sys/syscall.h>
#include <stdio.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
pid_t pid;
int status = 0;
//一组寄存器的值
struct user_regs_struct regs;
pid = fork();
if (pid == 0) { //子进程
if (ptrace(PTRACE_TRACEME,0,NULL,NULL) < 0) {
perror("ptrace TRACEME err");
return -1;
}
pid_t pid = getpid();
//给自己发信号,让父进程wait返回
if (kill(pid,SIGSTOP) != 0) {
perror("kill sigstop err");
}
printf("child exit\n");
return 0;
} else if (pid < 0) {
perror("fork err");
return -1;
}
//监听子进程的状态
wait(&status);
if (WIFEXITED(status))
return 0;
//获取子进程的寄存器
if (ptrace(PTRACE_GETREGS,pid,NULL,®s) < 0) {
perror("get regs err");
}
printf("rax = %llx\n",regs.rax);
printf("rip = %llx\n",regs.rip);
printf("rbp = %llx\n",regs.rbp);
printf("rsp = %llx\n",regs.rsp);
sleep(2);
//让子进程继续执行
if (ptrace(PTRACE_CONT,pid,NULL,NULL) < 0) {
perror("CONT err");
}
return 0;
}
输出:
rax = 0
rip = 7f20cf9d92a7
rbp = 7fff166972a0
rsp = 7fff16697198
child exit
这里使用了ptrace的三个枚举值
PTRACE_TRACEME:这个是被调试的进程使用的,使用之后父进程才可以去跟踪子进程
PTRACE_CONT:让暂停的子进程继续执行
PTRACE_GETREGS:获取对应的一组寄存器的值,(struct user_regs_struct)这个结构体定义在<sys/user.h>中,这个结构体保存了一组寄存器的信息
struct user_regs_struct
{
__extension__ unsigned long long int r15;
__extension__ unsigned long long int r14;
__extension__ unsigned long long int r13;
__extension__ unsigned long long int r12;
__extension__ unsigned long long int rbp;
__extension__ unsigned long long int rbx;
__extension__ unsigned long long int r11;
__extension__ unsigned long long int r10;
__extension__ unsigned long long int r9;
__extension__ unsigned long long int r8;
__extension__ unsigned long long int rax;
__extension__ unsigned long long int rcx;
__extension__ unsigned long long int rdx;
__extension__ unsigned long long int rsi;
__extension__ unsigned long long int rdi;
__extension__ unsigned long long int orig_rax;
__extension__ unsigned long long int rip;
__extension__ unsigned long long int cs;
__extension__ unsigned long long int eflags;
__extension__ unsigned long long int rsp;
__extension__ unsigned long long int ss;
__extension__ unsigned long long int fs_base;
__extension__ unsigned long long int gs_base;
__extension__ unsigned long long int ds;
__extension__ unsigned long long int es;
__extension__ unsigned long long int fs;
__extension__ unsigned long long int gs;
};
跟踪的流程
3.1.1 系统调用信息获取
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/reg.h>
#include <sys/user.h>
#include <sys/syscall.h>
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
pid_t pid;
int orig_rax;
int iscalling = 0;
int status = 0;
uint64_t arg1,arg2,arg3;
//一组寄存器的值
struct user_regs_struct regs;
pid = fork();
if (pid == 0) { //子进程
if (ptrace(PTRACE_TRACEME,0,NULL,NULL) < 0) {
perror("ptrace TRACEME err");
return -1;
}
pid_t pid = getpid();
if (kill(pid,SIGSTOP) != 0) {
perror("kill sigstop err");
}
write(STDOUT_FILENO,"aaaa -> ",8);
write(STDOUT_FILENO,"bbbb -> ",8);
write(STDOUT_FILENO,"cccc -> ",8);
return 0;
} else if (pid < 0) {
perror("fork err");
return -1;
}
//监听子进程的状态
wait(&status);
if (WIFEXITED(status))
return 0;
//让子进程在调用系统调用时暂停
if (ptrace(PTRACE_SYSCALL,pid,NULL,NULL) < 0) {
perror("ptrace SYSCALL err");
return -1;
}
while (1) {
wait(&status);
if (WIFEXITED(status))
break;
ptrace(PTRACE_GETREGS,pid,NULL,®s);//获取寄存器整个结构
orig_rax = regs.orig_rax;//获取系统调用号
if (orig_rax == SYS_write) {
if (!iscalling) {//系统调用前
iscalling = 1;
arg1 = regs.rdi;
arg2 = regs.rsi;
arg3 = regs.rdx;
} else {//系统调用后
printf("%lld = write(%ld,\"%s\",%ld)\n",regs.rax,arg1,(char *)arg2,arg3);
iscalling = 0;
}
}
//让子进程在调用系统调用时暂停
if (ptrace(PTRACE_SYSCALL,pid,NULL,NULL) < 0) {
perror("CONT err");
return -1;
}
}
return 0;
}
输出:
aaaa -> 8 = write(1,"aaaa -> ",8)
bbbb -> 8 = write(1,"bbbb -> ",8)
cccc -> 8 = write(1,"cccc -> ",8)
这里又使用了一个新的枚举值
PTRACE_SYSCALL:这个值表示子进程在调用系统调用前和调用系统调用后的时候暂停。
利用这个枚举,我们在系统调用前去获取调用的参数,在系统调用后去获取它的返回值。
系统调用的方式:
- syscall:
寄存器 rax 中存放系统调用号,同时系统调用返回值也存放在 rax 中
当系统调用参数小于等于6个时,参数则必须按顺序放到寄存器 rdi,rsi,rdx,r10,r8,r9中 - int 0x80 :
寄存器 rax 中存放系统调用号,同时返回值也存放在 rax 中
当系统调用参数小于等于6个时,参数则必须按顺序放到寄存器 rbx,rcx,rdx,rsi,rdi ,rbp中
在我的电脑上用的是syscall的调用方式,所以我在系统调用前保存了write系统调用了 rdi,rsi,rdx的值。在系统调用后去输出整个write调用的参数
regs.orig_rax 保存了系统调用号,可以利用系统调用的特性去获得调用时的参数。这个宏也是 ‘strace’的实现原理
3.2 内存信息
3.2.1 内存读取
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/reg.h>
#include <sys/user.h>
#include <sys/syscall.h>
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
pid_t pid;
int status = 0;
uint64_t num = 0;
pid = fork();
if (pid == 0) { //子进程
if (ptrace(PTRACE_TRACEME,0,NULL,NULL) < 0) {
perror("ptrace TRACEME err");
return -1;
}
pid_t pid = getpid();
num = 20;
if (kill(pid,SIGSTOP) != 0) {
perror("kill sigstop err");
}
return 0;
} else if (pid < 0) {
perror("fork err");
return -1;
}
//监听子进程的状态
wait(&status);
if (WIFEXITED(status))
return 0;
uint64_t tem = 0;
tem = ptrace(PTRACE_PEEKDATA,pid,&num,NULL);
printf("read num = %ld\n",tem);
printf("this num = %ld\n",num);
//让子进程继续跑
if (ptrace(PTRACE_CONT,pid,NULL,NULL) < 0) {
perror("ptrace SYSCALL err");
return -1;
}
return 0;
}
输出:
read num = 20
this num = 0
PTRACE_PEEKTEXT/PTRACE_PEEKDATA: 这两个宏是没有什么区别。表示读取某个地址的的值,读取宽度是8字节(64位)
我们定义了一个全局变量’num’,在子进程修改值后发送信号,然后父进程去读取这个地址的信息。(这里利用了fork()后父子进程的地址是一样的)
3.2.2 内存修改
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/reg.h>
#include <sys/user.h>
#include <sys/syscall.h>
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
pid_t pid;
int status = 0;
uint64_t num = 0;
pid = fork();
if (pid == 0) { //子进程
if (ptrace(PTRACE_TRACEME,0,NULL,NULL) < 0) {
perror("ptrace TRACEME err");
return -1;
}
printf("not write num = %ld\n",num);
pid_t pid = getpid();
if (kill(pid,SIGSTOP) != 0) {
perror("kill sigstop err");
}
printf("write end num = %ld\n",num);
return 0;
} else if (pid < 0) {
perror("fork err");
return -1;
}
//监听子进程的状态
wait(&status);
if (WIFEXITED(status))
return 0;
ptrace(PTRACE_POKEDATA,pid,&num,120);
//让子进程继续跑
if (ptrace(PTRACE_CONT,pid,NULL,NULL) < 0) {
perror("ptrace SYSCALL err");
return -1;
}
return 0;
}
输出:
not write num = 0
write end num = 120
PTRACE_POKETEXT/PTRACE_POKEDATA:和之前的读取宏一样,这个宏也是一样的。表示写入某个地址的数据,写入的宽度是8字节(64位)
定义一个变量’num’,在父进程修改前后输出
3.3 断点插入
3.3.1 软件断点
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/reg.h>
#include <sys/user.h>
#include <sys/syscall.h>
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#define INT_3 0xcc
void bar(int num)
{
printf("num = %d\n",num);
}
int main(int argc,char *argv[])
{
pid_t pid;
int status = 0;
struct user_regs_struct regs;
pid = fork();
if (pid == 0) { //子进程
if (ptrace(PTRACE_TRACEME,0,NULL,NULL) < 0) {
perror("ptrace TRACEME err");
return -1;
}
pid_t pid = getpid();
if (kill(pid,SIGSTOP) != 0) {
perror("kill sigstop err");
}
for (int i = 0;i < 10;i++) {
printf("%d\n",i);
bar(111);
}
return 0;
} else if (pid < 0) {
perror("fork err");
return -1;
}
//监听子进程的状态
wait(&status);
if (WIFEXITED(status))
return 0;
//保存原来的字节码
uint64_t orig_code = ptrace(PTRACE_PEEKTEXT,pid,(void *)bar,0);
//在地址的开头插入0xcc(插入软件断点)
ptrace(PTRACE_POKETEXT, pid, (void *)bar, (orig_code & 0xFFFFFFFFFFFFFF00) | INT_3);
//让子进程继续跑
if (ptrace(PTRACE_CONT,pid,NULL,NULL) < 0) {
perror("ptrace SYSCALL err");
return -1;
}
while (1) {
wait(&status);
if (WIFEXITED(status))
return 0;
//对比输出子进程断下的地址和bar地址
ptrace(PTRACE_GETREGS,pid,NULL,®s);
printf("0x%llx\n",regs.rip);
printf("%p\n",(void *)bar);
//恢复之前的代码值
ptrace(PTRACE_POKETEXT,pid,(void *)bar,orig_code);
//0xcc占一个字节,让ip寄存器恢复
regs.rip = regs.rip - 1;
//设置寄存器的值(主要用于恢复ip寄存器)
ptrace(PTRACE_SETREGS,pid,0,®s);
//用于看效果,确保子进程是真的断下
sleep(1);
//执行一条汇编指令,然后子进程会暂停
ptrace(PTRACE_SINGLESTEP,pid,0,0);
//等待子进程停止
wait(NULL);
//断点恢复
ptrace(PTRACE_POKETEXT, pid, (void *)bar, (orig_code & 0xFFFFFFFFFFFFFF00) | INT_3);
//子进程继续执行
if (ptrace(PTRACE_CONT,pid,NULL,NULL) < 0) {
perror("ptrace SYSCALL err");
return -1;
}
}
return 0;
}
输出:
0
0x55bfcbdda8ab
0x55bfcbdda8aa
num = 111
1
0x55bfcbdda8ab
0x55bfcbdda8aa
num = 111
2
0x55bfcbdda8ab
0x55bfcbdda8aa
num = 111
3
0x55bfcbdda8ab
0x55bfcbdda8aa
num = 111
4
0x55bfcbdda8ab
0x55bfcbdda8aa
num = 111
5
0x55bfcbdda8ab
0x55bfcbdda8aa
num = 111
6
0x55bfcbdda8ab
0x55bfcbdda8aa
num = 111
7
0x55bfcbdda8ab
0x55bfcbdda8aa
num = 111
8
0x55bfcbdda8ab
0x55bfcbdda8aa
num = 111
9
0x55bfcbdda8ab
0x55bfcbdda8aa
num = 111
PTRACE_SINGLESTEP:执行一条汇编指令,然后暂停。
软件断点的核心是要在代码段插入0xcc字节码(也就是int 3,3号中断)
我们这个程序在bar这个函数的开头插入0xcc使得子进程每次调用bar函数的时候断下,然后去恢复之前的指令去执行。执行完成之后继续插入断点
3.3.2 硬件断点
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/reg.h>
#include <sys/user.h>
#include <sys/syscall.h>
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#define DR_OFFSET(num) ((void *) (& ((struct user *) 0)->u_debugreg[num]))
void bar(int num)
{
printf("num = %d\n",num);
}
int main(int argc,char *argv[])
{
pid_t pid;
int status = 0;
struct user_regs_struct regs;
pid = fork();
if (pid == 0) { //子进程
if (ptrace(PTRACE_TRACEME,0,NULL,NULL) < 0) {
perror("ptrace TRACEME err");
return -1;
}
pid_t pid = getpid();
if (kill(pid,SIGSTOP) != 0) {
perror("kill sigstop err");
}
for (int i = 0;i < 10;i++) {
printf("%d\n",i);
bar(111);
}
return 0;
} else if (pid < 0) {
perror("fork err");
return -1;
}
//监听子进程的状态
wait(&status);
if (WIFEXITED(status))
return 0;
//设置dr0寄存器为bar的地址
if (ptrace(PTRACE_POKEUSER, pid, DR_OFFSET(0), (void *)bar) < 0) {
perror("tracer, faile to set DR_0\n");
}
uint64_t dr_7 = 0;
//设置对应的标准位使得dr0寄存器的地址生效
dr_7 = dr_7 | 0x01;//L0位,局部
dr_7 = dr_7 | 0x02;//G0位,全局
//设置dr7寄存器的值
if (ptrace(PTRACE_POKEUSER, pid, DR_OFFSET(7), dr_7) < 0) {
perror("tracer, faile to set DR_7\n");
}
//让子进程继续跑
if (ptrace(PTRACE_CONT,pid,NULL,NULL) < 0) {
perror("ptrace SYSCALL err");
return -1;
}
while (1) {
wait(&status);
if (WIFEXITED(status))
return 0;
ptrace(PTRACE_GETREGS,pid,NULL,®s);
//对比子进程当前断下的地址和函数bar的地址
printf("function bar() = %p\n",(void *)bar);
printf("break rip = %llx\n",regs.rip);
//观察子进程是否暂停
sleep(1);
if (ptrace(PTRACE_CONT,pid,NULL,NULL) < 0) {
perror("ptrace SYSCALL err");
return -1;
}
}
return 0;
}
输出:
0
function bar() = 0x55a4ef5908aa
break rip = 55a4ef5908aa
num = 111
1
function bar() = 0x55a4ef5908aa
break rip = 55a4ef5908aa
num = 111
2
function bar() = 0x55a4ef5908aa
break rip = 55a4ef5908aa
num = 111
3
function bar() = 0x55a4ef5908aa
break rip = 55a4ef5908aa
num = 111
4
function bar() = 0x55a4ef5908aa
break rip = 55a4ef5908aa
num = 111
5
function bar() = 0x55a4ef5908aa
break rip = 55a4ef5908aa
num = 111
6
function bar() = 0x55a4ef5908aa
break rip = 55a4ef5908aa
num = 111
7
function bar() = 0x55a4ef5908aa
break rip = 55a4ef5908aa
num = 111
8
function bar() = 0x55a4ef5908aa
break rip = 55a4ef5908aa
num = 111
9
function bar() = 0x55a4ef5908aa
break rip = 55a4ef5908aa
num = 111
PTRACE_POKEUSER:用于写如入struct user结构体的值,第3个参数是该结构体的成员的偏移位置,第4个参数表示写入的值。
这是这个结构体的定义
struct user
{
struct user_regs_struct regs;
int u_fpvalid;
struct user_fpregs_struct i387;
__extension__ unsigned long long int u_tsize;
__extension__ unsigned long long int u_dsize;
__extension__ unsigned long long int u_ssize;
__extension__ unsigned long long int start_code;
__extension__ unsigned long long int start_stack;
__extension__ long long int signal;
int reserved;
__extension__ union
{
struct user_regs_struct* u_ar0;
__extension__ unsigned long long int __u_ar0_word;
};
__extension__ union
{
struct user_fpregs_struct* u_fpstate;
__extension__ unsigned long long int __u_fpstate_word;
};
__extension__ unsigned long long int magic;
char u_comm [32];
__extension__ unsigned long long int u_debugreg [8];
};
其中第一个成员是我们之前使用过的寄存器组,最后一个成员是调试寄存器。
与之对应的还有PTRACE_PEEKUSER宏,这个宏是用来读取struct user结构体成员的值用的。
调试寄存器
dr0 - dr3寄存器:这个4个寄存器用于写入地址用的
dr4 - dr6寄存器:详情请看<<软件调试>>
dr7:
L0 - L3位:分别对应dr0-dr3,设置断点作用范围,如果被置位,那么将只对当前任务有效
G0-G3位:分别对应dr0-dr3,那么所有的任务都有效
(在我实验中好像这两个位没什么区别,如果知道原因的请留言,感谢)
R/W0-R/W3:读写位
00 执行断点
01 写入数据断点
10 I/O端口断点(只用于pentium+,需设置CR4的DE位)
11 读或写数据断点
LEN0-LEN3:指定内存操作的大小
00:1字节(执行断点只能是1字节长)
01:2字节
10:未定义或者是8字节(和cpu的系列有关系)
11:4字节
关于更多调试寄存器的理论知识推荐看《软件调试》
理解这些理论之后,在去观看写的demo,可以看到我们设置dr0寄存器的地址为bar函数的地址,然后设置dr7寄存器的L0和G0位让这个地址生效,之后子进程执行bar函数是就会断下
4. 调试程序
之前的demo都是在一个进程调试自己fork出来的子进程。如果我们要调试在文件系统中的程序或者真在运行的程序怎么办呢?
例如写一个test程序的代码
#include <stdio.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
write(STDOUT_FILENO,"aaaa\n",5);
write(STDOUT_FILENO,"bbbb\n",5);
write(STDOUT_FILENO,"cccc\n",5);
return 0;
}
我们编译成test可执行程序,我们如何去调试这个程序?
我们可以fork出一个子进程执行完ptrace后再去调用exec族的函数。
把我们之前调试系统调用的代码改一下
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/reg.h>
#include <sys/user.h>
#include <sys/syscall.h>
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
pid_t pid;
int orig_rax;
int iscalling = 0;
int status = 0;
uint64_t arg1,arg2,arg3;
//一组寄存器的值
struct user_regs_struct regs;
pid = fork();
if (pid == 0) { //子进程
if (ptrace(PTRACE_TRACEME,0,NULL,NULL) < 0) {
perror("ptrace TRACEME err");
return -1;
}
//调试当前的test程序
execl("./test","./test",NULL);
return 0;
} else if (pid < 0) {
perror("fork err");
return -1;
}
//监听子进程的状态
wait(&status);
if (WIFEXITED(status))
return 0;
//让子进程在调用系统调用时暂停
if (ptrace(PTRACE_SYSCALL,pid,NULL,NULL) < 0) {
perror("ptrace SYSCALL err");
return -1;
}
while (1) {
wait(&status);
if (WIFEXITED(status))
break;
ptrace(PTRACE_GETREGS,pid,NULL,®s);//获取寄存器整个结构
orig_rax = regs.orig_rax;//获取系统调用号
if (orig_rax == SYS_write) {
if (!iscalling) {//系统调用前
iscalling = 1;
arg1 = regs.rdi;
arg2 = regs.rsi;
arg3 = regs.rdx;
} else {//系统调用后
char buf[16] = {0};
//读取子进程write参数2地址的值
*((uint64_t *)buf) = ptrace(PTRACE_PEEKDATA,pid,(void *)arg2,NULL);
*((uint64_t *)(buf + 8)) = ptrace(PTRACE_PEEKDATA,pid,(void *)arg2,NULL);
printf("%lld = write(%ld,\"%s\",%ld)\n",regs.rax,arg1,buf,arg3);
iscalling = 0;
}
}
//让子进程在调用系统调用时暂停
if (ptrace(PTRACE_SYSCALL,pid,NULL,NULL) < 0) {
perror("CONT err");
return -1;
}
}
return 0;
}
输出:
aaaa
5 = write(1,"aaaa
",5)
bbbb
5 = write(1,"bbbb
",5)
cccc
5 = write(1,"cccc
",5)
在子进程执行完exec函数的时候会暂停,此时父进程wait返回,设置系统调用暂停的宏。然后进行系统调用参数获取。需要注意的是如果获取的值是个地址,需要获取其内容的话需要去读取这个地址的内存的值。这里用的是取巧的方式,由于我们知道这个内存的长度是6,然后调用了两次读的操作。
4.1 程序符号表获取
想要获取一个可执行程序的符号就必须知道这个可执行程序的结构,在linux中这种结构一般是elf文件格式,在windows中是pe文件格式。
这种可执行文件里面包含了字符串表,调试信息,全局变量,代码指令等
4.1.1 elf结构
elf结构是以’段’来组织结构的包含代码段、数据段等,
在elf最开始的地方有一个’文件头’,这个文件头包含了文件属性,是否可以被执行、目标的硬件、操作系统其中最重要的是包含了一个段表信息,这个段表信息可以让我们找到这个可执行程序的所有段的信息。
4.1.2 elf头
所有的elf相关的信息都定义在了/usr/include/elf.h
文件头的结构
typedef struct
{
unsigned char e_ident[EI_NIDENT]; //用于确定平台信息,操作系统、大小端等信息
Elf64_Half e_type; //文件类型,
Elf64_Half e_machine; //CPU平台属性
Elf64_Word e_version; //版本信息
Elf64_Addr e_entry; //入口地址
Elf64_Off e_phoff; //
Elf64_Off e_shoff; //段表在文件中的偏移
Elf64_Word e_flags; //
Elf64_Half e_ehsize; //elf文件头的的大小
Elf64_Half e_phentsize; /* Program header table entry size */
Elf64_Half e_phnum; /* Program header table entry count */
Elf64_Half e_shentsize; //段表描述符的大小 sizeof(Elf64_Shdr)
Elf64_Half e_shnum; //段表数量
Elf64_Half e_shstrndx; //字符串表的位置(段表的下表)
} Elf64_Ehdr;
其中我们关注的几个信息:
e_shoff 段表在文件中的偏移,用于找到所有的段
e_shnum 段表数量
e_shstrndx 字符串表的位置(段表的下表)
4.1.3 段表
段表是一个数组,数组的大小为段表头里的e_shnum
段表每一个数组项的结构
typedef struct
{
Elf64_Word sh_name; //段表名 (在字符串表的索引)
Elf64_Word sh_type; /* Section type */
Elf64_Xword sh_flags; /* Section flags */
Elf64_Addr sh_addr; //加载后的段表的虚拟地址
Elf64_Off sh_offset; //段在文件中的偏移
Elf64_Xword sh_size; //段的大小
Elf64_Word sh_link; /* Link to another section */
Elf64_Word sh_info; /* Additional section information */
Elf64_Xword sh_addralign; //段地址对齐(这个值的2的指数)
Elf64_Xword sh_entsize; /* Entry size if section holds table */
} Elf64_Shdr;
段表需要关注的信息;
sh_name 段表的名称(这个名称是字符串表的下标)
sh_offset 段在文件中的偏移
4.1.3 字符串表
字符串表的格式就是一个 char 类型的数组,每个段的名字可以根据下标去获得值。
下面写一个获取所有段名和段位置的程序
文件相关的代码
//获取文件大小
int get_file_size(FILE *fp)
{
if(fp == NULL) return -1;
fseek(fp, 0, SEEK_END);
int length = ftell(fp);
fseek(fp, 0, SEEK_SET);
return length;
}
//获取文件内容
int get_file_word(FILE *fp,char *word)
{
if(fp == NULL || word == NULL) return -1;
int seek = 0;
while (!feof(fp)) {
seek += fread(word+seek,1,1024,fp);
}
return 0;
}
char *open_file(const char *filepath)
{
FILE *fp = fopen(filepath,"r");
if(fp == NULL){
perror("fopen error");
return NULL;
}
int filesize = get_file_size(fp);
char *const word = (char *)calloc(1,filesize+1);
get_file_word(fp,word);
fclose(fp);
return word;
}
void close_file(char *p)
{
free(p);
}
elf段表获取
int main(int argc,char *argv[])
{
if (argc < 2) return 0;
//获取文件内容
char *word = open_file(argv[1]);
//文件头
Elf64_Ehdr *ehdr = (Elf64_Ehdr *)word;
int table_max = ehdr->e_shnum;//段表的数量
int shstrndx = ehdr->e_shstrndx;//字符串表在段表中的下标
Elf64_Shdr *stables = (Elf64_Shdr *)(word + ehdr->e_shoff);//段表数组
//找到字符串表的位置
char *shstrtab_addr = word + stables[shstrndx].sh_offset;
//输出段表名和段表位置
for (int i = 0;i < table_max;i++) {
printf("%3d %20s 0x%lx\n",i,shstrtab_addr + stables[i].sh_name,stables[i].sh_offset);
}
//释放资源
close_file(word);
return 0;
}
写一个demo测试
#include <stdio.h>
int add(int a,int b)
{
return a + b;
}
int sub(int a,int b)
{
return a - b;
}
int main(int argc,char *argv[])
{
return 0;
}
把两个程序编译
我把elf程序编译为test,demo编译为code
运行 ./test code
输出:
0 0x0
1 .interp 0x318
2 .note.gnu.property 0x338
3 .note.gnu.build-id 0x358
4 .note.ABI-tag 0x37c
5 .gnu.hash 0x3a0
6 .dynsym 0x3c8
7 .dynstr 0x458
8 .gnu.version 0x4d6
9 .gnu.version_r 0x4e8
10 .rela.dyn 0x508
11 .init 0x1000
12 .plt 0x1020
13 .plt.got 0x1030
14 .text 0x1040
15 .fini 0x11e8
16 .rodata 0x2000
17 .eh_frame_hdr 0x2004
18 .eh_frame 0x2050
19 .init_array 0x2df0
20 .fini_array 0x2df8
21 .dynamic 0x2e00
22 .got 0x2fc0
23 .data 0x3000
24 .bss 0x3010
25 .comment 0x3010
26 .symtab 0x3040
27 .strtab 0x3640
28 .shstrtab 0x3838
可以使用readelf -S code 命令去对比
4.1.4 符号表
刚刚我们用到一个段表就是字符串表
char *shstrtab_addr = word + stables[shstrndx].sh_offset;
我们用这种方式去获取,很明显字符串表就是一个字符数组
但不是每个段表都是字符数组,例如符号表就不是
符号表的结构
typedef struct
{
Elf64_Word st_name; //符号名字(在.strtab表的索引)
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf64_Section st_shndx; /* Section index */
Elf64_Addr st_value; /* Symbol value */
Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;
我们需要的信息:
st_name 符号名
st_value 值(函数就是地址)
st_info 低四位是符号的类型,高28位标识符号绑定的信息
st_size 段的大小
符号段包含了全局变量、函数等符号,我们只要提取函数的部分
struct func_org
{
char *name;
uint64_t addr;
};
struct fun_arr
{
char *word;
int size;
struct func_org data[0];
};
void *get_section_addr(Elf64_Shdr *stables,char *word,int table_max,int shstrndx,char *sectionname,int *sectionsize)
{
if(stables == NULL || word == NULL || sectionsize == NULL) return NULL;
//字符串表
char *shstrtab_addr = word + stables[shstrndx].sh_offset;
//查找段表名
int i = 0;
for(i = 0;i < table_max;i++){
if(strcmp(sectionname,shstrtab_addr + stables[i].sh_name) == 0) break;
}
if( i != table_max){
*sectionsize = stables[i].sh_size;
return word + stables[i].sh_offset;
}
return NULL;
}
int get_all_func(Elf64_Sym *syns,char *strtab_addr,struct fun_arr *funs,uint64_t base)
{
int count = 0;
//遍历符号表,找到函数符号
for(int i = 0;i < funs->size;i++) {
if ((syns[i].st_info & 0xf) == STT_FUNC && syns[i].st_value != 0) {
funs->data[count].name = strtab_addr + syns[i].st_name;
funs->data[count].addr = syns[i].st_value + base;
count++;
}
}
funs->size = count;
return 0;
}
int main(int argc,char *argv[])
{
if (argc < 2) return 0;
char *word = open_file(argv[1]);
Elf64_Ehdr *ehdr = (Elf64_Ehdr *)word;
int table_max = ehdr->e_shnum;
int shstrndx = ehdr->e_shstrndx;
Elf64_Shdr *stables = (Elf64_Shdr *)(word + ehdr->e_shoff);
char *shstrtab_addr = word + stables[shstrndx].sh_offset;
int size = 0;
//获取符号表的位置
Elf64_Sym *syns = (Elf64_Sym *)get_section_addr(stables,word,table_max,shstrndx,".symtab",&size);
int strsize = 0;
//回去符号名称的位置
char *strtab_addr = get_section_addr(stables,word,table_max,shstrndx,".strtab",&strsize);
//Elf64_Sym 数组的大小
size = size/sizeof(Elf64_Sym);
struct fun_arr *funs = (struct fun_arr *)calloc(1,sizeof(struct fun_arr) + sizeof(struct func_org) * size);
funs->size = size;
funs->word = word;
//获取函数符号和地址
get_all_func(syns,strtab_addr,funs,0);
struct func_org *p = funs->data;
for (int i = 0;i < funs->size;i++) {
printf("%-30s 0x%lx\n",p->name,p->addr);
p++;
}
close_file(word);
return 0;
}
./test code
输出:
deregister_tm_clones 0x1070
register_tm_clones 0x10a0
__do_global_dtors_aux 0x10e0
frame_dummy 0x1120
_init 0x1000
__libc_csu_fini 0x11e0
add 0x1129
_fini 0x11e8
__libc_csu_init 0x1170
_start 0x1040
main 0x1157
sub 0x1141
4.2 程序基地址获取
我们修改一下demo
#include <stdio.h>
int add(int a,int b)
{
return a + b;
}
int sub(int a,int b)
{
return a - b;
}
int main(int argc,char *argv[])
{
printf("main = %p\n",(void *)main);
printf("add = %p\n",(void *)add);
printf("sub = %p\n",(void *)sub);
return 0;
}
然后不断执行:
main = 0x7f369e329177
add = 0x7f369e329149
sub = 0x7f369e329161
yh•~/Code» ./code [15:46:51]
main = 0x7f38dc36e177
add = 0x7f38dc36e149
sub = 0x7f38dc36e161
yh•~/Code» ./code [15:46:52]
main = 0x7f90c8548177
add = 0x7f90c8548149
sub = 0x7f90c8548161
yh•~/Code» ./code [15:46:52]
main = 0x7fcc94f37177
add = 0x7fcc94f37149
sub = 0x7fcc94f37161
可以看到每次的运行的地址都不一样,我们看一下我们获取的函数地址
…
add 0x1149
_fini 0x1258
__libc_csu_init 0x11e0
_start 0x1060
main 0x1177
sub 0x1161
可以发现这个地址只是加了一个base,偏移量是完全一样的。
获取程序基地址的方法
在linux中程序运行时会创建/proc/pid 目录,其中这个目录包含了maps文件和exe软连接(执行程序的路径)。maps文件可以获取程序的基地址
第一列是映射地址的范围,第二列是内存的访问权限,第三列是文件映射的地址。
也就是我们只有找到文件映射的地址为0,并且程序名称是对应程序的那一列就可以获得程序的基地址
获取函数编写
uint64_t get_pid_base(pid_t pid)
{
char buf[BUFSIZE] = {0};
char *pro_maps_path = buf;
// open /proc/pid/maps
pro_maps_path += sprintf(pro_maps_path,"%s","/proc");
pro_maps_path += sprintf(pro_maps_path,"/%d",pid);
sprintf(pro_maps_path,"/%s","maps");
FILE *fp = fopen(buf,"rb");
if (!fp) {
perror("open file err");
return -1;
}
// read /proc/pid/exe
memset(buf,0,BUFSIZE);
char *pro_exe_path = buf;
pro_exe_path += sprintf(pro_exe_path,"%s","/proc");
pro_exe_path += sprintf(pro_exe_path,"/%d",pid);
sprintf(pro_exe_path,"/%s","exe");
char target[100] = {0};
int target_len = readlink(buf,target,100);
target[target_len] = 0;
memset(buf,0,BUFSIZE);
char *pro_addr = buf;
char *pro_maps = buf + 100;
char *pro_name = pro_maps + 100;
char *p = pro_name + 256;
char data[512] = {0};
while (!feof(fp)) {
fgets(data,sizeof(data),fp);
sscanf(data,"%[^ ] %[^ ] %[^ ] %[^ ] %[^ ] %[^ ]",pro_addr,p,pro_maps,p,p,pro_name);
//printf("pro_addr %s pro_maps %s pro_name %s --> %d %d\n",pro_addr,pro_maps,pro_name,memcmp(pro_name,target,target_len-1),memcmp(pro_maps,"00000000",8));
if (memcmp(pro_name,target,target_len-1) == 0 && memcmp(pro_maps,"00000000",8) == 0) {
fclose(fp);
memset(p,0,10);
sscanf(pro_addr,"%[^-]",p);
uint64_t num = 0;
str2uint64(p,num);
return num;
}
memset(data,0,sizeof(data));
}
printf("not find addr\n");
fclose(fp);
return 0;
}
封装好了函数我们写一个测试程序验证一下
int main(int argc,char *argv[])
{
pid_t pid = fork();
if (pid == 0) {
execl("./code","code",NULL);
} else if (pid < 0) {
perror("fork err");
return -1;
}
sleep(1); //保证子进程先运行
uint64_t base = get_pid_base(pid);
printf("base = 0x%lx\n",base);
return 0;
}
demo 程序简单的修改
#include <stdio.h>
#include <unistd.h>
int add(int a,int b)
{
return a + b;
}
int sub(int a,int b)
{
return a - b;
}
int main(int argc,char *argv[])
{
printf("main = %p\n",(void *)main);
printf("add = %p\n",(void *)add);
printf("sub = %p\n",(void *)sub);
sleep(2);//运行完先不退出,等父进程运行完
return 0;
}
我们把demo编译成code,在父进程中执行exec然后去获取子进程的基地址验证。运行是要主要两点
- 保证子进程exec完,如果不运行完,/proc/pid和父进程是类似的
- 子进程要比父进程后面退出,保证父进程获取到基地址
程序输出:
main = 0x5633033f6197
add = 0x5633033f6169
sub = 0x5633033f6181
base = 0x5633033f5000
我们观察我们获得的符号地址:
deregister_tm_clones 0x10b0
register_tm_clones 0x10e0
__do_global_dtors_aux 0x1120
frame_dummy 0x1160
_init 0x1000
__libc_csu_fini 0x1280
add 0x1169
_fini 0x1288
__libc_csu_init 0x1210
_start 0x1080
main 0x1197
sub 0x1181
可以对比一下,说明我们的基地址获取是正确的
4.3 read_pro_mem和write_pro_mem的封装
在之前用ptrace修改内存时,我们必须值操作8自己的内存数据,因此我们插入断点时需要先读,然后再写。如果内存大于8字节又需要重复去调用。这样的操作异常的繁杂,所以我们封装read_pro_mem和write_pro_mem 方便我们的内存读写
#define WORD (sizeof(void *))
int read_pro_mem(pid_t child,uint64_t addr,char *str,int len)
{
int i = 0;
int n = len / WORD;
char *current_p = str;
uint64_t data;
//能被整除的部分
for (;i < n;i++) {
if ((data = ptrace(PTRACE_PEEKTEXT,child,addr+i*WORD,NULL)) < 0) {
perror("read mem err");
return -1;
}
memcpy(current_p,&data,WORD);
current_p += WORD;
}
//余数的部分
int remainder = len % WORD;
if (remainder != 0) {
if ((data = ptrace(PTRACE_PEEKTEXT,child,addr+i*WORD,NULL)) < 0) {
perror("read mem err");
return -1;
}
//拷贝剩余字节数
memcpy(current_p,&data,remainder);
}
return 0;
}
int write_pro_mem(pid_t child,uint64_t addr,char *str,int len)
{
int i = 0;
int n = len / WORD;
char *current_p = str;
uint64_t data;
//能被整除的部分
for (;i < n;i++) {
memcpy(&data,current_p,WORD);
if (ptrace(PTRACE_POKETEXT,child,addr +i*WORD,data) < 0) {
perror("write mem err");
return -1;
}
current_p += WORD;
}
//余数的部分
int remainder = len % WORD;
if (remainder != 0) {
//先读整个内存段
if ((data = ptrace(PTRACE_PEEKTEXT,child,addr+i*WORD,NULL)) < 0) {
perror("read mem err");
return -1;
}
//把需要改的部分填充掉
memcpy(&data,current_p,remainder);
if (ptrace(PTRACE_POKETEXT,child,addr+i*WORD,data) < 0) {
perror("write mem err");
return -1;
}
}
return 0;
}
这两个函数的封装很简单,只要分出整除部分和余数部分就可以了
整除部分只要循环调用就行
余数部分的读操作只要拷贝部分数据就行,写操作需要先读-修改-写入的步骤
4.4 断点插入
继续写一个测试的demo
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int print_int(int a)
{
printf("%d\n",a);
return 0;
}
int main(int argc,char *argv[])
{
printf("print_int = %p\n",(void *)print_int);
for (int i = 0;i < 10;i++) {
print_int(i);
}
return 0;
}
我们用我们之前编写的技术点值实现这个程序每掉一次print_int 我们就断下断点输出当前的地址值并且睡眠
- 使用fork + execl执行这个程序
- 获取这个程序的基地址
- 获取这个程序函数的符号表
- 找到print_int函数的地址,使用read_pro_mem保存原字节码,使用write_pro_mem写入断点(0xcc)
技术点都已经有了,只剩下把之前的代码整合。
已经实现的函数不再写一遍了
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/reg.h>
#include <sys/user.h>
#include <sys/syscall.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <elf.h>
#define INT_3 0xcc
#define BUFSIZE 1024
#define str2uint64(str,num) sscanf(str,"%lx",&num)
#define WORD (sizeof(void *))
struct fun_arr *dbuf(const char *path,uint64_t base)
{
char *word = open_file(path);
Elf64_Ehdr *ehdr = (Elf64_Ehdr *)word;
int table_max = ehdr->e_shnum;
int shstrndx = ehdr->e_shstrndx;
Elf64_Shdr *stables = (Elf64_Shdr *)(word + ehdr->e_shoff);
char *shstrtab_addr = word + stables[shstrndx].sh_offset;
int size = 0;
//获取符号表的位置
Elf64_Sym *syns = (Elf64_Sym *)get_section_addr(stables,word,table_max,shstrndx,".symtab",&size);
int strsize = 0;
//回去符号名称的位置
char *strtab_addr = get_section_addr(stables,word,table_max,shstrndx,".strtab",&strsize);
//Elf64_Sym 数组的大小
size = size/sizeof(Elf64_Sym);
struct fun_arr *funs = (struct fun_arr *)calloc(1,sizeof(struct fun_arr) + sizeof(struct func_org) * size);
funs->size = size;
funs->word = word;
//获取函数符号和地址
get_all_func(syns,strtab_addr,funs,base);
return funs;
}
void close_dbug(struct fun_arr *p)
{
close_file(p->word);
free(p);
}
int find_func(struct fun_arr *funs,const char *func)
{
if (!funs || !func) return -1;
for (int i = 0;i < funs->size;i++) {
if (memcmp(funs->data[i].name,func,strlen(func)) == 0) {
return i;
}
}
return -1;
}
int main(int argc,char *argv[])
{
pid_t pid;
int orig_rax;
int iscalling = 0;
int status = 0;
//一组寄存器的值
struct user_regs_struct regs;
pid = fork();
if (pid == 0) { //子进程
if (ptrace(PTRACE_TRACEME,0,NULL,NULL) < 0) {
perror("ptrace TRACEME err");
return -1;
}
//调试deno,这个是我编译后的名字
execl("./dbuf_break_test","dbuf_break_test",NULL);
return 0;
} else if (pid < 0) {
perror("fork err");
return -1;
}
//监听子进程的状态
wait(&status);
if (WIFEXITED(status))
return 0;
//找到print_int的地址
uint64_t base = get_pid_base(pid);
struct fun_arr *funs = dbuf("./dbuf_break_test",base);
int index = find_func(funs,"print_int");
uint64_t fun_addr = funs->data[index].addr;
//往print_int函数插入断点
uint8_t orig_code = 0;
read_pro_mem(pid,fun_addr,(void *)&orig_code,1);
char bit = INT_3;
write_pro_mem(pid,fun_addr,&bit,1);
if (ptrace(PTRACE_CONT,pid,NULL,NULL) < 0) {
perror("CONT err");
return -1;
}
while (1) {
wait(&status);
if (WIFEXITED(status))
break;
ptrace(PTRACE_GETREGS,pid,NULL,®s);//获取寄存器整个结构
printf("0x%llx\n",regs.rip);
//恢复之前的代码值
write_pro_mem(pid,fun_addr,&orig_code,1);
//0xcc占一个字节,让ip寄存器恢复
regs.rip = regs.rip - 1;
//设置寄存器的值(主要用于恢复ip寄存器)
ptrace(PTRACE_SETREGS,pid,0,®s);
//用于看效果,确保子进程是真的断下
sleep(1);
//执行一条汇编指令,然后子进程会暂停
ptrace(PTRACE_SINGLESTEP,pid,0,0);
//等待子进程停止
wait(NULL);
//断点恢复
char bit = INT_3;
write_pro_mem(pid,fun_addr,&bit,1);
//让子进程在调用系统调用时暂停
if (ptrace(PTRACE_CONT,pid,NULL,NULL) < 0) {
perror("CONT err");
return -1;
}
}
close_dbug(funs);
return 0;
}
输出:
print_int = 0x56131eca364a
0x56131eca364b
0
0x56131eca364b
1
0x56131eca364b
2
0x56131eca364b
3
0x56131eca364b
4
0x56131eca364b
5
0x56131eca364b
6
0x56131eca364b
7
0x56131eca364b
8
0x56131eca364b
9
4.5 attach一个进程
attach附加调试,我们使用调试最多的一种手段,常用于解决服务未响应、死循环、死锁等问题。ptrace提供也提供了附加进程的模式。
我们先延长一下被调试进程的时间让我们有充足的时间去attach进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int print_int(int a)
{
printf("%d\n",a);
return 0;
}
int main(int argc,char *argv[])
{
printf("print_int = %p\n",(void *)print_int);
printf("pid = %d\n",getpid());
sleep(10);
for (int i = 0;i < 10;i++) {
print_int(i);
}
return 0;
}
调试进程修改为attach
int main(int argc,char *argv[])
{
pid_t pid;
int orig_rax;
int iscalling = 0;
int status = 0;
//一组寄存器的值
struct user_regs_struct regs;
if (argc < 2) return 0;
pid = atoi(argv[1]);
//attach
if (ptrace(PTRACE_ATTACH,pid,NULL,NULL) < 0) {
perror("attch err");
}
//监听子进程的状态
waitpid(pid,&status,0);
if (WIFEXITED(status))
return 0;
//找到print_int的地址
uint64_t base = get_pid_base(pid);
struct fun_arr *funs = dbuf("./dbuf_break_test",base);
int index = find_func(funs,"print_int");
uint64_t fun_addr = funs->data[index].addr;
//往print_int函数插入断点
uint8_t orig_code = 0;
read_pro_mem(pid,fun_addr,(void *)&orig_code,1);
char bit = INT_3;
write_pro_mem(pid,fun_addr,&bit,1);
if (ptrace(PTRACE_CONT,pid,NULL,NULL) < 0) {
perror("CONT err");
return -1;
}
while (1) {
wait(&status);
if (WIFEXITED(status))
break;
ptrace(PTRACE_GETREGS,pid,NULL,®s);//获取寄存器整个结构
printf("0x%llx\n",regs.rip);
//恢复之前的代码值
write_pro_mem(pid,fun_addr,&orig_code,1);
//0xcc占一个字节,让ip寄存器恢复
regs.rip = regs.rip - 1;
//设置寄存器的值(主要用于恢复ip寄存器)
ptrace(PTRACE_SETREGS,pid,0,®s);
//用于看效果,确保子进程是真的断下
sleep(1);
//执行一条汇编指令,然后子进程会暂停
ptrace(PTRACE_SINGLESTEP,pid,0,0);
//等待子进程停止
wait(NULL);
//断点恢复
char bit = INT_3;
write_pro_mem(pid,fun_addr,&bit,1);
//让子进程在调用系统调用时暂停
if (ptrace(PTRACE_CONT,pid,NULL,NULL) < 0) {
perror("CONT err");
return -1;
}
}
close_dbug(funs);
return 0;
}
编译完直接 ‘./attach pid’ 就可以看到现象了
具体的代码可以看我GitHub上面 https://github.com/huoyang11/test_dbug
我把整个demo整理了一个添加了简单的命令
目前封装的命令有
dbug 程序路径(调试一个程序)
attach pid (附加一个进程)
这俩个命令必须一开始调用
其他命令:
b 函数名 (软件断点)
c (进程继续执行)
show (查看程序的符号)
showreg (查看当前的寄存器值)
watch (硬件断点)
q 退出
exit 退出
quit 退出
showbps 查看断点
更多推荐
所有评论(0)