• 主页
  • 归档
Articles Change The World About me

  • 主页
  • 归档

Linux用户态Hook

2022-08-27

0x00 引言

  Hook即是函数调用劫持(用自己写的函数替换目标库中的函数),在Hook成功后就可以在自己的函数去实现监控程序执行、参数检查、过滤、拦截、修改、放行等操作。
  目前Linux上实现用户态Hook的主流方法主要有两种: 库打桩、注入与热更新。

0x01 基础知识

1
2
3
4
5
6
1 RING0/RING3
2 LD_PRELOAD环境变量
3 ELF文件结构
4 PLT与GOT
4.1 动态库函数寻址流程
4.2 相关节

1、RING0/RING3
  Intel的CPU将特权级别分为四个级别[1]: RING0,RING1,RING2,RING3。Linux系统包含上述所有级别,Windows系统只使用RING0和RING3。
  内核的代码运行在最高运行级别RING0(内核态)上,可以使用特权指令,控制中断、修改页表、访问设备等等。应用程序的代码运行在最低运行级别上RING3(用户态)上,不能做受控操作,如果要访问磁盘,写文件,那必须通过执行系统调用。

2、LD_PRELOAD环境变量
  LD_PRELOAD是Linux系统的一个环境变量,它可以影响程序的运行时的链接,它允许你定义在程序运行前优先加载的动态链接库。其优先级甚至高于LD_LIBRARY_PATH环境变量(指定动态库搜索路径)。

3、ELF文件结构
  ELF(Executableand Linking Format)即为可执行和链接格式,在Unix系统中,ELF文件包括.o .so以及可执行程序。ELF格式文件的基本结构如下:


ELF Header - ELF头: 存储着一些机器和该ELF文件的基本信息。指出了段头表与节头表在文件中的位置;
ELF Program Header Table - 段头表(程序头): 保存了所有段描述信息;
ELF Section Header Table - 节头表: 保存了所有节描述信息;

  可执行程序的内容是由段(Segment)组成的(程序执行必须要包含段),.o文件的内容是由节(Section)组成的。段由一个或多个具有相同属性的节组成,节头表中保存了在链接阶段(从.o文件生成可执行程序)具体需要将节连接到哪个段的信息。可执行文件载入内存时,会分为由段组织的内存块,这些内存块信息都会保存在段头表中。
  在Linux中,如果向看ELF文件的详细信息,可以使用readelf工具:

1
$ readelf -e 'ELF文件路径'

4、PLT与GOT
  PLT(Procedure Linkage Table)即过程链接表在链接阶段被链接到数据段,GOT(Global Offset Table)即全局偏移量表在链接阶段被链接到代码段。前者在ELF中主要包括.plt节、.rela.plt节,后者在ELF中主要包括.got节、.got.plt节,二者共同作用于将动态库函数链接到程序中。

4.1 动态库函数寻址流程
  现在思考一个问题: ELF是怎么找到函数地址的呢?
  要理解这个问题,首先我们得明确这个需要执行的函数究竟在哪里实现,基本可以分为以下三种情况:

  • 函数定义在其它.o中。链接阶段可直接重定位
  • 函数定义在动态库内,动态库动态链接。链接阶段无法直接重定位,这种情况我们在热更新中会解释
  • 函数定义在动态库内,动态库动态加载。链接阶段无法直接重定位

  为了解决上述第三种情况,我们需要使用到PLT与GOT,下图是动态库函数寻址的过程:


1.直接重定位: 链接阶段,链接器直接在其他.o文件找到函数地址并替换原任意地址;
2.链接时重定位: 链接阶段,链接器生成一小段额外的stub代码片段(这段代码能够在运行阶段获取函数真是地址),并将stub函数地址替换原任意地址,并将此地址记录到PLT相关节中;
3.运行时重定位: 运行阶段,将库加载到内存中,并获取到目标函数在库中的位置,并将stub函数实现内的目标函数地址替换为内存中的真实地址,并最终执行目标函数。

4.2 相关节

节名 作用
字符信息 .dynsym 外部调用符号的基本信息
.dynstr 外部调用符号的字符名,与.dynsym序号对应,与.rela.plt节中的序号成映射关系
PLT .rela.plt PLT新生成桩(stub)函数的偏移地址,其指向位置存在于.got.plt节中
.rela.dyn 已保存的重定位信息
GOT .rela.got GOT最终函数地址的偏移地址

