天天看點

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);
    }
}      

繼續閱讀