剑客
关注科技互联网

如何进行Linux平台共享库替换

*本文原创作者:gaearrow,本文属FreeBuf原创奖励计划,未经许可禁止转载 

共享库基础知识

程序由源代码变成可执行文件,一般可以分解为四个步骤,分别是预处理(Prepressing)、编译(Compilation)、汇编(Assembly)和链接(Linking)。预处理过程主要处理源代码中以“#”开始的预编译指令;编译过程把预处理完成的文件进行词法、语法、语义等分析并产生相应的汇编代码文件;汇编过程将汇编代码文件翻译成机器可以执行的目标文件;链接过程将汇编生成的目标文件集合相连接并生成最终的可执行文件。

链接方式分为静态链接和动态链接,静态链接分发程序只需要生成的可执行文件,动态链接分发程序不仅需要可执行文件,还要包含相应的库文件。该库文件在Windows平台称为动态链接库(Dynamic-Link Library,DLL),在Linux平台一般称为共享库(Shared Object,SO)。

Linux平台SO替换可以分为静态替换和动态替换:静态替换利用文件操作直接替换SO,新SO在下次加载时生效;动态替换利用代码注入替换目标进程内存空间,实现新SO的加载和替换,新SO立即生效。

静态替换

针对未被加载的SO,利用复制命令(cp new.so old.so)即可直接完成静态替换,新SO在下次加载时生效。对于已经加载的原SO,直接用新SO复制替换将会导致相应程序崩溃,此种情况可以使用删除原SO(rm -f old.so)或修改原SO名称(mv old.so oldx.so)后,再复制新SO的方法代替,新SO同样在下次加载时生效。

程序崩溃的原因是复制替换操作会破坏系统访问原SO的索引节点inode,导致系统找不到原SO。系统为每个加载到内存中的文件创建对应的inode,用来管理该文件,inode包含了文件的元信息,如文件字节数、拥有者ID、读写执行权限等。系统以inode标识程 序加载的SO,不再关心文件名,修改SO名称并未改变对应inode,因此程序可以继续正常运行;删除SO只是无法查看,系统直到程序释放SO后才真正删除SO和inode,因此程序也可以继续正常运行;但是在直接复制替换时,新SO将会继承原SO的inode,程序无法继续访问原SO,从而导致程序崩溃。

动态替换

针对已经被程序加载的SO,为了实现不停止程序,替换后的SO立即生效的目的,可以采用动态替换。

动态替换的对象既可以是SO整体,也可以是SO中的特定函数。两者的区别主要是整体替换需要在特定函数替换的基础上再增加SO加载及输出函数重定位等过程。由于时间有限, 本文仅介绍特定函数动态替换的基本原理和初步实现,SO整体替换感兴趣的 读者可以自行尝试。

SO特定函数动态替换主要包括三个关键过程:控制目标进程,构造替换内容和确定替换地址,实际上依次解决的就是利用什么替换、替换什么内容和替换到哪里的问题。

控制目标进程

为实现对其它进程空间或运行进行控制, Linux平台提供了进程跟踪函数Ptrace()(类似于Windows平台的创建远程线程函数CreateRemoteThread())。

Ptrace()函数引用头文件和参数形式如下:

#include<sys/ptrace.h>
int ptrace(int request,int pid,int addr,int data);

其中,request参数决定了该函数的调用功能。

PTRACE_ATTACH/ PTRACE_DETACH
int ptrace(PTRACE_ATTACH,Pid,NULL,NULL);
int ptrace(PTRACE_DETACH,Pid,NULL,NULL);

分别实现跟踪和结束跟踪目标进程功能。Pid表示目标进程标识符。成功跟踪后,被跟踪进程将成为当前进程的子进程,并进入中止状态。

PTRACE_POKEDATA
int ptrace(PTRACE_?POKEDATA,Pid,Addr,Data);

实现向目标进程内存中写入一个字节数据功能。Pid表示目标进程标识符,Addr存储写入的内存地址,Data为要写入的数据。