注意: 外部调用分为动态链接、动态加载
1.动态链接: 在编译时声明动态库的存在,在运行期间链接动态库,意味着动态库在编译时对编译器可见。动态链接的库函数字符名与源代码中不一致(比如print_str函数会变为_Z9print_stri),查询字符可使用 readelf -a ‘ELF路径’ |grep ‘函数名’;
2.动态加载: 在运行期间动态加载库,是由用户自己链接而不是由链接器(动态库都用extern “C”修饰,并在进程中用dlopen动态加载),在编译期间可以不需要知道动态库的存在。动态加载的库函数字符名即为源代码中的函数名

0x02 库打桩

  Linux系统下的库打桩(Library Interpositioning)功能允许你拦截对系统标准库中某个目标函数的调用,取而代之执行自己的包装函数。

1
2
1 单进程库打桩
2 全局库打桩

1、单进程库打桩
  单进程库打桩只替换某进程中调用的系统函数,但是前提是必须获得其源码或.o文件。
  execve函数在父进程中fork一个子进程,在子进程中调用exec函数启动新的程序。exec函数族一共有六个,其中execve为内核级系统调用,所有其他函数最终都会调用到它。在这里我们直接拦截execve函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* hook.c */
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>

int execve(const char* filename, char* const argv[], char* const envp[])
{
// 在被调用时,打印命令名称与进程PID
int pid = getpid();
printf("execve(%s); pid: %d\n", filename, pid);

ssize_t (*execvep)(const char* filename, char* const argv[], char* const envp[]);

execvep = dlsym(RTLD_NEXT, "execve");
return execvep(filename, argv, envp);
}

  假设有如下调用execve函数执行’ls -ah’的源码文件:

1
2
3
4
5
6
7
8
9
10
11
12
/* main.cpp */
#include <stdio.h>
#include <unistd.h>

int main()
{
char* argv[] = {"ls", "-ah", NULL};
char* envp[] = {0, NULL};
execve("/bin/ls", argv, envp);

return 0;
}

  在编译时,只需要包含hook.c即可:

1
2
3
4
5
6
7
8
9
10
11
12
# 编译
# '-dl' 参数实现 hook.c 中的dlsym dlopen函数
# 情况一: 源码编译
$ gcc main.cpp hook.c -o test -ldl
# 情况二: .o文件编译
$ gcc main.o hook.c -o test -ldl
# 情况三: 直接修改elf库
$ gcc -shared -fPIC hook.c -o hook.so -ldl
$ patchelf --add-needed $PWD/hook.so test

# 执行
$ ./test

2、全局库打桩
  全局库打桩[2]实际上实现方式与单进程库打桩一致,只是其利用了在Linux系统下LD_PRELOAD环境变量能够允许所有进程优先加载指定函数的特性,实现了libc(系统)函数调用劫持。其执行流程如下:

  代码无需改动,只是在编译时,修改LD_PRELOAD环境变量即可:

1
2
3
4
5
6
7
8
9
10
# 编译
# '-dl' 参数实现 hook.c 中的dlsym dlopen函数
$ gcc -shared -fPIC hook.c -o hook.so -ldl
$ gcc main.cpp -o test

# 将hook.so写入LD_PRELOAD
$ export LD_PRELOAD=$PWD/hook.so

# 执行
$ ./test

0x03 注入与热更新

  注入与热更新[3]实现Hook的方法,是为了将目标进程外部调用的动态库中的某个函数替换为其他动态库中的某个函数。其Hook思路,可以简单地理解为首先将hook.so注入到目标进程内存空间中,然后使用hook.so中的函数地址热更新替代target.so中的函数地址。

1
2
1 注入
2 热更新

1、注入
  目标进程Test在最开始时并不能直接运行hook.so中的函数,因为hook.so并不在进程的内存空间,因此我们需要实现的第一步就是将hook.so注入到Test内存空间中。比较简单的实现方法是,假设Test依赖libc库,那么我们可以劫持进程执行系统调用dlopen,并加载我们的hook库。
  至于如何使进程执行系统调用dlopen,只需要修改CPU寄存器即可:


rdi / rsi / rdx / r10 / r8 / r9:参数1~6
rax:系统调用号
rip:执行代码地址

2、热更新
  在将hook.so注入目标进程Test后,我们需要使用hook.so中的函数替换掉动态库target.so中的某个函数。
  为了实现这个目的,我们必须先理解ELF调用外部函数的关系图(注意foo1与foo2调用区别):


注意: 外部调用寻找函数地址也不一定用到PLT与GOT
1.foo1: 不需要PLT与GOT寻址。比如直接在target.so中实现的函数
2.foo2: 需要PLT与GOT寻址。比如在target.so中调用的libc库函数

  根据上图,在调用foo1和foo2的真正实现中,其调用过程是:

1
2
3
4
5
6
7
8
9
10
# foo1
1) .dynstr中记录了所有外部函数字符,找到foo1字符序号(.dynstr与.dynsym共用一套序号)
2) .dynsym中记录foo1函数偏移地址,其指向位置存在于.text节中;

# foo2
1) .dynstr中记录了所有外部函数字符,找到foo2字符序号(.dynstr与.dynsym共用一套序号)
2) .dynsym中记录foo2函数偏移地址,其指向位置不存在于.text节中;
3) 根据foo2序号,找到在.rela.plt节中映射的新序号,确定foo2在.rela.plt节中位置;
4) 属PLT: .rela.plt节真正保存foo2_stub函数偏移地址,其指向位置存在于.rela.got节中;
5) 属GOT: .rela.got节真正保存foo2偏移地址。

  在理解了ELF调用外部函数的过程之后,我们针对foo1与foo2两种情况,可采用如下方法对目标函数进行热更新:

0x04 引用文献

[1]https://blog.csdn.net/weixin_43742894/article/details/90180219
[2]https://www.cnblogs.com/lion-cheng/p/16018700.html#库打桩机制
[3]https://cloud.tencent.com/developer/article/1759520

  • Linux 计算机技术

展开全文 >>

LinuxIO模型

2022-08-18

0x00 引言

  Linux系统中的IO模型主要分为五种[1]:1.阻塞IO;2.非阻塞IO;3.IO多路复用;4.信号驱动IO;5.异步IO
  前四种都可以归类为同步IO,目前最常用的是IO多路复用,本文将详细介绍这五种IO模型,参考文献[2]。

0x01 同步IO模型 - 阻塞IO

1
2
1 单线程
2 多线程

1、单线程
  在没有使用IO多路复用机制时,有BIO、NIO两种实现方式,但是会出现阻塞或者开销大的问题。其中前者即为阻塞IO,服务端会阻塞在accept函数,在客户端connect建立连接后,又会阻塞在read函数,直到客户端执行write函数。

2、多线程
  为了解决单线程无法处理多个IO的问题,可以使用多线程的方式:

0x02 同步IO模型 - 非阻塞IO

  虽然多线程阻塞IO能够使每个线程各自处理自己的dowork(),但是这还称不上非阻塞IO,因为每个线程中的read函数仍然会阻塞当前线程。非阻塞IO(NIO)需要通过设置,将read函数稍作改变,执行read时会判断fd文件描述符(下文简称fd)是否就绪,若没有则返回-1直接退出,而不是阻塞在原地一直等待fd就绪。

0x03 同步IO模型 - IO多路复用

  非阻塞IO还存在许多可优化的地方,比如不需要为每个fd开一个线程去监听。解决方案是分化一个管理线程用于循环read每个fd。
  现在线程就分成了三类:1.主线程(监听连接);2.管理线程(遍历fd);工作线程(处理业务)
  虽然从每一个子线程read自己的fd变成了只有一个管理线程read所有的fd,但fd一旦多起来,管理线程在用户态read未就绪的fd时也存在着一定的性能消耗。select/poll/epoll即是为解决这个问题而产生的:

1
2
1 select/poll
2 *epoll
select poll epoll
数据结构 bitmap 数组 红黑树
最大连接数 1024 无上限 无上限
fd拷贝 每次调用selec拷贝 每次调用poll拷贝 fd首次调用epoll_ctl拷贝,每次调用epoll_wait不拷贝
工作效率 轮询O:(n) 轮询:O(n) 回调:O(1)

1、select/poll
  select是操作系统提供的系统调用函数,通过它我们可以把一个文件描述符的数组发给操作系统,让操作系统去遍历,确定哪个文件描述符可以读写,然后告诉我们去处理。
  poll跟select几乎一模一样,只是取消了select的最大fd个数1024的限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/time.h>
#include<unistd.h>
#include<string.h>

/*
int select(
int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout
);
nfds:监控的文件描述符集里最大文件描述符加1
readfds:监控有读数据到达文件描述符集合,传入传出参数
writefds:监控写数据到达文件描述符集合,传入传出参数
exceptfds:监控异常发生达文件描述符集合, 传入传出参数
timeout:定时阻塞监控时间,3种情况
1.NULL,永远等下去
2.设置timeval,等待固定时间
3.设置timeval里时间均为0,检查描述字后立即返回,轮询

FD_CLR(inr fd,fd_set* set):用来清除描述词组set中相关fd 的位
FD_ISSET(int fd,fd_set *set):用来测试描述词组set中相关fd 的位是否为真(也就是是否已经就绪)
FD_SET(int fd,fd_set*set):用来设置描述词组set中相关fd的位
FD_ZERO(fd_set *set):用来清除描述词组set的全部位
*/

