Игра с ptrace, часть II

В первой части статьи мы увидели, как ptrace может использоваться для отслеживания системных вызовов и изменения аргументов системных вызовов. В этой статье мы исследуем передовые методы, такие как установка точек останова и внедрение кода в работающие программы. Отладчики используют эти методы для установки точек останова и выполнения обработчиков отладки. Как и в части I, весь код в этой статье для 64-bit платформы.

Присоединение к запущенному процессу

В первой части мы запускали процесс, который отслеживался как дочерний после вызова ptrace (PTRACE_TRACEME, ..). Если вы просто хотите увидеть, как процесс выполняет системные вызовы, и отслеживать программу, этого будет достаточно. Если вы хотите отследить или отладить уже запущенный процесс, следует использовать ptrace (PTRACE_ATTACH, ..).

Когда ptrace (PTRACE_ATTACH, ..) вызывается с отслеживаемым pid, это примерно эквивалентно процессу, вызывающему ptrace (PTRACE_TRACEME, ..) и становящемуся потомком процесса трассировки. Отслеживаемому процессу отправляется сигнал SIGSTOP, поэтому мы можем проверить и изменить процесс как обычно. После того, как мы закончили с изменениями или трассировкой, мы можем позволить отслеживаемому процессу продолжаться самостоятельно, вызывая ptrace (PTRACE_DETACH, ..).

Ниже приведен код небольшой программы для трассировки:

For i As Long = 0 To 20

    ? "My counter: "; i

    Sleep(2000 , 1) 

Next

Сохраните программу как dummy2.bas. Скомпилируйте и запустите:

fbc dummy2.bas
./dummy2 &

Вызов с символом "&" отобразит в терминале pid процесса , который нам понадобится для передачи в командной строке "программе-отладчику". Теперь мы можем подключиться к dummy2, используя код ниже (ptrace.bas):

#INCLUDE "ptrace.bi"

Dim As pid_t traced_process

Dim As user_regs_struct regs

Dim As Integer lins

traced_process = Val(Command())

If traced_process Then
    
    ptrace(PTRACE_ATTACH, traced_process, NULL, NULL)
    
    wait_(null)
    
    ptrace(PTRACE_GETREGS, traced_process, NULL, @regs)
    
    lins = ptrace(PTRACE_PEEKTEXT, traced_process, Cast(Any Ptr,regs.rip), NULL)
    
    printf(!"EIP: %lx Instruction executed: %lx\n", regs.rip, lins)
    
    ptrace(PTRACE_DETACH, traced_process, NULL, NULL)
    
Endif

Скомпилируйте данный код как обычно и запустите с pid (в моем случае 4167) примерно так:

sudo ./ptrace 4167

Да, да приходится запускать от суперпользователя. Это же самое касается и любого другого отладчика, когда необходимо присоединиться к процессу. Связано это с мерами безопасности. Возможно у вас на Linux стоят другие разрешения. У меня Linux Mint 18.3 , который основывается на версии Ubuntu 16.04 . Примерно до версии Ubuntu 11 , прав суперпользователя не требовалось. При желании можно убрать эти ограничения временно, отредактировав файл /proc/sys/kernel/yama/ptrace_scope и записав туда 0 . Или убрать ограничения постоянно , отредактировав файл /etc/sysctl.d/10-ptrace.conf и записав туда 0 . (во втором случае , изменения вступят в силу после перезагрузки).

Вышеприведенная программа просто подключается к процессу, ожидает его остановки, проверяет его rip (указатель инструкции) и отключается.

Для внедрения кода используйте ptrace (PTRACE_POKETEXT, ..) или ptrace (PTRACE_POKEDATA, ..) после остановки отслеживаемого процесса.

Установка точек останова

Как отладчики устанавливают точки останова? Как правило, они заменяют команду, которая должна быть выполнена, на команду прерывания (int 3), так что, когда отслеживаемая программа останавливается, программа трассировки, отладчик, может проверить ее. Он заменит исходную инструкцию, как только программа отслеживания продолжит отслеживаемый процесс. Вот пример:

#INCLUDE "ptrace.bi"

Dim As pid_t traced_process

Dim As user_regs_struct regs

