Игра с ptrace, часть I
Привет всем! Данная статья является вольным переводом статьи Pradeep Padala. Статья была написана для 32-bit системы Linux. Если взять исходные тексты из статьи и попытаться их использовать на 64-х битной системе , то большая часть будет работать неправильно. А примеры из II части неправильно работают даже на 32-bit системе. Все дело конечно в том, что с момента написания статьи прошло слишком много времени (многие изменилось). Помните , что все исходные коды , приведенные в этой статье для 64-bit платформы ( в оригинальной статье для 32-bit платформы). Так что , это не просто перевод , а переосмысленная статья, которая конечно же базируется на первоначальном интеллектуальном труде Pradeep Padala.
-------------------------------------------------------------------------------------------------------------------
Задумывались
ли вы, как системные вызовы могут быть перехвачены? Вы когда-нибудь пытались
обмануть ядро, изменив аргументы системного вызова? Задумывались ли вы, как
отладчики останавливают запущенный процесс и позволяют вам контролировать
процесс?
Если вы думаете об использовании сложного программирования ядра для выполнения задачи, то вы ошибаетесь. Linux предоставляет элегантный механизм для достижения всех этих целей: системный вызов ptrace (Process Trace). Ptrace предоставляет механизм, с помощью которого родительский процесс может наблюдать и контролировать выполнение другого процесса. Он может проверять и изменять свой основной образ и регистры и используется главным образом для реализации отладки точек останова и отслеживания системных вызовов.
В этой статье мы узнаем, как перехватить системный вызов и изменить его аргументы. Во второй части статьи мы будем изучать передовые методы - установка точек останова и внедрение кода в работающую программу. Мы заглянем в регистры дочернего процесса и сегмент данных и изменим их содержимое. Мы также опишем способ внедрения кода, чтобы процесс можно было остановить и выполнить произвольные инструкции.
Основы
Операционные системы предлагают услуги через стандартный механизм, называемый
системными вызовами. Они предоставляют стандартный API для доступа к базовому
оборудованию и низкоуровневым службам, таким как файловые системы. Когда процесс
хочет вызвать системный вызов, он помещает аргументы системных вызовов в
регистры и вызывает прерывание 0x80 (для i386) и syscall (для x86-64). Это
прерывание похоже на вход в режим ядра, и ядро выполнит системный вызов после
изучения аргументов.
В архитектуре i386, номер системного вызова заносится в
регистр %eax. Аргументы этого системного вызова заносятся в регистры %ebx, %ecx,
%edx, %esi и %edi в указанном порядке. Например, вызов:
Write(2, "Hello", 5)
ориентировочно можно было бы перевести в:
movl $4, %eax
movl $2, %ebx
movl $hello,%ecx
movl $5, %edx
Int $0x80
где $hello указывает на строковый литерал "Hello".
В архитектуре
64-bit , номер системного вызова заносится в регистр RAX , а аргументы заносятся
в регистры: %rdi, %rsi, %rdx, %rcx, %r8 и %r9 для пользовательского уровня. А
для интерфейса ядра %rdi, %rsi, %rdx, %r10, %r8 и %r9 . А системный вызов
осуществляется с помощью инструкции syscall. Примерно это выглядит так:
mov $1, %rax mov $2, %rdi mov $hello,%rsi mov $5, %rdx syscall
Итак, где же появляется ptrace? Перед выполнением системного вызова ядро
проверяет, отслеживается ли процесс. Если это так, ядро останавливает процесс и
дает контроль над процессом отслеживания, чтобы оно могло просматривать и
изменять регистры отслеживаемого процесса.
Давайте поясним это на примере того, как этот процесс работает. Но прежде я выкладываю заголовочный файл, который необходим для всех примеров ниже (в том числе для второй части статьи):
ptrace.bi:
#INCLUDE "crt.bi" #INCLUDE "crt/linux/unistd.bi" #INCLUDE "crt/linux/fcntl.bi" '/* Type of the REQUEST argument to `ptrace.' */ Enum ptrace_request '/* Indicate that the process making this request should be traced. 'All signals received by this process can be intercepted by its 'parent, and its parent can use the other `ptrace' requests. */ PTRACE_TRACEME = 0, #DEFINE PT_TRACE_ME PTRACE_TRACEME '/* Return the word in the process's text space at address ADDR. */ PTRACE_PEEKTEXT = 1, #DEFINE PT_READ_I PTRACE_PEEKTEXT '/* Return the word in the process's data space at address ADDR. */ PTRACE_PEEKDATA = 2, #DEFINE PT_READ_D PTRACE_PEEKDATA '/* Return the word in the process's user area at offset ADDR. */ PTRACE_PEEKUSER = 3, #DEFINE PT_READ_U PTRACE_PEEKUSER '/* Write the word DATA into the process's text space at address ADDR. */ PTRACE_POKETEXT = 4, #DEFINE PT_WRITE_I PTRACE_POKETEXT '/* Write the word DATA into the process's data space at address ADDR. */ PTRACE_POKEDATA = 5, #DEFINE PT_WRITE_D PTRACE_POKEDATA '/* Write the word DATA into the process's user area at offset ADDR. */ PTRACE_POKEUSER = 6, #DEFINE PT_WRITE_U PTRACE_POKEUSER '/* Continue the process. */ PTRACE_CONT = 7, #DEFINE PT_CONTINUE PTRACE_CONT '/* Kill the process. */ PTRACE_KILL = 8, #DEFINE PT_KILL PTRACE_KILL '/* Single step the process. 'This is not supported on all machines. */ PTRACE_SINGLESTEP = 9, #DEFINE PT_STEP PTRACE_SINGLESTEP '/* Get all general purpose registers used by a processes. 'This is not supported on all machines. */ PTRACE_GETREGS = 12, #DEFINE PT_GETREGS PTRACE_GETREGS '/* Set all general purpose registers used by a processes. 'This is not supported on all machines. */ PTRACE_SETREGS = 13, #DEFINE PT_SETREGS PTRACE_SETREGS '/* Get all floating point registers used by a processes. 'This is not supported on all machines. */ PTRACE_GETFPREGS = 14, #DEFINE PT_GETFPREGS PTRACE_GETFPREGS '/* Set all floating point registers used by a processes. 'This is not supported on all machines. */ PTRACE_SETFPREGS = 15, #DEFINE PT_SETFPREGS PTRACE_SETFPREGS '/* Attach to a process that is already running. */ PTRACE_ATTACH = 16, #DEFINE PT_ATTACH PTRACE_ATTACH '/* Detach from a process attached to with PTRACE_ATTACH. */ PTRACE_DETACH = 17, #DEFINE PT_DETACH PTRACE_DETACH '/* Get all extended floating point registers used by a processes. 'This is not supported on all machines. */ PTRACE_GETFPXREGS = 18, #DEFINE PT_GETFPXREGS PTRACE_GETFPXREGS '/* Set all extended floating point registers used by a processes. 'This is not supported on all machines. */ PTRACE_SETFPXREGS = 19, #DEFINE PT_SETFPXREGS PTRACE_SETFPXREGS '/* Continue and stop at the next (return from) syscall. */ PTRACE_SYSCALL = 24, #DEFINE PT_SYSCALL PTRACE_SYSCALL '/* Set ptrace filter options. */ PTRACE_SETOPTIONS = &h4200, #DEFINE PT_SETOPTIONS PTRACE_SETOPTIONS '/* Get last ptrace message. */ PTRACE_GETEVENTMSG = &h4201, #DEFINE PT_GETEVENTMSG PTRACE_GETEVENTMSG '/* Get siginfo for process. */ PTRACE_GETSIGINFO = &h4202, #DEFINE PT_GETSIGINFO PTRACE_GETSIGINFO '/* Set new siginfo for process. */ PTRACE_SETSIGINFO = &h4203 #DEFINE PT_SETSIGINFO PTRACE_SETSIGINFO End Enum Type user_regs_struct As Uinteger r15 As Uinteger r14 As Uinteger r13 As Uinteger r12 As Uinteger rbp As Uinteger rbx As Uinteger r11 As Uinteger r10 As Uinteger r9 As Uinteger r8 As Uinteger rax As Uinteger rcx As Uinteger rdx As Uinteger rsi As Uinteger rdi As Uinteger orig_rax As Uinteger rip As Uinteger cs As Uinteger eflags As Uinteger rsp As Uinteger ss As Uinteger fs_base As Uinteger gs_base As Uinteger ds As Uinteger es As Uinteger fs As Uinteger gs End Type Extern "C" Declare Function wait_ Alias "wait" (wiStatus As Integer Ptr) As pid_t Declare Function ptrace(request As ptrace_request, pid As pid_t, addr As Any Ptr, uData As Any Ptr) As Integer End Extern Const ORIG_RAX =15 Const RAX =10 Const RBX =5 Const RCX =11 Const RDX =12 Const RSI =13 Const RDI =14 Const SYS_write =1
И так, вот пример:
#INCLUDE "ptrace.bi" Dim As pid_t pidChild Dim As Long lOrigRAX pidChild = fork() If pidChild = 0 Then ptrace(PTRACE_TRACEME, 0, NULL, NULL) execl("/bin/ls", "ls", NULL) Else wait_(NULL) lOrigRAX = ptrace(PTRACE_PEEKUSER, pidChild , Cast(Any Ptr,Cint(8 * ORIG_RAX)), NULL) printf(!"The child made a " _ _ !"system call %ld\n", lOrigRAX) ptrace(PTRACE_CONT, pidChild, NULL, NULL) Endif Sleep
При запуске эта программа печатает:
The child made a system call 59
наряду с выводом ls. Системный вызов номер 59 - execve, это первый системный вызов, выполняемый дочерним процессом. Для справки номера системных вызовов можно найти в /usr/include/x86_64-linux-gnu/asm/unistd_64.h Как видно из примера, процесс разветвляется с помощью fork() , и дочерний процесс -это именно тот процесс, который мы хотим отследить. Перед запуском exec дочерний процесс вызывает ptrace с первым аргументом, равным PTRACE_TRACEME. Это сообщает ядру, что процесс отслеживается, и когда дочерний процесс выполняет системный вызов execve, он передает управление своему родителю. Родитель ожидает уведомления от ядра с помощью вызова wait (). Затем родитель может проверить аргументы системного вызова или выполнить другие действия, такие как просмотр регистров.Когда происходит системный вызов, ядро сохраняет исходное содержимое регистра rax, который содержит номер системного вызова. Мы можем прочитать это значение из дочернего сегмента USER, вызвав ptrace с первым аргументом PTRACE_PEEKUSER, как показано выше. После того как мы закончили проверку системного вызова, дочерний процесс может продолжить вызов ptrace с первым аргументом PTRACE_CONT, который позволяет продолжить системный вызов.
Параметры ptrace
ptrace вызывается с четырьмя аргументами:
Long ptrace (Enum __ptrace_request request, pid_t pid, void * addr, void * Data);
Первый аргумент определяет поведение ptrace и то, как используются другие аргументы. Значение запроса должно быть одним из: 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. Значение каждого из этих запросов будет объяснено в оставшейся части статьи.
Чтение параметров системного вызова
Вызывая ptrace с PTRACE_PEEKUSER в качестве первого аргумента, мы можем
проверить содержимое области USER, где хранится содержимое регистра и другая
информация. Ядро хранит содержимое регистров в этой области, чтобы родительский
процесс исследовал их через ptrace.
Давайте покажем это на примере:
#INCLUDE "ptrace.bi" Dim As pid_t pidChild Dim As Long lOrigRAX, lRax Dim As Integer lParams(2) Dim As Integer iStatus Dim As Integer insyscall Dim As user_regs_struct regs pidChild = fork() If pidChild = 0 Then ptrace(PTRACE_TRACEME, 0, NULL, NULL) execl("/bin/ls", "ls", NULL) Else Do wait_ (@iStatus) If (iStatus And &h7f) = 0 Then Exit Do lOrigRAX = ptrace(PTRACE_PEEKUSER, pidChild, Cast(Any Ptr,Cint(8 * ORIG_RAX)), NULL) If lOrigRAX = SYS_write Then If insyscall = 0 Then '/* Syscall entry */ insyscall = 1 lParams(0) = ptrace(PTRACE_PEEKUSER , pidChild, Cast(Any Ptr, 8 * RDI), NULL) lParams(1) = ptrace(PTRACE_PEEKUSER, pidChild, Cast(Any Ptr, 8 * RSI), NULL) lParams(2) = ptrace(PTRACE_PEEKUSER, pidChild, Cast(Any Ptr, 8 * RDX), NULL) printf(!"Write called with %ld, %ld, %ld\n", lParams(0), lParams(1), lParams(2)) Else '{ /* Syscall exit */ lRax = ptrace(PTRACE_PEEKUSER, pidChild, Cast(Any Ptr,Cint(8 * RAX)), NULL) printf(!"Write returned with %ld\n", lRax) insyscall = 0 Endif Endif ptrace(PTRACE_SYSCALL, pidChild, NULL, NULL) Loop Endif Sleep
Эта программа должна напечатать вывод, подобный следующему:
ppadala@linux:~/ptrace > ls a.out dummy.s ptrace.txt libgpm.html registers.c syscallparams.c dummy ptrace.html simple.c ppadala@linux:~/ptrace > ./a.out Write called with 1, 1075154944, 48 a.out dummy.s ptrace.txt Write returned with 48 Write called with 1, 1075154944, 59 libgpm.html registers.c syscallparams.c Write returned with 59 Write called with 1, 1075154944, 30 dummy ptrace.html simple.c Write returned with 30
Здесь мы отслеживаем системные вызовы write. В данном случае ls выполнила три системных вызова write. Вызов ptrace с первым аргументом PTRACE_SYSCALL заставляет ядро останавливать дочерний процесс всякий раз, когда выполняется вход или выход из системного вызова. Это эквивалентно выполнению PTRACE_CONT и остановке при следующем входе / выходе из системного вызова. В предыдущем примере мы использовали PTRACE_PEEKUSER для просмотра аргументов системного вызова write. Когда происходит возврат системного вызова, возвращаемое значение помещается в %rax, и его можно прочитать, как показано в этом примере. Переменная iStatus в вызове wait используется для проверки выхода ребенка. Это типичный способ проверить, был ли ребенок остановлен ptrace или смог выйти.
Чтение значений регистра
Если вы хотите прочитать значения регистров во время входа или выхода из системного вызова, процедура, показанная выше, может быть громоздкой. Вызов ptrace с первым аргументом PTRACE_GETREGS поместит все регистры в один вызов. Код для получения значений регистра выглядит так:
#INCLUDE "ptrace.bi" Dim As pid_t pidChild Dim As Long lOrigRAX, lRax Dim As Integer iStatus Dim As Integer insyscall Dim As user_regs_struct regs pidChild = fork() If pidChild = 0 Then ptrace(PTRACE_TRACEME, 0, NULL, NULL) execl("/bin/ls", "ls", NULL) Else Do wait_ (@iStatus) If (iStatus And &h7f) = 0 Then Exit Do lOrigRAX = ptrace(PTRACE_PEEKUSER, pidChild, Cast(Any Ptr,Cint(8 * ORIG_RAX)), NULL) If lOrigRAX = SYS_write Then If insyscall = 0 Then '/* Syscall entry */ insyscall = 1 ptrace(PTRACE_GETREGS, pidChild, NULL, @regs) printf(!"Write called with %ld, %ld, %ld\n", regs.rdi, regs.rsi, regs.rdx) Else '{ /* Syscall exit */ lRax = ptrace(PTRACE_PEEKUSER, pidChild, Cast(Any Ptr,Cint(8 * RAX)), NULL) printf(!"Write returned "_ _ !"with %ld\n", lRax) insyscall = 0 Endif Endif ptrace(PTRACE_SYSCALL, pidChild, NULL, NULL) Loop Endif Sleep
Этот код аналогичен предыдущему примеру, за исключением вызова ptrace с параметром PTRACE_GETREGS. Здесь мы использовали структуру user_regs_struct, определенную в /usr/include/x86_64-linux-gnu/sys/user.h , чтобы прочитать значения регистра.
Веселимся
Теперь пришло время повеселиться. В следующем примере мы перевернем строку, переданную системному вызову write:
#INCLUDE "ptrace.bi" Sub reversestring(s As Zstring Ptr) Dim sTemp As String = *s For i As Long = 0 To Len(sTemp) -1 (*s)[i] = sTemp[(Len(sTemp) -1)-i] Next End Sub Sub getdata( pidChild As pid_t, iAddr As Integer, szBuf As Zstring Ptr, iLen As Integer) Dim As Zstring Ptr szTempAddr Dim As Integer i, j Union u Dim As Long iValue Dim As Byte bBytes(4) End Union Dim uData As u i = 0 j = iLen / 4 szTempAddr = szBuf Do uData.iValue = ptrace(PTRACE_PEEKDATA, pidChild, Cast(Any Ptr,iAddr + i * 4), NULL) memcpy(szTempAddr, @(uData.bBytes(0)), 4) i+=1 szTempAddr += 4 If i>=j Then Exit Do Loop j = iLen Mod 4 If j <> 0 Then uData.iValue = ptrace(PTRACE_PEEKDATA, pidChild, Cast(Any Ptr,iAddr + i * 4), NULL) memcpy(szTempAddr, @(uData.bBytes(0)), j) Endif szBuf[iLen] = !"\0" End Sub Sub putdata(pidChild As pid_t , iAddr As Integer,szBuf As Zstring Ptr,iLen As Integer) Dim As Zstring Ptr szTempAddr Dim As Integer i, j Union u Dim As Long iValue Dim As Byte bBytes(4) End Union Dim uData As u i = 0 j = iLen / 4 szTempAddr = szBuf While i < j memcpy(@(uData.bBytes(0)), szTempAddr, 4) ptrace(PTRACE_POKEDATA, pidChild, Cast(Any Ptr,iAddr + i * 4), Cast(Any Ptr,Cint(uData.iValue))) i+=1 szTempAddr += 4 Wend j = iLen Mod 4 If j <> 0 Then memcpy(@(uData.bBytes(0)), szTempAddr, j) ptrace(PTRACE_POKEDATA, pidChild, Cast(Any Ptr,iAddr + i * 4), Cast(Any Ptr,Cint(uData.iValue))) Endif End Sub Dim As pid_t pidChild pidChild = fork() If pidChild = 0 Then ptrace(PTRACE_TRACEME, 0, NULL, NULL) execl("/bin/ls", "ls", NULL) Else Dim As Long lOrigRAX Dim As Integer iParams(3) Dim As Integer iStatus Dim As Zstring Ptr szBuf, szTempAddr Dim As Integer iToogle = 0 Do wait_(@iStatus) If (iStatus And &h7f) = 0 Then Exit Do lOrigRAX = ptrace(PTRACE_PEEKUSER, pidChild, Cast(Any Ptr,8 * ORIG_RAX), NULL) If lOrigRAX = SYS_write Then If iToogle = 0 Then iToogle = 1 iParams(0) = ptrace(PTRACE_PEEKUSER, pidChild, Cast(Any Ptr,8 * RDI), NULL) iParams(1) = ptrace(PTRACE_PEEKUSER, pidChild, Cast(Any Ptr,8 * RSI), NULL) iParams(2) = ptrace(PTRACE_PEEKUSER, pidChild, Cast(Any Ptr,8 * RDX), NULL) szBuf = calloc((iParams(2)+10) , 1) getdata(pidChild, iParams(1), szBuf, iParams(2)) reversestring(szBuf) putdata(pidChild, iParams(1), szBuf, iParams(2)) Else iToogle = 0 Endif Endif ptrace(PTRACE_SYSCALL, pidChild, NULL, NULL) Loop Endif Sleep
Вывод выглядит примерно так:
ppadala@linux: ~ / ptrace> ls a.out dummy.s ptrace.txt libgpm.html registers.c syscallparams.c пустышка ptrace.html simple.c ppadala@linux: ~ / ptrace> ./a.out txt.ecartp s.ymmud tuo.a c.sretsiger lmth.mpgbil c.llacys_egnahc c.elpmis lmth.ecartp ymmud
В этом примере используются все ранее обсужденные концепции, а также некоторые другие. В нем мы используем вызовы ptrace с PTRACE_POKEDATA для изменения значений данных. Он работает точно так же, как PTRACE_PEEKDATA, за исключением того, что он читает и записывает данные, которые потомок передает в аргументах системному вызову, тогда как PEEKDATA только читает данные.
Пошаговый
ptrace предоставляет функции для пошагового выполнения кода дочернего
элемента. Вызов ptrace (PTRACE_SINGLESTEP, ..) сообщает ядру, что нужно
останавливать дочерний элемент при каждой инструкции и позволить родительскому
элементу получить контроль. В следующем примере показан способ чтения
инструкции, выполняемой при выполнении системного вызова. Я создал небольшой
фиктивный исполняемый файл, чтобы вы могли понять, что происходит, вместо того,
чтобы беспокоиться о вызовах, сделанных libc.
Вот код для dummy1.s. Он
написан на ассемблере. Для компиляции используйте gcc -o dummy1
dummy1.s :
.data hello: .string "hello world\n" .globl main main: mov $1, %rax /* syscall number */ mov $1, %rdi /* stdout */ mov $hello, %rsi /* buffer */ mov $12, %rdx /* Len */ syscall /* Exit */ mov $60, %rax /* syscall number */ mov $0, %rdi /* Exit status */ syscall
Программа-пример, которая пошагово выполняет приведенный выше код:
#INCLUDE "ptrace.bi" Dim As pid_t pidChild Dim As Long lOrigRAX pidChild = fork() If pidChild = 0 Then ptrace(PTRACE_TRACEME, 0, NULL, NULL) execl("./dummy1", "dummy1", NULL) Else Dim As Integer iStatus Union u Dim As Long iValue Dim As Byte bBytes(4) End Union Dim uData As u Dim As user_regs_struct regs Dim As Integer iStart = 0 Dim As Long lins Do wait_(@iStatus) If (iStatus And &h7f) = 0 Then Exit Do ptrace(PTRACE_GETREGS, pidChild, NULL, @regs) If iStart = 1 Then lins = ptrace(PTRACE_PEEKTEXT, pidChild, Cast(Any Ptr , regs.rip), NULL) printf(!"EIP: %lx Instruction executed: %lx\n", regs.rip, lins) Endif If regs.orig_rax = SYS_write Then iStart = 1 ptrace(PTRACE_SINGLESTEP, pidChild, NULL, NULL) Else ptrace(PTRACE_SYSCALL, pidChild, NULL, NULL) Endif Loop Endif Sleep
Возможно, вам придется взглянуть на руководство Intel, чтобы разобраться в том, что напечатает эта программа. Использование пошагового выполнения кода для более сложных процессов, таких как установка точек останова (брекпоинтов), требует тщательного проектирования и более сложного кода. Во второй части мы увидим, как можно вставлять точки останова и вводить код в работающую программу.
Оригинал статьи , написанный Pradeep Padala: https://www.linuxjournal.com/article/6100?page=0,3