This page looks best with JavaScript enabled

gdb

 ·  ☕ 11 min read

介紹

aa

GDB(GNU Debugger)是Richard Stallman為了GNU所開發的其中一個套件

debug format

目前linux elf的debug format是DWARF所以我們先只探討DWARF,
當編譯的時候加-g代表就是塞入debug資訊到執行檔

1
gcc main.c -g

GDB指令

環境準備

安裝gdb-dashboard

接下來實驗搭配gdb-dashboard套件比較美觀請先安裝

1
wget -P ~ https://git.io/.gdbinit

準備要實驗的程式

1
2
3
4
5
#include<stdio.h>
int main(){
printf("hello, world!\n");
return 0;
}

啟動GDB

  • gdb executablename
    • gdb --args executablename arg1 arg2 arg3 打開並且跟蹤程式,如果有參數可以塞入
    • 因為要被你追蹤的程式還沒啟動所以需要這些指令:r,start,starti
  • sudo gdb --pid 123 跟蹤已經執行的程式利用pid,因為跟蹤其他程式所以需要root權限

進入GDB後

最原始版本gdb可以使用的指令

進入gdb後

  • start 開始並且中斷在main函數,沒有symbol table無法使用
  • starti 在沒有symbol table之下,我們只能從頭看組合語言,所以我們需要starti從第一行組合語言開始並且中斷
  • n 執行下一行原始碼,遇到function call不會中斷,等到function call執行完成才中斷
  • ni 執行下一行組合語言,遇到function call不會中斷,等到function call執行完成才中斷
  • s 執行下一行原始碼,遇到function call會進入並且中斷
  • si 執行下一行組合語言,遇到function call會進入並且中斷
  • b main 設定中斷在main函數(main名稱是紀錄在elf的symbol tablereadelf -s 執行檔名),如果gcc設定-s代表刪除程式所有symbol table,你就無法找到main的位置
  • b *0x123 設定中斷在0x123記憶體位置
  • j *0x123 跳到0x123記憶體位置並且會馬上繼續執行
  • c 持續執行等到下一個中斷b出現
  • info b 列出已經設定哪些中斷
  • d 1 刪除Num 1的中斷
  • r 開始執行/重新執行,會一直跑下去直到程式中斷或是結束跑完
  • catch
    • syscall
      • write catch syscall write可以暫停當你呼叫write syscall之後
  • q離開gdb
  • handle SIGTSTP nostop nopass print 這裡SIGTSTP可以改成你要設定的訊號(Control+C發送SIGINT,Control+Z發送SIGTSTP),或是all全部signal一起設定
    • stop/nostop 設定收到SIGTSTP訊號gdb要停止嗎?
    • pass/nopass 設定收到SIGTSTP訊號gdb要轉傳給被追蹤的程式嗎?
    • print/noprint 設定收到SIGTSTP訊號gdb要顯示嗎?
  • info signals 顯示目前signals的設定
  • set disable-randomization off 這個指令可以gdb隨機載入執行檔到不同記憶體位置
    • 作業系統因為有ASLR的安全機制會把ELF File Types Dynamic(ET_DYN)型態的執行檔(又稱PIC1或PIE2)載入到隨機的虛擬記憶體位置,但gdb會為了好追蹤偷偷關閉ASLR載入到固定的記憶體位置
    • 觀察gcc -no-pie main.c gcc main.c 在gdb差別?
  • python 在gdb裡面啟動python,用end來關閉,目的是可以拿來擴展gdb功能

原始碼進入點都是main?

點我下載範例檔案go-elf

在debug此檔案go-elf到底要中斷在哪裡才能進入主程式.

這個範例希望讓大家體會這個main名稱只是約定好的,不同程式語言它可能自己定義

提示

readelf -s go-elf 

main.main 才是go語言編譯出來的 main進入點

b main.main

有趣的程式

點我下載x86_64-abi

  • 希望學到簡單c inline assambly
  • 希望了解某些暫存器在system v abi的規範下有特殊用途
  • 不用main
  • 不用printf libary
  • 輸出hello, world!

