1. hook技术概述   

hook技术是一种拦截用户函数调用的技术。通过hook技术可以实现统计用户对某些函数的调用次数,对函数注入新的功能的目标。在Linux平台,Hook技术可以分成用户和内核两个层面,每个类比中都存在不同的hook技术。本文主要介绍针对动态链接技术的PLT hook。

2. 代码实例

    首先我们先用一个实例来向大家展示一下PLT hook的效果。代码的功能是验证用户在命令行输入的密码,hook的目标是strcmp函数,通过将strcmp函数的返回值置为0,达到无论用户输入任何密码,即使是错误的,都返回验证通过的提示。 
    先编写passwd.c。该代码的作用是调用strcmp函数并判断用户输入的密码是否正确,并打印相应的提示。文件中只有一个函数就是check_is_authenticated。

  其次编写我们的main.c 该函数实现了将要替换strcmp函数的my_strcmp,和使hook生效的hook函数。代码的具体细节

 #include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <inttypes.h>
#include <execinfo.h>
#include <sys/user.h>
#include <sys/mman.h>
#include "passwd.h" #define PAGE_SHIFT 12
#define PAGE_SIZE (_AC(1,UL) << PAGE_SHIFT)
#define PAGE_MASK (~(PAGE_SIZE-1)) #define __AC(X,Y) (X##Y)
#define _AC(X,Y) __AC(X,Y) #define PAGE_START(addr) ((addr) & PAGE_MASK)
#define PAGE_END(addr) (PAGE_START(addr) + PAGE_SIZE) int my_strcmp(const char *s1, const char *s2)
{
return ;
} uintptr_t get_base_addr(char *libname)
{
FILE *fp;
char line[];
//char base_addr[1024];
uintptr_t base_addr = ; if (NULL == (fp = fopen("/proc/self/maps", "r"))) {
perror("open err");
return -;
} while (NULL != fgets(line, sizeof(line), fp)) {
if (NULL != strstr(line, libname)) {
sscanf(line, "%"PRIxPTR"-%*lx %*4s 00000000", &base_addr);
printf("line2:%s, base_addr:%"PRIxPTR"\n", line, base_addr);
break;
}
}
fclose(fp); return base_addr;
} void hook() {
uintptr_t base_addr;
uintptr_t addr;
//1. get the base addr of libpasswd.so
base_addr = get_base_addr("libpasswd");
if ( == base_addr) return;
addr = base_addr + 0x201020;
//2. add the write permisson
mprotect((void *)PAGE_START(addr), PAGE_SIZE, PROT_READ|PROT_WRITE);
//3. replace our hook func my_strcmp
*(void **)addr = my_strcmp;
//4. clear the cache
__builtin___clear_cache((void *)PAGE_START(addr),
(void *)PAGE_END(addr));
} int main()
{
hook();
check_is_authenticated("abcd");
return ;
}
  接下来我们将passwd.c编译成动态库libpasswd.so,main.c调用libpasswd.so中的check_is_authenticated函数。我们是用下列gcc命令将passwd.c编译为动态库,其中-fpic是生成位置无关代码,-shared表示我们生成的是一个动态库,不需要main函数的参与。

  在将libpasswd.so放置到/usr/lib/目录中之后,使用以下命令编译main.c

  运行之后我们 可以看到验证结果始终是正确的,说明我们对于libpasswd.so中的strcmp函数的Hook已经生效。

怎么样,是不是很神奇呀!下面我们就来看看plt hook到底是怎么实现的。

