精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

Linux系統調用Hook:如何給內核接口 “裝監控”?

系統 Linux
在編寫 Hook 代碼時,要充分考慮內核版本和架構的兼容性??梢酝ㄟ^宏定義等方式,根據不同的內核版本和架構進行不同的代碼處理。同時,關注內核的更新和變化,及時調整 Hook 代碼。

在 Linux 系統的運行脈絡中,系統調用是用戶程序與內核交互的核心通道。但你是否想過,如何實時追蹤這些交互?如何在不破壞原有邏輯的前提下,給內核接口裝上 “監控”?這正是 Linux 系統調用 Hook 技術的魅力所在。它像一把精巧的 “鉤子”,能悄無聲息地附著在系統調用的關鍵節點上,既不阻斷正常的調用流程,又能精準捕獲每一次交互細節 —— 從參數傳遞到返回值生成,從調用頻率到異常行為。

無論是開發調試時追蹤程序行為,還是安全防護中監測惡意調用,甚至是性能優化時分析資源消耗,這種 “監控” 能力都不可或缺。然而,給內核接口裝 “監控” 絕非易事。內核空間的嚴格權限控制、不同版本的兼容性差異、以及誤操作可能引發的系統崩潰風險,都讓這項技術充滿挑戰。接下來,我們就揭開 Linux 系統調用 Hook 的神秘面紗,看看它如何實現對內核接口的 “隱形監控”。

一、Linux系統調用簡介

系統調用(syscall)是一個通用的概念,它既包括應用層系統函數庫的調用,也包括ring0層系統提供的syscall_table提供的系統api。

1.1系統調用的概念

系統調用是操作系統內核提供給用戶空間應用程序使用的接口。當應用程序需要訪問硬件資源(如磁盤、網絡)、創建進程、分配內存等操作時,就會通過系統調用陷入內核態,由內核來完成這些任務。例如,常見的文件讀寫函數read和write,在底層實際上就是通過系統調用實現的。

1.2系統調用的實現機制

在 Linux 中,系統調用通過軟件中斷實現。以 x86 架構為例,應用程序執行int 0x80指令(在較新的內核中,也使用sysenter指令),觸發一個軟件中斷,CPU 會切換到內核態,然后根據系統調用號在系統調用表中找到對應的內核函數進行執行。系統調用表是一個存儲了所有系統調用函數指針的數組,每個系統調用都有唯一的編號,通過這個編號可以快速定位到相應的處理函數。

我們必須要明白,Hook技術是一個相對較寬的話題,因為操作系統從ring3到ring0是分層次的結構,在每一個層次上都可以進行相應的Hook,它們使用的技術方法以及取得的效果也是不盡相同的。本文的主題是"系統調用的Hook學習","系統調用的Hook"是我們的目的,而要實現這個目的可以有很多方法,本文試圖盡量覆蓋從ring3到ring0中所涉及到的Hook技術,來實現系統調用的監控功能。

二、Hook技術詳解

2.1Ring3中Hook技術

⑴LD_PRELOAD動態連接.so函數劫持

LD_PRELOAD hook技術屬于so依賴劫持技術的一種實現,所以要討論這種技術的技術原理,我們先來看一下linux操作系統加載so的底層原理。

括Linux系統在內的很多開源系統都是基于Glibc的,動態鏈接的ELF可執行文件在啟動時同時會啟動動態鏈接器(/lib/ld-linux.so.X),程序所依賴的共享對象全部由動態鏈接器負責裝載和初始化,所以這里所謂的共享庫的查找過程,本質上就是動態鏈接器(/lib/ld-linux.so.X)對共享庫路徑的搜索過程,搜索過程如下:

/etc/ld.so.cache:Linux為了加速LD_PRELOAD的搜索過程,在系統中建立了一個ldconfig程序,這個程序負責

  • 將共享庫下的各個共享庫維護一個SO-NAME(一一對應的符號鏈接),這樣每個共享庫的SO-NAME就能夠指向正確的共享庫文件
  • 將全部SO-NAME收集起來,集中放到/etc/ld.so.cache文件里面,并建立一個SO-NAME的緩存
  • 當動態鏈接器要查找共享庫時,它可以直接從/etc/ld.so.cache里面查找。所以,如果我們在系統指定的共享庫目錄下添加、刪除或更新任何一個共享庫,或者我們更改了/etc/ld.so.conf、/etc/ld.preload的配置,都應該運行一次ldconfig這個程序,以便更新SO-NAME和/etc/ld.so.cache。很多軟件包的安裝程序在結束共享庫安裝以后都會調用ldconfig

根據/etc/ld.so.preload中的配置進行搜索(LD_PRELOAD):這個配置文件中保存了需要搜索的共享庫路徑,Linux動態共享庫加載器根據順序進行逐行廣度搜索

根據環境變量LD_LIBRARY_PATH指定的動態庫搜索路徑:

根據ELF文件中的配置信息:任何一個動態鏈接的模塊所依賴的模塊路徑保存在".dynamic"段中,由DT_NEED類型的項表示,動態鏈接器會按照這個路徑去查找DT_RPATH所指定的路徑,編譯目標代碼時,可以對gcc加入鏈接參數"-Wl,-rpath"指定動態庫搜索路徑。

  • DT_NEED段中保存的是絕對路徑,則動態鏈接器直接按照這個路徑進行直接加載
  • DT_NEED段中保存的是相對路徑,動態鏈接器會在按照一個約定的順序進行庫文件查找下列路徑:/lib、/usr/lib、/etc/ld.so.conf中配置指定的搜索路徑

可以看到,LD_PRELOAD是Linux系統中啟動新進程首先要加載so的搜索路徑,所以它可以影響程序的運行時的鏈接(Runtime linker),它允許你定義在程序運行前"優先加載"的動態鏈接庫。

我們只要在通過LD_PRELOAD加載的.so中編寫我們需要hook的同名函數,根據Linux對外部動態共享庫的符號引入全局符號表的處理,后引入的符號會被省略,即系統原始的.so(/lib64/libc.so.6)中的符號會被省略。

通過strace program也可以看到,Linux是優先加載LD_PRELOAD指明的.so,然后再加載系統默認的.so的:

圖片圖片

⑵通過自寫.so文件劫持LD_PRELOAD

①demo例子

正常程序main.c:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    if( strcmp(argv[1], "test") )
    {
        printf("Incorrect password\n");
    }
    else
    {
        printf("Correct password\n");
    }
    return 0;
}

用于劫持函數的.so代碼hook.c

#include <stdio.h>
#include <string.h>
#include <dlfcn.h>
/*
hook的目標是strcmp,所以typedef了一個STRCMP函數指針
hook的目的是要控制函數行為,從原庫libc.so.6中拿到strcmp指針,保存成old_strcmp以備調用
*/
typedef int(*STRCMP)(const char*, const char*);

int strcmp(const char *s1, const char *s2)
{
    static void *handle = NULL;
    static STRCMP old_strcmp = NULL;

    if( !handle )
    {
        handle = dlopen("libc.so.6", RTLD_LAZY);
        old_strcmp = (STRCMP)dlsym(handle, "strcmp");
    }
    printf("oops!!! hack function invoked. s1=<%s> s2=<%s>\n", s1, s2);
    return old_strcmp(s1, s2);
}

編譯:

gcc -o test main.c 
gcc -fPIC -shared -o hook.so hook.c -ldl

運行:

LD_PRELOAD=./hook.so ./test 123
②hook function注意事項

