原文地址:https://www.yanbinghu.com/2019/07/28/59484.html

前言

假如由于调试需要,你希望原先代码中的malloc函数更换为你自己写好的malloc函数,该怎么办呢?如何对程序进行”偷梁换柱“?

打桩机制

LInux链接器有强大的库打桩机制,它允许你对共享库的代码进行截取,从而执行自己的代码。而为了调试,你通常可以在自己的代码中加入一些调试信息,例如,调用次数,打印信息,调用时间等等。本文将介绍三种打桩机制,分别在编译的不同阶段。如果你还不了解这几个阶段,建议你阅读《hello程序是如何变成可执行文件的》。

编译时打桩

编译时打桩在源代码级别进行替换。我们很容易通过#define指令来完成这件事情。首先我们定义自己的头文件mymalloc.h:

#define malloc(size) mymalloc(size)
void *mymalloc(size_t size)

由于在这里使用了#define指令,我们后面需要malloc的地方都会被mymalloc替代。
而mymalloc.c代码如下:

#ifdef MYMOCK //只有MYMOCK编译选项是,这段代码才会编译进去
#include<stdio.h>
#include<stdlib.h>
/*打桩函数*/
void *mymalloc(size_t size)
{void *ptr = malloc(size);printf("ptr is %p\n",ptr);return ptr;
}
#endif

注意第一行,我们需要在gcc中传入编译选项MYMOCK(自定义,代码与传入的一致即可)。

我们在main.c中调用它:

#include<stdio.h>
#include"malloc.h"
int main()
{char *p = malloc(64);free(p);return 0;
}

编译运行:

$ gcc -DMYMOCK -c mymalloc.c
$ gcc -I . -o main main.c mymalloc.o
$ ./main
ptr is 0xdbd010

编译时还使用-I参数,告诉编译器从当前目录下寻找头文件malloc.h,因此,main函数中的malloc调用将会被替换成mymalloc。而在mymalloc.c中的则使用原始的malloc函数,最终达到“偷梁换柱”的效果。

实际上你也可以通过仅仅预编译来很清楚的看到其中的变化:

$ gcc -I . -E -o main.i main.c

查看main.i,你会发现,使用malloc的地方,都被替换成了mymalloc。

小结一下前面的步骤:

  • 打桩函数内部不要打桩,即mymalloc.c中要使用原始的malloc函数,不然会造成循环调用
  • 通过#define指令,将外部调用malloc的地方都替换为mymalloc
  • 分开编译mymalloc.c和外部调用代码,最终链接

这种方式打桩需要能够访问源代码才能完成。

链接时打桩

顾名思义,链接时打桩是在链接时替换需要的函数。Linux链接器支持用–wrap,f的方式来进行打桩,链接时符号f解析成__wrap_f,还会把__real_f解析成f。什么意思呢?我们修改前面mymalloc.c的代码如下:

//来源:公众号【编程珠玑】
//网站:https://www.yanbinghu.com
#ifdef MYMOCK //只有MYMOCK编译选项是,这段代码才会编译进去
#include<stdio.h>
#include<stdlib.h>
void *__real_malloc(size_t size);//注意声明
/*打桩函数*/
void *__wrap_malloc(size_t size)
{void *ptr = __real_malloc(size);//最后会被解析成mallocprintf("ptr is %p\n",ptr);return ptr;
}
#endif

注意将main.c中包含的malloc.h那一行去掉。

编译运行:

$ gcc -DMYMOCK mymalloc.c
$ gcc -c main.c
$ gcc -Wl,--wrap,malloc -o main main.o mymalloc.o
$ ./main
ptr is 0x95f010

我们特别关注mymalloc.c中的代码,利用链接器的打桩机制,最后在main函数中调用malloc,将会去调用__wrap_malloc,而__real_malloc将会被解析成真正的malloc,从而达到“偷梁换柱”的效果。

可以看到的是,这种打桩方式至少需要能够访问可重定位文件。

来源:公众号【编程珠玑】
网站:https://www.yanbinghu.com/2019/07/28/59484.html

运行时打桩

前面两种打桩方式,一种需要访问源代码,另外一种至少要访问可重定位文件。可运行时打桩没有这么多要求。运行时打桩可以通过设置LD_PRELOAD环境变量,达到在你加载一个动态库或者解析一个符号时,先从LD_PRELOAD指定的目录下的库去寻找需要的符号,然后再去其他库中寻找。
同样我们修改mymalloc.c:

//来源:公众号【编程珠玑】
//网站:https://www.yanbinghu.com
#ifdef MYMOCK //只有MYMOCK编译选项是,这段代码才会编译进去
#define _GNU_SOURCE  //这行特别注意加上
#include<stdio.h>
#include<stdlib.h>
#include<dlfcn.h>
extern FILE *stdout;
/*打桩的malloc函数*/
void *malloc(size_t size)
{static int calltimes;calltimes++;/*函数指针*/void *(*realMalloc)(size_t size) = NULL;char *error;realMalloc = dlsym(RTLD_NEXT,"malloc");//RTLD_NEXTif(NULL == realMalloc){error = dlerror();fputs(error,stdout);return NULL;}void *ptr = realMalloc(size);if(1 == calltimes){printf("ptr is %p\n",ptr);} calltimes = 0;return ptr;
}
#endif