3. PLT hook原理

  说了这么久,PLT hook到底是怎么实现的呢?到底什么是PLT呢?下面我就带大家了解以下到底什么是PLT。
    在了解PLT之前我们需要先了解下什么是共享库和动态链接技术。静态链接技术,是以一种将多个可链接目标文件链接为一个独立的可执行文件的过程。在链接的过程中,连接器会将静态库中的函数完整的复制到可执行文件的文本段中。在一个运行较多进程的系统中,这种链接方式对于内存消耗是不可小觑的。如下图所示是两种不同的连接方式生成的可执行文件的大小,可以看到二者相差极大。为了解决这个问题,共享库诞生了,共享库是一个目标模块或者说目标文件,在运行或者加载时,可以加载到内存的任意位置,并和内存中的程序链接起来,这个过程是一个叫做动态链接器的组件完成的。在完成链接的过程中,链接器仅仅复制一些重定位和符号表信息到可执行文件中,大大减小了可执行文件的大小。

   PLT(Procedure Linkage Table)全程过程链接表,主要用于协助程序完成延迟加载的功能,假设程序调用一个动态库中的函数,因为动态库可以被加载到内存中的任意位置,因此我们无法去预测这个函数的运行时地址。正常的做法是为该函数调用生成一个重定位记录,然后动态链接器在程序加载的时候去解析它。但是这并不符合位置无关代码的做法,因为需要链接器修改调用模块的文本段。GNU使用延迟加载的技术去解决这个问题,把对函数地址的解析延迟到了对于函数的实际调用的时刻。使用这种技术,在第一次函数调用时的开销较大,但是在之后的调用中只会花费一条指令和一个间接的内存引用。
   延迟调用的完成需要PLT和GOT(Global Offset Table)全局偏移量表协作完成。如果一个目标模块调用任何在共享库中的函数,那么它就有自己的GOT和PLT。GOT是数据段的一部分,PLT是代码段的一部分。下面我们来看看PLT和GOT表中的内容:
  PLT。PLT表中的每一项是一个16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。PLT[1]是系统启动函数的条目。从第三项开始是用户调用的动态库函数的条目。GOT。GOT中每个表项是8字节的地址类型数据。和PLT类似,GOT[0]和GOT[1]也是特殊条目,包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在ld-linux.so模块中的入口点。其余的每一个条目对应一个被调用的函数,其地址在需要时进行解析。每个条目都有一个对应的PLT条目。下面我们还是以一个实例来解释一下延迟调用的过程。
  下列代码是一个动态库libpasswd.so中调用strcmp函数的过程,从下列代码,我们可以看到对于strcmp函数的调用,实际上调用的是PLT中的strcmp条目。strcmp条目实际上是三条汇编代码,让我们这些汇编语句都做了什么。在560行,使用jmpq指令跳转到GOT中strcmp对应的条目,GOT初始时都指向它对应的PLT条目的第二条指令,在这里也就是"pushq $0x0"。在把strcmp的ID(0x0)压入栈中后,使用jmpq指令跳转到PLT[0], 前面说过PLT[0]存储的是动态链接器相关的条目,先使用pushq命令将GOT[1]中存储的动态连接器的一个参数压栈("pushq 0x200ab2(%rip)")。值得注意的是这里的0x200ab2是一个常量,这里用到了位置无关代码(PIC)的相关知识,不再赘述。然后使用jmpq指令,通过GOT[2]中存储的动态链接器的地址间接跳转到动态链接器中,这里要注意代码中"*"的意义,"*"表示获取地址对应的内存,类似指针变量的解引用运算符。在跳转到动态链接器之后,连接器通过我们刚刚压入栈中的两个值(被调用函数的ID, 和GOT[1])来确定strcmp函数的运行时地址,并用这个地址重写strcmp对应的GOT条目,在将控制传递给strcmp函数。

  以上就是第一次调用strcmp函数的流程,可以看出开销还是不小的。在后续的调用中,首先还是跳转到strcmp的plt条目中,然后执行jmpq *0x200aaa(%rip)指令,不同的是这是strcmp对应的GOT条目已经被写入了strcmp的运行时地址,所以这条jmp指令直接将程序的执行跳转到strcmp函数中。那么是不是只要在strcmp对应的GOT条目中写入我们自己的函数的地址,就可以将控制跳转到自己实现的函数中了嘛。本着这样的思路我们来看看开始时的代码。

