Playing with ptrace, Part II
by Pradeep Padala [email protected] http://www.cise.ufl.edu/~ppadala
Created 2002-11-01 02:00
翻譯: Magic.D E-mail: [email protected]
在第一部分中我們已經看到ptrace怎麼擷取子程序的系統調用以及改變系統調用的參數。在這篇文章中,我們将要研究如何在子程序中設定斷點和往運作中的程式裡插入代碼。實際上調試器就是用這種方法來設定斷點和執行調試句柄。與前面一樣,這裡的所有代碼都是針對i386平台的。
附着在程序上
在第一部分鐘,我們使用ptrace(PTRACE_TRACEME, …)來跟蹤一個子程序,如果你隻是想要看程序是怎麼進行系統調用和跟蹤程式的,這個做法是不錯的。但如果你要對運作中的程序進行調試,則需要使用 ptrace( PTRACE_ATTACH, ….)
當 ptrace( PTRACE_ATTACH, …)在被調用的時候傳入了子程序的pid時, 它大體是與ptrace( PTRACE_TRACEME, …)的行為相同的,它會向子程序發送SIGSTOP信号,于是我們可以察看和修改子程序,然後使用 ptrace( PTRACE_DETACH, …)來使子程序繼續運作下去。
下面是調試程式的一個簡單例子
?View Code C
int main()
{
int i;
for(i = 0;i < 10; ++i) {
printf("My counter: %d ", i);
sleep(2);
}
return 0;
}
将上面的代碼儲存為dummy2.c。按下面的方法編譯運作:
gcc -o dummy2 dummy2.c
./dummy2 &
現在我們可以用下面的代碼來附着到dummy2上。
?View Code C
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <linux/user.h> /* For user_regs_struct
etc. */
int main(int argc, char *argv[])
{ pid_t traced_process;
struct user_regs_struct regs;
long ins;
if(argc != 2) {
printf("Usage: %s <pid to be traced>\n",
argv[0], argv[1]);
exit(1);
}
traced_process = atoi(argv[1]);
ptrace(PTRACE_ATTACH, traced_process,
NULL, NULL);
wait(NULL);
ptrace(PTRACE_GETREGS, traced_process,
NULL, ®s);
ins = ptrace(PTRACE_PEEKTEXT, traced_process,
regs.eip, NULL);
printf("EIP: %lx Instruction executed: %lx\n",
regs.eip, ins);
ptrace(PTRACE_DETACH, traced_process,
NULL, NULL);
return 0;
}
上面的程式僅僅是附着在子程序上,等待它結束,并測量它的eip( 指令指針)然後釋放子程序。
設定斷點
調試器是怎麼設定斷點的呢?通常是将目前将要執行的指令替換成trap指令,于是被調試的程式就會在這裡停滞,這時調試器就可以察看被調試程式的資訊了。被調試程式恢複運作以後調試器會把原指令再放回來。這裡是一個例子:
?View Code C
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <linux/user.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);
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);
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 * 4, data.val);
++i;
laddr += long_size;
}
j = len % long_size;
if(j != 0) {
memcpy(data.chars, laddr, j);
ptrace(PTRACE_POKEDATA, child,
addr + i * 4, data.val);
}
}
int main(int argc, char *argv[])
{ pid_t traced_process;
struct user_regs_struct regs, newregs;
long ins;
/* int 0x80, int3 */
char code[] = {0xcd,0x80,0xcc,0};
char backup[4];
if(argc != 2) {
printf("Usage: %s <pid to be traced>\n",
argv[0], argv[1]);
exit(1);
}
traced_process = atoi(argv[1]);
ptrace(PTRACE_ATTACH, traced_process,
NULL, NULL);
wait(NULL);
ptrace(PTRACE_GETREGS, traced_process,
NULL, ®s);
/* Copy instructions into a backup variable */
getdata(traced_process, regs.eip, backup, 3);
/* Put the breakpoint */
putdata(traced_process, regs.eip, code, 3);
/* Let the process continue and execute
the int 3 instruction */
ptrace(PTRACE_CONT, traced_process, NULL, NULL);
wait(NULL);
printf("The process stopped, putting back "
"the original instructions\n");
printf("Press <enter> to continue\n");
getchar();
putdata(traced_process, regs.eip, backup, 3);
/* Setting the eip back to the original
instruction to let the process continue */
ptrace(PTRACE_SETREGS, traced_process,
NULL, ®s);
ptrace(PTRACE_DETACH, traced_process,
NULL, NULL);
return 0;
}
上面的程式将把三個byte的内容進行替換以執行trap指令,等被調試程序停滞以後,我們把原指令再替換回來并把eip修改為原來的值。下面的圖中示範了指令的執行過程