const int LEN = 1024;
int fds[2]; //只监测标准输入与输出这两个文件描述符
int main()
{
int std_in = 0;
int std_out = 1;
int fds_max = 1;
fd_set reads, writes;
struct timeval timeout;

fds[0] = std_in;
fds[1] = std_out;

while(1) {
FD_ZERO(&reads);
FD_ZERO(&writes);
FD_SET(std_in, &reads); //标准输入关注的是读事件
FD_SET(std_out, &writes); //标准输出关注的是写事件
timeout.tv_sec = 5;
timeout.tv_usec = 0;
switch(select(fds_max + 1, &reads, &writes, NULL, &timeout)) {
case 0:
printf("select time out ......\n");
break;
case -1:
perror("select");
break;
default:
//可以从标准输入中读
if(FD_ISSET(fds[0], &reads)) {
char buf[LEN];
memset(buf, '\0', LEN);
gets(buf);
printf("echo: %s\n", buf);

if(strncmp(buf, "quit", 4) == 0)
exit(0);
}
break;
}
}
}

2、epoll
  epoll也就是现在市面上使用最多的方式,解决了刚刚select所说的三个缺点:内核存在拷贝,内核是同步IO,只返回个数

0x04 同步IO模型 - 信号驱动IO

0x05 异步IO模型

0x06 引用文献

[1]https://cloud.tencent.com/developer/article/1005481
[2]https://blog.csdn.net/weixin_44806108/article/details/124748656

  • 计算机技术,Linux

展开全文 >>

Linux图形界面显示原理

2022-08-17

0x00 引言

  Linux图形界面包括前后端,后端的选择方案有著名的”X Window System”和”Wayland”,前端包括以下几个重要组成部分:显示管理器、窗口管理器、文件管理器以及GTK+/QT图形库等。
  1984年MIT大学创造了”X Window System”(XWindow),这是在UNIX系统构建图形化视窗系统的设计方案,又称X规范(协议)。并在1987年形成了第11版(X11),目前使用最广的软件实现是Xorg。
  2008年,RedHat的员工创造了Wayland。Wayland是在XWindow基础上实现的一种协议,前者去除了后者中不必要的设计(比如将X Server和窗口管理器整合到一起作为服务端)。但目前而言,前者仍有许多缺陷,比如在远程桌面能力不足,重度依赖Linux内核技术不易移植到其他系统平台。
  前后端分离的好处是,用户在Linux中可以随意切换前端,享受不同界面风格。比较常用的前端界面环境有Gnome、KDE、Unity、XFCE、LXDE、DDE等。

0x01 X Window System

1
2
1 设计原理
2 通信流程

1、设计原理

  • X11采用了C/S的架构,在其设计下,整个图形视窗系统主要分为3个部分:
    • X Server(X服务器)。一方面负责和设备驱动交互,监听显示器和键盘鼠标;另一方面响应X Client需求传递键盘、鼠标事件、绘制图形文字等
    • X Client(X客户端)。由GTK+/QT等图形库开发实现,负责在收到设备事件后计算出绘图数据,由于本身没有绘制能力,只能向X Server发送绘制请求和绘图数据。X Client可以和X Server在同一个主机上,也可以通过TCP/IP网络连接。
    • Window Manager(窗口管理器),或者叫合成器(Compositor)。掌管各X Client的窗口视觉外观,如形状、排列、移动、重叠渲染甚至是半透明效果等。注意窗管并非X Server的一部分,而是一个特殊的X Client程序。

2、通信流程

0x02 Wayland

  Wayland将X Server和窗口管理器整合到一起作为服务端,也可以称为合成器(Compositor),注意在XWindow中窗管被称为合成器。

0x03 显示管理器

  显示管理器用于开机后显示登陆界面,并启动窗口管理器等XWindow组件。如果没有显示管理器,Linux开机会显示命令行登陆界面,需要使用命令行登陆后手动启动X server和窗口管理器才能显示GUI。

0x04 窗口管理器