4. 代码分析

代码分成两部分,一个是被hook的动态库libpasswd.so和调用被hook动态库的文件main.c。这里要强调的是我们拦截的是动态库中的函数。下面我来讲解一下核心的hook函数。
  1. 第一步我们要获取libpasswd在进程中的首地址,时刻记住我们拦截的是动态库中的函数,将来要替换的GOT[strcmp]也属于libpasswd。
  2. 第二步就是获取libpasswd中调用strcmp对应的GOT条目的地址,为什么是addr = base_addr + 0x201020呢?我们回到上文的libpasswd的plt段,可以看到strcmp的plt条目的第一条指令已经写出了相应的GOT条目的地址就是0x201020。又因为我们拦截的动态库的strcmp函数,所以必须加上libpasswd在main中的首地址。

    3.  因为我们要写入目标进程的数据段,所以必须给相应的页增加写权限,这里使用mprotect函数来调整相应页的权限。

         4.  第四步就是将相应的GOT条目的内容替换为我们的函数的地址。这一句值得说道说道,*(void **)addr = my_strcmp;大家觉得这句和 (void *)addr = my_strcmp有什么区别呢?毕竟对*(void **) = (void *)看上去也是正确的啊!起初我也有这样的疑惑,但是仔细想想,其实并不是这样,我们要记住GOT表中的每一项都存储的是一个地址,同时addr存储的是相应的GOT条目的地址,因此我们必须先将addr强转为一个指向指针类型的指针。再使用星号解引用这样获取的就是GOT条目中存储的内容。这里需要理解的是指针类型其实和int char double是一样的也是一种数据类型。将上面的代码转换为以下形式也许更好理解:*( (void *) *addr) = my_strcmp。

 void hook() {
uintptr_t base_addr;
uintptr_t addr;
//1. get the base addr of libpasswd.so
base_addr = get_base_addr("libpasswd");
if ( == base_addr) return;
addr = base_addr + 0x201020;
//2. add the write permisson
mprotect((void *)PAGE_START(addr), PAGE_SIZE, PROT_READ|PROT_WRITE);
//3. replace our hook func my_strcmp
*(void **)addr = my_strcmp;
//4. clear the cache
__builtin___clear_cache((void *)PAGE_START(addr),
(void *)PAGE_END(addr));
}
        5. 最后一步我们需要清除硬件缓存,因为GOT表项的内容很可能已经绕过内存被缓存到了硬件缓存中,可能会导致hook失败。
  以上就是我了解到的PLT hook的全部内容,这里我们还只是hook自己的进程中调用的动态库的函数。对于其他进程的函数因为涉及到读取/proc/map文件和修改页权限,因此必须要root权限才能够运行。具体如何实现在下一篇文章中,我们再进行探讨。