在編寫用于function hook的.so文件的時候,要考慮以下幾個因素

1. Hook函數的覆蓋完備性
對于Linux下的指令執行來說,有7個Glibc API都可是實現指令執行功能,對這些API對要進行Hook
/*
#include <unistd.h>
int execl(const char *pathname, const char *arg0, ... /* (char *)0 */ );
int execv(const char *pathname, char *const argv[]);
int execle(const char *pathname, const char *arg0, .../* (char *)0, char *const envp[] */ );
int execve(const char *pathname, char *const argv[], char *const envp[]);
int execlp(const char *filename, const char *arg0, ... /* (char *)0 */ );
int execvp(const char *filename, char *const argv[]);
int fexecve(int fd, char *const argv[], char *const envp[]);
http://www.2cto.com/os/201410/342362.html
*/

2. 當前系統中存在function hook的重名覆蓋問題
    1) /etc/ld.so.preload中填寫了多條.so加載條目
    2) 其他程序通過"export LD_PRELOAD=.."臨時指定了待加載so的路徑
在很多情況下,出于系統管理或者集群系統日志收集的目的,運維人員會向系統中注入.so文件,對特定function函數進行hook,這個時候,當我們注入的.so文件中的hook function和原有的hook function存在同名的情況,Linux會自動忽略之后載入了hook function,這種情況我們稱之為"共享對象全局符號介入"

3. 注入.so對特定function函數進行hook要保持原始業務的兼容性
典型的hook的做法應該是
hook_function()
{
    save ori_function_address;
    /*
    do something in here
    span some time delay
    */
    call ori_function;
}
hook函數在執行完自己的邏輯后,應該要及時調用被hook前的"原始函數",保持對原有業務邏輯的透明

4. 盡量減小hook函數對原有調用邏輯的延時
hook_function()
{
    save ori_function_address;
    /*
    do something in here
    span some time delay
    */
    call ori_function;
}
hook這個操作是一定會對原有的代碼調用執行邏輯產生延時的,我們需要盡量減少從函數入口到"call ori_function"這塊的代碼邏輯,讓代碼邏輯盡可能早的去"call ori_function"
在一些極端特殊的場景下,存在對單次API調用延時極其嚴格的情況,如果延時過長可能會導致原始業務邏輯代碼執行失敗

如果需要不僅僅是替換掉原有庫函數,而且還希望最終將函數邏輯傳遞到原有系統函數,實現透明hook(完成業務邏輯的同時不影響正常的系統行為)、維持調用鏈,那么需要用到RTLD_NEXT

當調用dlsym的時候傳入RTLD_NEXT參數,gcc的共享庫加載器會按照"裝載順序(load order)(即先來后到的順序)"獲取"下一個共享庫"中的符號地址
/*
Specifies the next object after this one that defines name. This one refers to the object containing the invocation of dlsym(). The next object is the one found upon the application of a load order symbol resolution algorithm (see dlopen()). The next object is either one of global scope (because it was introduced as part of the original process image or because it was added with a dlopen() operation including the RTLD_GLOBAL flag), or is an object that was included in the same dlopen() operation that loaded this one.
The RTLD_NEXT flag is useful to navigate an intentionally created hierarchy of multiply-defined symbols created through interposition. For example, if a program wished to create an implementation of malloc() that embedded some statistics gathering about memory allocations, such an implementation could use the real malloc() definition to perform the memory allocation-and itself only embed the necessary logic to implement the statistics gathering function.
http://pubs.opengroup.org/onlinepubs/009695399/functions/dlsym.html
http://www.newsmth.net/nForum/#!article/KernelTech/413
*/

code example

// used for getting the orginal exported function address
#if defined(RTLD_NEXT)
#  define REAL_LIBC RTLD_NEXT
#else
#  define REAL_LIBC ((void *) -1L)
#endif

//REAL_LIBC代表當前調用鏈中緊接著下一個共享庫,從調用方鏈接映射列表中的下一個關聯目標文件獲取符號
#define FN(ptr,type,name,args)  ptr = (type (*)args)dlsym (REAL_LIBC, name)

...
FN(func,int,"execve",(const char *, char **const, char **const));

我們知道,如果當前進程空間中已經存在某個同名的符號,則后載入的so的同名函數符號會被忽略,但是不影響so的載入,先后載入的so會形成一個鏈式的依賴關系,通過RTLD_NEXT可以遍歷

③SO功能代碼編寫

這個小節我們來完成一個基本的進程、網絡、模塊加載監控的小demo。

1. 指令執行
    1) execve
    2) execv
2. 網絡連接
    1) connect
3. LKM模塊加載
    1) init_module

hook.c

#include <stdio.h>
#include <string.h>
#include <dlfcn.h>

#include <stdlib.h>
#include <sys/types.h>  
#include <string.h>
#include <unistd.h>
#include <limits.h>

#include <netinet/in.h> 
#include <linux/ip.h>
#include <linux/tcp.h>

#if defined(RTLD_NEXT)
#  define REAL_LIBC RTLD_NEXT
#else
#  define REAL_LIBC ((void *) -1L)
#endif

#define FN(ptr, type, name, args)  ptr = (type (*)args)dlsym (REAL_LIBC, name)

int execve(const char *filename, char *const argv[], char *const envp[])
{
    static int (*func)(const char *, char **, char **);
    FN(func,int,"execve",(const char *, char **const, char **const)); 

    //print the log
    printf("filename: %s, argv[0]: %s, envp:%s\n", filename, argv[0], envp);

    return (*func) (filename, (char**) argv, (char **) envp);
} 

int execv(const char *filename, char *const argv[]) 
{
    static int (*func)(const char *, char **);
    FN(func,int,"execv", (const char *, char **const)); 

    //print the log
    printf("filename: %s, argv[0]: %s\n", filename, argv[0]);

    return (*func) (filename, (char **) argv);
}  

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) 
{ 
    static int (*func)(int, const struct sockaddr *, socklen_t);
    FN(func,int,"connect", (int, const struct sockaddr *, socklen_t)); 

    /*
    print the log
    獲取、打印參數信息的時候需要注意
    1. 加鎖
    2. 拷貝到本地棧區變量中
    3. 然后再打印
    調試的時候發現直接獲取打印會導致core dump
    */
    printf("socket connect hooked!!\n");

    //return (*func) (sockfd, (const struct sockaddr *) addr, (socklen_t)addrlen);
    return (*func) (sockfd, addr, addrlen);
}  

int init_module(void *module_image, unsigned long len, const char *param_values) 
{ 
    static int (*func)(void *, unsigned long, const char *);
    FN(func,int,"init_module",(void *, unsigned long, const char *)); 

    /*
    print the log
    lkm的加載不需要取參數,只需要捕獲事件本身即可
    */
    printf("lkm load hooked!!\n");

    return (*func) ((void *)module_image, (unsigned long)len, (const char *)param_values);
}

編譯,并裝載。

//編譯出一個so文件
gcc -fPIC -shared -o hook.so hook.c -ldl

添加LD_PRELOAD有很多種方式。

1. 臨時一次性添加(當條指令有效)
LD_PRELOAD=./hook.so nc www.baidu.com 80  
/*
LD_PRELOAD后面接的是具體的庫文件全路徑,可以連接多個路徑
程序加載時,LD_PRELOAD加載路徑優先級高于/etc/ld.so.preload
*/

