首先学习Linux操作系统的知识背景
是 ELF 格式
三种文件都可以是 ELF 可执行文件 .so的动态链接库 可重定向文件
我们可以通过 file 命令查看 是否是linux文件(一般我们通过查壳工具也可以实现)
file /user/bin/ls一般通过编译C 是通过 gcc 编译器 编译C++ 一般是G++ 编译器

可以发现 其实和Win的PE格式类似
ELF文件头 这是一个结构体 文件头后 都是ELF 正文部分
ELF 包含两个方法 一个是链接视图 一个是执行视图
两个主要是 运行的对象不同 链接视图是编译器编译的时候 分析的
执行视图是运行的时候分析的

除了和win类似的节概念外 还有一个 段的概念 包含多个节
操作系统加载 ELF 文件的时候 通过 段单位加载对齐
上述两个视图 在ELF文件中 通过两个表 索引
对于 段 通过 程序头部表 进行索引
对于 节 通过 节区头部表 进行索引
这两个表的信息 又被登记在 最开始的头部 中
我们可以通过 readelf 读取信息
readelf -h /user/bin/ls下图是 linux 的 ELF 格式

我们知道 windows下 内核 是区分 线程和进程的 通过 EPROCESS 和 ETHREAD 维护进程和线程
Linux出现的时候 线程的概念还不够清晰 进程 通过内核一个 task_struct 维护 代表一个任务
所以 一个进程 = 一个线程 = 一个任务 所以没有体现多线程
当多线程出现了 task_struct 转变 成一个线程 并且增加一个 组ID 的成员 同一个进程的多个进程 组ID 相同
并且task_struct 使用同一个虚拟地址空间
从而 进程的结构体 转变为线程的结构体
之前我们学习了 编写一个C语言 首先会进入NTDLL 等多个DLL 然后通过 编译器的main调用 定位到 用户代码入口 从而 执行我们写的用户函数
Win上通过CreateProcess 进行进程的构建
但是Linux 是通过 fork 函数 (一次调用 两次返回) 复制当前进程 其中内容是非常相同的
这样就出现了一个 新的 task_struct 通过父进程构建子进程的时候 返回给两个进程的是 一个是子进程的PID 一个是0(子进程)可以通过这点分析是在 父进程还是子进程中
通过fork 函数后 我们构架了两个一模一样的 进程 下面通过 execve 调用新程序
进入内核执行 execve 函数后 修改保存在栈中的上下文信息的EIP 从而实现 寻找OEP
Linux运行的过程是通过_start 函数初始化 语言库 然后通过 __libc_start_main (在libc.so 库中) 然后实现调用main

我们上面学习了 Linux程序运行原理 这里开始学习一个动态调试
vim test.c输入i 开始输入代码
#include <stdio.h>
void hello(){
printf("hello world");
}
int main(int argc,char** argv){
for (int i=0;i<argc; i++){
printf("argv[%d]:%s\n",i,argv[i]);
}
hello();
return 0;
}
sudo apt update
sudo apt install build-essential最后通过 gcc编译
gcc -g -o test test.c
chmod 777 test~/Desktop_win master !34 ?156 ❯ ./test -123
argv[0]:./test
argv[1]:-123
hello world# 执行权限
我们可以通过 ls -l
-rwxrwxrwx 1 root root 17312 Feb 23 18:49 test这是执行权限