PLT hook笔记的更多相关文章

  1. Linux Hook 笔记

    相信很多人对"Hook"都不会陌生,其中文翻译为"钩子".在编程中, 钩子表示一个可以允许编程者插入自定义程序的地方,通常是打包好的程序中提供的接口. 比如,我 ...

  2. hook笔记①

    汇编中push 0x*** retn表示跳转到某个地址继续执行 取消hook时会在多线程环境中可能被检测 去掉函数框架可以规避寄存器cpu前后状态监测 #pragma comment(linker,& ...

  3. hook笔记②

  4. 基于Android的ELF PLT/GOT符号和重定向过程ELF Hook实现(by 低端农业代码 2014.10.27)

    介绍 技术原因写这篇文章,有两种: 一个是在大多数在线叙述性说明发现PLT/GOT第二十符号重定向过程定向x86的,例<Redirecting functions in shared ELF l ...

  5. 基于Android的ELF PLT/GOT符号重定向过程及ELF Hook实现(by 低端码农 2014.10.27)

    引言 写这篇技术文的原因,主要有两个: 其一是发现网上大部分描写叙述PLT/GOT符号重定向过程的文章都是针对x86的.比方<Redirecting functions in shared EL ...

  6. 羽夏笔记——Hook攻防基础

    写在前面   本笔记是由本人独自整理出来的,图片来源于网络.本人非计算机专业,可能对本教程涉及的事物没有了解的足够深入,如有错误,欢迎批评指正. 如有好的建议,欢迎反馈.码字不易,如果本篇文章有帮助你 ...

  7. 孙鑫MFC学习笔记20:Hook编程

    1.HOOK拦截消息,设置越后的钩子优先级越高(钩子队列)2.SetWindowHookEx设置钩子    如果thread identifier为0或其他进程创建的线程,回调函数需要在动态链接库中声 ...

  8. CI框架源码阅读笔记6 扩展钩子 Hook.php

    CI框架允许你在不修改系统核心代码的基础上添加或者更改系统的核心功能(如重写缓存.输出等).例如,在系统开启hook的条件下(config.php中$config['enable_hooks'] = ...

  9. 我的Hook学习笔记

    关于Hook 一.基本概念: 钩子(Hook),是Windows消息处理机制的一个平台,应用程序能够在上面设置子程以监视指定窗体的某种消息,并且所监视的窗体能够是其它进程所创建的.当消息到达后,在目标 ...

随机推荐

  1. 什么是java的线程安全?同步,异步

    线程是比进程更小的执行单位,是在进程基础上进行的进一步划分.所谓多线程是指进程在执行过程中可以产生多个同时存在.同时运行的线程.多进程机制可以合理利用资源,提高程序的运行效率.一个进程至少包含一个线程 ...

  2. 【Bell-Ford 算法】CLRS Exercise 24.1-4,24.1-6

    本文是一篇笔记,大部分内容取自 CLRS 第三版,第 24.1 节. Exercise 24.1-4 Modify the Bellman-Ford algorithm so that it sets ...

  3. [Luogu5686] 和积和

    Description 给定两个下标从\(1\)到\(n\)编号的序列 \(a_i,b_i\),定义函数\(S(l,r)(1\le l\le r\le n)\)为: \[\sum_{i=l}^r a_ ...

  4. redis 学习(二)-- 通用命令

    redis 学习(二)-- 通用命令 1. keys pattern 含义:查找所有符合给定模式(pattern)的key 命令 含义 keys * 遍历所有 key keys he[h-l]* 遍历 ...

  5. C# 枚举转集合

    记录一下,方便自己下次使用. public class EnumHelper { /// <summary> /// 将枚举转为集合 /// </summary> /// &l ...

  6. 102、如何滚动更新 Service (Swarm09)

    参考https://www.cnblogs.com/CloudMan6/p/7988455.html   在前面的实验中,我们部署了多个副本的服务,本节将讨论如何滚动更新每一个副本.   滚动更新降低 ...

  7. bilibili小程序项目总结

    1.关于mock的使用 第一步:先到Mock官网(http://mockjs.com/)上面熟悉一下基本用法 第一步:具体使用实例: 下载wxMock.js和mock.js文件 下载地址:https: ...

  8. Django框架——基础之路由系统(urls.py)11111111

    1.URL路由系统前言 URL是Web服务的入口,用户通过浏览器发送过来的任何请求,都是发送到一个指定的URL地址,然后被响应. 在Django项目中编写路由,就是向外暴露我们接收哪些URL的请求,除 ...

  9. 工作中apache 403的一个小问题

    最近在虚拟机上安装hadoop, 需要设备本地的网络源,所以启用了apache. 由于需要,首先修改了家目录的位置 指向/opt/www   然后修改家目录的配置文件 修改完成之后重启服务,访问目录 ...

  10. redis和mongodb面试题(一)

    ● 请你回答一下mongodb和redis的区别 参考回答: 内存管理机制上:Redis 数据全部存在内存,定期写入磁盘,当内存不够时,可以选择指定的 LRU 算法删除数据.MongoDB 数据存在内 ...