2. 添加到環境變量LD_PRELOAD中(當前會話SESSION有效)
export LD_PRELOAD=/zhenghan/snoopylog/hook.so
//"/zhenghan/snoopylog/"是編譯.so文件的目錄
unset LD_PRELOAD

3. 添加到環境變量LD_LIBRARY_PATH中
假如現在需要在已有的環境變量上添加新的路徑名,則采用如下方式
LD_LIBRARY_PATH=/zhenghan/snoopylog/hook.so:$LD_LIBRARY_PATH.(newdirs是新的路徑串)
/*
LD_LIBRARY_PATH指定查找路徑,這個路徑優先級別高于系統預設的路徑
*/

4. 添加到系統配置文件中
vim /etc/ld.so.preload
add /zhenghan/snoopylog/hook.so

5. 添加到配置文件目錄中
cat /etc/ld.so.conf
//include ld.so.conf.d/*.conf

效果測試:

1. 指令執行
在代碼中手動調用: execve(argv[1], newargv, newenviron);

2. 網絡連接
執行: nc www.baidu.com 80

3. LKM模塊加載
編寫測試LKM模塊,執行: insmod hello.ko

在真實的環境中,socket的網絡連接存在大量的連接失敗,非阻塞等待等等情況,這些都會觸發connect的hook調用,對于connect的hook來說,我們需要對以下的事情進行過濾。

1. 區分IPv4、IPv6
根據connect參數中的(struct sockaddr *addr)->sa_family進行判斷

2. 區分執行成功、執行失敗
如果本次connect調用執行失敗,則不應該繼續進行參數獲取
int ret_code = (*func) (sockfd, addr, addrlen);
int tmp_errno = errno;
if (ret_code == -1 && tmp_errno != EINPROGRESS)
{
    return ret_code;
}

3. 區分TCP、UDP連接
對于TCP和UDP來說,它們都可以發起connect請求,我們需要從中過濾出TCP Connect請求
#include <sys/types.h>
#include <sys/socket.h>

int getsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);
/*
#include <sys/types.h>
#include <sys/socket.h>
main()
{
   int s;
   int optval;
   int optlen = sizeof(int);
   if((s = socket(AF_INET, SOCK_STREAM, 0)) < 0)
   perror("socket");
   getsockopt(s, SOL_SOCKET, SO_TYPE, &optval, &optlen);
   printf("optval = %d\n", optval);
   close(s);
}
*/
執行:
optval = 1 //SOCK_STREAM 的定義正是此值
④劫持效果測試

指令執行監控

execve.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
   char *newargv[] = { NULL, "hello", "world", NULL };
   char *newenviron[] = { NULL };

   if (argc != 2) 
   {
       fprintf(stderr, "Usage: %s <file-to-exec>\n", argv[0]);
       exit(EXIT_FAILURE);
   }

   newargv[0] = argv[1];

   execve(argv[1], newargv, newenviron);
   perror("execve");   /* execve() only returns on error */
   exit(EXIT_FAILURE);
}
//gcc -o execve execve.c

myecho.c

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    int j;

    for (j = 0; j < argc; j++)
        printf("argv[%d]: %s\n", j, argv[j]);

    exit(EXIT_SUCCESS);
}
//gcc -o myecho myecho.c

圖片圖片

可以看到,LD_PRELOAD在所有程序代碼庫加載前優先加載,對glibc中的導出函數進行了hook

網絡連接監控

圖片圖片

模塊加載監控:hello.c

#include <linux/module.h>    // included for all kernel modules
#include <linux/kernel.h>    // included for KERN_INFO
#include <linux/init.h>        // included for __init and __exit macros
#include <linux/cred.h>
#include <linux/sched.h>

static int __init hello_init(void)
{ 
    struct cred *currentCred;
    currentCred = current->cred;    
    printk(KERN_INFO "uid = %d\n", currentCred->uid);
    printk(KERN_INFO "gid = %d\n", currentCred->gid);
    printk(KERN_INFO "suid = %d\n", currentCred->suid);
    printk(KERN_INFO "sgid = %d\n", currentCred->sgid);
    printk(KERN_INFO "euid = %d\n", currentCred->euid);
    printk(KERN_INFO "egid = %d\n", currentCred->egid);  

    printk(KERN_INFO "Hello world!\n"); 
    return 0;    // Non-zero return means that the module couldn't be loaded.
}

static void __exit hello_cleanup(void)
{
    printk(KERN_INFO "Cleaning up module.\n");
}

module_init(hello_init);
module_exit(hello_cleanup);

Makefile

obj-m := hello.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

all:
    $(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
    $(MAKE) -C $(KDIR) M=$(PWD) clean

加載模塊:insmod hello.ko

圖片圖片

使用snoopy進行execve/execv、connect、init_module hook

snoopy會監控服務器上的命令執行,并記錄到syslog。

本質上,snoopy是利用ld_preload技術實現so依賴劫持的,只是它的工程化完善度更高,日志采集和日志整理傳輸這方面已經幫助我們完成了。

#cat /etc/ld.so.preload
/usr/local/snoopy/lib/snoopy.so

基于PD_PRELOAD、LD_LIBRARY_PATH環境變量劫持繞過Hook模塊

我們知道,snoopy監控服務器上的指令執行,是通過修改系統的共享庫預加載配置文件(/etc/ld.so.preload)實現,但是這種方式存在一個被黑客繞過的可能

圖片圖片

LD_PRELOAD的加載順序優先于/etc/ld.so.preload的配置項,黑客可以利用這點來強制覆蓋共享庫的加載順序

1. 強制指定LD_PRELOAD的環境變量    
export LD_PRELOAD=/lib64/libc.so.6
bash
/*
新啟動的bash終端默認會使用LD_PRELOAD的共享庫路徑
*/

2. LD_PRELOAD="/lib64/libc.so.6" bash 
/*
重新開啟一個加載了默認libc.so.6共享庫的bash session
因為對于libc.so.6來說,它沒有使用dlsym去動態獲取API Function調用鏈條的RTL_NEXT函數,即調用鏈是斷開的
*/

在這個新的Bash下執行的指令,因為都不會調用到snoopy的hook函數,所以也不會被記錄下來。

基于ptrace()調試技術進行API Hook

在Linux下,除了使用LD_PRELOAD這種被動Glibc API注入方式,還可以使用基于調試器(Debuger)思想的ptrace()主動注入方式,總體思路如下:

  1. 使用Linux Module、或者LSM掛載點對進程的啟動動作進行實時的監控,并通過Ring0-Ring3通信,通知到Ring3程序有新進程啟動的動作
  2. 用ptrace函數attach上目標進程
  3. 讓目標進程的執行流程跳轉到mmap函數來分配一小段內存空間
  4. 把一段機器碼拷貝到目標進程中剛分配的內存中去
  5. 最后讓目標進程的執行流程跳轉到注入的代碼執行

通過靜態編碼繞過LD_PRELOAD機制監控

通過靜態鏈接方式編譯so模塊:

gcc -o test test.c -static

在靜態鏈接的模式下,程序不會去搜索系統中的so文件(不同是系統默認的、還是第三方加入的),所以也就不會調用到Hook SO模塊。

通過內聯匯編的方式繞過LD_PRELOAD機制監控

使用內嵌匯編的形式直接通過syscall指令使用系統調用功能,同樣也不會調用到Glibc提供的API。

asm("movq $2, %%rax\n\t syscal:"=a"(ret));

2.2Ring0中Hook技術

傳統的kernel inline hook技術就是修改內核函數的opcode,通過寫入jmp或push ret等指令跳轉到新的內核函數中,從何達到劫持的目的。

  • 我們知道實現一個系統調用的函數中一定會遞歸的嵌套有很多的子函數,即它必定要調用它的下層函數。
  • 從匯編的角度來說,對一個子函數的調用是采用"段內相對短跳轉 jmp offset"來實現的,即CPU根據offset來進行一個偏移量的跳轉。如果我們把下層函數在上層函數中的offset替換成我們"Hook函數"的offset,這樣上層函數調用下層函數時,就會跳到我們的"Hook函數"中。
  • 我們就可以在"Hook函數"中做過濾和劫持內容的工作

以sys_read作為例子:

\linux-2.6.32.63\fs\read_write.c

asmlinkage ssize_t sys_read(unsigned int fd, char __user * buf, size_t count)
{
        struct file *file;
        ssize_t ret = -EBADF;
        int fput_needed;

        file = fget_light(fd, &fput_needed);
        if (file) 
    {
                loff_t pos = file_pos_read(file);
                ret = vfs_read(file, buf, count, &pos);
                file_pos_write(file, pos);
                fput_light(file, fput_needed);
        }

        return ret;
}
EXPORT_SYMBOL_GPL(sys_read);

在sys_read()中,調用了子函數vfs_read()來完成讀取數據的操作,在sys_read()中調用子函數vfs_read()的匯編命令是:

call 0xc106d75c <vfs_read>

等同于:

jmp offset(相對于sys_read()的基址偏移)

所以,我們的思路很明確,找到call 0xc106d75c <vfs_read>這條匯編,把其中的offset改成我們的Hook函數對應的offset,就可以實現劫持目的了

1. 搜索sys_read的opcode 2. 如果發現是call指令,根據call后面的offset計算要跳轉的地址是不是我們要hook的函數地址 1) 如果"不是"就重新計算Hook函數的offset,用Hook函數的offset替換原來的offset 2) 如果"已經是"Hook函數的offset,則說明函數已經處于被劫持狀態了,我們的Hook引擎應該直接忽略跳過,避免重復劫持