找找主程式的進入點?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
void print_asm(char *arg1,long int size){
    __asm__ volatile(
        "mov $1, %%rax\n\t"    //system call 編碼
        "mov $1, %%rdi\n\t"   //arg1:fd 設定1 代表把字串輸入/dev/stdout 這裡就是螢幕輸出地方
        "mov %0,%%rsi\n\t"   //arg2:輸入字串記憶體位置
        "mov %1, %%rdx\n\t" //arg3:這裡輸入字串長度 ,可以跟記憶體位置搭配來輸出到螢幕
        "syscall"//x64 要用此呼叫systemcall 不能在使用int $0x80
        :                      //需要輸出的參數,沒有用到
        :"m" (arg1),"m" (size) //需要輸入的參數,並且紀錄在記憶體就好
    );
}
void _start() {
    char *d="hello, world!\n";
    print_asm(d,14);
    asm("mov $60,%rax\n\t"
        "syscall\n\t"
    );
}

gcc -nostdlib main.c -no-pie

handle signal範例

  • 了解到被追蹤的程式的signal,會被追蹤者掌控
  • 對程式按CTRL+Z默認的行為是發送(SIGTSTP)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include<signal.h>  
void handle_signal(int sig) 
{ 
    printf("我收到SIGNAL拉!! %d\n", sig); 
} 
int main()
{ 
    printf("pid/tid: %ld\tppid: %ld\n", (long)getpid(), (long)getppid());
    signal(SIGTSTP,handle_signal);
    
    while (1){}
    return 0;
}

準備環境

1.編譯

1
gcc signal.c -o signal

2.在signal的終端機 執行 signal

1
./signal

出現pid/tid: 123 ppid: 321
123就是signal程式的pid等等會用到(這裡的數字不會跟我一樣)

3.開啟另一個終端機讓gdb去追蹤signal程式

1
sudo gdb --pid 123

實驗
gdb處理(handle)signal.c收到CTRL+Z(SIGTSTP)訊號的時候GDB不停止(nostop)不轉傳給signal.c(nopass)但是要在GDB介面輸出訊息(print)

1.在gdb的終端機輸入

1
handle SIGTSTP nostop nopass print

2.在gdb的終端機輸入csignal.c繼續執行

1
c

3.在signal的終端機按下CTRL+Z觀察

如果上面nopass改成pass會有什麼差別?

做給你看拉

aa

gdb-dashboard

dashboard後按tab兩下可以看到有哪些指令可以用

“查詢0x123後10bytes的記憶體”

dashboard memory watch 0x123 10

Debug Python by GDB

剛才的指令我們已經學會如何動態分析編譯語言(Compiled language)接下來會教學如何分析直譯語言(Interpreted language)我們以python為範例