Dim As Integer iCode

Dim As Integer iBackup

traced_process = Val(Command())

If traced_process Then
    
    ptrace(PTRACE_ATTACH, traced_process, NULL, NULL)
    
    wait_(NULL)
    
    ptrace(PTRACE_GETREGS, traced_process, NULL, @regs)
    
    ' Copy instructions into a backup variable  
    iBackup = ptrace(PTRACE_PEEKTEXT, traced_process, Cast(Any Ptr,regs.rip), 0)
    
    ' Put the breakpoint
    iCode = (iBackup And &hffffffffffffff00) Or &hcc
    
    ptrace(PTRACE_POKETEXT, traced_process, Cast(Any Ptr,regs.rip), Cast(Any Ptr,iCode))
    
    '/* 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()
    
    ptrace(PTRACE_POKETEXT, traced_process, Cast(Any Ptr,regs.rip), Cast(Any Ptr,iBackup))
    
    '/* Setting the rip back to the original
    'instruction to let the process continue */
    ptrace(PTRACE_SETREGS, traced_process, NULL, @regs)
    
    ptrace(PTRACE_DETACH, traced_process, NULL, NULL)       
    
Endif


Здесь мы после присоединения к процессу:
1) сохраняем данные (регистров и команды по адресу, на который указывает регистр RIP)
2) заменяем текущую команду на команду прерывания int 3 (code: CC)
3) даем возможность выполниться команде прерывания (в итоге регистр RIP увеличивается на 1)
4) ждем от пользователя нажатия клавишы "Enter"
5) восстанавливаем данные по первоначальному адресу RIP в исходное состояние
5) устанавливаем регистры в исходное состояние
7) отсоединяемся от исследуемого процесса, давая возможность выполняться самостоятельно

Теперь, когда у нас есть четкое представление о том, как устанавливаются точки останова, давайте вставим некоторые байты кода в работающую программу. Эти байты кода будут печатать «Hello World».
Следующая программа представляет собой простую программу «Hello World» с модификациями в соответствии с нашими потребностями. Скомпилируйте следующую программу обычным образом (fbc hello.bas):

Asm
    jmp forward
    backward:
    pop rsi
    mov rax, 1
    mov rdi, 1
    mov rdx , 12
    syscall
    Int 3
    nop
    nop
    nop
    nop
    nop
    nop
    forward:
    Call backward
    .string "Hello World\n\0"
End Asm


Прыжки туда-сюда в коде нужны, чтобы получить адрес строки «Hello World». Команды nop нужны для выравнивания кода.
Именно этот код будем внедрять в исследуемый процесс. Но для этого , нам надо получить его HEX коды. Для этой цели будем использовать программу objdump и любой HEX редактор. Для начала посмотрим адреса расположения секций:

objdump -h ./hello

Увидим их имена , размеры и пр. Нас интересует секция .Text (то есть секция кода):

Инд     Имя      Размер         VMA                  LMA                  Файл              Вырав
...............................
12      .text    00002152     00000000004015e0     00000000004015e0     000015e0            2**4
...............................

Ага , запомнили , что адрес этой секции начинается с 4015e0 , ну а если смотреть в HEX редакторе , то 000015e0

Далее выводим дизассемблированный листинг секции .TEXT c синаксисом intel:

objdump -M intel -j .text -d ./hello

Пролистываем до раздела "main" и легко находим наш код:

codefromobjdump.png

Среди прочего кода , легко находится начало (код почти идентичен оригиналу) и конец (по характерным байтам окончания строковой переменной 0A 00).  Нам остается открыть файл hello в любом hex редакторе (в моем случае это bless hex editor) и скопировать hex коды в диапазоне адресов: 172a-175c:

copybytesfrombless.png

Теперь , когда у нас есть внедряемый кусок кода , давайте пристроим его , используя те же принципы , что и в прошлом примере. Разница лишь в том , что в прошлом примере заменялся маленький кусок кода (по сути один байт) , а в этот раз нужно будет заменить , сохранить и восстановить 51 байт. Вот пример:

#INCLUDE "ptrace.bi"

Dim As pid_t traced_process

Dim As user_regs_struct regs

Dim As Integer iLen = 51

