有一个以前用的软件,一直在用,非常稳定,现在由于业务需要,在显示结果后面,根据配置文件,再进行一次正则过滤。
给原来开发软件的公司打电话,打不通了……大家可以发挥想象力!总之就是没人管了。
技术选型只是很小的改动,在重新开发一套,时间不允许,而且重新开发的也不一定稳定。所以最后决定在原文件上进行修改,这样时间最短,成本最低。
可行性方案1. 在原来的文件上进行二进制修改,考虑到汇编语言开发效率太低,不可取。【放弃】
2. 修改原程序,在合适的地方,使用懒加载方式,调用自己写的so文件,然后在so文件中实现功能。【可以实现,但既要修改原文件,还要写so文件,可以当做备选方案】
3. 不修改原程序,LD_PRELOAD动态连接.so函数劫持,然后对需要进行修改的关键点进行补丁。【不修改原文件,只是利用so文件,进行补丁,新功能在so文件中完成,此方案可以考虑】
任务开始运行环境:
[root@centos ~]# uname -a Linux centos 2.6.32-431.29.2.el6.x86_64 #1 SMP Tue Sep 9 21:36:05 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux [root@centos ~]# lsb_release -a LSB Version: :base-4.0-amd64:base-4.0-noarch:core-4.0-amd64:core-4.0-noarch:graphics-4.0-amd64:graphics-4.0-noarch:printing-4.0-amd64:printing-4.0-noarch Distributor ID: CentOS Description: CentOS release 6.4 (Final) Release: 6.4 Codename: Final 关键点分析升级关键点分析
.text:XXXXXXXXXXXXXXXX 00 00 00 00 00 00 mov rdi, rbp ; s1 .text:XXXXXXXXXXXXXXXX 00 00 00 00 00 00 mov esi, offset aVer3 ; "Ver3.0" .text:XXXXXXXXXXXXXXXX 00 00 00 00 00 00 lea rbp, [rbx+40h] .text:XXXXXXXXXXXXXXXX 00 00 00 00 00 00 call _strcasecmp ====》我们就来欺骗这个函数 .text:XXXXXXXXXXXXXXXX 00 00 00 00 00 00 test eax, eax .text:XXXXXXXXXXXXXXXX 00 00 00 00 00 00 jz short goto_upgrade解密算法关键点分析
.text:XXXXXXXXXXXXXXXX 00 00 00 00 00 00 mov [rsp+338h+var_330], esi .text:XXXXXXXXXXXXXXXX 00 00 00 00 00 00 mov qword ptr [rsp+338h+var_338], rax ; int .text:XXXXXXXXXXXXXXXX 00 00 00 00 00 00 mov rsi, rbp ; char * .text:XXXXXXXXXXXXXXXX 00 00 00 00 00 00 call decode_data ; 解密字符串 .text:XXXXXXXXXXXXXXXX 00 00 00 00 00 00 mov rax, [rsp+338h+var_D8] ==>我patch的是这个地方 .text:XXXXXXXXXXXXXXXX 00 00 00 00 00 00 cmp [rsp+268h], rax ; rax=解密后的字符串 .text:XXXXXXXXXXXXXXXX 0F 84 44 02 00 00 jz loc_452910 选定方案使用ida分析文件后发现原程序启动后会调用系统的**strcasecmp**函数,来比较版本号进行升级。
好,那么我们就模拟系统libc.so.6文件中的strcasecmp,决定使用【方案3】。
流程图要写东西,总要先画个流程图吧,这样不至于偏离目标。
下图是原来程序的运行流程图:
程序运行后,会先调用比较函数比较版本号,然后就是一些内部的黑盒逻辑处理了,(由于一些原因,不便透露程序名称,所以隐去了,请大家谅解)如果解密成功,就会把解密出来的内容显示在屏幕上。
下图是【方案3】实现的流程图
我们使用LD_PRELOAD=myhook.so 原文件名称,提前加载so文件,当原程序准备比较版本的时候,这时就会调用红色框中的流程,这个流程中,我们做了二件事,第一读取配置文件,第二,修改原文件中关键点在解密成功后,在加一层判断代码,把这个代码指向我们so文件中的函数,也就是下图的蓝色框内部分。其他部分同上。
#p#分页标题#e#
strcmpcase替换
//懒加载方式,调用真正的strcasecmp函数 int Mystrcasecmp (const char *s1,const char *s2) { static void *handle = NULL; static STRCASECMP old_strcasecmp = NULL; if (!handle) { handle = dlopen ("libc.so.6",RTLD_LAZY); old_strcasecmp = (STRCASECMP)dlsym (handle,"strcasecmp"); } return old_strcasecmp (s1,s2); } //软件会调用我的这个函数 int strcasecmp (const char *pszSrc,const char *pszDest) { loadconfig(); //这里我用来加载我自己的配置文件 write_code(); //用来patch功能的代码 return Mystrcasecmp (pszSrc,pszDest); }读取配置文件
void loadconfig() { if (pFile = fopen ("/root/conf.txt","rt")) { 。。。。。 //按自己的规则加载配置文件 fclose (pFile); pFile = NULL; g_bReady = 1; //只加载一次 } }编写正则表达式过滤代码
int myRegularcheck(const char* decode_str) { //根据配置文件,开始做事 }编写原文件和so文件之间的HOOK代码
__asm__ __volatile__( ***"movq 0x260(%rsp),%rax;\n\t" ***"cmp 0x268(%rsp),%rax;\n\t" "push %rax;\n\t" ===>保存现场 "push %rcx;\n\t" ===>保存现场 "push %rdx;\n\t" ===>保存现场 "push %rbx;\n\t" ===>保存现场 "push %rbp;\n\t" ===>保存现场 "push %rsi;\n\t" ===>保存现场 "push %rdi;\n\t" ===>保存现场 ***"jz FATD;\n\t" ===>保存现场 ***"movq %rsp,%rdi" ===>解密后的字符串 ***"call *myRegularcheck" ===>正则表达试过滤 ***"test %eax,%eax;\n\t" ===>比对失败 ***"jz FATD;\n\t" "pop %rdi;\n\t" ===>恢复现场 "pop %rsi;\n\t" ===>恢复现场 "pop %rbp;\n\t" ===>恢复现场 "pop %rbx;\n\t" ===>恢复现场 "pop %rdx;\n\t" ===>恢复现场 "pop %rcx;\n\t" ===>恢复现场 "pop %rax;\n\t" ===>恢复现场 ***"jmpq *0x00000000\n\t" ==>显示字符串 ***"FATD:\n\t" "pop %rdi;\n\t" ===>恢复现场 "pop %rsi;\n\t" ===>恢复现场 "pop %rbp;\n\t" ===>恢复现场 "pop %rbx;\n\t" ===>恢复现场 "pop %rdx;\n\t" ===>恢复现场 "pop %rcx;\n\t" ===>恢复现场 "pop %rax;\n\t" ===>恢复现场 ***"jmpq *0x00000000" ===>不显示字符串 );上面的代码主要是保存现场和恢复现场的代码多,所以看起来比较乱,为了程序的稳定性,还是需要这样做的,其实主要代码没有几行。
so文件中,修改主程序代码,让原文件解密算法后,执行我们写的正则表达式代码
#p#分页标题#e#利用mprotect,先把主程序要修改的代码段,修改为可写可执行
int mem_protect_open( char* start_addr , int len) { unsigned int pagesize = 0; pagesize=getpagesize(); char* ptr=(unsigned char *)((long)start_addr&(~(pagesize-1))); return mprotect( ptr ,pagesize , PROT_READ|PROT_WRITE|PROT_EXEC); }编译so文件
gcc -fPIC -shared -o myhook.so myhook.c -ldl -g
加上-g,方便以后gdb调试
LD_PRELOAD启动原程序并提前加载myhook.so文件
LD_PRELOAD=./myhook.so ./原文件gdb中看so的注入情况
用gdb调试程序
gdb attach pid
(gdb) info shared
From To Syms Read Shared Object Library
0x00007f28329b4de0 0x00007f28329b57b8 Yes ./myhook.so
0x000000337700dba0 0x0000003377042d78 Yes (*) /usr/lib64/libcurl.so.4
0x0000003810002120 0x000000381000d3a8 Yes (*) /lib64/libz.so.1
0x000000380ec00de0 0x000000380ec01998 Yes (*) /lib64/libdl.so.2
0x000000380f802140 0x000000380f8054f8 Yes (*) /lib64/librt.so.1
0x000000380f405660 0x000000380f410eb8 Yes (*) /lib64/libpthread.so.0
0x0000003f77c563f0 0x0000003f77cc3376 Yes (*) /usr/lib64/libstdc++.so.6
0x000000380fc03e70 0x000000380fc43f48 Yes (*) /lib64/libm.so.6
0x0000003f77802910 0x0000003f77812f78 Yes (*) /lib64/libgcc_s.so.1
0x000000380f01ea20 0x000000380f13f76c Yes (*) /lib64/libc.so.6
0x000000380e800b00 0x000000380e8198db Yes (*) /lib64/ld-linux-x86-64.so.2
0x0000003811002f00 0x0000003811007418 Yes (*) /lib64/libidn.so.11
0x000000381280e750 0x000000381283b408 Yes (*) /lib64/libldap-2.4.so.2
0x000000381dc0ac30 0x000000381dc38728 Yes (*) /lib64/libgssapi_krb5.so.2
0x000000381c41b430 0x000000381c494a78 Yes (*) /lib64/libkrb5.so.3
0x000000381c0043d0 0x000000381c01d5a8 Yes (*) /lib64/libk5crypto.so.3
0x000000381d000ee0 0x000000381d001db8 Yes (*) /lib64/libplds4.so
0x000000381d401410 0x000000381d402b48 Yes (*) /lib64/libplc4.so
0x000000381cc0d210 0x000000381cc2cdd8 Yes (*) /lib64/libnspr4.so
0x000000381b4046e0 0x000000381b414578 Yes (*) /usr/lib64/libsasl2.so.2
0x000000381bc02a40 0x000000381bc080c8 Yes (*) /lib64/libkrb5support.so.0
0x000000381b000bf0 0x000000381b0011d8 Yes (*) /lib64/libkeyutils.so.1
0x0000003376818200 0x00000033768538d8 Yes (*) /usr/lib64/libssl.so.10
0x0000003376469d80 0x000000337655ef98 Yes (*) /usr/lib64/libcrypto.so.10
0x0000003817400c00 0x00000038174059a8 Yes (*) /lib64/libcrypt.so.1
0x0000003810805850 0x0000003810815cc8 Yes (*) /lib64/libselinux.so.1
0x00000038170036c0 0x000000381704a868 Yes (*) /lib64/libfreebl3.so
0x00007f28316b61f0 0x00007f28316be648 Yes (*) /lib64/libnss_files.so.2
0x00007f28314af000 0x00007f28314b2328 Yes (*) /lib64/libnss_dns.so.2
调试在strcasecmp处调试
这个地方是系统内部,帮我们做的处理,我们无需关心,如果有兴趣的朋友可以看linux源码
在patch后代码的地方调试
#p#分页标题#e# .text:XXXXXXXXXXXXXXXX 00 00 00 00 00 00 mov [rsp+338h+var_330], esi .text:XXXXXXXXXXXXXXXX 00 00 00 00 00 00 mov qword ptr [rsp+338h+var_338], rax ; int .text:XXXXXXXXXXXXXXXX 00 00 00 00 00 00 mov rsi, rbp ; char * .text:XXXXXXXXXXXXXXXX 00 00 00 00 00 00 call decode_data ; 解密字符串 .text:XXXXXXXXXXXXXXXX 00 00 00 00 00 00 jmp cs:qword_4526BC ==>调用myhook.so中的函数 .text:XXXXXXXXXXXXXXXX 00 00 00 00 00 00 qword_4526BC dq 7F28329B5200h .text:XXXXXXXXXXXXXXXX 0F 84 44 02 00 00 nop(6)测试效果
完全实现了需求,软件名字,我还是隐去了,大家都明白。
如何预防1. 对mprotect进行anti,让mprotect失去作用。
对代码段的属性进行检测,看有没有被修改为可写模式。
通过hook mprotect的方式,静止对本程序进行使用mprotect。
2. 在加密/解密的代码段,进行保护,多次检查,防止被修改或注入。
call前后的代码是不是被非法篡改。
最好对加密解密后的字符串,进行一次或多次检测。
3. 调用系统函数的时候,自己指定路径去调用。
可以效仿这段代码,最好能定义成一个宏,或者模板,方便调用
static void *handle = NULL;
static STRCASECMP old_strcasecmp = NULL;
if (!handle)
{
handle = dlopen ("libc.so.6",RTLD_LAZY);
old_strcasecmp = (STRCASECMP)dlsym (handle,"strcasecmp");