python有各種直譯器的實作pypy(python-JIT),Jython(java),cpython(C),IronPython(c#)

一般人用的python是cpython也就是吉多·范羅蘇姆(Guido van Rossum)python之父所開發的

接下來的範例只探討利用gdb注入cpython
利用查閱(Python/C API Reference Manual)學會如何控制python直譯器(python直譯器教學),進而利用gdb注入python程式到執行中的python程序裏面

範例

引用來源Extending GDB with Python - Lisa Roach
此實驗的架構圖

aa

準備環境

創建main.py並且放入此程式

1
2
3
4
5
6
import time
import os
print("pid:"+str(os.getpid()))
我的密碼="ggininder"
while True:
    time.sleep(1234)

實驗

1.啟動main.py會看到pid的數值這裡舉例pid:123(你的pid會跟我不一樣)

python main.py

2.開啟另一個終端機輸入下面指令去追蹤pid:123程序

1
sudo gdb --pid 123

告訴python直譯器的GIL要給我用資源

1
call (int)PyGILState_Ensure()

告訴python直譯器我想執行的程式

1
call (int)PyRun_SimpleString("print(globals())")

告訴python直譯器的GIL還你資源拉

1
call (void)PyGILState_Release(1)

觀察看看發生什麼事??

Extending GDB with Python

介紹

gdb支持PythonGuile兩種程式來強化gdb,我想很少人會Guile吧??

可以看到有許多gdb的擴展套件他們都是用python寫的
-pwndbg
-peda
-gef

下圖可以看到目前大部份強化GDB的工具都是利用python開發所以我們只教python擴展gdb
aa

如何載入擴展

  • GDB啟動的時候會載入~/.gdbinit
  • gdb -x 我的套件.py
  • 進入gdb打python

GDB原理

接下來以x64 linux v5.10作範例

ptrace

c語言內嵌入組合語言呼叫systemcall->呼叫syscall到核心的過程->syscall執行的過程->最後回去你的主程式

gdb底層是利用ptrace完成(strace也是利用ptrade完成3 )接下來簡單聊一下ptrace systemcall

linux kernel裡面稱被追蹤的程式叫作tracee,追蹤人的程式叫tracer

點選ptrace syscall可以看到ptrace實作的地方,有個叫SYSCALL_DEFINE4的macro

SYSCALL_DEFINE4->經過編譯器前置處理->__x64_sys_ptrace 

在x64 linux v5.10的ptrace的名稱

__x64_sys_ptrace(long request, long pid, unsigned long addr, unsigned long data)

PTRACE_ATTACH

跟作業系統報備哪個程式要被追蹤

ptrace(PTRACE_ATTACH, pid, NULL, NULL) 細節請看ptrace.c

僵屍範例

複製下面dummy-loop-prog.c的程式執行

PTRACE_ATTACH完成後需使用waitpid()確保被ptrace追蹤的程式已經停止了

以上滿足才能開始ptrace動作,讀取暫存器,讀寫記憶體,跳到某行

正常駭客要解決

  • W^X policy
    • 目前作業系統都有W^X policy所以一塊記憶體只能是寫或是執行不能同時擁有
  • ASLR(Address space layout randomization)
    • gdb會關閉ASLR讓你以為位置固定,但正常程式會啟用並且保護
    • 隨機記憶體位置,駭客就不知道要跳到哪執行某些函數

cat /proc/[pid]/maps

breakpoint

??

範例程式

預備知識assambly與systemcall

範例PTRACE_ATTACH的過程

sequenceDiagram
    autonumber
    loop 迴圈
        dummy-loop-prog.c->>dummy-loop-prog.c: TASK_RUNNING(R)
    end
    injector.c->>作業系統: PTRACE_ATTACH dummy-loop-prog.c
    作業系統->>dummy-loop-prog.c: SIGSTOP
    Note left of 作業系統: dummy-loop-prog.c變為TASK_TRACED狀態
    Note right of injector.c: waitpid()
    loop 迴圈
        dummy-loop-prog.c->>dummy-loop-prog.c: TASK_TRACED(t)
    end

injector.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <stdio.h>
#include <stdlib.h>
#include <sys/ptrace.h> /* ptrace() */
#include <sys/wait.h>   /* wait() */
 
#include <sys/user.h>   /* struct user_regs_struct */

static char shellcode[] =
    "\x48\xc7\xc0\x3c\x00\x00\x00" //mov $0x3c,%rax
    "\x48\xc7\xc7\x4e\x00\x00\x00" 	//mov $78,%rdi 
    "\x0f\x05";//syscall
// ...->__VA_ARGS__ (VA=variadic)
#define OUT_MSG(x, ...) printf("* " x "\n",## __VA_ARGS__)
#define ERR_MSG(x) printf("\t[Error] " x "\n")

int main(int argc, char *argv[])
{ 
    int pid, offset;
    struct user_regs_struct regs;
    /*確認有沒有輸入pid*/
    OUT_MSG("Injector starts.");
    if (argc < 2) {
        ERR_MSG("PID required in parameter.");
        return -1;
    }
    //把輸入的pid字串轉成int
    pid = atoi(argv[1]);
    OUT_MSG("Attaching process (PID=%d)...", pid);
    //開始PTRACE_ATTACH
    if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) < 0) {
        ERR_MSG("Fail to ptrace process");
        ptrace(PTRACE_DETACH, pid, NULL, NULL);
        return -1;
    }

    OUT_MSG("Process attached.");
    //確保tracee已經停止
    if (waitpid(pid, NULL, WUNTRACED) < 0) {
        ERR_MSG("WUNTRACED");
        exit(1);
    }
    /*拿到tracee的暫存器*/
    OUT_MSG("Getting registers from process.");
    if (ptrace(PTRACE_GETREGS, pid, NULL, &regs) < 0) {
        ERR_MSG("Fail to get registers.");
        ptrace(PTRACE_DETACH, pid, NULL, NULL);
        exit(1);
    }
    /*開始再rip與之後寫入shellcode*/
    OUT_MSG("Injecting shellcode into process...");
    for (offset = 0; offset < sizeof(shellcode); offset++) {
        if (ptrace(PTRACE_POKETEXT, pid,
                   regs.rip + offset,
                   *(int *) &shellcode[offset])) {
            ERR_MSG("Fail to inject.");
            ptrace(PTRACE_DETACH, pid, NULL, NULL);
            exit(1);
        }
    }

    /**/
    OUT_MSG("Detach process (PID=%d).", pid);
    ptrace(PTRACE_DETACH, pid, NULL, NULL);
    OUT_MSG("Done");
    return 0;
}