Dim As Byte bBackup(50)

Dim As Byte bInsertCode(50) = _
{ &heb , &h1f , &h5e , &h48 , &hc7 , &hc0 , &h01 , &h00 ,_
&h00 , &h00 , &h48 , &hc7 , &hc7 , &h01 , &h00 , &h00 ,_
&h00 , &h48 , &hc7 , &hc2 , &h0c , &h00 , &h00 , &h00 ,_
&h0f , &h05 , &hcc , &h90 , &h90 , &h90 , &h90 , &h90 ,_
&h90 , &he8 , &hdc , &hff , &hff , &hff , &h48 , &h65 ,_
&h6c , &h6c , &h6f , &h20 , &h57 , &h6f , &h72 , &h6c ,_
&h64 , &h0a , &h00 }

Sub getdata( pidChild As pid_t, iAddr As  Integer, szBuf As Zstring Ptr, iLen As Integer)
    
    Dim As Integer i, j
    
    Dim As Integer iReturnData
    
    j = iLen / 8
    
    While i < j 
        
        iReturnData = ptrace(PTRACE_PEEKDATA, pidChild, Cast(Any Ptr,iAddr + i * 8), NULL)
        
        memcpy(szBuf, @iReturnData, 8)
        
        i+=1
        
        szBuf += 8
        
    Wend
    
    j = iLen Mod 8
    
    If j <> 0 Then
        
        iReturnData = ptrace(PTRACE_PEEKDATA, pidChild, Cast(Any Ptr,iAddr + i * 8), NULL)
        
        memcpy(szBuf, @iReturnData, j)
        
    Endif
    
End Sub

Sub putdata(pidChild As pid_t , iAddr As Integer,szBuf As Zstring Ptr,iLen As Integer)
    
    Dim As Integer i, j
    
    Dim As Integer iSendData
    
    Dim As Zstring Ptr szTempAddr = szBuf
    
    j = iLen / 8
    
    While i < j 
        
        memcpy(@iSendData, szTempAddr, 8)
        
        ptrace(PTRACE_POKEDATA, pidChild, Cast(Any Ptr,iAddr + i * 8), Cast(Any Ptr,iSendData))
        
        i+=1
        
        szTempAddr += 8
        
    Wend
    
    j = iLen Mod 8
    
    If j <> 0  Then
        
        memcpy(@iSendData, szTempAddr, j)
        
        ptrace(PTRACE_POKEDATA, pidChild, Cast(Any Ptr,iAddr + i * 8), Cast(Any Ptr,iSendData))
        
    Endif
    
End Sub


traced_process = Val(Command())

If traced_process Then
    
    ptrace(PTRACE_ATTACH, traced_process, NULL, NULL)
    
    wait_(NULL)
    
    ptrace(PTRACE_GETREGS, traced_process, NULL, @regs)
    
    getdata(traced_process, regs.rip, @bBackup(0), iLen)
    
    putdata(traced_process, regs.rip, @bInsertCode(0), iLen)
    
    ptrace(PTRACE_SETREGS, traced_process, NULL, @regs)
    
    ptrace(PTRACE_CONT, traced_process, NULL, NULL)
    
    wait_(NULL)
    
    printf(!"The process stopped, putting back the original instructions\n")
    
    putdata(traced_process, regs.rip, @bBackup(0), iLen)
    
    ptrace(PTRACE_SETREGS, traced_process, NULL, @regs)
    
    printf(!"Letting it continue with original flow\n")
    
    ptrace(PTRACE_DETACH, traced_process, NULL, NULL)        
    
Endif

В примере выше появились две процедуры для получения и отправки любого объема данных. В остальном как я и писал , все похоже на предыдущий пример.

Внедрение кода в свободное пространство

В предыдущем примере мы внедрили код непосредственно в поток исполняемых инструкций. Однако отладчики могут запутаться в таком поведении, поэтому давайте найдем свободное место в процессе и вставим туда код. И так загрузим файл dummy2 в objdump. Сначала найдем место или ту точку в коде , где основной код прервется и где будет подмена RIP(регистра адреса). Я решил это сделать до процедур вывода в терминал, в общем "облюбовал" адрес &h4018с7:

addressinject.png