除了以上本文中用到的功能,Ptrace()函数还提供数据读取(PTRACE_PEEKDATA)、终止进程(PTRACE_KILL)和重新运行(PTRACE_CONT)等功能,针对Intel386平台还提供读取和设置寄存器等功能。

如何进行Linux平台共享库替换

图 1特定函数替换主要过程活动图

SO特定函数动态替换主要过程活动图如图1所示,其中跟踪和结束跟踪目标进程过程由Ptrace()函数直接完成,替换目标进程内存过程由Ptrace()配合构造的替换内容共同完成。

构造替换内容

构造替换内容主要包括两方面工作,一是分析被替换函数的特征,确定替换空间结构和堆栈恢复指令;二是完成替换函数的编译、Shellcode提取及再构造。

被替换函数特征分析

测试被替换函数hello()存在于SO的libfso.so中,由主程序main载入并以固定周期循环调用,其反汇编代码如图2所示。由图可知,方框标识的代码为堆栈平衡和函数返回指令,函数实际执行部分为地址hello+8至hello+60之间。因此包含堆栈恢复和函数返回指令(2字节)的最大可替换空间地址为hello+8至hello+61,共计54个字节。

如何进行Linux平台共享库替换 ?

图 2测试被替换函数反汇编代码

替换函数编译再构造

测试替换函数汇编代码如图3所示,功能为输出Hello World!字符,其中方框标识的空指令是为堆栈恢复和函数返回指令预留的存储空间。

经过编译提取后可以得到46个字节的Shellcode,为了避免程序误将helloworld字符理解成指令执行导致程序崩溃,还需要将原函数的堆栈恢复和函数返回指令拷贝至预留的空指令位置,提前返回函数。成功替换后的被替换函数结构如图4所示,其中小方格内为堆栈平衡和函数返回指令,大方格内为构造的替换内容。

如何进行Linux平台共享库替换

图 3测试替换函数汇编代码

如何进行Linux平台共享库替换

图 4成功替换后实际结构

确定替换地址

由于地址加载随机化ASLR(Address space layout randomization)的影响,SO加载到内存的基地址并不固定,但是函数相对于基地址的偏移地址是固定不变的。因此对被替换函数所在SO进行反汇编分析,可以确定被替换函数替换位置首地址,再配合linux提供的进程虚拟地址空间查看命令得到SO加载基地址,简单相减即可得到被替换函数位置相对偏移地址。待准备实施替换时,只需再次利用进程虚拟地址空间查看命令获得基地址加上相对地址就可以确定实际替换首地址。

图5中地址0x2b6901e0f000为测试程序SO加载基地址,图6中0x2b6901e0f514为函数替换位置首地址,相减得到0×514,即为被替换函数替换起始位置的相对偏移地址。

如何进行Linux平台共享库替换

图 5共享库加载基地址

如何进行Linux平台共享库替换 ?

图 6替换位置首地址

测试实例

测试环境

CentOS 6.6 (Final)Linux version 2.6.32

gcc version 4.4.7 20120313

nasm version 2.07

前文截图为64位系统情况,以下为32位系统情况。

被动态替换程序

【动态链接库】

//fso.c
//gcc -c -Wall -Werror -fPIC fso.c
//gcc -shared -o libfso.so fso.o

#include <stdio.h>

void hello()
{
    int i = 0;
    int j = 0;
    printf("Hello Myboy!/n");
    for(i=0; i<10000000; i++)
    {
        j++;
    }
}

以上代码中的for循环,仅仅是为了确保Shellcode在替换时具有足够的存储空间。

【被动态替换程序】

//main.c
//gcc -L /home/mycos/so -Wall -o main main.c -lfso
//export LD_LIBRARY_PATH=/home/mycos/so :$LD_LIBRARY_PATH

#include <stdio.h>

extern void hello(void);

int main()
{
    int i = 0;
    printf("This is Main!/n");
    while(1)
    {
        if(i%10 == 0) printf("/n");
        sleep(1);
        hello();
        i++;
    }   
    return 0;
}

详细信息或出现错误请参见 加载动态链接库so程序简单实例