poc:

/*
參數:
1. handler是上層函數的地址,這里就是sys_read的地址
2. old_func是要替換的函數地址,這里就是vfs_read
3. new_func是新函數的地址,這里就是new_vfs_read的地址
*/
unsigned int patch_kernel_func(unsigned int handler, unsigned int old_func, 
        unsigned int new_func)
{
    unsigned char *p = (unsigned char *)handler;
    unsigned char buf[4] = "\x00\x00\x00\x00";
    unsigned int offset = 0;
    unsigned int orig = 0;
    int i = 0;

    DbgPrint("\n*** hook engine: start patch func at: 0x%08x\n", old_func);

    while (1) {
        if (i > 512)
            return 0;

        if (p[0] == 0xe8) {
            DbgPrint("*** hook engine: found opcode 0x%02x\n", p[0]);

            DbgPrint("*** hook engine: call addr: 0x%08x\n", 
                (unsigned int)p);
            buf[0] = p[1];
            buf[1] = p[2];
            buf[2] = p[3];
            buf[3] = p[4];

            DbgPrint("*** hook engine: 0x%02x 0x%02x 0x%02x 0x%02x\n", 
                p[1], p[2], p[3], p[4]);

                offset = *(unsigned int *)buf;
                DbgPrint("*** hook engine: offset: 0x%08x\n", offset);

                orig = offset + (unsigned int)p + 5;
                DbgPrint("*** hook engine: original func: 0x%08x\n", orig);

            if (orig == old_func) {
                DbgPrint("*** hook engine: found old func at"
                    " 0x%08x\n", 
                    old_func);

                DbgPrint("%d\n", i);
                break;
            }
        }
        p++;
        i++;
    }

    offset = new_func - (unsigned int)p - 5;
    DbgPrint("*** hook engine: new func offset: 0x%08x\n", offset);

    p[1] = (offset & 0x000000ff);
    p[2] = (offset & 0x0000ff00) >> 8;
    p[3] = (offset & 0x00ff0000) >> 16;
    p[4] = (offset & 0xff000000) >> 24;

    DbgPrint("*** hook engine: pachted new func offset.\n");

    return orig;
}

對于這類劫持攻擊,目前常見的做法是fireeye的"函數返回地址污點檢測",通過對原有指令返回位置的匯編代碼作污點標記,通過查找jmp,push ret等指令來進行防御。

⑴利用0x80中斷劫持system_call->sys_call_table進行系統調用Hook

我們知道,要對系統調用(sys_call_table)進行替換,卻必須要獲取該地址后才可以進行替換。但是Linux 2.6版的內核出于安全的考慮沒有將系統調用列表基地址的符號sys_call_table導出,但是我們可以采取一些hacking的方式進行獲取。

因為系統調用都是通過0x80中斷來進行的,故可以通過查找0x80中斷的處理程序來獲得sys_call_table的地址。其基本步驟是

1. 獲取中斷描述符表(IDT)的地址(使用C ASM匯編) 2. 從中查找0x80中斷(系統調用中斷)的服務例程(8*0x80偏移) 3. 搜索該例程的內存空間, 4. 從其中獲取sys_call_table(保存所有系統調用例程的入口地址)的地址

編程示例

find_sys_call_table.c

#include <linux/module.h>
#include <linux/kernel.h>

// 中斷描述符表寄存器結構
struct 
{
    unsigned short limit;
    unsigned int base;
} __attribute__((packed)) idtr;


// 中斷描述符表結構
struct 
{
    unsigned short off1;
    unsigned short sel;
    unsigned char none, flags;
    unsigned short off2;
} __attribute__((packed)) idt;

// 查找sys_call_table的地址
void disp_sys_call_table(void)
{
    unsigned int sys_call_off;
    unsigned int sys_call_table;
    char* p;
    int i;

    // 獲取中斷描述符表寄存器的地址
    asm("sidt %0":"=m"(idtr));
    printk("addr of idtr: %x\n", &idtr);

    // 獲取0x80中斷處理程序的地址
    memcpy(&idt, idtr.base+8*0x80, sizeof(idt));
    sys_call_off=((idt.off2<<16)|idt.off1);
    printk("addr of idt 0x80: %x\n", sys_call_off);

    // 從0x80中斷服務例程中搜索sys_call_table的地址
    p=sys_call_off;
    for (i=0; i<100; i++)
    {
        if (p=='\xff' && p[i+1]=='\x14' && p[i+2]=='\x85')
        {
            sys_call_table=*(unsigned int*)(p+i+3);
            printk("addr of sys_call_table: %x\n", sys_call_table);
            return ;
        }
    }
}

// 模塊載入時被調用
static int __init init_get_sys_call_table(void)
{
    disp_sys_call_table();
    return 0;
}

module_init(init_get_sys_call_table);

// 模塊卸載時被調用
static void __exit exit_get_sys_call_table(void)
{
}

module_exit(exit_get_sys_call_table);

// 模塊信息
MODULE_LICENSE("GPL2.0");
MODULE_AUTHOR("LittleHann");

Makefile

obj-m := find_sys_call_table.o

編譯

make -C /usr/src/kernels/2.6.32-358.el6.i686 M=$(pwd) modules

測試效果

dmesg| tail

圖片圖片

獲取到了sys_call_table的基地址之后,我們就可以修改指定offset對應的系統調用了,從而達到劫持系統調用的目的。