1
2
3
4
1 窗口层级
2 子框架重定向
3 二次排版
4 窗口合成管理器

  窗口管理器是前端的核心组件,其主要负责管理每个独立窗口如何显示/移动,怎样反馈输入,怎样组织各个窗口。目前常用的窗管有XFCE的xfwm,Gnome的mutter,KDE的Kwin等。XWindow并未指定一个专用的窗口管理器,也没有定义窗口管理器的行为,因此用户可以任意替换。关于如何写一个窗口管理器可以参考文献[1]。
  窗口管理器的本质上就是一个常规的X Client,它并没有超级用户权限,也并不能直接调用内核函数。但是,窗管能通过X Server提供的特殊API来控制屏幕显示(X Server至多只会有一个窗口管理器的存在)。窗管与X Server的交互是通过属性和事件机制实现的。下面将详细介绍窗管实现的细节:

1、窗口层级
  在显示器显示的时候,所有窗口的管理实际上是一个树形结构,这个树的根就是顶层窗口(一个虚拟的,不可见的窗口,但是它的大小跟屏幕一样,同时会永远的存在)。有Qt开发经验的都清楚,每个界面应用(X Client)都有一个QWindows(QWidget)作为主窗口,而所有的主窗口最后都会挂载到顶层窗口中。

2、子框架重定向
  在X Window System的设计理念中,X Server并不直接管理窗口显示,这个功能均转交给窗管实现。实际上如果没有窗管,那么显示就由X Server直接处理,然而在有窗管存在的情况下,显示过程中窗管会先拦截X Client对界面显示操作的请求,这种拦截技术被称为子框架重定向(Substructure Redirection)。

  X Server在任何情况下,只允许一个窗口管理器注册子框架重定向。当有第二个窗管在对一个已被注册子框架重定向的X Server进行注册时,原先的窗管不会从X Sever解注册,断连或者崩溃。

3、二次排版

  为了实现窗口风格的统一,窗口管理器在接收到X Client的窗口绘制时会主动为其添加包含标题栏的框架窗口(Frame Window),这也是为什么Qt窗口应用默认会有标题栏的原因。

4、窗口合成管理器
  现代的界面显示已经不能满足于窗口了,例如Shift Switcher:

  为了实现更加复杂的显示,出现了离屏内存缓冲区的概念(Wayland中使用的DRM技术)。实现步骤如下:


1. 渲染每一个顶层窗口以及它的内部UI节点到一个离屏内存缓冲区,而不是直接输出到硬件上;
2.根据我们的设计,使用旋转,扭曲这样的方式来变化每个缓冲区的样式;
3.将这些经过变化的缓冲区融合到一个最终的缓冲区中,同时还需要将背景和其他需要展示的UI也合并进来;
4.将这个最终缓冲区的内容渲染到屏幕上。

0x05 文件管理器

  管理硬盘的分区显示。

0x06 常见问题

  1.通过Windows的远程桌面功能远程连接到Linux的桌面系统,无法显示水印进程。而直接在Linux上安装向日葵,并远程连接,可以正常显示水印进程。

0x07 引用文献

[1]https://jichu4n.com/posts/how-x-window-managers-work-and-how-to-write-one-part-i

  • 计算机技术

展开全文 >>

上一页1234…20下一页
© 2023 JailbreakFox by Hexo
本站总访问量16181次
  • Articles
  • Change The World
  • About me

tag:

  • 计算机基础
  • 计算机技术
  • Linux 工具
  • 视觉,应用, AI
  • 工具
  • 计算机技术,Linux
  • 内核
  • Linux 计算机技术
  • 计算机技术 Linux
  • 计算机技术,Linux内核
  • 机器学习,神经网络,算法
  • Linux
  • 机器人
  • pytorch,神经网络,机器学习
  • 强化学习,算法
  • 计算机技术 黑客技术
  • 工具, 信息聚合
  • 数据库
  • 计算机技术,编译工具
  • 食物 茶
  • 工具,流
  • 游戏设计,游戏AI,入门
  • 信息安全技术
  • 配置
  • 视觉,入门
  • 数字货币 区块链
  • 远程桌面, 工具
  • 计算机技术,打包工具
  • 技巧

    缺失模块。
    1、请确保node版本大于6.2
    2、在博客根目录(注意不是yilia根目录)执行以下命令:
    npm i hexo-generator-json-content --save

    3、在根目录_config.yml里添加配置:

      jsonContent:
        meta: false
        pages: false
        posts:
          title: true
          date: true
          path: true
          text: false
          raw: false
          content: false
          slug: false
          updated: false
          comments: false
          link: false
          permalink: false
          excerpt: false
          categories: false
          tags: true
    

  • 卞神
致力于创造风靡全球的机器人

迈向CyberPunk的新世界
Never STOP!