Shellcode

【hello.asm】

; 32-bit "Hello World!" in CentOS 6 i686
; nasm -felf32 hello.asm -o hello.o
; ld -s -o hello hello.o

global _start

_start:
    jmp string

code:

    pop     ecx
    mov     eax, 0x4
    mov     ebx, 0x1
    mov     edx, 0xD
    int     0x80

    nop
    nop
    nop
    nop
    nop
    nop
    nop
    nop

string:
    call    code
    db 'Hello world!',0x0a

以上空指令nop,是为堆栈恢复和函数返回指令预留的存储空间,预留空指令空间必须比被替换程序动态库堆栈恢复和函数返回指令占用的空间大。

【提取Shellcode】

for i in $(objdump -d hello |grep "^ " |cut -f2); do echo -n '/x'$i; done; echo

执行以上命令得到如下Shellcode,共计49个字节,但是该Shellcode还不能直接使用,需要根据目标程序动态链接库实际情况进行修改。

/xe9/x1a/x00/x00/x00/x59/xb8/x04/x00/x00/x00/xbb/x01/x00/x00/x00/xba/x0d/x00/x00/x00/xcd/x80/x90/x90/x90/x90/x90/x90/x90/x90/xe8/xdf/xff/xff/xff/x48/x65/x6c/x6c/x6f/x20/x77/x6f/x72/x6c/x64/x21/x0a

【修改Shellcode】

利用GDB调试被替换程序,得到动态库中函数堆栈恢复和函数返回的代码。

[mycos@localhost so]$ gdb main

(gdb) b hello
Breakpoint 1 at 0x804842c

(gdb) r
Starting program: /home/mycos/so/main 
This is Main!

Breakpoint 1, 0x00111434 in hello () from /home/mycos/so/libfso.so

(gdb) disas hello
Dump of assembler code for function hello:
   0x00111430 <+0>:     push   %ebp
   0x00111431 <+1>:     mov    %esp,%ebp
   0x00111433 <+3>:     push   %ebx
=> 0x00111434 <+4>:     sub    $0x24,%esp
   0x00111437 <+7>:     call   0x111429 <__i686.get_pc_thunk.bx>
   0x0011143c <+12>:    add    $0x1190,%ebx
   0x00111442 <+18>:    movl   $0x0,-0x10(%ebp)
   0x00111449 <+25>:    movl   $0x0,-0xc(%ebp)
   0x00111450 <+32>:    lea    -0x10f8(%ebx),%eax
   0x00111456 <+38>:    mov    %eax,(%esp)
   0x00111459 <+41>:    call   0x111334 <puts@plt>
   0x0011145e <+46>:    movl   $0x0,-0x10(%ebp)
   0x00111465 <+53>:    jmp    0x11146f <hello+63>
   0x00111467 <+55>:    addl   $0x1,-0xc(%ebp)
   0x0011146b <+59>:    addl   $0x1,-0x10(%ebp)
   0x0011146f <+63>:    cmpl   $0x98967f,-0x10(%ebp)
   0x00111476 <+70>:    jle    0x111467 <hello+55>
   0x00111478 <+72>:    add    $0x24,%esp
   0x0011147b <+75>:    pop    %ebx
   0x0011147c <+76>:    pop    %ebp
   0x0011147d <+77>:    ret    
End of assembler dump.
(gdb) x /8xb 0x111478
0x111478 <hello+72>:    0x83    0xc4    0x24    0x5b    0x5d    0xc3    0x90    0x90

分析动态库hello函数的汇编代码,可以较容易的判断出恢复堆栈和函数返回的地址从0×00111478到0x0011147d共6个字节,且该函数实际可被shellcode覆盖填充的部分从0×00111437(函数开始的堆栈平衡处理部分不能覆盖,易导致异常)到0x0011147d共70个字节大于shellcode的49个字节,满足替换空间要求。因此最后得到的Shellcode如下:

/xe9/x1a/x00/x00/x00/x59/xb8/x04/x00/x00/x00/xbb/x01/x00/x00/x00/xba/x0d/x00/x00/x00/xcd/x80/x83/xc4/x24/x5b/x5d/xc3/x90/x90/xe8/xdf/xff/xff/xff/x48/x65/x6c/x6c/x6f/x20/x77/x6f/x72/x6c/x64/x21/x0a

查看被替换程序的内存分配情况,得到动态链接库加载基址(0×00111000),结合上面GDB调试得到的函数替换起始地址(0×00111437),可以确定替换地址的固定偏移量为0×437,因此只要替换程序确定动态链接库基址,即可利用固定偏移量得到替换起始地址。

[mycos@localhost asm]$ ps aux | grep main
mycos    13626  0.0  1.3  22292 14376 pts/2    S+   00:17   0:00 gdb main
mycos    13628  0.0  0.0   1872   344 pts/2    T    00:17   0:00 /home/mycos/so/main
mycos    13793  0.0  0.0   4352   724 pts/3    S+   01:08   0:00 grep main

[mycos@localhost asm]$ cat /proc/13628/maps
00110000-00111000 r-xp 00000000 00:00 0          [vdso]
00111000-00112000 r-xp 00000000 08:02 1054157    /home/mycos/so/libfso.so
00112000-00113000 rw-p 00000000 08:02 1054157    /home/mycos/so/libfso.so
008ed000-0090b000 r-xp 00000000 08:02 1053999    /lib/ld-2.12.so
0090b000-0090c000 r--p 0001d000 08:02 1053999    /lib/ld-2.12.so
0090c000-0090d000 rw-p 0001e000 08:02 1053999    /lib/ld-2.12.so
00913000-00aa3000 r-xp 00000000 08:02 1054095    /lib/libc-2.12.so
00aa3000-00aa5000 r--p 00190000 08:02 1054095    /lib/libc-2.12.so
00aa5000-00aa6000 rw-p 00192000 08:02 1054095    /lib/libc-2.12.so
00aa6000-00aa9000 rw-p 00000000 00:00 0 
08048000-08049000 r-xp 00000000 08:02 1054152    /home/mycos/so/main
08049000-0804a000 rw-p 00000000 08:02 1054152    /home/mycos/so/main
b7ff1000-b7ff2000 rw-p 00000000 00:00 0 
b7ffe000-b8000000 rw-p 00000000 00:00 0 
bffeb000-c0000000 rw-p 00000000 00:00 0          [stack]

动态替换程序

//injectso.c
//gcc injectso.c -o injectso
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <sys/file.h>
#include <stdio.h>
#include <string.h>

const int long_size = sizeof(long);

void getdata(pid_t child, long addr, char *str, int len)
{
    char *laddr;
    int i,j;
    union u{
        long val;
        char chars[long_size];
    }data;

    i = 0;
    j = len / long_size;
    laddr = str;
    while(i < j){
        data.val = ptrace(PTRACE_PEEKDATA, child, addr + i*4, NULL);
        if (data.val < 0) {
            printf("getdata1 Failed! /n");
            return;
        }

        memcpy(laddr, data.chars, long_size);
        ++i;
        laddr += long_size;
    }
    j = len % long_size;
    if(j != 0){
        data.val = ptrace(PTRACE_PEEKDATA, child, addr + i*4, NULL);
        if (data.val < 0) {
            printf("getdata2 Failed! /n");
            return;
        }
        memcpy(laddr, data.chars, j);
    }
    str[len] = ' ';
}

void putdata(pid_t child, long addr, char *str, int len)
{
    char *laddr;
    int i,j;
    union u{
        long val;
        char chars[long_size];
    }data;

    long rst; 

    i = 0;
    j = len / long_size;
    laddr = str;
    while(i < j){
        memcpy(data.chars, laddr, long_size);
        rst = ptrace(PTRACE_POKEDATA, child, addr + i*4, data.val);
        if (rst < 0) {
            printf("Putdata1 Failed! /n");
            return;
        }
        ++i;
        laddr += long_size;
    }
    j = len % long_size;
    if(j != 0){
        memcpy(data.chars, laddr, j);
        rst = ptrace(PTRACE_POKEDATA, child, addr + i*4, data.val);
        if (rst < 0) {
            printf("Putdata2 Failed! /n");
            return;
        }
    }
}

