Linux进程地址空间
Linux进程的地址空间
有什么工具可以来查看进程的地址空间
- pmap
- cat /proc/ [pid] / maps
- gdb
- readelf
- objdump
有的程序刚开始执行就结束了(比如打印一个东西就退出),如果要查看这个进程的地址空间。那怎么办?
使用gdb。
使用gdb命令 info inferiors得到进程的pid
- 该命令打印gdb当前管理的inferiors列表,每个inferior都有自己的不同地址空间,inferior与进程对应。
得到进程的pid后,使用命令 !pmap [pid], 在gdb中使用shell命令需要在前面加上 !。
同样,在gdb中,还可以使用 !cat /proc/ [pid] /maps来查看进程的地址空间。
- 其实pmap就是使用系统中的 /proc/[pid]/这个文件实现的。
怎么证明呢?使用 strace strace pmap [pid]
1 |
|
动态链接
gdb调试starti之后,查看进程的地址空间:
1 |
|
我们还发现,在按下starti后,有一条信息:
1 |
|
说明动态链接的第一条指令在/lib64/ld-linux-x86-64.so.2中,甚至在地址空间中此时也没有libc这个库。
在状态机在刚刚被初始化的一瞬间,在进程里面还没有printf。
动态链接的ELF文件中,有一个INTERP, 就是这里的ld-linux-x86-64.so.2, 需要另外一个程序,才能执行现在这个程序,对于动态链接来说,这就是加载器。
再在main函数上打个断点,continue后再打印一次进程的地址空间
1 |
|
如上,我们发现libc已经有了。加载器也还在,未来可能还需要这个加载器加载其他动态链接库。
其他小细节
1 |
|
在库都加载完成后,用 !cat /proc/14776//maps 查看该进程的地址空间
1 |
|
我们发现这两行, vvar 和 vdso 是什么?
不进入内核的系统调用。
vvar is a memory region that contains kernel variables that are frequently accessed by user-space programs. These variables are read-only and can be accessed directly by the user-space programs without making a system call.
例如
当前的时间, 系统页面大小
vdso is a memory region that contains a small shared library provided by the kernel. This library contains a set of functions that are commonly used by user-space programs and can be excuted directly in user mode, without the need for a system call.
进程地址空间的管理
操作系统应该提供一个修改进程地址空间的系统调用
1 |
|
本质:在状态机状态上增加/删除/修改一段可访问的内存
- mmap: 可以用来申请内存 (MAP_ANONYMOUS),也可以把文件 “搬到” 进程地址空间中
一小段示例代码
1 |
|
疑问:这个程序运行会不会需要很长的时间,因为它分配了那么多的内存?
其实一瞬间就完成了。也就是说,在使用mmap的时候,只是在操作系统中标记了这个进程这么多的内存,这个进程中这些内存还并没有开始分配,只是在后面用到了才会产生缺页中断。
入侵地址空间
进程 (M, R 状态机) 在 “无情执行指令机器” 上执行
- 状态机是一个封闭世界
- 但如果允许一个进程对其他进程的地址空间有访问权?
一些入侵地址空间的例子
- 调试器(gdb)
- gdb 可以任意观测和修改程序的状态
- Profiler (perf)
入侵进程地址空间 (1): 金山游侠
1 |
|
代码导读
- va_list,va_start()、va_arg() 和 va_end() 是什么
这可以使C语言实现变长参数。
va_list 是一个类型,用于表示可变参数列表。
va_start() 宏用于初始化可变参数列表
va_arg() 宏用于访问可变参数列表中的下一个参数
va_end() 宏用于结束可变参数列表的访问 - 一段小例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24#include <stdio.h>
#include <stdarg.h>
double average(int count, ...) {
va_list ap;
int i;
double total = 0.0;
va_start(ap, count); // 初始化可变参数列表
for (i = 0; i < count; i++) {
total += va_arg(ap, double); // 获取下一个参数
}
va_end(ap); // 结束可变参数列表
return total / count;
}
int main() {
double avg = average(5, 1.0, 2.0, 3.0, 4.0, 5.0);
printf("平均值为:%f\n", avg);
return 0;
}popen函数
popen()会调用fork()产生子进程,然后从子进程中调用/bin/sh -c来执行参数command的指令。参数type可使用“r”代表读取,“w”代表写入。依照此type值,popen()会建立管道连到子进程的标准输出设备或标准输入设备,然后返回一个文件指针。随后进程便可利用此文件指针来读取子进程的输出设备或是写入到子进程的标准输入设备中。
- 也就是说,首先获取游戏进程的名字后,先创建一个子进程执行 pidof [name]的命令,可以获取游戏进程pid。然后fscanf(pid_fp, “%d”, &g->pid) != 1读取该进程pid。
接着打开/proc/[pid]/mem这个文件。g->memfd指向这个文件。1
2
3
4
5
6
7
8snprintf(buf, sizeof(buf), "/proc/%d/mem", g->pid);
g->memfd = open(buf, O_RDWR);
if (g->memfd < 0) {
perror("/proc/[pid]/mem");
ret = -1;
goto release;
} - scan函数
在用pmap得到虚拟地址区域后,就可以把这个区域映射到入侵程序的地址空间中,并得到起始地址。1
char *mem = calloc(size + 16, 1); // Ignores error handling for
- 之后把 /proc/pid/mem的文件偏移设为虚拟地址区域起始处。
- 如果想要在/proc/pid/mem 文件访问进程的虚拟地址 0x12345678,您需要将文件偏移量设置为 0x12345678。
并把这个区域的内存全部写入入侵进程的地址空间中。1
2lseek(g->memfd, start, SEEK_SET); // Don't do this in production!
size = read(g->memfd, mem, size);
- 然后就可以根据偏移,可以把相应的地址对应起来了。大致意思就是在入侵地址里暴力寻找符合条件的地址,然后根据找的的符合条件的地址,由于偏移是一样的,也就可以把这个地址对应到被入侵进程的相应虚拟地址区域中了。
入侵进程地址空间 (2): 变速齿轮
用修改程序系统调用的手段来欺骗程序对时间的认识,就可以实现游戏的加速和减速。
简单的一段C程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int hp = 10000;
__attribute__((noinline)) int hit(int damage) {
return hp - damage;
}
int main() {
while (1) {
hp = hit(rand() % 10);
printf("hp = %d\n", hp);
usleep(10000);
if (hp < 0) {
printf("Game Over\n");
break;
}
}
}python 脚本
1 |
|