相信很多人對"Hook"都不會陌生,其中文翻譯為"鈎子".在程式設計中,
鈎子表示一個可以允許程式設計者插入自定義程式的地方,通常是打包好的程式中提供的接口.
比如,我們想要提供一段代碼來分析程式中某段邏輯路徑被執行的頻率,或者想要在其中
插入更多功能時就會用到鈎子. 鈎子都是以固定的目的提供給使用者的,并且一般都有文檔說明.
通過Hook,我們可以暫停系統調用,或者通過改變系統調用的參數來改變正常的輸出結果,
甚至可以中止一個目前運作中的程序并且将控制權轉移到自己手上.
基本概念
作業系統通過一系列稱為系統調用的方法來提供各種服務.他們提供了标準的API來通路下面的
硬體裝置和底層服務,比如檔案系統. 以32位系統為例,當程序運作系統調用前,會先把系統調用号放到寄存器
%eax
中,并且将該系統調用的參數依次放入寄存器
%ebx, %ecx, %edx 以及 %esi 和 %edi
中.
以write系統調用為例:
write(2, "Hello", 5);
在32位系統中會轉換成:
movl $1, %eax
movl $2, %ebx
movl $hello,%ecx
movl $5, %edx
int $0x80
其中
1
為write的系統調用号, 所有的系統調用号碼定義在
unistd.h
檔案中. $hello表示字元串
"Hello"的位址; 32位Linux系統通過0x80中斷來進行系統調用.
如果是64位系統則有所不同, 使用者層應用層用整數寄存器
%rdi, %rsi, %rdx, %rcx, %r8 以及 %r9
來傳參,
而核心接口用
%rdi, %rsi, %rdx, %r10, %r8 以及 %r10
來傳參. 并且用
syscall
指令而不是80中斷
來進行系統調用. 相同之處是都用寄存器
%rax
來儲存調用号和傳回值.
更多關于32位和64位彙編指令的差別可以參考stack overflow的總結,
因為我目前環境是64位Linux,是以下文的操作都以64位系統為例.
程序追蹤
上面說到鈎子一般由程式提供,那麼作業系統核心作為一個程式,是否有提供相應的鈎子呢?
答案是肯定的,
ptrace
(Process Trace)系統調用就提供了這樣的功能. ptrace提供了許多
方法來觀察和控制其他程序的執行, 并且可以檢查和修改其核心鏡像和寄存器. 通常用來
作為調試器(如gdb)或用來跟蹤各種其他系統調用.
那麼,ptrace在程式運作的哪個階段起作用呢? 答案是在執行系統調用之前. 核心會先檢查是否
程序正在被追蹤, 如果是的話, 核心會停止程序并将控制權轉移給追蹤程序, 是以其可以檢視和
修改被追蹤程序的寄存器. 舉例說明:
#include <stdio.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/reg.h> /* For constants ORIG_RAX etc */
int main()
{ pid_t child;
long orig_rax;
child = fork();
if(child == 0) {
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
execl("/bin/ls", "ls", NULL);
}
else { wait(NULL);
orig_rax = ptrace(PTRACE_PEEKUSER,
child, 8 * ORIG_RAX,
NULL);
printf("The child made a "
"system call %ld\n", orig_rax);
ptrace(PTRACE_CONT, child, NULL, NULL);
}
return 0;
}
程式編譯運作後輸出:
The child made a system call 59
以及
ls
的結果. 系統調用号59是
__NR_execve
, 由子程序調用的
execl
産生.
在上面的例子中我們可以看見, 父程序fork了一個子程序,并且在子程序中進行系統調用.
在執行調用前,子程序運作了ptrace,并設定第一個參數為
PTRACE_TRACEME
, 這告訴核心
目前程序正在被追蹤. 是以當子程序運作到execl時, 會把控制權轉回父程序. 父程序用wait
函數(系統調用)來等待核心通知. 然後就可以檢視系統調用的參數以及做其他事情.
當系統調用出現的時候, 核心會儲存原始的rax寄存器值(其中包含系統調用号), 我們可以
從子程序的
USER
段讀取這個值, 這裡是使用ptrace并且設定第一個參數為
PTRACE_PEEKUSER
.
當我們檢查完了系統調用之後, 可以調用ptrace并設定參數
PTRACE_CONT
讓子程序繼續運作.
值得一提的是, 這裡的child為子程序的程序ID, 由fork函數傳回.
寄存器讀寫
ptrace函數通過四個參數來調用, 其原型為:
long ptrace(enum __ptrace_request request,
pid_t pid,
void *addr,
void *data);
其中第一個參數決定了ptrace的行為以及其他參數的含義, request的值可以是下列值中的一個:
PTRACE_TRACEME, PTRACE_PEEKTEXT, PTRACE_PEEKDATA, PTRACE_PEEKUSER, PTRACE_POKETEXT,
PTRACE_POKEDATA, PTRACE_POKEUSER, PTRACE_GETREGS, PTRACE_GETFPREGS, PTRACE_SETREGS,
PTRACE_SETFPREGS, PTRACE_CONT, PTRACE_SYSCALL, PTRACE_SINGLESTEP, PTRACE_DETACH.
在系統調用追蹤中, 常見的流程如下圖所示:

讀取系統調用參數
系統調用的參數按順序存放在rbx,rcx...之中,是以以write系統調用為例看如何讀取寄存器的值:
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/reg.h> /* For constants ORIG_EAX etc */
#include <sys/user.h>
#include <sys/syscall.h> /* SYS_write */
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", "-l", "-h", 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, ®s);
if(!iscalling) {
iscalling = 1;
printf("SYS_write call with %lld, %lld, %lld\n",
regs.rdi, regs.rsi, regs.rdx);
}
else {
printf("SYS_write call return %lld\n", regs.rax);
iscalling = 0;
}
}
ptrace(PTRACE_SYSCALL, child, NULL, NULL);
}
}
return 0;
}
編譯運新有如下輸出:
SYS_write call with 1, 140693012086784, 10
total 32K
SYS_write call return 10
SYS_write call with 1, 140693012086784, 45
-rwxr-xr-x 1 lxy lxy 13K Feb 21 12:19 a.out
SYS_write call return 45
SYS_write call with 1, 140693012086784, 46
-rw-r--r-- 1 lxy lxy 1.5K Feb 20 20:52 test.c
SYS_write call return 46
SYS_write call with 1, 140693012086784, 53
-rw-r--r-- 1 lxy lxy 5.0K Feb 21 12:19 trace_write.c
SYS_write call return 53
可以看到我們的
ls -l -h
指令中, 發生了四次write系統調用.這裡讀取寄存器的時候可以用之前
的
PTRACE_PEEKUSER
參數,也可以直接用
PTRACE_PEEKUSER
參數将寄存器的值讀取到結構體
user_regs_struct
,
該結構體定義在
sys/user.h
中.
程式中WIFEXITED函數(宏)用來檢查子程序是被ptrace暫停的還是準備退出, 可以通過
wait(2)
的man page
檢視詳細的内容. 其中還有個值得一提的參數是
PTRACE_SYSCALL
,其作用是使核心在子程序進入和
退出系統調用時都将其暫停, 等價于調用
PTRACE_CONT
并且在下一個
entry/exit
系統調用前暫停.
修改系統調用參數
假設我們現在要修改write系統調用的參數進而修改列印的内容,根據文檔可知,其第二個參數為write字元串的位址,
第三個參數為字元串的位元組數,是以我們可以用:
val = ptrace(PTRACE_PEEKDATA, child, addr, NULL);
來得到字元串的内容. 值得一提的是, 由于ptrace的傳回值是long型的,是以一次最多隻能讀取sizeof(long)個位元組 的資料,可以多次讀取
addr + i*sizeof(long)
然後合并得到最終的字元串内容. 在64bit系統下一次可以讀取64/8=8位元組的資料.
修改字元串後,可以用:
ptrace(PTRACE_POKEDATA, child, addr, data);
來更新系統調用參數. 同樣一次隻能更新8位元組,是以需要分多次将結果放到long型的data裡,再按順序更新到
addr + i*sizeof(long)
中.
一個讀取參數字元串值的例子如下:
#define 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 * 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';
}
值得一提的是union類型可以用來很友善地往64bit寄存器(long型)讀寫和轉換其他類型(如char)格式的資料.
追蹤其他程式的程序
上面舉的例子都是追蹤并修改聲明了PTRACE_TRACEME的子程序的,那麼我們能否追蹤其他獨立的正在運作的程序呢?
使用
PTRACE_ATTACH
參數就可以追蹤正在運作的程式:
ptrace(PTRACE_ATTACH, pid, NULL, NULL)
其中pid位想要追蹤的程序的程序id. 目前程序會給被追蹤程序發送SIGSTOP信号,但不要求立即停止,
一般會等待子程序完成目前調用. ATTACH之後就和操作fork出來的TRACEME子程序一樣操作就好了.
如果要結束追蹤,則再調用
PTRACE_DETACH
即可.
動态注入指令
用過gdb等調試器的人都知道,debugger工具可以給程式打斷點和單步運作等. 這些功能其實也能用ptrace實作,
其原理就是ATTACH并追蹤正在運作的程序, 讀取其指令寄存器IR(32bit系統為%eip, 64位系統為%rip)的内容,
備份後替換成目标指令,再使其傳回運作;此時被追蹤程序就會執行我們替換的指令. 運作完注入的指令之後,
我們再恢複原程序的IR,進而達到改變原程式運作邏輯的目的. talk is cheap, 先寫個循環列印的程式:
//victim.c
int main() {
while(1) {
printf("Hello, ptrace! [pid:%d]\n", getpid());
sleep(2);
}
return 0;
}
程式運作後會每隔2秒會列印到終端.然後再另外編寫一個程式:
//attach.c
int main(int argc, char *argv[]) {
if(argc!=2) {
printf("Usage: %s pid\n", argv[0]);
return 1;
}
pid_t victim = atoi(argv[1]);
struct user_regs_struct regs;
/* int 0x80, int3 */
unsigned char code[] = {0xcd,0x80,0xcc,0x00,0,0,0,0};
char backup[8];
ptrace(PTRACE_ATTACH, victim, NULL, NULL);
long inst;
wait(NULL);
ptrace(PTRACE_GETREGS, victim, NULL, ®s);
inst = ptrace(PTRACE_PEEKTEXT, victim, regs.rip, NULL);
printf("Victim: EIP:0x%llx INST: 0x%lx\n", regs.rip, inst);
/* Copy instructions into a backup variable */
getdata(victim, regs.rip, backup, 7);
/* Put the breakpoint */
putdata(victim, regs.rip, code, 7);
/* Let the process continue and execute the int 3 instruction */
ptrace(PTRACE_CONT, victim, NULL, NULL);
wait(NULL);
printf("Press Enter to continue ptraced process.\n");
getchar();
putdata(victim, regs.rip, backup, 7);
ptrace(PTRACE_SETREGS, victim, NULL, ®s);
ptrace(PTRACE_CONT, victim, NULL, NULL);
ptrace(PTRACE_DETACH, victim, NULL, NULL);
return 0;
}
運作後會将一直循環輸出的程序暫停, 再按回車使得程序恢複循環輸出. 其中putdata和getdata在上文中已經介紹過了.
我們用之前替換寄存器内容的方法,将%rip的内容修改為
int 3
的機器碼, 使得對應程序暫停執行;
恢複寄存器狀态時使用的是
PTRACE_SETREGS
參數. 值得一提的是對于不同的處理器架構, 其使用的寄存器名稱
也不盡相同, 在不同的機器上允許時代碼也要作相應的修改.
這裡注入的代碼長度隻有8個位元組, 而且是用shellcode的格式注入, 但實際中我們可以在目标程序中動态加載庫檔案(.so),
包括标準庫檔案(如libc.so)和我們自己編譯的庫檔案, 進而可以通過傳遞函數位址和參數來進行複雜的注入,限于篇幅暫不細說.
不過需要注意的是動态連結庫挂載的位址是動态确定的, 可以在
/proc/$pid/maps
檔案中檢視, 其中$pid為程序id.
參考資料
- playing with ptrace part I
- playing with ptrace part II
- 安卓動态調試之Hook
部落格位址:
- pppan.net
- 有價值炮灰-部落格園
歡迎交流,文章轉載請注明出處.
轉載于:https://www.cnblogs.com/pannengzhi/p/5203467.html