⑵獲取sys_call_table的常用方法

①通過dump獲取絕對地址

模擬出一個call *sys_call_table(,%eax,4),然后看其機器碼,然后在system_call的附近基于這個特征進行尋找

#include <stdio.h>
void fun1()
{
        printf("fun1/n");
}
void fun2()
{
        printf("fun2/n");
}
unsigned int sys_call_table[2] = {fun1, fun2};
int main(int argc, char **argv)
{
        asm("call *sys_call_table(%eax,4");
}

編譯
gcc test.c -o test

objdump進行dump
objdump -D ./test | grep sys_call_table
②通過/boot/System.map-2.6.32-358.el6.i686文件查找
cd /boot
grep sys_call_table System.map-2.6.32-358.el6.i686
③通過讀取/dev/kmem虛擬內存全鏡像設備文件獲得sys_call_table地址

Linux下/dev/mem和/dev/kmem的區別:

/dev/mem: 物理內存的全鏡像。可以用來訪問物理內存。比如: 1) X用來訪問顯卡的物理內存, 2) 嵌入式中訪問GPIO。用法一般就是open,然后mmap,接著可以使用map之后的地址來訪問物理內存。這其實就是實現用戶空間驅動的一種方法。2. /dev/kmem: kernel看到的虛擬內存的全鏡像。可以用來: 1) 訪問kernel的內容,查看kernel的變量, 2) 用作rootkit之類的

code

#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>

int kfd; 
struct
{
    unsigned short limit;
    unsigned int base;
} __attribute__ ((packed)) idtr;

struct
{
    unsigned short off1;
    unsigned short sel;
    unsigned char none, flags;
    unsigned short off2;
} __attribute__ ((packed)) idt;

int readkmem (unsigned char *mem,  unsigned off,  int bytes)
{
    if (lseek64 (kfd, (unsigned long long) off,  SEEK_SET) != off)
    {
        return -1;
    } 
    if (read (kfd, mem, bytes) != bytes) 
    {
        return -1;
    } 
}

int main (void)
{
    unsigned long sct_off;
    unsigned long sct;
    unsigned char *p, code[255];
    int i; 
    /* request IDT and fill struct */ 

    asm ("sidt %0":"=m" (idtr));

    if ((kfd = open ("/dev/kmem", O_RDONLY)) == -1)
    {
        perror("open");
        exit(-1);
    } 
    if (readkmem ((unsigned char *)&idt, idtr.base + 8 * 0x80, sizeof (idt)) == -1)
    {
        printf("Failed to read from /dev/kmem\n");
        exit(-1);
    } 

    sct_off = (idt.off2 << 16) | idt.off1; 

    if (readkmem (code, sct_off, 0x100) == -1)
    {
        printf("Failed to read from /dev/kmem\n");
        exit(-1);
    }

    /* find the code sequence that calls SCT */ 

    sct = 0;
    for (i = 0; i < 255; i++)
    {
        if (code[i] == 0xff && code[i+1] == 0x14 &&  code[i+2] == 0x85) 
        {
            sct = code[i+3] + (code[i+4] << 8) +  (code[i+5] << 16) + (code[i+6] << 24);
        }
    }
    if (sct)
    {
        printf ("sys_call_table: 0x%x\n", sct);
    }

    close (kfd);
}
④通過函數特征碼循環搜索獲取sys_call_table地址 (64 bit)
unsigned long **find_sys_call_table() 
{ 
    unsigned long ptr;
    unsigned long *p;

    for (ptr = (unsigned long)sys_close; ptr < (unsigned long)&loops_per_jiffy; ptr += sizeof(void *)) 
    {    
        p = (unsigned long *)ptr;

        if (p[__NR_close] == (unsigned long)sys_close) 
    {
            printk(KERN_DEBUG "Found the sys_call_table!!!\n");
            return (unsigned long **)p;
        }
    }

    return NULL;
}

要特別注意的是代碼中進行函數地址搜索的代碼:if (p[__NR_close] == (unsigned long)sys_close)

在64bit Linux下,函數的地址是8字節的,所以要使用unsigned long

我們可以在linux下執行以下兩條指令

grep sys_close System.map-2.6.32-358.el6.i686
grep loops_per_jiffy System.map-2.6.32-358.el6.i686

圖片圖片

可以看到,系統調用表sys_call_table中的函數地址都落在這個地址區間中,因此我們可以使用loop搜索的方法去獲取sys_call_table的基地址

⑶通過kprobe方式動態獲取kallsyms_lookup_name,然后利用kallsyms_lookup_name獲取sys_call_table的地址

通過kprobe的函數hook掛鉤機制,可以獲取內核中任意函數的入口地址,我們可以先獲取"kallsyms_lookup_name"函數的入口地址

//get symbol name by  "kprobe.addr"
//when register a kprobe on succefully return,the structure of kprobe save the symbol address at "kprobe.addr"
//just return this value
static void* aquire_symbol_by_kprobe(char* symbol_name)
{
    void *symbol_addr=NULL;
    struct kprobe kp;

    do
    {
       memset(&kp,0,sizeof(kp));
       kp.symbol_name=symbol_name;
       kp.pre_handler=kprobe_pre;
       if(register_kprobe(&kp)!=0)
       {
            break;
       }
       //this is the address of  "symbol_name"
       symbol_addr=(void*)kp.addr;

       //now kprobe is not used any more,so unregister it
       unregister_kprobe(&kp);

    }while(false);

    return symbol_addr;
}

//調用之
tmp_lookup_func = aquire_symbol_by_kprobe("kallsyms_lookup_name");

kallsyms_lookup_name()可以用于獲取內核導出符號表中的符號地址,而sys_call_table的地址也存在于內核導出符號表中,我們可以使用kallsyms_lookup_name()獲取到sys_call_table的基地址

(void**)kallsyms_lookup_name("sys_call_table");

⑷利用Linux內核機制kprobe機制(kprobes, jprobe和kretprobe)進行系統調用Hook

kprobe是一個動態地收集調試和性能信息的工具,它從Dprobe項目派生而來,它幾乎可以跟蹤任何函數或被執行的指令以及一些異步事件。它的基本工作機制是:

  • 1. 用戶指定一個探測點,并把一個用戶定義的處理函數關聯到該探測點
  • 2. 在注冊探測點的時候,對被探測函數的指令碼進行替換,替換為int 3的指令碼
  • 3. 在執行int 3的異常執行中,通過通知鏈的方式調用kprobe的異常處理函數
  • 4. 在kprobe的異常出來函數中,判斷是否存在pre_handler鉤子,存在則執行
  • 5. 執行完后,準備進入單步調試,通過設置EFLAGS中的TF標志位,并且把異常返回的地址修改為保存的原指令
  • 6. 代碼返回,執行原有指令,執行結束后觸發單步異常 7. 在單步異常的處理中,清除單步標志,執行post_handler流程,并最終返回

從原理上來說,kprobe的這種機制屬于系統提供的"回調訂閱",和netfilter是類似的,linux內核通過在某些代碼執行流程中給出回調函數接口供程序員訂閱,內核開發人員可以在這些回調點上注冊(訂閱)自定義的處理函數,同時還可以獲取到相應的狀態信息,方便進行過濾、分析