这就是最常使用的调试工具 我们首先编写一下基本的Linux C语言程序 我这里是通过WSL2 构建的虚拟机
有两个方法可以打开 gdb test
或者 gdb 然后 file test
最后通过 run / r
Type "apropos word" to search for commands related to "word"...
Reading symbols from test...
(gdb) r
Starting program: /mnt/c/Users/12455/Desktop/test
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
argv[0]:/mnt/c/Users/12455/Desktop/test
hello world[Inferior 1 (process 1848) exited normally]这里可以发现 成功 执行了 但是没有进入调试 状态 因为我没有给程序下断点
break main / b main
(gdb) b main
Breakpoint 1 at 0x555555555163: file test.c, line 9.
(gdb) r
Starting program: /mnt/c/Users/12455/Desktop/test
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, main (argc=1, argv=0x7fffffffd958) at test.c:9
9 for (int i=0;i<argc; i++){可以发现 成功断点了
我们可以通过 list 查看 源代码
我们还可以通过
(gdb) list
3 void hello(){
4 printf("hello world");
5 }
6
7
8 int main(int argc,char** argv){
9 for (int i=0;i<argc; i++){
10 printf("argv[%d]:%s\n",i,argv[i]);
11 }
12 hello();
(gdb)然后 b 11 对行进行下断点
在汇编调试的过程中
b *地址 下断点
我们通过 disassemble 查看 汇编代码
(gdb) disassemble
Dump of assembler code for function main:
0x0000555555555154 <+0>: push %rbp
0x0000555555555155 <+1>: mov %rsp,%rbp
0x0000555555555158 <+4>: sub $0x20,%rsp
0x000055555555515c <+8>: mov %edi,-0x14(%rbp)
0x000055555555515f <+11>: mov %rsi,-0x20(%rbp)
=> 0x0000555555555163 <+15>: movl $0x0,-0x4(%rbp)
0x000055555555516a <+22>: jmp 0x5555555551a0 <main+76>
0x000055555555516c <+24>: mov -0x4(%rbp),%eax
0x000055555555516f <+27>: cltq
0x0000555555555171 <+29>: lea 0x0(,%rax,8),%rdx
0x0000555555555179 <+37>: mov -0x20(%rbp),%rax
0x000055555555517d <+41>: add %rdx,%rax
0x0000555555555180 <+44>: mov (%rax),%rdx
0x0000555555555183 <+47>: mov -0x4(%rbp),%eax
0x0000555555555186 <+50>: mov %eax,%esi
0x0000555555555188 <+52>: lea 0xe81(%rip),%rax # 0x555555556010
0x000055555555518f <+59>: mov %rax,%rdi
0x0000555555555192 <+62>: mov $0x0,%eax
0x0000555555555197 <+67>: call 0x555555555030 <printf@plt>
0x000055555555519c <+72>: addl $0x1,-0x4(%rbp)
0x00005555555551a0 <+76>: mov -0x4(%rbp),%eax
0x00005555555551a3 <+79>: cmp -0x14(%rbp),%eax
0x00005555555551a6 <+82>: jl 0x55555555516c <main+24>
0x00005555555551a8 <+84>: mov $0x0,%eax
0x00005555555551ad <+89>: call 0x555555555139 <hello>
0x00005555555551b2 <+94>: mov $0x0,%eax
0x00005555555551b7 <+99>: leave
0x00005555555551b8 <+100>: ret我们还可以通过 info r 查看寄存器的内容 可以发现 这里格式我们不习惯 我们需要转化为 intel格式 这里默认是 ATT格式
set disassembly-flavor intel
(gdb) set disassembly-flavor intel
(gdb) disassemble
Dump of assembler code for function main:
0x0000555555555154 <+0>: push rbp
0x0000555555555155 <+1>: mov rbp,rsp
0x0000555555555158 <+4>: sub rsp,0x20
0x000055555555515c <+8>: mov DWORD PTR [rbp-0x14],edi
0x000055555555515f <+11>: mov QWORD PTR [rbp-0x20],rsi
=> 0x0000555555555163 <+15>: mov DWORD PTR [rbp-0x4],0x0
0x000055555555516a <+22>: jmp 0x5555555551a0 <main+76>
0x000055555555516c <+24>: mov eax,DWORD PTR [rbp-0x4]
0x000055555555516f <+27>: cdqe
0x0000555555555171 <+29>: lea rdx,[rax*8+0x0]
0x0000555555555179 <+37>: mov rax,QWORD PTR [rbp-0x20]
0x000055555555517d <+41>: add rax,rdx
0x0000555555555180 <+44>: mov rdx,QWORD PTR [rax]
0x0000555555555183 <+47>: mov eax,DWORD PTR [rbp-0x4]
0x0000555555555186 <+50>: mov esi,eax
0x0000555555555188 <+52>: lea rax,[rip+0xe81] # 0x555555556010
0x000055555555518f <+59>: mov rdi,rax
0x0000555555555192 <+62>: mov eax,0x0
0x0000555555555197 <+67>: call 0x555555555030 <printf@plt>
0x000055555555519c <+72>: add DWORD PTR [rbp-0x4],0x1
0x00005555555551a0 <+76>: mov eax,DWORD PTR [rbp-0x4]
0x00005555555551a3 <+79>: cmp eax,DWORD PTR [rbp-0x14]
0x00005555555551a6 <+82>: jl 0x55555555516c <main+24>
0x00005555555551a8 <+84>: mov eax,0x0
0x00005555555551ad <+89>: call 0x555555555139 <hello>
0x00005555555551b2 <+94>: mov eax,0x0
0x00005555555551b7 <+99>: leave
0x00005555555551b8 <+100>: retinfo b
(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000555555555163 in main at test.c:9
breakpoint already hit 1 time
2 breakpoint keep y 0x00005555555551a8 in main at test.c:12d 1 删除断点
c 命令即可继续运行
单步步入 s /step
单步步过 n / next
Breakpoint 4, main (argc=1, argv=0x7fffffffd958) at test.c:9
9 for (int i=0;i<argc; i++){
(gdb) s
10 printf("argv[%d]:%s\n",i,argv[i]);
(gdb) s
argv[0]:/mnt/c/Users/12455/Desktop/test
9 for (int i=0;i<argc; i++){
(gdb) s
12 hello();
(gdb) s
hello () at test.c:4
4 printf("hello world");
(gdb) s
5 }
(gdb) s
main (argc=1, argv=0x7fffffffd958) at test.c:13
13 return 0;
(gdb) s
14 }
(gdb) s
hello world[Inferior 1 (process 1887) exited normally]汇编级别需要通过 si 和 ni 调试
backtrace /bt 查看堆栈信息
(gdb) backtrace
#0 main (argc=1, argv=0x7fffffffd958) at test.c:9
(gdb) bt
#0 main (argc=1, argv=0x7fffffffd958) at test.c:9x /nfu 其中 n 是字节个数 f 是格式 u 是什么单位查看 x /100cb 也就是 100字节 字符串方法展示 byte

我们可以通过 help x 查看帮助指令
shell 命令 比如我需要清空屏幕 shell clear
我们需要挂载到运行的程序上
#include <stdio.h>
void hello(){
printf("hello world");
}
int main(int argc,char** argv){
for (int i=0;i<argc; i++){
printf("argv[%d]:%s\n",i,argv[i]);
}
char buff[100];
fgets(buff,100,stdin);
hello();
return 0;
}重新运行
我们可以通过 ps命令 获取运行的 程序
ps -elf | grep test ✘ 0|127 root@xioa 19:20:53
0 S root 2212 1987 0 80 0 - 640 - 19:20 pts/2 00:00:00 ./test
0 S root 2278 214 0 80 0 - 1632 pipe_r 19:20 pts/0 00:00:00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-dir=.idea --exclude-dir=.tox --exclude-dir=.venv --exclude-dir=venv test发现PID是2212 通过 attach 命令附加进入
(gdb) attach 2212
Attaching to process 2212
Reading symbols from /mnt/c/Users/12455/Desktop/test...
Reading symbols from /lib/x86_64-linux-gnu/libc.so.6...
(No debugging symbols found in /lib/x86_64-linux-gnu/libc.so.6)
Reading symbols from /lib64/ld-linux-x86-64.so.2...
(No debugging symbols found in /lib64/ld-linux-x86-64.so.2)
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
0x00007fc304f90687 in ?? () from /lib/x86_64-linux-gnu/libc.so.6查看堆栈信息
(gdb) bt
#0 0x00007fc304f90687 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
#1 0x00007fc304f906ad in ?? () from /lib/x86_64-linux-gnu/libc.so.6
#2 0x00007fc305004ea6 in read () from /lib/x86_64-linux-gnu/libc.so.6
#3 0x00007fc304f8b861 in _IO_file_underflow () from /lib/x86_64-linux-gnu/libc.so.6
#4 0x00007fc304f8dbeb in _IO_default_uflow () from /lib/x86_64-linux-gnu/libc.so.6
#5 0x00007fc304f808ca in _IO_getline_info () from /lib/x86_64-linux-gnu/libc.so.6
#6 0x00007fc304f7f70a in fgets () from /lib/x86_64-linux-gnu/libc.so.6
#7 0x00005589d35a31d0 in main (argc=1, argv=0x7ffe98bcd848) at test.c:13可以发现 确实是 通过 main 调用了fgets函数
这样就可以继续调试了
这就是 调试机制的基本原理

通过GDB 启动目标程序的过程中

GDB 会设置一个 0 用于说明 跟踪自己
也就是注入 SO文件
LD_PRELOAD把我们需要注入的内容 写入环境变量中 从而实现注入 这也就是Linux的预加载机制
LD_PRELOAD=./inject.so ./test 准备一个C语言 gcc -g -o test test.c
#include <stdio.h>
void hello(){
printf("hello world");
}
int main(int argc,char** argv){
for (int i=0;i<argc; i++){
printf("argv[%d]:%s\n",i,argv[i]);
}
char buff[100];
fgets(buff,100,stdin);
hello();
return 0;
}
写一个 so文件 gcc -fPIC -shared inject.c -o inject.so
#include <stdio.h>
__attribute__((constructor))
void testEntry(){
printf("I'm load !!!!\n");
}
~/Desktop_win/linux master !34 ?156 ❯ LD_PRELOAD=./inject.so ./test root@xioa 19:32:36
I'm load !!!!
argv[0]:./test实现注入成功
只可以注入一个新的进程
ptrace注入https://github.com/gaffe23/linux-inject