Далее для того , чтобы найти свободное место (куда можно вставить код hello), можно загрузить файл dummy2 в отладчик. В моем случае это edb-debugger. Открываем отладчик, загружаем туда файл и оказываемся в адресном пространстве библиотеки ld-2.23.so. Нажимаем один раз кнопку "RUN" и оказываемся в адресном пространстве нашего исследуемого файла. Заходим в меню View->Memory Regions и видим , что исполняемые инструкции могут располагаться в диапазоне 400000-405000:

memoryregions.png

Пролистываем окно кода ближе к концу и видим в конце кучу свободного места , забитого нулями:

addressspace.png

Я выбрал адрес &h404945. Ну вот и все , теперь можно посмотреть на пример:

#INCLUDE "ptrace.bi"

Dim As pid_t traced_process

Dim As user_regs_struct regs

Dim As Integer iLen = 51

Dim As Integer iBpAddress = &h4018C7 ' адрес , где мы остановимся и подменим содержимое регистра RIP

Dim As Integer iAddressInjectCode = &h404945 ' адрес , где будет записан внедряемый код для выполнения 

Dim As Byte bBackup(50) ' для сохранения данных по адресу &h404945

Dim As Integer iSmallBackup ' для сохранения данных по адресу &h4018C7

Dim As Byte bInsertCode(50) = _
{ &heb , &h1f , &h5e , &h48 , &hc7 , &hc0 , &h01 , &h00 ,_
&h00 , &h00 , &h48 , &hc7 , &hc7 , &h01 , &h00 , &h00 ,_
&h00 , &h48 , &hc7 , &hc2 , &h0c , &h00 , &h00 , &h00 ,_
&h0f , &h05 , &hcc , &h90 , &h90 , &h90 , &h90 , &h90 ,_
&h90 , &he8 , &hdc , &hff , &hff , &hff , &h48 , &h65 ,_
&h6c , &h6c , &h6f , &h20 , &h57 , &h6f , &h72 , &h6c ,_
&h64 , &h0a , &h00 }

Sub getdata( pidChild As pid_t, iAddr As  Integer, szBuf As Zstring Ptr, iLen As Integer)
    
    Dim As Integer i, j
    
    Dim As Integer iReturnData
    
    j = iLen / 8
    
    While i < j 
        
        iReturnData = ptrace(PTRACE_PEEKDATA, pidChild, Cast(Any Ptr,iAddr + i * 8), NULL)
        
        memcpy(szBuf, @iReturnData, 8)
        
        i+=1
        
        szBuf += 8
        
    Wend
    
    j = iLen Mod 8
    
    If j <> 0 Then
        
        iReturnData = ptrace(PTRACE_PEEKDATA, pidChild, Cast(Any Ptr,iAddr + i * 8), NULL)
        
        memcpy(szBuf, @iReturnData, j)
        
    Endif
    
End Sub

Sub putdata(pidChild As pid_t , iAddr As Integer,szBuf As Zstring Ptr,iLen As Integer)
    
    Dim As Integer i, j
    
    Dim As Integer iSendData
    
    Dim As Zstring Ptr szTempAddr = szBuf
    
    j = iLen / 8
    
    While i < j 
        
        memcpy(@iSendData, szTempAddr, 8)
        
        ptrace(PTRACE_POKEDATA, pidChild, Cast(Any Ptr,iAddr + i * 8), Cast(Any Ptr,iSendData))
        
        i+=1
        
        szTempAddr += 8
        
    Wend
    
    j = iLen Mod 8
    
    If j <> 0  Then
        
        memcpy(@iSendData, szTempAddr, j)
        
        ptrace(PTRACE_POKEDATA, pidChild, Cast(Any Ptr,iAddr + i * 8), Cast(Any Ptr,iSendData))
        
    Endif
    
End Sub


traced_process = Val(Command())