kprobe實現了三種類型的探測點:

  • 1. kprobes kprobes是可以被插入到內核的任何指令位置的探測點,kprobe允許在同一地址注冊多個kprobes,但是不能同時在該地址上有多個jprobes
  • 2. jprobe jprobe則只能被插入到一個內核函數的入口
  • 3. kretprobe(也叫返回探測點) 而kretprobe則是在指定的內核函數返回時才被執行

在本文中,我們可以使用kprobe的程序實現作一個內核模塊,模塊的初始化函數來負責安裝探測點,退出函數卸載那些被安裝的探測點。kprobe提供了接口函數(APIs)來安裝或卸載探測點。目前kprobe支持如下架構:i386、x86_64、ppc64、ia64(不支持對slot1指令的探測)、sparc64 (返回探測還沒有實現)

三、Linux系統調用中常見的Hook技術方法

3.1基于函數指針的 Hook

在 Linux 內核中,系統調用函數的入口地址存儲在系統調用表中,這個表實際上是一個函數指針數組。基于函數指針的 Hook 方法就是直接修改系統調用表中對應函數的指針,使其指向我們自定義的 Hook 函數。

實現步驟:首先需要找到系統調用表的地址,這在不同的內核版本和架構上可能有所不同。然后,通過修改內存中的函數指針,將其指向自定義的 Hook 函數。在 Hook 函數中,可以先執行自己的邏輯,然后再調用原來的系統調用函數(如果需要的話)。

示例代碼(簡化示意):

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/syscalls.h>

// 保存原來的系統調用函數指針
asmlinkage long (*original_sys_open)(const char __user *, int, umode_t);

// 自定義的Hook函數
asmlinkage long my_sys_open(const char __user *filename, int flags, umode_t mode) {
    printk(KERN_INFO "MyHook: Opening file: %s\n", filename);
    // 調用原來的系統調用函數
    return original_sys_open(filename, flags, mode);
}

static int __init my_init(void) {
    // 獲取系統調用表地址
    unsigned long *sys_call_table = (unsigned long *)sys_call_table;
    // 保存原來的系統調用函數指針
    original_sys_open = (void *)sys_call_table[__NR_open];
    // 修改系統調用表中的函數指針
    sys_call_table[__NR_open] = (unsigned long)my_sys_open;
    return 0;
}

