1. 文章参考

[原创]一窥GDB原理-Pwn

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,&regs) < 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,&regs);//获取寄存器整个结构
  
        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,&regs);
        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,&regs);

        //用于看效果,确保子进程是真的断下
        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,&regs);
        //对比子进程当前断下的地址和函数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,&regs);//获取寄存器整个结构
        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,&regs);//获取寄存器整个结构
        
        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,&regs);

        //用于看效果,确保子进程是真的断下
        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,&regs);//获取寄存器整个结构
        
        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,&regs);

        //用于看效果,确保子进程是真的断下
        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 查看断点

Logo

更多推荐