1. 程序停滞後
2. 替換入trap指令
3.斷點成功,控制權交給了調試器
4. 繼續運作,将原指令替換回來并将eip複原
在了解了斷點的機制以後,往運作中的程式裡面添加指令也不再是難事了,下面的代碼會使原程式多出一個”hello world”的輸出
這時一個簡單的”hello world”程式,當然為了我們的特殊需要作了點修改:
?View Code C
void main()
{
__asm__("
jmp forward
backward:
popl %esi # Get the address of
# hello world string
movl $4, %eax # Do write system call
movl $2, %ebx
movl %esi, %ecx
movl $12, %edx
int $0x80
int3 # Breakpoint. Here the
# program will stop and
# give control back to
# the parent
forward:
call backward
.string \"Hello World\\n\""
);
}
使用
gcc -o hello hello.c
來編譯它。
在backward和forward之間的跳轉是為了使程式能夠找到”hello world” 字元串的位址。
使用GDB我們可以得到上面那段程式的機器碼。啟動GDB,然後對程式進行反彙編:
?View Code C
(gdb) disassemble main
Dump of assembler code for function main:
0x80483e0 <main>: push %ebp
0x80483e1 <main+1>: mov %esp,%ebp
0x80483e3 <main+3>: jmp 0x80483fa <forward>
End of assembler dump.
(gdb) disassemble forward
Dump of assembler code for function forward:
0x80483fa <forward>: call 0x80483e5 <backward>
0x80483ff <forward+5>: dec %eax
0x8048400 <forward+6>: gs
0x8048401 <forward+7>: insb (%dx),%es:(%edi)
0x8048402 <forward+8>: insb (%dx),%es:(%edi)
0x8048403 <forward+9>: outsl %ds:(%esi),(%dx)
0x8048404 <forward+10>: and %dl,0x6f(%edi)
0x8048407 <forward+13>: jb 0x8048475
0x8048409 <forward+15>: or %fs:(%eax),%al
0x804840c <forward+18>: mov %ebp,%esp
0x804840e <forward+20>: pop %ebp
0x804840f <forward+21>: ret
End of assembler dump.
(gdb) disassemble backward
Dump of assembler code for function backward:
0x80483e5 <backward>: pop %esi
0x80483e6 <backward+1>: mov $0x4,%eax
0x80483eb <backward+6>: mov $0x2,%ebx
0x80483f0 <backward+11>: mov %esi,%ecx
0x80483f2 <backward+13>: mov $0xc,%edx
0x80483f7 <backward+18>: int $0x80
0x80483f9 <backward+20>: int3
End of assembler dump.
我們需要使用從man+3到backward+20之間的位元組碼,總共41位元組。使用GDB中的x指令來察看機器碼。
?View Code C
(gdb) x/40bx main+3
<main+3>: eb 15 5e b8 04 00 00 00
<backward+6>: bb 02 00 00 00 89 f1 ba
<backward+14>: 0c 00 00 00 cd 80 cc
<forward+1>: e6 ff ff ff 48 65 6c 6c
<forward+9>: 6f 20 57 6f 72 6c 64 0a
已經有了我們想要執行的指令,還等什麼呢?隻管把它們根前面那個例子一樣插入到被調試程式中去!
代碼:
?View Code C
int main(int argc, char *argv[])
{ pid_t traced_process;
struct user_regs_struct regs, newregs;
long ins;
int len = 41;
char insertcode[] =
"\xeb\x15\x5e\xb8\x04\x00"
"\x00\x00\xbb\x02\x00\x00\x00\x89\xf1\xba"
"\x0c\x00\x00\x00\xcd\x80\xcc\xe8\xe6\xff"
"\xff\xff\x48\x65\x6c\x6c\x6f\x20\x57\x6f"
"\x72\x6c\x64\x0a\x00";
char backup[len];
if(argc != 2) {
printf("Usage: %s <pid to be traced>\n",
argv[0], argv[1]);
exit(1);
}
traced_process = atoi(argv[1]);
ptrace(PTRACE_ATTACH, traced_process,
NULL, NULL);
wait(NULL);
ptrace(PTRACE_GETREGS, traced_process,
NULL, ®s);
getdata(traced_process, regs.eip, backup, len);
putdata(traced_process, regs.eip,
insertcode, len);
ptrace(PTRACE_SETREGS, traced_process,
NULL, ®s);
ptrace(PTRACE_CONT, traced_process,
NULL, NULL);
wait(NULL);
printf("The process stopped, Putting back "
"the original instructions\n");
putdata(traced_process, regs.eip, backup, len);
ptrace(PTRACE_SETREGS, traced_process,
NULL, ®s);
printf("Letting it continue with "
"original flow\n");
ptrace(PTRACE_DETACH, traced_process,
NULL, NULL);
return 0;
}
将代碼插入到自由空間
在前面的例子中我們将代碼直接插入到了正在執行的指令流中,然而,調試器可能會被這種行為弄糊塗,是以我們決定把指令插入到程序中的自由空間中去。通過察看/proc/pid/maps可以知道這個程序中自由空間的分布。接下來這個函數可以找到這個記憶體映射的起始點:
?View Code C
long freespaceaddr(pid_t pid)
{
FILE *fp;
char filename[30];
char line[85];
long addr;
char str[20];
sprintf(filename, "/proc/%d/maps", pid);
fp = fopen(filename, "r");
if(fp == NULL)
exit(1);
while(fgets(line, 85, fp) != NULL) {
sscanf(line, "%lx-%*lx %*s %*s %s", &addr,
str, str, str, str);
if(strcmp(str, "00:00") == 0)
break;
}
fclose(fp);
return addr;
}
在/proc/pid/maps中的每一行都對應了程序中一段記憶體區域。主函數的代碼如下:
?View Code C
int main(int argc, char *argv[])
{ pid_t traced_process;
struct user_regs_struct oldregs, regs;
long ins;
int len = 41;
char insertcode[] =
"\xeb\x15\x5e\xb8\x04\x00"
"\x00\x00\xbb\x02\x00\x00\x00\x89\xf1\xba"
"\x0c\x00\x00\x00\xcd\x80\xcc\xe8\xe6\xff"
"\xff\xff\x48\x65\x6c\x6c\x6f\x20\x57\x6f"
"\x72\x6c\x64\x0a\x00";
char backup[len];
long addr;
if(argc != 2) {
printf("Usage: %s <pid to be traced>\n",
argv[0], argv[1]);
exit(1);
}
traced_process = atoi(argv[1]);
ptrace(PTRACE_ATTACH, traced_process,
NULL, NULL);
wait(NULL);
ptrace(PTRACE_GETREGS, traced_process,
NULL, ®s);
addr = freespaceaddr(traced_process);
getdata(traced_process, addr, backup, len);
putdata(traced_process, addr, insertcode, len);
memcpy(&oldregs, ®s, sizeof(regs));
regs.eip = addr;
ptrace(PTRACE_SETREGS, traced_process,
NULL, ®s);
ptrace(PTRACE_CONT, traced_process,
NULL, NULL);
wait(NULL);
printf("The process stopped, Putting back "
"the original instructions\n");
putdata(traced_process, addr, backup, len);
ptrace(PTRACE_SETREGS, traced_process,
NULL, &oldregs);
printf("Letting it continue with "
"original flow\n");
ptrace(PTRACE_DETACH, traced_process,
NULL, NULL);
return 0;
}
ptrace的幕後工作
那麼,在使用ptrace的時候,核心裡發生了聲麼呢?這裡有一段簡要的說明:當一個程序調用了 ptrace( PTRACE_TRACEME, …)之後,核心為該程序設定了一個标記,注明該程序将被跟蹤。核心中的相關原代碼如下:
?View Code C
Source: arch/i386/kernel/ptrace.c
if (request == PTRACE_TRACEME) {
/* are we already being traced? */
if (current->ptrace & PT_PTRACED)
goto out;
/* set the ptrace bit in the process flags. */
current->ptrace |= PT_PTRACED;
ret = 0;
goto out;
}
一次系統調用完成之後,核心察看那個标記,然後執行trace系統調用(如果這個程序正處于被跟蹤狀态的話)。其彙編的細節可以在 arh/i386/kernel/entry.S中找到。
現在讓我們來看看這個sys_trace()函數(位于 arch/i386/kernel/ptrace.c )。它停止子程序,然後發送一個信号給父程序,告訴它子程序已經停滞,這個信号會激活正處于等待狀态的父程序,讓父程序進行相關處理。父程序在完成相關操作以後就調用ptrace( PTRACE_CONT, …)或者 ptrace( PTRACE_SYSCALL, …), 這将喚醒子程序,核心此時所作的是調用一個叫wake_up_process() 的程序排程函數。其他的一些系統架構可能會通過發送SIGCHLD給子程序來達到這個目的。
小結:
ptrace函數可能會讓人們覺得很奇特,因為它居然可以檢測和修改一個運作中的程式。這種技術主要是在調試器和系統調用跟蹤程式中使用。它使程式員可以在使用者級别做更多有意思的事情。已經有過很多在使用者級别下擴充作業系統得嘗試,比如UFO,一個使用者級别的檔案系統擴充,它使用ptrace來實作一些安全機制。
相關連結:
本文的源代碼 下載下傳
Playing with ptrace, Part I — 玩轉ptrace(一)
本文位址:
http://www.kgdb.info/gdb/playing_with_ptrace_part_ii/
版權所有 © 轉載時必須以連結形式注明作者和原始出處!
轉載于:https://www.cnblogs.com/napoleon_liu/articles/1955677.html