PLTフックノート

1.フック技術概要   

     フック技術は、関数呼び出しをインターセプトするようテクノロジの利用者です。ユーザー統計関数は、新たな目的関数を注入し、いくつかの関数の呼び出しの数のためのフック技術によって達成することができます。Linuxプラットフォームでは、フックのユーザーとカーネル技術は、2つのレベルに分けることができ、それぞれのアナロジー異なるフック技術があります。本論文では、動的リンク技術のためのPLTのフックを説明しています。

例2のコード

    まずは、あなたのPLTフックの効果の一部を表示する例を使用してみましょう。機能コードは、ユーザーのパスワードは、コマンドラインを入力し検証することであるフックの目標は、間違っていても、かかわらず、すべてのユーザ入力パスワードの、strcmpの関数であり、リターンのstrcmp関数値によって検証プロンプトを返し達成するために、0に設定されています。 
    あなたのpasswd.cを書きます。コードの役割は、関数strcmp関数を呼び出すと、ユーザーが入力したパスワードが正しいかを判断し、適切なプロンプトを印刷することです。ファイル一つだけの機能がcheck_is_authenticatedされます。

  第二に、我々は、main.cの機能は、関数strcmp関数my_strcmp、およびフック関数のフック効果を交換する実現書きます。コードの具体的な詳細

1の#include <stdio.hの>
 2の#include <STDLIB.H>
 3の#include <unistd.h>
 4の#include < 文字列・H>
 5の#include <inttypes.h>
 6の#include <execinfo.h>
 7#含めるは<sys / user.h>
 8の#includeは<sys / mman.h>
 9の#include " passwd.h " 
10  
11  の#define PAGE_SHIFT 12
 12  の#define PAGE_SIZE(_AC(1、UL)<< PAGE_SHIFT)
 13  の#define PAGE_MASK(〜(PAGE_SIZE-1))
 14  
15  の#define __AC(X、
Y)(X ## Y) 16 #define _AC(X、Y)__AC(X、Y)
 17  
18  の#define PAGE_START(ADDR)((ADDR)&PAGE_MASK)
 19  の#define PAGE_END(ADDR)(PAGE_START(ADDR)+ PAGE_SIZE)
 20  
21  のint my_strcmp(CONST  チャー * S1と、CONST  のchar *のS2) 
 22  {
 23      リターン 0 24  }
 25  
26いるuintptr_tのget_base_addr(チャー *のlibnameに) 
 27  {
 28      のFILE *のFP。
29      チャーライン[ 1024 ]。
30      // チャーBASE_ADDR [1024]。
31      uintptr_tをするBASE_ADDR = 0 32  
33      であれば(NULL ==(FP =のfopen(" / PROC /自己/マップ"" R " ))){
 34          にperror(" オープンERR " )。
35          リターン - 1 36      }
 37  
38      ながら(NULL!=関数fgets(線、はsizeof (ライン)、FP)){
 39          であれば(NULL!= はstrstr(ライン、libnameに)){
 40              のsscanf(ライン、" " PRIxPTR " - %※のLX%* 4S 00000000 "、&BASE_ADDR)。
41              のprintf(" LINE2:%sは、BASE_ADDR:%" PRIxPTR " \ n " 、ライン、BASE_ADDR)。
42              ブレーク;
43          } 
 44      }
 45      FCLOSE(FP)。
46  
47      リターンBASE_ADDR。
48  }
 49  
50  空隙フック(){
 51      uintptr_tをするBASE_ADDR。
52      uintptr_tをaddrに、
53      //1 libpasswd.soのベースADDRを取得し
54      BASE_ADDR = get_base_addr(" libpasswd " )。
55      であれば0 == BASE_ADDR)のリターン;
56      ADDR = BASE_ADDR + 0x201020 57      // 2.書き込みpermisson追加
58      MPROTECT((無効 *)PAGE_START(ADDR)を、PAGE_SIZE、PROT_READ | PROT_WRITE)。
59      // 3.交換する私たちのフックにfunc my_strcmp 
60      *(無効 **)のaddr = my_strcmp。 
61      // 4.キャッシュをクリア
62     __builtin ___ clear_cache((ボイド * )PAGE_START(ADDR)、
 63ボイド * )PAGE_END(ADDR))。
64  }
 65  
66  のint main()の 
 67  {
 68      フック()。
69      (check_is_authenticated " ABCDを" )。
70      リターン 0 ;
71 }
  次はcheck_is_authenticated機能にlibpasswd.so呼び出すmain.cのコンパイル済みのDLLのlibpasswd.soを、passwd.cます。私たちの世代は、動的ライブラリは、main関数に参加するために必要とされていないで表現する-shared我々は、位置独立コードを生成-fpicある動的ライブラリにコンパイル次のコマンドpasswd.cのGCCを使用しています。

  libpasswd.soは/ usr / lib /ディレクトリに置かれた後、次のコマンドは、main.cのをコンパイルします

  実行した後、私たちは私たちが力の関数strcmpフック用libpasswd.so常に正しい結果を、表示します。

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

おすすめ

転載: www.cnblogs.com/dennis-wong/p/11229372.html