long getsobaseaddr(pid_t pid, char* soname)
{
    FILE *fp;
    char filename[30];
    char line[100];
    long addr;
    char str[100];
    sprintf(filename, "/proc/%d/maps", pid);
    fp = fopen(filename, "r");
    if(fp == NULL)
        return 1;
    while(fgets(line, 100, fp) != NULL) {
        sscanf(line, "%x-%*s %*s %*s %*s %*s %s", &addr, 
               str, str, str, str, str, str);
        if(strstr(str, soname) != NULL)
            break;
    }
    fclose(fp);
    return addr + 0x437; //offset
}

int main(int argc, char *argv[])
{
    pid_t traced_process;
    struct user_regs_struct regs;
    int len = 49;

    /* hello world */
    char code[] =
        "/xe9/x1a/x00/x00/x00/x59/xb8/x04"
        "/x00/x00/x00/xbb/x01/x00/x00/x00"
        "/xba/x0d/x00/x00/x00/xcd/x80/x83"
        "/xc4/x24/x5b/x5d/xc3/x90/x90/xe8"
        "/xe1/xff/xff/xff/x48/x65/x6c/x6c"
        "/x6f/x20/x77/x6f/x72/x6c/x64/x21"
        "/x0a";

    if(argc != 2) {
        printf("PID?/n");
        return 1;
    }

    traced_process = atoi(argv[1]);
    ptrace(PTRACE_ATTACH, traced_process, NULL, NULL);
    int pid = wait(NULL);
    printf("Attach Pid: %d/n",pid);
    ptrace(PTRACE_GETREGS, traced_process, NULL, &regs);

    long helloaddr = getsobaseaddr(traced_process, "libfso.so");
    printf("Inject So Addr: %p/n", helloaddr);
    putdata(traced_process, helloaddr, code, len);

    ptrace(PTRACE_SETREGS, traced_process, NULL, &regs);
    ptrace(PTRACE_DETACH, traced_process, NULL, NULL);
    return 0;
}

在x86_64平台下,以上代码需要进行适当修改:一是头文件由 sys/user.h 改成 linux/user.h;二是getdata/putdata函数中涉及地址的位置由 4 改成 8;三是user_regs_struct结构体的指令寄存器由 eip 改成 rip;四是shellcode修改适用于x86_64平台。

执行结果

执行动态替换程序,被替换程序输出由Hello Myboy! 变成 Hello world! 且继续正常运行,说明替换已成功。

[mycos@localhost so]$ ps aux | grep main
mycos    13834 12.2  0.0   1872   384 pts/0    T    21:16   0:03 ./main
mycos    13293  0.0  0.0   4352   720 pts/1    S+   21:16   0:00 grep main
[mycos@localhost so]$ ./injectso 13834
[mycos@localhost so]$ ./main
This is Main!

Hello Myboy!
Hello Myboy!
Hello Myboy!
Hello Myboy!
Hello Myboy!
Hello Myboy!
Hello Myboy!
Hello Myboy!
Hello Myboy!
Hello Myboy!

Hello Myboy!
Hello Myboy!
Hello world!
Hello world!
Hello world!
Hello world!

参 考

http://www.linuxidc.com/Linux/2011-01/31622.htm

http://www.cnblogs.com/itech/archive/2012/05/15/2502284.html

http://blog.csdn.net/myarrow/article/details/9630377

http://blog.csdn.net/tju355/article/details/6884696

http://theantway.com/2013/01/notes-for-playing-with-ptrace-on-64-bits-ubuntu-12-10/

http://www.cnblogs.com/wangkangluo1/archive/2012/06/05/2535484.html

http://blog.sina.com.cn/s/blog_4ac74e9a0100n7w1.html

*本文原创作者:gaearrow,本文属FreeBuf原创奖励计划,未经许可禁止转载 

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址