Krótkie wprowadzenie, jak korzystać z gdb do debugowania kodu w asemblerze.
Wykorzystajmy poniższy kawałek kodu:
; "#define": SYS_EXIT equ 60 ARRAY_SIZE equ 10 global _start section .bss ; dane zainicjalizowane zerami, można czytać i pisać alignb 16 ; z wyrównanymi adresami CPU może działać szybciej array_to: resq ARRAY_SIZE section .data ; dane (mogą być już zainicjalizowane), można czytać i pisać alignb 16 array_from: dq 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 section .rodata ; dane, można tylko czytać extra: db 42 ; 8-bitowa liczba section .text ; kod wykonywalny _start: ; for (i=0; i < ARRAY_SIZE; i++) ; array_to[ARRAY_SIZE - 1 - i] = array_from[i] + 42 ; Poniżej: ; rax = i ; rcx = &array_to[ARRAY_SIZE - 1 - i] ; r8 = 42 mov rax, 0 ; i = 0 mov rcx, array_to + (ARRAY_SIZE - 1)* 8 ; rcx = &array_to[ARRAY_SIZE - 1] (tę stałą wyliczy nasm) movsx r8, byte [extra] ; r8 = 42 (8bit -> 64bit) jmp loop_cond loop_body: mov rdx, [array_from + rax*8] ; rdx = array_from[i] add rdx, r8 ; rdx = array_from[i] + 42 mov [rcx], rdx ; array_to[ARRAY_SIZE - 1 - i] = array_from[i] + 42 inc rax ; i++ lea rcx, [rcx - 8] ; rcx = --(&array_to[ARRAY_SIZE - 1 - i]) loop_cond: ; while (i < ARRAY_SIZE) cmp rax, ARRAY_SIZE jl loop_body ; i traktuję jako signed ; exit(0) mov eax, SYS_EXIT xor edi, edi syscall
Kompilujemy z opcjami -g -F dwarf
:
nasm -f elf64 -g -F dwarf -o ./prog.o ./prog.asm ld -o ./prog ./prog.o
Uruchamiamy gdb na naszym programie:
gdb ./prog
Ustawiamy składnię intelową:
set disassembly-flavor intel
Ustawiamy breakpoint na etykiecie _start
:
breakpoint _startMożna pisać polecenia skrótowo:
b _startPoniżej formę skróconą będę pisał po "|".
Uruchamiamy wykonywanie programu:
run | r
Wykonywanie programu zatrzyma się na ustawionym breakpoint – u nas na początku _start
.
Deasemblujemy kod:
disassemble | disasZobaczymy podobny widok jak ten z objdump. Strzałką zaznaczona jest instrukcja, która jest kolejna do wykonania.
Podglądamy aktualne wartości rejestrów:
info registers | i r
Wykonujemy krok programu:
stepi | si
Patrzymy, że faktycznie wykonaliśmy 1 instrukcję:
disas
Wykonujemy kolejny krok:
si
Podglądamy wartość rejestru r8
:
print $r8 | p $r8Dziesiętnie:
p/d $r8
Podglądamy bajt w pamięci pod adresem extra
dziesiętnie:
x/db &extra
Wykonujemy krok:
si
Podglądamy wartość r8
:
p/d $r8Powinno być 42.
Patrzymy, co wykona się następnie (gdzie skoczymy):
sikika razy. Po 6 razach powinniśmy być przed
inc rax
.
Patrzymy, co jest pod adresem zapisanym w rcx
(wyświetlamy dziesiętnie wartość 64bit):
x/dg $rcx
Patrzymy, jak wygląda aktualnie "tablica" array_to
:
p (long long[10])array_to
Dodajmy breakpoint po pętli, przed wywołaniem sys_exit
:
b prog.asm:50w 50 linii programu z pliku prog.asm.
Kontynuujemy wykonanie do kolejnego breakpointu:
continue | c
Patrzymy, czy tablica została poprawnie wypełniona:
p/d (long long[10])array_to
Sprawdzamy, czy rejestry mają oczekiwane przez nas wartości:
i r
Kończymy pracę z gdb:
quit | q
To jest krótki pokaz użycia gdb. Oczywiście, potrafi ono dużo więcej (np. modyfikować wartości rejestrów w czasie wykonywania programu). Kto zainteresowany, odsyłam do Internetu.
Ściągawka poleceń gdb: GDB cheatsheet
Jeśli ktoś używa dużo gdb, polecam PEDA - Python Exploit Development Assistance for GDB.