static void __exit my_exit(void) {
    // 恢復原來的系統調用函數指針
    unsigned long *sys_call_table = (unsigned long *)sys_call_table;
    sys_call_table[__NR_open] = (unsigned long)original_sys_open;
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");

優點與局限性:這種方法簡單直接,效果顯著。但是,由于直接修改系統調用表,可能會導致內核的穩定性問題,并且在不同的內核版本之間移植性較差,因為系統調用表的結構和地址可能會發生變化。

3.2基于 GOT(Global Offset Table)的 Hook

在用戶空間的動態鏈接庫中,GOT 是一個重要的數據結構,用于存儲外部函數的地址。基于 GOT 的 Hook 方法主要應用于用戶空間的程序,通過修改 GOT 表中函數的地址,實現對系統調用的 Hook。

實現步驟:首先需要找到目標函數在 GOT 表中的項,然后修改該項的內容,使其指向自定義的 Hook 函數。在 Hook 函數中,可以進行自己的邏輯處理,最后再通過調用原來的函數地址(保存在一個臨時變量中)來執行原函數的功能。

示例代碼(以 C 語言和動態鏈接庫為例):

#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

// 自定義的Hook函數
int my_open(const char *pathname, int flags, mode_t mode) {
    printf("MyHook: Opening file: %s\n", pathname);
    // 獲取原來的open函數指針
    int (*original_open)(const char *, int, mode_t) = dlsym(RTLD_NEXT, "open");
    // 調用原來的open函數
    return original_open(pathname, flags, mode);
}

// 通過環境變量LD_PRELOAD加載這個庫時,會優先使用這個函數
__attribute__((constructor)) void my_init(void) {
    void *handle = dlopen(NULL, RTLD_NOW);
    if (!handle) {
        fprintf(stderr, "Error opening library: %s\n", dlerror());
        exit(EXIT_FAILURE);
    }
    // 使用dlsym獲取原來的open函數指針(這里只是示例,實際可能需要更復雜的處理)
    int (*original_open)(const char *, int, mode_t) = dlsym(handle, "open");
    if (!original_open) {
        fprintf(stderr, "Error getting original open function: %s\n", dlerror());
        dlclose(handle);
        exit(EXIT_FAILURE);
    }
    // 這里可以通過一些技巧修改GOT表中open函數的地址,使其指向my_open函數
    // 具體實現因不同系統和編譯器而異,此處簡化示意
}

優點與局限性:這種方法主要在用戶空間操作,對內核的影響較小,相對安全穩定。而且,它可以針對特定的用戶程序進行 Hook,具有較好的靈活性。但是,它只能 Hook 用戶空間調用的系統調用函數,對于內核內部直接調用的系統調用則無法生效。

3.3基于內核模塊的 Kprobes Hook

Kprobes 是 Linux 內核提供的一種動態探測機制,它允許開發者在不修改內核源代碼的情況下,對內核函數的執行進行探測和干預?;?Kprobes 的 Hook 技術就是利用這一機制來實現對系統調用的 Hook。

實現步驟:首先需要注冊一個 Kprobe 結構體,指定要 Hook 的內核函數以及在函數執行前、執行后和發生異常時的回調函數。然后,通過內核提供的函數將 Kprobe 注冊到內核中。在回調函數中,可以編寫自己的 Hook 邏輯。

示例代碼(簡化示意):

#include <linux/module.h>
#include <linux/kprobes.h>

// 定義一個Kprobe結構體
static struct kprobe my_kprobe = {
  .symbol_name = "__x64_sys_open",  // 要Hook的系統調用函數名
};

// 函數執行前的回調函數
static int pre_handler(struct kprobe *p, struct pt_regs *regs) {
    printk(KERN_INFO "MyHook: Before open system call\n");
    return 0;
}

// 函數執行后的回調函數
static void post_handler(struct kprobe *p, struct pt_regs *regs, unsigned long flags) {
    printk(KERN_INFO "MyHook: After open system call\n");
}

// 發生異常時的回調函數
static void fault_handler(struct kprobe *p, struct pt_regs *regs, int trapnr) {
    printk(KERN_INFO "MyHook: Fault in open system call\n");
}

static int __init my_init(void) {
    // 設置回調函數
    my_kprobe.pre_handler = pre_handler;
    my_kprobe.post_handler = post_handler;
    my_kprobe.fault_handler = fault_handler;
    // 注冊Kprobe
    if (register_kprobe(&my_kprobe) < 0) {
        printk(KERN_INFO "Failed to register kprobe\n");
        return -1;
    }
    return 0;
}

static void __exit my_exit(void) {
    // 注銷Kprobe
    unregister_kprobe(&my_kprobe);
    printk(KERN_INFO "Kprobe unregistered\n");
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");

優點與局限性:Kprobes Hook 技術功能強大,能夠深入內核內部進行 Hook 操作,并且對內核的影響相對較小,因為它是一種動態探測機制,不需要修改內核的靜態代碼。然而,它的實現相對復雜,需要對內核機制有較深入的理解,并且在使用不當的情況下,可能會對內核性能產生一定的影響。

四、Hook技術挑戰與應對

4.1內核穩定性問題

無論是直接修改系統調用表還是使用 Kprobes 等技術,都可能對內核的穩定性造成潛在威脅。一旦 Hook 代碼出現錯誤,可能導致內核崩潰或者出現不可預測的行為。

應對方法:在編寫 Hook 代碼時,要進行嚴格的測試和調試,確保代碼的正確性和穩定性。同時,可以采用一些安全機制,如對修改的內存區域進行備份,以便在出現問題時能夠快速恢復。

4.2兼容性問題

不同的 Linux 內核版本和架構在系統調用表結構、函數命名等方面可能存在差異,這給 Hook 技術的跨版本和跨架構應用帶來了困難。

應對方法:在編寫 Hook 代碼時,要充分考慮內核版本和架構的兼容性。可以通過宏定義等方式,根據不同的內核版本和架構進行不同的代碼處理。同時,關注內核的更新和變化,及時調整 Hook 代碼。

4.3安全風險

Hook 技術如果被惡意利用,可能會對系統安全造成嚴重威脅。例如,惡意軟件可以通過 Hook 關鍵系統調用,隱藏自己的行為或者獲取敏感信息。

應對方法:加強系統的安全防護,使用安全檢測工具及時發現和阻止惡意的 Hook 行為。同時,對于合法的 Hook 應用,要進行嚴格的權限管理和審計,確保 Hook 技術的使用是安全可靠的。

責任編輯:武曉燕 來源: 深度Linux
相關推薦

2025-11-03 04:00:00

2025-10-09 11:10:00

開發操作系統Linux

2025-07-28 03:00:00

2023-09-18 11:34:17

Linux系統

2025-10-28 03:00:00

2009-09-21 09:51:19

LoadRunnerLinux系統監控Linux

2021-03-15 15:18:16

鴻蒙HarmonyOS應用

2015-07-28 09:19:10

Linux內核

2022-07-19 15:01:34

Linux

2016-09-26 13:50:52

Linux系統性能

2010-05-26 12:57:59

linux 系統監控

2015-12-17 14:32:46

NmonLinux性能

2020-01-02 10:04:32

Linux 系統 數據

2017-03-09 14:58:19

GPartedLinux磁盤分區

2009-12-18 15:28:19

Linux內核

2016-07-22 10:50:56

Linux內核無線子系統

2009-12-17 14:34:24

Linux系統內核

2021-06-03 08:03:13

網絡

2023-12-26 15:06:00

Linux內核轉儲

2020-12-29 09:11:33

LinuxLinux內核
點贊
收藏

51CTO技術棧公眾號

九一在线视频| 免费又黄又爽又猛大片午夜| 一区二区三区自拍视频| 精品久久久久久久久久久久久| 欧美成人综合一区| 一级特黄特色的免费大片视频| 欧美日韩国产免费观看 | 在线免费观看av的网站| 91福利国产在线观看菠萝蜜| 91美女视频网站| 成人xxxx视频| 国产又黄又猛又粗又爽| 亚洲国产精品日韩专区av有中文 | 一本高清dvd不卡在线观看| 精品日韩在线播放| 嫩草精品影院| 丁香婷婷综合五月| 成人亚洲激情网| youjizz在线视频| 欧美特黄a级高清免费大片a级| 亚洲欧美日韩爽爽影院| 欧美一级片黄色| 国产剧情一区二区在线观看| 欧美怡红院视频| 丁香花在线影院观看在线播放| 三区四区电影在线观看| 26uuu亚洲| 国产一区精品在线| 国产成年妇视频| 国产在线日韩欧美| 国产精品久久久久福利| 久久黄色精品视频| 亚洲国产清纯| 欧美成人第一页| 国产精品综合激情| 欧美一级淫片| 国产丝袜精品第一页| 动漫美女无遮挡免费| 国产日韩一区二区三免费高清 | 99热在线只有精品| 蜜臀av性久久久久蜜臀aⅴ四虎| 91精品国产高清久久久久久91| 久久r这里只有精品| 久久久久亚洲| 久久人体大胆视频| 午夜黄色福利视频| 日韩国产在线| 影音先锋日韩有码| 久操视频在线观看免费| 国产一区二区三区探花| 亚洲新声在线观看| 波多野结衣片子| 亚洲精品小区久久久久久| 精品视频在线观看日韩| 三叶草欧洲码在线| 亚洲三级网址| 亚洲天堂第二页| 一区二区三区四区免费| 亚洲妇女av| 亚洲欧美国产一本综合首页| 9.1成人看片| 美女久久久久| 一区二区av在线| 日本免费网站视频| 91精品91| 久久久久久尹人网香蕉| 91蜜桃视频在线观看| 亚洲欧美不卡| 国产精品视频男人的天堂| 亚洲中文字幕在线观看| 韩国午夜理伦三级不卡影院| 亚洲字幕在线观看| 国模无码一区二区三区| 久久午夜电影网| 亚洲欧美99| 污污片在线免费视频| 午夜精品视频在线观看| 国产福利一区视频| 成人噜噜噜噜| 国产网站欧美日韩免费精品在线观看 | 小黄鸭精品aⅴ导航网站入口| 在线免费观看一区| 超碰在线免费av| 开心激情综合| 日韩中文字幕久久| 福利一区二区三区四区| 影音先锋亚洲精品| 国产精品国产三级国产专播精品人| 91福利免费视频| zzijzzij亚洲日本少妇熟睡| 五月天色一区| 狂野欧美性猛交xxxxx视频| 日韩欧美中文免费| 中文字幕国产高清| 日韩av中文字幕一区| 在线日韩精品视频| 国产精品第二十页| 蜜桃在线一区二区三区| 国产高清一区二区三区| 国产精品秘入口| 亚洲午夜av在线| 色免费在线视频| 精品国产一区二区三区成人影院| 国产午夜精品免费一区二区三区| 麻豆亚洲av熟女国产一区二| 日韩精品亚洲专区| 国产精品亚洲综合| 生活片a∨在线观看| 欧美日韩免费网站| 制服.丝袜.亚洲.中文.综合懂| 国产亚洲一区二区三区不卡| 久久久噜久噜久久综合| 国产精品传媒在线观看| 不卡视频一二三四| 国产人妻互换一区二区| 成人交换视频| 精品亚洲aⅴ在线观看| 波多野结衣亚洲色图| 蜜臀99久久精品久久久久久软件| 精品一卡二卡三卡四卡日本乱码 | 人妻少妇精品无码专区| 亚洲人成影院在线观看| 天天爽天天爽夜夜爽| 色狠狠久久av综合| 久久久久久久爱| 国产模特av私拍大尺度| 国产精品美女www爽爽爽| 欧美黄色一级片视频| 久久久久观看| 国模私拍视频一区| www五月婷婷| 亚洲视频一区二区在线| 色播五月激情五月| 日韩在线视频精品| 国产精品第3页| 国产中文字幕在线视频| 色诱亚洲精品久久久久久| av鲁丝一区鲁丝二区鲁丝三区| 狠狠入ady亚洲精品| 国产欧美一区二区三区久久| 1769视频在线播放免费观看| 欧美三级日韩在线| 久久精品三级视频| 美女网站视频久久| 亚洲国产一区二区三区在线| jvid一区二区三区| 中文字幕一区二区三区电影| 中文字幕精品一区二| 国产视频一区不卡| 在线免费观看视频黄| 久久社区一区| 91色精品视频在线| 日韩伦理电影网站| 亚洲精品一区二区精华| 久久久午夜影院| 26uuu亚洲| 亚洲第一狼人区| 欧美freesextv| 亚洲一区精品电影| 超碰在线网站| 国产丝袜一区二区三区| 中文字幕在线2018| 亚洲日本乱码在线观看| 国产51自产区| 国产农村妇女毛片精品久久莱园子| 欧美成人蜜桃| 欧美91在线|欧美| 久久91精品国产| 日本国产在线观看| 91极品视觉盛宴| 日本 欧美 国产| 国产999精品久久久久久| 青青草视频在线免费播放 | 亚洲午夜未满十八勿入免费观看全集| 337p粉嫩色噜噜噜大肥臀| 中文字幕视频一区二区三区久| 在线免费观看av网| 夜久久久久久| 亚洲午夜久久久影院伊人| 韩国一区二区三区视频| 午夜伦理精品一区| freemovies性欧美| 精品少妇一区二区三区视频免付费 | 邻居大乳一区二区三区| 欧美日韩dvd在线观看| 国产在线观看99| 欧美国产国产综合| 蜜臀aⅴ国产精品久久久国产老师| 香蕉视频成人在线观看| 欧美另类videos| 国产欧美日韩精品一区二区免费 | 国产吃瓜黑料一区二区| 丝袜a∨在线一区二区三区不卡| 99精品视频网站| 欧美黄色影院| 成人激情视频在线播放| 国产黄大片在线观看| 色偷偷av一区二区三区| 午夜成人免费影院| 91精品国产综合久久蜜臀| 毛片在线免费视频| 亚洲精品视频观看| 小早川怜子久久精品中文字幕| 国产精品综合一区二区三区| 欧美黑人又粗又大又爽免费| 亚洲毛片播放| 中国老女人av| 日韩综合一区| 欧美国产综合视频| 9l亚洲国产成人精品一区二三| 国产极品精品在线观看| sm久久捆绑调教精品一区| 不卡中文字幕av| 二人午夜免费观看在线视频| 亚洲成人亚洲激情| www.天堂在线| 51精品国自产在线| 国产裸体美女永久免费无遮挡| 五月婷婷激情综合| 久久久久无码国产精品不卡| 中文字幕在线免费不卡| 国产高清一区二区三区四区| 99r精品视频| 精品人妻一区二区免费| 国产精品一二三四五| 中文字幕中文在线| 日本不卡不码高清免费观看 | 精品无人区卡一卡二卡三乱码免费卡| 成人一级片网站| 亚洲少妇自拍| 国产美女主播在线播放| 伊人久久综合| 免费网站永久免费观看| 欧美91精品| 2021国产视频| 欧美日韩天堂| 人人妻人人澡人人爽欧美一区| 中文一区一区三区免费在线观看| av电影一区二区三区| 午夜av一区| 日韩精品第1页| 综合久久一区| 久久久久久久香蕉| 国产精品vip| 日韩亚洲欧美视频| 亚洲少妇在线| 国产精品无码av无码| 天堂av在线一区| 91最新在线观看| 美女在线视频一区| 在线免费观看av网| 丁香六月久久综合狠狠色| 成人午夜精品无码区| 99re热视频精品| wwwwww日本| 国产精品午夜春色av| 登山的目的在线| 一区二区三区在线播| 日产精品久久久久| 欧美丝袜一区二区| 伊人网站在线观看| 日韩一级免费观看| 性感美女福利视频| 国产一区二区三区在线观看网站 | 一本色道久久综合狠狠躁的番外| 黑人巨大精品欧美一区二区小视频| 日韩精品免费一区二区夜夜嗨 | 懂色一区二区三区av片| 欧美1区2区3区4区| 日本不卡久久| 日本女优一区| 日韩免费在线观看av| 亚欧美中日韩视频| 最新av免费在线观看| 高清久久久久久| 亚洲精品国产一区黑色丝袜| 中文字幕一区二区在线观看| 国产一级做a爱免费视频| 色婷婷综合久久久久中文一区二区| 在线观看不卡的av| 亚洲成年人在线播放| porn视频在线观看| 欧美大片在线免费观看| 综合在线影院| www.久久久| 欧美一二区在线观看| www.成年人视频| 欧美aaaaaa午夜精品| 一级全黄裸体片| 国产亚洲综合色| 九九九国产视频| 欧美日韩第一区日日骚| 四虎免费在线观看| 色吧影院999| 国产不卡人人| 亚洲综合在线播放| 国产日产精品_国产精品毛片| 黄色一级大片免费| 天堂久久久久va久久久久| 波多野结衣中文字幕在线播放| 久久精品一区八戒影视| 免费一级黄色大片| 欧美日韩在线三区| 青青草视频在线免费观看| 久久久国产一区二区| 在线日本欧美| 久久精品国产综合精品| 欧美日韩一区二区国产| 色哟哟精品视频| 2023国产精品| 日韩福利片在线观看| 欧美日韩一区二区三区视频| 同心难改在线观看| 欧美成人激情在线| 欧美天堂一区二区| 日韩国产欧美一区| 亚洲欧美日韩一区在线观看| wwwxxxx在线观看| 亚洲天堂精品视频| 中文字幕在线观看欧美| 亚洲午夜精品视频| 成人免费看视频网站| 精品国产一区二区三区四区精华| 欧美日本一区| 亚洲国产欧美91| 亚洲视频免费在线| 国产欧美一级片| 久久精品国产亚洲7777| 欧美国产视频| 亚洲激情电影在线| 美女脱光内衣内裤视频久久影院| 91成人破解版| 欧美亚日韩国产aⅴ精品中极品| 男女污视频在线观看| 欧洲成人免费aa| 亚洲精品aaaaa| 青青草原成人网| 久久久久久免费| 69xxxx国产| 中文字幕免费精品一区| 国产一区二区三区四区五区3d| 日韩欧美一区二区视频在线播放| 久久青草久久| 在线观看日本中文字幕| 欧美性猛交xxxxxx富婆| 最新av网站在线观看| 成人综合网网址| 欧美日韩国产欧| 亚洲麻豆一区二区三区| 无吗不卡中文字幕| 日韩有码电影| 国产精品久久久久免费a∨| 日韩国产一区二区| 免费看的av网站| 亚洲va韩国va欧美va| 三级在线观看| 国产精品一区二区3区| 亚洲网色网站| 美女露出粉嫩尿囗让男人桶| 第一福利永久视频精品| 久草在线青青草| 国产主播喷水一区二区| 欧美69视频| 野外性满足hd| 欧美日本精品一区二区三区| a在线免费观看| 精品综合久久久| 日日摸夜夜添夜夜添精品视频| www中文在线| 日韩一区二区三区视频在线观看| 国产va在线视频| 亚洲欧美久久234| 成人一区二区在线观看| 东京热一区二区三区四区| www.亚洲一区| 精品亚洲精品| 伊人国产在线视频| 亚洲国产欧美在线| 春暖花开成人亚洲区| 亚洲综合精品伊人久久| 久久av一区| 日韩欧美国产成人精品免费| 亚洲国产欧美一区| 成人a在线观看高清电影| 欧美国产视频一区| 欧美激情一区在线观看| 黄色av网址在线| 国产精品入口日韩视频大尺度| 国产精品大片| 91成人精品一区二区| 亚洲第一视频网站| 欧美一级做一级爱a做片性| 一卡二卡三卡视频| 国产精品青草综合久久久久99| 女人18毛片水真多18精品| 国产精品久在线观看| 99精品国产一区二区青青牛奶| 亚洲伦理一区二区三区| 亚洲男人天堂2024| eeuss鲁片一区二区三区|