天天看点

Linux Hook方法

linux hook是一个非常常见且成熟的技术,用户态Hook的方法有很多中,本次主要记录下LD_PRELOAD动态链接库劫持方法。

LD_PRELOAD是一个全局变量,linux程序在运行时会优先加载该路径变量下的动态链接库,因此我们只需要通过设置LD_PRELOAD加载我们编写的同名函数,后续的加载过程中就会忽略后面的同名函数,从而完成对系统库的劫持。

一般情况下linux动态加载库的顺序为LD_PRELOAD>LD_LIBRARY_PATH>/etc/ld.so.cache>/lib>/usr/lib

glibc是为GUN操作系统开发的一个C标准库,动态位于libc.so.6,其中的函数也是我们常用来Hook的对象。

下面以劫持connect为例,实现对连接端口的伪造。

正常的连接程序main.c:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
​
int main(){
    int fd;
    struct sockaddr_in remote_addr;
    remote_addr.sin_family = AF_INET;
    remote_addr.sin_port = htons(1234);
    remote_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    fd = socket(AF_INET,SOCK_STREAM,0);
    if (connect(fd,(struct sockaddr*)&remote_addr,sizeof(remote_addr))){
        exit(0);
    }
    
    close(fd);
}      

劫持connect函数的hook.c代码如下:

​
#include <stdio.h>
#include <netinet/in.h>
#include <string.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
​
// 声明一个connect函数指针
typedef int (*CONNECT)(int, const struct sockaddr *, socklen_t);
 
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
{
    static void *handle = NULL;
    static CONNECT old_con = NULL;
    handle = dlopen("libc.so.6", RTLD_LAZY);
    old_con = (CONNECT)dlsym(handle, "connect");
    struct sockaddr_in *tmp = (struct sockaddr_in *)addr;
    tmp->sin_port = htons(1235);
​
    printf("connect function invoked, connect ip : %s \n ",inet_ntoa(tmp->sin_addr));
    printf("connect port changes to %d\n",ntohs(tmp->sin_port));
    return old_con(sockfd, (struct sockaddr *)tmp , addrlen);
}      

这样就实现了对目的连接端口的篡改。

看一下效果:

[[email protected] libchook]# gcc main.c  -o main
[[email protected] libchook]# gcc -fPIC -shared -o hook.so hook.c
[[email protected] libchook]# LD_PRELOAD=./hook.so ./main
connect function invoked, connect ip : 127.0.0.1 
 connect port changes to 1235      

ptrace大法

但是现在基于golang的程序越来越多,而大多数golang程序都是静态编译的,会将静态链接库封装到二进制文件中,因此动态HOOK具有很大的局限性,为了解决该问题,一般可以采用内核级Hook,如果直接进行内核编程,代码写起来太费劲,因此选用神奇ptrace大法,既能服用线程的用户态代码,也能实现内核级别的Hook,很是舒服。

ptrace是一种系统调用,函数声明如下:

      #include <sys/ptrace.h>
       long ptrace(enum __ptrace_request request, pid_t pid,
                   void *addr, void *data);      

(1) request : 请求ptrace执行的操作

(2) pid: 目标进程的ID

(3) addr: 目标进程的地址

(4) data: 不同操作下写入或读取的操作数据

执行的操作有如下几种:

Linux Hook方法

贴一个经典的ptrace示例:

#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/reg.h>   /* For constants ORIG_RAX etc */
#include <sys/user.h>
#include <sys/syscall.h> /* SYS_write */
#include <stdio.h>
#include <unistd.h>
int main() {
    pid_t child;
    long orig_rax;
    int status;
    int iscalling = 0;
    struct user_regs_struct regs;
​
    child = fork();
    if(child == 0) {
        ptrace(PTRACE_TRACEME, 0, NULL, NULL);
        execl("/bin/ls", "ls",NULL);
    } else {
        while(1) {
            wait(&status);
            if(WIFEXITED(status))
                break;
            orig_rax = ptrace(PTRACE_PEEKUSER,
                              child, 8 * ORIG_RAX,
                              NULL); // 获取当前系统调用值
            if(orig_rax == SYS_write) {
                ptrace(PTRACE_GETREGS, child, NULL, &regs); // 获取参数并放置寄存器结构体regs中
                if(!iscalling) {
                    iscalling = 1; // 调用前获取初始化参数
                    printf("SYS_write call with %lld, %lld, %lld\n",
                            regs.rdi, regs.rsi, regs.rdx);
                }
            }
            ptrace(PTRACE_SYSCALL, child, NULL, NULL);
        }
    }
    return 0;
}      

fork后获取进程pid,pid为0表示主进程,否则为子进程,主进程中设置PTRACE_TRACEME标志进行探测,子进程中获取进行ptrace跟踪与修改等操作。

读取内存数据以及写入数据可以参考下面代码:

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; // 用union方便数据读取与判断
    i = 0;
    j = len / long_size; // 每次获取long_size
    laddr = str;
    while(i < j) {
        data.val = ptrace(PTRACE_PEEKDATA,
                          child, addr + i * 8,
                          NULL);
        if(data.val == -1)
            if(errno) {
                printf("READ error: %s\n", strerror(errno));
            }
        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 * 8,
                          NULL);
        memcpy(laddr, data.chars, j);
    }
    str[len] = '\0';
}
​
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;
    i =0;
    j = len / long_size;
    laddr = str;
    while(i < j) {
        memcpy(data.chars, laddr, long_size);
        ptrace(PTRACE_POKEDATA, child,
            addr + i *8, data.val);
            ++i;
        laddr += long_size;
    }
    j = len % long_size;
    if(j != 0) {
        memcpy(data.chars, laddr, j);
        ptrace(PTRACE_POKEDATA, child,
        addr + i *8, data.val);
    }
}      

继续阅读