在mymalloc.c的代码中,由于我们自己的打桩函数也叫malloc,因此我们通过运行时链接调用malloc函数,以便获取malloc的地址,而不是直接调用。并且是以RTLD_NEXT方式。

将mymalloc.c制作成动态库(动态库的制作和使用参考《库的制作与两种使用方式》):

$ gcc -DMYMOCK -shared -fPIC -o libmymalloc.so mymalloc.c -ldl
$ gcc -o main main.c  //重新编译main
$ LD_PRELOAD="./libmymalloc.so"
$ ./main
Segmentation fault (core dumped)

然而非常不幸的是,最后core dumped了,我们用gdb(参考《Linux常用命令-开发调试篇》)查看调用栈:

(gdb)bt
#0  0x00007fe0ca83518e in _IO_vfprintf_internal (s=0x7fe0cabad620 <_IO_2_1_stdout_>, format=0x7fe0cabb26dd "ptr is %p\n", ap=ap@entry=0x7ffcbd652058) at vfprintf.c:1267
#1  0x00007fe0ca83d899 in __printf (format=<optimised out>) at printf.c:33
#2  0x00007fe0cabb26cc in malloc () from ./mymalloc.so
#3  0x00007fe0ca8551d5 in __GI__IO_file_doallocate (fp=0x7fe0cabad620 <_IO_2_1_stdout_>) at filedoalloc.c:127
#4  0x00007fe0ca863594 in __GI__IO_doallocbuf (fp=fp@entry=0x7fe0cabad620 <_IO_2_1_stdout_>) at genops.c:398
#5  0x00007fe0ca8628f8 in _IO_new_file_overflow (f=0x7fe0cabad620 <_IO_2_1_stdout_>, ch=-1) at fileops.c:820
#6  0x00007fe0ca86128d in _IO_new_file_xsputn (f=0x7fe0cabad620 <_IO_2_1_stdout_>, data=0x7fe0cabb26dd, n=7)at fileops.c:1331
#7  0x00007fe0ca835241 in _IO_vfprintf_internal (

我们从调用栈基本可以推断,其中有反复调用,那就是说在mymalloc.c中的malloc函数中,有的语句也调用了malloc,导致了最终的反复调用。解决这种问题有两个方法:

  • 避免反复调用
  • 使用不调用打桩函数的函数,即不调用其中的printf

我们采用下面这种方式来避免反复调用,开始调用时,置调用次数为1,最后置0,如果发现调用次数不为0 ,则不调用。

#ifdef MYMOCK //只有MYMOCK编译选项是,这段代码才会编译进去
#define _GNU_SOURCE  //这行特别注意加上
#include<stdio.h>
#include<stdlib.h>
#include<dlfcn.h>
extern FILE *stdout;
/*打桩的malloc函数*/
void *malloc(size_t size)
{/*调用次数+1*/static int calltimes;calltimes++;/*函数指针*/void *(*realMalloc)(size_t size) = NULL;char *error;realMalloc = dlsym(RTLD_NEXT,"malloc");//RTLD_NEXTif(NULL == realMalloc){error = dlerror();fputs(error,stdout);return NULL;}void *ptr = realMalloc(size);/*如果是第一次调用,则调用printf,否则不调用*/if(1 == calltimes){printf("ptr is %p\n",ptr);} calltimes = 0;return ptr;
}
#endif

当然这样的写法在多线程中也是有问题的,如何改进?

至此,就达到了我们需要的结果:

./main
ptr is 0x245c010

实际上,你会发现,在设置了这个环境变量的终端下,这个打桩的动作对所有程序都生效:

$ ls
ptr is 0x1f1a040
ptr is 0x1f1a680
ptr is 0x1f1a700
ptr is 0x1f1a040
ptr is 0x1f1a060
ptr is 0x1f1a040

那么怎么取消呢:

$ unset LD_PRELOAD

在这里也可以看到,这个机制虽然强大,同样也非常危险,因为不怀好意者可以通过这种方式恶意攻击你的程序。比如说,有个程序中checkPass的接口用来校验密码,如果这个时候使用另外一个动态库,实现自己的checkPass函数,并且设置LD_PRELOAD环境变量,就可以达到跳过密码检查的目的。

总结

怎么样,是不是觉得很神奇?尤其是最后一种方式,可以达到对任何程序进行”偷梁换柱“,对于问题的定位和程序的调试非常有帮助。但是,需要特别注意的是,采用最后一种方式打桩时,最好避免打桩函数内部还调用了打桩函数,这样会导致难以预料的后果,另外由于这种打桩机制对所有程序都有效,因此也非常危险,需要特别注意。

