转自:http://www.cnblogs.com/leo0000/p/5632642.html
声明:以下的代码成果,是参考了网上的injso技术,在本文的最后会给出地址,同时非常感谢injso技术原作者的分享。
但是injso文章中的代码存在一些问题,所以后面出现的代码是经过作者修改和检测的。也正因为这些错误,加深了我的学习深度。
最近因为在学习一些调试的技术,但是很少有提到如何在函数运行时实现函数替换的。
为什么会想到这一点?因为在学习调试时,难免会看到一些内核方面的调试技术,内核中的调试有一个kprobe,很强大,可以实现运行时的函数替换。其原理就是hook,钩子,但是学习了这个kprobe之后会发现,kprobe内部有检测所要钩的函数是不是属于内核空间,必须是内核函数才能实现替换。而实际上,我的工作大部分还是在应用层的,所以想要实现应用程序的热补丁技术。
一些基础的知识这边的就不展开了,需要的基础有,elf文件格式,ptrace,waitpid,应用程序间通信时的信号,汇编。
- 1、elf文件加载过程
elf简单地说是由以下四部分组成的,elf文件头,program header和section header,内容。其中program header是运行时使用的,而section header并不会被加载进程序运行空间,但他们可以在编译时被指定该段的加载地址等信息,当然一般这个链接脚本.lds是由gcc默认的。
第一步,加载elf文件头,检验文件类型版本等,重要的是找到program header的地址和header的个数,如果连接器脚本是默认的,那么elf文件头会被加载在0x804800地址处。
第二步,加载program header,接着扫描program header,找到一个类型为PT_INTERP的program header,这个header里面放着的是有关解释器的地址,这时候将解释器程序的elf文件头加载进来。一般是这样:
INTERP 0x000134 0x08048134 0x08048134 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
第三步,扫描program header,如果类型为PT_LOAD,则将该段加载进来。
第四步,判断是否需要解释器程序,如果需要,把解释器程序加载进来,并把程序入口设置为解释器程序的地址。否则是应用程序本身的入口。反汇编为_start标号。
第五步,设置命令行传入的参数等应用程序需要的信息。
第六步,解释器程序开始运行,加载程序需要的库,填写重定向符号表中的地址信息。
- 2.elf文件动态链接过程
上一步,解释器程序根据program header已经将应用程序的段都加载进内存了,接下来再扫描program header,找到类型为PT_DYNAMIC,这里面包含了很多由section header描述的内容,包括重定向表,符号表,字符串表等等。解释器需要这个段描述的一些信息。
DT_NEEDED描述了所需要的动态库名称,DT_REL描述了重定位表地址,DT_JMPREL描述了重定位表地址(这个表是懒惰链接使用的),DT_PLTGOT全局偏移表地址。
此时解释器程序就可以根据所需要的动态库,将其加载进内存。每一个被加载进来的库的相关信息会被记录在link_map结构中,这个结构是一个链表,保存了所有的动态信息。
其中,全局偏移表got,got[0]保存了PT_DYNAMIC的起始地址,got[1]保存link_map的地址,而link_map中就可以找到PT_DYNAMIC的起始地址,和下一个或者上一个共享文件或者可执行文件的link_map地址。
DT_REL这个重定向表中的符号必须在此时就被解析完成。
而DT_JMPREL这个重定向表中的符号可以在运行时再解析。
所有的库和符号全部解析完成之后,解释器程序就会把控制权交给可执行文件的_start。程序开始执行。
- 3.替换函数和被替换函数
被替换程序源码。
1 2 3 4 5 6 7 8 9 |
|
替换新库代码。
1 2 3 4 5 6 7 |
|
够简单明了吧,如果替换成功,目标程序将会一直输出“哈哈哈哈哈哈”。
- 4.功能函数
ptrace相关代码:
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 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 |
|
这里面的东西我就不展开了,对ptrace的学习,请自行man。
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 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 |
|
作者所做的修改,读者可以对比文章最后的连接中的代码。
这边对于程序的具体解释,就不具体展开了。
需要注意的是,原来是采用_dl_open的方式加载库函数,但是ld库并没有这个符号导出。而libc库中导出了一个可以加载库的__libc_dlopen_mode函数。
- 5.主函数
先说一下流程,
a.获取被跟踪进程的link_map地址
b.根据link_map给出的信息,搜索符号表,遍历每一个link_map中的符号表,直到找到想要找的符号。这里是printf或者__libc_dlopen_mode函数
c.将库路径包括库名称传递给调用__libc_dlopen_mode的函数,该函数即call__libc_dlopen_mode会把__libc_dlopen_mode函数需要的参数,路径和加载方式压栈,在让被跟踪进
程开始运行之前,压入一个非法地址,当__libc_dlopen_mode返回时返回到一个非法地址时,就会发生中断,此时跟踪进程可以waitpid跟踪到。好,设置寄存器,并让被跟踪进程开
始运行。打开库之后,被跟踪进程因中断而被跟踪进程再次获得控制权。
d.再一次根据之前保存的link_map信息,当然完全可以直接用上一次搜索结果结束之后的link_map往后找,因为新库一定在最后,但是本文还是从头开始找,找到新库中的
newmyprint地址。
e.还是根据link_map信息查找printf的重定向地址,在rel.dyn节中,有关这个rel.dyn和rel.plt等节之间的关系,可以看我的其他博文。
f.将newmyprint的地址填入printf的重定向地址。
g.将被跟踪进程原先的寄存器设置回去,释放控制。
h.被跟踪进程开始输出“哈哈哈哈哈”。
上源码:
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 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
|
这里面有一个很重要的地方,如果不先在目标进程中调用printf就不能够调用__lib_dlopen_mode成功,这个原因很奇怪,根据当时的core文件来看崩溃在了下面的这个函数,原因是_dl_open_hook这个全局变量为0,但实际上运行过printf之后,这个_dl_open_hook还是0。这个有待后续检验。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
运行结果:
[email protected]:injso# ./test
1467364356 : original
injso successed
hahahahahahahahahahahahahaha
- 6.如何替换未导出符号的地址
被替换函数源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
这个怎么来替换fun1函数的地址呢?
首先反汇编得到main的机器码,如下,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
可以看到在地址0x8048496处的机器码是跳转到fun1函数的,那么这个ffffffb9就是call的操作数,操作数地址0x8048497,也就是说把这个地址中的数值改掉就可以了,有关这个call或者jmp的地址计算可以查看我的另外一篇博文。
有关这个如何跳转的方法,已经在主函数的代码中给出了,但是被我注释掉了,大家感兴趣的话,可以自己试试。
效果:
[email protected]:lib2lib# ./a.out
i = ffffff46
fun1
injso successed
hahahahahahaha^C
这里面的无关代码,大家仔细看,是为了证明call的函数地址计算方式的。
- 7.总结
那么讲到现在的话,已经实现了不管函数符号是否导出都可以实现运行时替换的代码。
这里面主要的技术是,elf文件格式,运行时加载的过程,跳转地址的计算,运行时链接的过程,也就是plt表(当然这个也可以从我的另一篇博文中看到)。
比较遗憾的是有关那个奔溃,有网友如果找到了原因,请回复下,3q。当然我也会自己再研究下。
最后补上全局变量和头文件:
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 |
|
- 8.修正
针对在调用__libc_dlopen_mode函数之前需要调用printf的问题,终于让我在晚上解决了。
首先,我尝试了调用其他函数而不是printf函数,发现效果一样,包括第一次是调用__libc_dlopen_mode,第二次对该函数的调用都可以成功。
其次,那么现在问题就集中在了这两个__libc_dlopen_mode调用之间的差别在哪里,程序段肯定是一致的,栈也是一致的,而堆空间未使用,还有一个重要的因素,那就是寄存器。
最后,发现在调用__libc_dlopen_mode前,有四个寄存器不同,分别是eax,orig_eax,eflags和esp。我一开始认为,通用寄存器eax和orig_eax不会对程序的执行造成影响。但是通过实验,仅调一次__libc_dlopen_mode,部分寄存器赋正确执行时的值,发现对eax和orig_eax被赋于正确执行时的值时,程序可以正常运行,而且不仅仅必须是一种值,比如eax可以是0,1,0xffffffff,很多值都可以,但是被赋予0xfffffdfc和0xfffffdff等值时会失败,试验过并不是因为d这一位决定的,0xfffffdf0或者d00是可以运行成功的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
在实验中,还发现对eax赋于不正确的值时,当时忘了记了,还让程序跑飞了。崩了,但是新库已经加载上了。所以这个函数替换还是有一定的风险,或者说libc库本身存在一定的bug。
所以现在问题找到了,在于eax和orig_eax上,但是对__libc_dlopen_mode反汇编发现,eax在函数开头就被赋予了通过栈传递的参数2的值,所以eax不应该影响程序的运行,但实际上影响了,这一点让我觉得很奇怪,如果有任何网友对这个原因知晓的话,麻烦回复,万分感谢。
linux共享库注射地址:http://www.docin.com/p-634172083.html
__simple原创
转载请注明出处