Polecam przeczytać jeszcze raz, na spokojnie, zwłaszcza dzisiejszą część scenariusza -- jest tam wiele ważnych informacji.
Odnośnie instrukcji skoków warunkowych, zwracam Państwa uwagę na ramkę (bo chyba nie zrobiłem tego na zajęciach):
Ważne
Do porównywania liczb bez znaku używa się innych instrukcji skoku niż do porównywania liczb ze znakiem.
Cześć materiałów dostępnych w Internecie (w tym podlinkowanych przeze mnie) dotyczy architektury x86 (32-bitowej), więc opisują instrukcje 32-bitowe. Oczmaywiście, na architekturze x86_64 (64-bitowej) większość z nich (wszystkie?, prawie wszystkie?) mają odpowiedniki 64-bitowe -- prawie zawsze (zawsze?) wystarczy użyć tej samej instrukcji, tylko że z rejestrem 64-bitowym.
Co dokładnie robi instrukcja, jakie argumenty przyjmuje, jakie flagi ustawia, itd, oczywiście najdokładniej opisane jest w dokumentacji Intela (i innych producentów CPU x86_64). Jest tam wykorzystywany następujący format opisu instrukcji, np.: ADD r/m64, imm32
-- oznacza to, że lewym argumentem tej instrukcji ADD
może być albo 64bit rejestr, albo 64bit miejsce w pamięci, natomiast prawym 32bit liczba wpisana w kodzie. Oprócz tego znajdziemy słowny opis (z ew. "specjalnymi" właściwościami instrukcji). Np. ta konkretna:
Add imm32 sign-extended to 64-bits to r/m64.
Pominęliśmy zadanie A5, rozwiązanie dla zainteresowanych:
xor?
:ebx
.nop
?:jne
jest wyrównany.i
rejestr ebx
, a nie np. rejestr r10d
, którego wartości nie musiałby zachowywać na stosie (zaoszczędziłby dwie instrukcje)?:ebx
nie będzie zmodyfikowany przez funkcję bar
, a rejestr r10d
mógłby być -- trzeba by było odłożyć na stos wartość rejestru r10
przed wywołaniem funkcji bar
i przywrócić jego wartość po powrocie z funkcji. Więc też zużylibyśmy 2 instrukcje.rdi
.Krótkie wprowadzenie, jak korzystać z gdb do debuggowania 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:
r 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.
Jeśli ktoś używa dużo gdb, polecam PEDA - Python Exploit Development Assistance for GDB.
Czytając różne materiały w Internecie mogą się Państwo spotkać z inną składnią zapisu asemblera -- składnią AT&T, np:
push %rbp mov %rsp,%rbp mov $0x0,%eax movl $0x0,-0x4(%rbp) movl $0x5,-0x8(%rbp) mov -0x8(%rbp),%ecx add $0x6,%ecx mov %ecx,-0xc(%rbp) pop %rbp ret
Zestawienie różnic można znaleźć np. tu.
Najbardzie (chyba) podchwytliwa -- kolejność argumentów: mov eax, 1
= movl $1, %eax
. Podchwytliwe to jest przy porównywniu!!
Pan Wojciech (nie mylę imienia?) znalazł ciekawy przykład. Program:
int* fun(){ int b; return (&b); }kompiluje się do:
0000000000000000Dlaczego zwracana jest wartość 0?: 0: 31 c0 xor eax,eax 2: c3 ret
Odpowiedź można znaleźć na przykład tu:
The compiler has generated the right instructions. What you are doing is not defined by the C standard, so the compiler is free to do whatever it pleases. In this case it seems that GCC pleases to return a null pointer, probably so that your program will fail as fast as possible.Rzeczywiście, zwracanie adresu zmiennej lokalnej funkcji jest słabym pomysłem.