公众号【编程珠玑】,专注分享Linux/C/C++等原创内容

“偷梁换柱”的库打桩机制相关推荐

  1. Linux 库打桩机制

    库打桩机制 Linux 链接器截获对共享库函数调用,转而执行自己的代码. 创建一个包装函数,对库函数进行包装(装饰器模式).利用打桩机制欺骗系统去调用包装函数. 编译时打桩 // malloc.c / ...

  2. Linux 程序开发 之 库打桩机制

    目录 前言 一.库打桩定义 二.编译时打桩 三.链接时打桩 四.运行时打桩 五.处理目标文件的工具 前言   Linux 链接器支持一个很强大的技术,称为库打桩(library interpositi ...

  3. Linux下的LD_PRELOAD环境变量与库打桩

    Linux下的LD_PRELOAD环境变量与库打桩 LD_PRELOAD是Linux系统的一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的 ...

  4. 动态链接--打桩机制

    链接器的输出为可执行文件.可以在链接器上做事情,加载器是运行前置,加载器也可以做很多事情. 动态链接,在加载器加载可执行文件时,先加载和运行动态链接器,然后动态链接器动态链接共享库.用于解决静态库的缺 ...

  5. Linux C 库打桩技术

    文章目录 1.前言 2. 测试环境 3.编译时打桩 3.1.相关文件 3.1.1.main.c 3.1.2.newcalloc.h 3.1.3.newcalloc.c 3.2.编译命令 3.3.运行情 ...

  6. linux程序打桩,一文搞懂linux的库打桩

    Linux下的链接器支持一个强大的库打桩(library interpositioning),允许你阻拦对系统标准库中某个目标函数的调用,取而代之执行自己的包装函数.它可以给我们带来两个好处,一是通过 ...

  7. Linux、C/C++、CS-Notes

    Linux的很多基础问题,到现在依旧解决的不是很好 ,实践太少都是要背锅的. 目录 Linux: 编译链接: 科学上网: C++ 1.课程 2.STL 3.语言 IDE Qt CS-Note Linu ...

  8. 《深入理解计算机系统(原书第三版)》pdf

    下载地址:网盘下载 内容简介  · · · · · · 和第2版相比,本版内容上*大的变化是,从以IA32和x86-64为基础转变为完全以x86-64为基础.主要更新如下: 基于x86-64,大量地重 ...

  9. 深入理解计算机系统--链接

    编译时打桩,编译命令 gcc -DCOMPILETIME -c my_malloc.c gcc -I. -o main main.c my_malloc.o //my_malloc.h #define ...

最新文章

  1. 使用 SQL Server 2012 Analysis Services Tabular Mode 表格建模 图文教程
  2. Codeforces 472D
  3. Spring MVC – Flash属性
  4. (6)css盒子模型(基础下)
  5. 计算机网络最短路径路由选择,最短路径算法Dijkstra算法在路由选择中的应用.pdf...
  6. java 调用plc程序_从老师傅那里偷学来的PLC宝贵经验!
  7. CENTOS7.8忘记ROOT密码,重置密码步骤
  8. 前端知识 | 简析ES6
  9. CDN的基本原理和基础架构
  10. 【目标定位】基于matlab扩展卡尔曼滤波目标定位仿真【含Matlab源码 128期】
  11. aauto的listview(类似于C#的datagrid) 支持文件拖拽获取名单
  12. php 老是报错没有定义,php中的错误处理与异常处理机制介绍
  13. svn恢复误删文件步骤
  14. 解决SVN没有中文选项
  15. Excel-制作简单的环形柱状图
  16. gazebo创建机器人模型01
  17. 华硕T100 安装linux,【华硕T100TA3740评测】双硬盘组合 华硕T100TA挑战存储极限(全文)_华硕 T100TA3740_笔记本评测-中关村在线...
  18. SAP中常见的Debug技巧(02)-跳过代码执行
  19. Redis-使用和原理
  20. 关于迅雷播放器的模仿

热门文章

  1. UVA 11205 - The broken pedometer
  2. SQL Server 变更数据捕获(CDC)
  3. 【编程思想】计算机领域的所有问题都可以通过增加一个间接的中间层来解决
  4. 富途php面试经验,忍不住想吐槽一下富途二面体验
  5. Redis是什么?Redis有哪些数据类型?Redis怎么集群?
  6. 【计算机网络】分层结构(OSI 7层模型、TCP/TP 4层模型、5层参考模型)
  7. Linux复习题:2判断题
  8. 【限流保护】Springboot接口限流
  9. 苹果市值重回第一;今年我国95所高校新增人工智能专业;美国新技术可使机器人获得触觉感知 | 每日大事件...
  10. 四步把流量提升淘宝店铺流量