If traced_process Then
    
    ptrace(PTRACE_ATTACH, traced_process, NULL, NULL)
    
    wait_(NULL)
    
    Dim As Integer iTempData , iTempLen = 8
    
    getdata(traced_process, iBpAddress, Cast(Any Ptr,@iSmallBackup), iTempLen)
    
    iTempData = Cint(iSmallBackup & &hffffffffffffff00) Or &hCC
    
    putdata(traced_process, iBpAddress , Cast(Any Ptr,@iTempData), iTempLen)
    
    ptrace(PTRACE_CONT, traced_process, NULL, NULL)
    
    wait_(NULL)
    
    ptrace(PTRACE_GETREGS, traced_process, NULL, @regs)
    
    putdata(traced_process, iBpAddress , Cast(Any Ptr,@iSmallBackup), iTempLen)
    
    getdata(traced_process, iAddressInjectCode, @bBackup(0), iLen)
    
    putdata(traced_process, iAddressInjectCode ,@bInsertCode(0) , iLen)
    
    regs.rip = iAddressInjectCode
    
    ptrace(PTRACE_SETREGS, traced_process, NULL, @regs)
    
    ptrace(PTRACE_CONT, traced_process, NULL, NULL)
    
    wait_(NULL)
    
    putdata(traced_process, iAddressInjectCode , @bBackup(0), iLen)
    
    regs.rip = iBpAddress
    
    ptrace(PTRACE_SETREGS, traced_process, NULL, @regs)
    
    ptrace(PTRACE_DETACH, traced_process, NULL, NULL)        
    
Endif

О том, что происходит в коде:
1) После присоединения к процессу, сохраняем то, что лежит по адресу &h4018с7 и запишем туда код брекпоинта (&hСС).
2) Дадим команду на продолжение работы процесса и процесс остановится по адресу &h4018с7
3) Сохраним регистры и восстановим содержимое по адресу &h4018с7 (по сути уберем брекпоинт)
4) Далее сохраним данные, которые лежат в свободной области начиная с адреса &h404945 (51 байт). Можно было это и не делать , ведь там все равно нули , но мало ли...
5) Далее запишем наш внедряемый код начиная с адреса &h404945
6) Подменим регистр RIP , записав туда &h404945 и выполним команду установки\обновления регистров с помощью ptrace(PTRACE_SETREGS...
7) Заставим процесс выполняться дальше с помощью ptrace(PTRACE_CONT... По сути начнет выполняться наш внедряемый код с адреса &h404945 и процесс остановится на инструкции int 3 .
8) Восстановим данные , начиная с адреса &h404945 , на первоначальные нули :) Запишем в регистр RIP адрес &h4018с7, где мы прервали процесс dummy2 . Восстановим регистры (в изначальные значения) с помощью  ptrace(PTRACE_SETREGS...
9) Отсоединимся от процесса dummy2, пусть он дальше работает сам по себе.

 

-------------------------------------------------------------------------------------------

 

Ниже идет просто перевод на русский язык без особого осмысления мною и без желания что-либо проверять.

За кулисами

Так что же происходит внутри ядра? Как реализован ptrace? Этот раздел может быть отдельной статьей; Тем не менее, вот краткое описание того, что происходит. Когда процесс вызывает ptrace с помощью PTRACE_TRACEME, ядро устанавливает флаги процесса, чтобы отразить, что он отслеживается:

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, если процесс отслеживается. Детали можно найти в arch/i386/kernel/entry.S.

Теперь мы находимся в функции sys_trace (), как определено в arch/i386/kernel/ptrace.c. Он останавливает ребенка и отправляет родителю сигнал, уведомляющий, что ребенок остановлен. Это пробуждает ожидающего родителя и делает магию ptrace. Когда родительский объект завершен и он вызывает ptrace (PTRACE_CONT, ..) или ptrace (PTRACE_SYSCALL, ..), он пробуждает дочерний элемент, вызывая функцию планировщика wake_up_process (). Некоторые другие архитектуры могут реализовать это, посылая сигнал SIGCHLD ребенку.

Вывод

ptrace может показаться волшебным для некоторых людей, потому что он может проверять и изменять запущенную программу. Обычно используется отладчиками и программами трассировки системных вызовов, такими как ptrace. Это открывает интересные возможности для расширения пользовательского режима. Было сделано много попыток расширить операционную систему на уровне пользователя. ptrace также используется для использования механизмов безопасности.

Оригинал статьи , написанный Pradeep Padala: https://www.linuxjournal.com/article/6210