• linux应用hook实例(含源码分析)


    目录

    一、简单的系统函数hook

    1. 程序编写

    2. 二进制文件分析

    ​编辑

    3. 注入程序编写

    二、PRELOAD HOOK (动态库注入一)

    三、PRELOAD HOOK (动态库注入二)


            函数地址替换是最简单的hook方式,在开发的时候发现C开发的程序和C++开发的程序hook还是有差别的。C开发的程序函数几乎是可以直接使用readelf查看,因此在查找函数偏移量的时候相对简单一些,但是如果是C++开发无法直接使用readelf命令查看函数地址,因此需要动态跟踪函数地址,找到函数偏移量然后针对该函数地址进行hook。

    一、简单的系统函数hook

    我们以getpid函数为例,该函数是linux系统中的系统函数。

    1. 程序编写

    该程序是我们希望被hook的目标程序。代码如下【main.cc】:

    1. #include
    2. #include
    3. #include
    4. // 替换getpid函数
    5. int mygetpid(){
    6. return 520;
    7. }
    8. int main(){
    9. while(true){
    10. printf("the pid is %d\n",getpid());
    11. sleep(1);
    12. }
    13. return 0;
    14. }

    【makefile】文件如下:

    1. main:clean
    2. g++ main.cc -o main
    3. clean:
    4. rm -rf main

    2. 二进制文件分析

    执行命令如下:

    readelf -S main

     我们首先需要关注的是节头中的这几项

     其中0x404000是got.plt表在程序中的偏移位置,另外两个表以此类推,接下来,我们具体看看got.plt表中有哪些内容:

    0x404028是getpid系统函数地址。

    方法一:查看自定义函数

    readelf  -a main | grep -i mygetpid 

     0x401142该函数地址是我们需要将该函数(mygetpid)注入到目标函数(getpid)。

    方法二:查看自定义函数

    nm main | grep getpid

    方法三:查看自定义函数

            通常应用发布是release版本,也许没有调试信息,因此需要安装调试信息包,才能查到对应函数地址。

            以浏览器为例:

            org.chromium-dbgsym_**_amd64.deb

            通过dpkg -L org.chromium-dbgsym找到调试信息文件。

            最后通过nm 调试信息文件获取函数地址。

    3. 注入程序编写

            我们现在知道了两个函数地址mygetpid函数以及getpid函数。getpid函数是正常调用系统函数地址。

            我们可以通过修改程序的内存地址来控制函数的流程。在linux中内存修改可以通过修改/proc/{$pid}/mem函数来完成。所以我们替换掉对应偏移量的地址。

           编写注入程序如下:

    1. vim inject.c
    2. --------------------------------------------
    3. #include
    4. #include
    5. #include
    6. int main(int argc,char* argv[]) {
    7. int pid = atoi(argv[1]);
    8. unsigned long offset = 0x404028;
    9. unsigned long myfunctionaddr = 0x401142;
    10. char filename[32];
    11. snprintf(filename, sizeof(filename),"/proc/%d/mem",pid);
    12. int fd = open(filename, O_RDWR|O_SYNC);
    13. lseek(fd,offset,SEEK_SET);
    14. write(fd,&myfunctionaddr, sizeof(unsigned long));
    15. return 0;
    16. }
    17. -----------------------------------
    18. gcc -o inect inject.c

    我们来看看演示效果

    操作步骤如下:

    1. 运行目标程序。

    2. 运行注入程序(需要带参数,参数为目标程序的pid)

    3. 目标程序的函数地址被修改。

     在演示中我们可以看到他的getpid函数被强制修改为mygetpid进行调用。

    二、PRELOAD HOOK (动态库注入一)

            我们可以看到在上面的例子中,我们修改了内存内容指向了程序的另外一个函数,但是该函数还是目标程序中已经存在的函数,那么我们需要如何修改为自己的函数呢?

            Linux系统中,ELF格式的导入表只存储了符号(包括导出的全局对象、全局变量以及全局函数)名,因此在进程加载器初始化外部符号时,从模块链表头开始按模块搜索直到遇到该符号名。

            这样就可以利用该特性,复写目标程序的函数(函数名一致),也就是我们构造一个和被注入函数一模一样的函数,让他优先加载我们的函数,这样也就可以达到hook的效果。

            linux提供了一个环境变量LD_PRELOAD,这个环境变量可以让目标程序启动时,优先加载我们指定的so。

    1. vim preloadso.c
    2. -------------------------------------------
    3. #include
    4. int getpid(void){
    5. printf("i hook the getpid function!\n");
    6. return 420;
    7. }
    8. --------------------------------------------
    9. gcc -o preloadso.so -shared -fPIC preloadso.c

    接下来使用LD_PRELOAD环境变量运行之前编写的目标程序main,结果如下:

    LD_PRELOAD=./preloadso.so ./main

     我们可以看到目标程序main的getpid函数已经被so文件中的getpid覆盖。

    源码地址:hook/getpid · master · 三雷科技 / QT博客案例 · GitCode博客https://blog.csdn.net/arv002 博客中介绍项目源码 贪吃蛇、扫雷、svg转图工具等https://gitcode.net/arv002/qt/-/tree/master/hook/getpid

    三、PRELOAD HOOK (动态库注入二)

    1. 原理说明

            如果我们准备注入的函数名称与带注入的名称不一致怎么办?我们可以通过修改跳转地址的方式来进行hook,PRELOAD+内存修改。修改逻辑如下:

    1) 获取程序基地址。

    通过查找内存maps可以获取程序的动态收地址。

    cat /proc/{进程id}/maps

    2) 通过偏移量获取函数地址。(偏移量是不会改变的,除非修改代码)

    函数运行地址=程序基地址+偏移量

    3) 修改函数地址的指令,转为跳转指令。

    1. // offset为函数地址在软件中的偏移量,nm命令可以获取。
    2. unsigned long ShowFunAddress = baseAddress + offset;
    3. // 打开指定函数偏移的位置。
    4. lseek(fd,ShowFunAddress,SEEK_SET);
    5. // 开始注入代码
    6. // 伪造jmp指令
    7. unsigned char jmpcmd[14] = {0}; // JMP远跳只支持32位程序 64位程序地址占8个字节 寻址有问题
    8. jmpcmd[0] = 0xFF; // 当JMP指令为 FF 25 00 00 00 00时,会取下面的8个字节作为跳转地址
    9. jmpcmd[1] = 0x25; // 因此可以使用14个字节作为指令 (FF 25 00 00 00 00) + dstaddr
    10. jmpcmd[2] = 0x00;
    11. jmpcmd[3] = 0x00;
    12. jmpcmd[4] = 0x00;
    13. jmpcmd[5] = 0x00;
    14. unsigned long dstaddr = (unsigned long)myfunctionaddr;
    15. memcpy(&jmpcmd[6], &dstaddr, sizeof(dstaddr));
    16. // 注入代码
    17. write(fd,&jmpcmd, sizeof(jmpcmd));

    note:

    1) 伪造的函数需要和原函数一模一样,否则会出现奔溃。

    2) 伪造函数的逻辑需要和原函数无冲突,否则会导致后续程序运行奔溃。

    4) 修改内存地址指令跳转到指定函数地址。

    如果为了节约指令地址可以使用该命令

    48 B8  $offset * 8  FF E0

    翻译成汇编

    1. mov 地址 %rax
    2. jmp %rax

    2. 代码样例

    1 ) 编写目标程序

    代码如下:main.cc

    1. #include
    2. #include
    3. #include
    4. #include
    5. void showText(){
    6. printf("this is main function!\r\n");
    7. }
    8. int main(){
    9. while(true){
    10. showText();
    11. sleep(1);
    12. }
    13. return 0;
    14. }

    makefile文件如下

    1. main: clean
    2. g++ main.cc -o main
    3. clean:
    4. rm -rf main

    2)编写so动态库

    common.cc

    1. #include "common.h"
    2. int char_to_hex(char x){
    3. switch (x)
    4. {
    5. case '0':
    6. return 0;
    7. case '1':
    8. return 1;
    9. break;
    10. case '2':
    11. return 2;
    12. case '3':
    13. return 3;
    14. case '4':
    15. return 4;
    16. case '5':
    17. return 5;
    18. case '6':
    19. return 6;
    20. case '7':
    21. return 7;
    22. case '8':
    23. return 8;
    24. case '9':
    25. return 9;
    26. case 'a':
    27. case 'A':
    28. return 10;
    29. case 'b':
    30. case 'B':
    31. return 11;
    32. case 'c':
    33. case 'C':
    34. return 12;
    35. case 'd':
    36. case 'D':
    37. return 13;
    38. case 'e':
    39. case 'E':
    40. return 14;
    41. case 'f':
    42. case 'F':
    43. return 15;
    44. default:
    45. return 0;
    46. break;
    47. }
    48. return 0;
    49. }
    50. unsigned long str_to_hex(string str){
    51. unsigned long result = 0;
    52. for(int i = str.length()-1 ; i>= 0 ;i--){
    53. unsigned long base = 1;
    54. if(str.at(i) == '0') continue;
    55. for(int j = 0 ; j < str.length()- 1 - i ; j++){
    56. base *= 16;
    57. }
    58. result += char_to_hex(str.at(i))*base;
    59. }
    60. return result;
    61. }
    62. // 获取程序基地址
    63. unsigned long getBaseAddress(string name){
    64. string cmd = "cat /proc/" + to_string(getpid()) + "/maps | grep " + name +" | head -n 1 | awk -F '-' '{print $1}'";
    65. // cout <
    66. int res = -1;
    67. int ret = -1;
    68. FILE * fp;
    69. if ((fp = popen(cmd.c_str(), "r") ) == NULL)
    70. {
    71. printf("Popen Error!\n");
    72. return 0;
    73. }
    74. char pRetMsg[512] ={0};
    75. while(fgets(pRetMsg, 512, fp) != NULL){
    76. // printf("safdad:%p ,%s,%d \r\n",&pRetMsg,pRetMsg,getpid()); //print all info
    77. if(NULL != strstr(pRetMsg, HDD_MOUNT_DIR)){
    78. printf("got df info:\n");
    79. ret = 0;
    80. break;
    81. }
    82. }
    83. if ((res = pclose(fp)) == -1){
    84. printf("close popenerror!\n");
    85. return 0;
    86. }
    87. pRetMsg[strlen(pRetMsg)-1] = '\0';
    88. // cout <
    89. return str_to_hex(string(pRetMsg));
    90. }

    common.h

    1. #ifndef _COMMON_H
    2. #define _COMMON_H
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. using namespace std;
    10. #define HDD_MOUNT_DIR "yubo.wang"
    11. // char转数字
    12. int char_to_hex(char x);
    13. // 16进制字符串转码
    14. unsigned long str_to_hex(string str);
    15. // 获取程序基地址
    16. unsigned long getBaseAddress(string name);
    17. #endif

    preloadso.c

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include "common.h"
    10. void showHook(){
    11. printf("this is hook function!\n");
    12. }
    13. int hookFunction(){
    14. char strProcessPath[1024] = {0};
    15. if(readlink("/proc/self/exe", strProcessPath,1023) <=0){
    16. return -1;
    17. }
    18. char *strProcessName = strrchr(strProcessPath, '/');
    19. if(strcmp(strProcessName,"/main")){return 0;}
    20. unsigned long baseAddress = getBaseAddress("main");
    21. unsigned long offset =0x401132-0x400000;
    22. // 打开浏览器证书弹窗的地址为 基地址+偏移量。
    23. unsigned long ShowFunAddress = baseAddress + offset;
    24. printf(" baseAddress:%llu, ShowFunAddress:%llu \r\n",baseAddress,ShowFunAddress);
    25. // 获取将要注入的函数地址。
    26. unsigned long myfunctionaddr = (unsigned long)&showHook;
    27. printf(" baseAddress:%llu, ShowFunAddress:%llu ,myfunctionaddr:%llu \r\n",baseAddress,ShowFunAddress,myfunctionaddr);
    28. // 打开程序内存。
    29. char filename[32];
    30. snprintf(filename, sizeof(filename),"/proc/%d/mem",getpid());
    31. int fd = 0;
    32. if((fd = open(filename, O_RDWR|O_SYNC)) ==-1)
    33. {
    34. return -1;
    35. }
    36. // 打开指定函数偏移的位置。
    37. lseek(fd,ShowFunAddress,SEEK_SET);
    38. // 开始注入代码
    39. // 伪造jmp指令
    40. unsigned char jmpcmd[14] = {0}; // JMP远跳只支持32位程序 64位程序地址占8个字节 寻址有问题
    41. jmpcmd[0] = 0xFF; // 当JMP指令为 FF 25 00 00 00 00时,会取下面的8个字节作为跳转地址
    42. jmpcmd[1] = 0x25; // 因此可以使用14个字节作为指令 (FF 25 00 00 00 00) + dstaddr
    43. jmpcmd[2] = 0x00;
    44. jmpcmd[3] = 0x00;
    45. jmpcmd[4] = 0x00;
    46. jmpcmd[5] = 0x00;
    47. unsigned long dstaddr = (unsigned long)myfunctionaddr;
    48. memcpy(&jmpcmd[6], &dstaddr, sizeof(dstaddr));
    49. // 注入代码
    50. write(fd,&jmpcmd, sizeof(jmpcmd));
    51. int res = -1;
    52. if ((res = close(fd)) == -1){
    53. printf("close popenerror!\n");
    54. return 0;
    55. }
    56. return 0;
    57. }
    58. int a=hookFunction();

    makefile文件

    1. preloadso.so:clean
    2. g++ -o preloadso.so -shared -fPIC preloadso.c common.cc common.h
    3. clean:
    4. rm -rf preloadso.so
    5. install:
    6. rm -rf ../main/preloadso.so
    7. cp preloadso.so ../main/

    3)运行注入

    将编译好的so文件拷贝到main函数下运行。

    LD_PRELOAD=./preloadso.so ./main

     可以看到原始的代码已经不执行了,他执行到了我们hook的代码中。

    源码地址:hook/jmp · master · 三雷科技 / QT博客案例 · GitCode博客https://blog.csdn.net/arv002 博客中介绍项目源码 贪吃蛇、扫雷、svg转图工具等https://gitcode.net/arv002/qt/-/tree/master/hook/jmp

    四、函数地址获取

            上面已经说了如何获取函数地址,都是静态获取,c语言可以通过函数名称来获取函数地址。

    方法如下:

    1. int (*f)(int ,int);
    2. f = (int(*)(int,int))dlsym(RTLD_NEXT,"add");

     可以在程序运行的时候通过名称直接获取。

    但是对于c++其导出的函数名不能直接通过dlsym函数获取。因此还是需要使用静态的方式获取函数偏移量来定位函数地址。如果哪位大神看到该文章有办法获取的话,能不能告知一下小弟。

  • 相关阅读:
    工业设计公司有哪些设计思维?关键有什么?
    【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】
    ACREL DC energy meter Application in Indonesia
    .NET基础面试题
    C++学习笔记(Ⅲ):C++核心编程
    Nginx部署vue项目和配置代理
    LeetCode //C - 208. Implement Trie (Prefix Tree)
    Flutter中的多线程如何使用
    一个软件打磨了24年,被安装超过100亿次,居然赚不到钱?
    Java面试题:链表-反转链表
  • 原文地址:https://blog.csdn.net/arv002/article/details/126284568