dummy-loop-prog.c

這個版本是作最簡單while迴圈程式

1
2
3
4
5
6
7
8
9
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
int main()
{ 
    printf("pid/tid: %ld\tppid: %ld\n", (long)getpid(), (long)getppid());
    while (1){}
    return 0;
}

這個版本利用PTRACE_TRACEMEtracee直接被生父追蹤,所以其他程序無法再追蹤他了,來避免被其他程式動態分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <unistd.h>//getpid,getppid
#include <stdio.h>//printf
#include <sys/ptrace.h>//ptrace,PTRACE_TRACEME
int main()
{ 
printf("pid/tid: %ld\tppid: %ld\n", (long)getpid(), (long)getppid());
ptrace(PTRACE_TRACEME);
while (1){}
return 0;
}

編譯

1
gcc -o injector injector.c -g
1
gcc -o dummy-loop-prog dummy-loop-prog.c -g 

實驗

  • 開啟終端機啟動要被注入的程式
1
2
./dummy-loop-prog 
pid/tid: 123	ppid: 321
  • 開啟新的終端機,這個範例dummy-loop-prog 的pid為123
    sudo給injector權限注入pid為123的程式
1
sudo ./injector 123
  • dummy-loop-prog終端機下
1
2
echo $?
78

這個78是injector注入的

問題?

  • 希望把78改成其他數字

利用ftrace了解ptrace

先學學ftrace基礎
我們可以利用剛才的程式配合ftrace我們可以感受程式流進去核心後做了什麼事

DWARF

這裡聊ELF的debug section與GDB如何讀取ELF的DWARF資訊並debug

debug section壓縮

編譯main.c輸出debug資訊(-g)在link階段(-Wl)放入(--compress-debug-sections=zlib-gnu)參數讓debug壓縮
gcc -Wl,--compress-debug-sections=zlib-gnu -g main.c

詳細參考ld

  • --compress-debug-sections=
    • zlib
      • 與zlib-gabi一樣
    • zlib-gnu
      • 壓縮DWARF並且更改debug section名稱.debug_xxx->.zdebug_xxx
    • zlib-gabi
      • 壓縮DWARF並且不會更改debug section名稱

引用

trace基本Linux系統呼叫
jserv-以 ptrace 系統呼叫來追蹤/修改行程
jserv-ptrace / SIGTRAP / int3 的關聯
Infecting Running Processes
MAKE STACK EXECUTABLE AGAIN
ptrace() Tutorial
Command set-permission(mprotect)
strace 是如何工作的
How does strace work
Linux task_struct parent 和 real_parent 的区别
How debuggers work: Part 2 - Breakpoints


  1. position-independent code ↩︎

  2. position-independent executable ↩︎

  3. Intercepting and Emulating Linux System Calls with Ptrace ↩︎

Share on

呂奕珣
WRITTEN BY
呂奕珣
SDN ML FINTECH HFT