Obsah
1. Frameworky Capstone a Keystone: základ pro tvorbu assemblerů a disassemblerů
2. Proč je vlastně zapotřebí „nový“ assembler a disassembler?
3. Instalace rozhraní mezi Pythonem a frameworky Capstone a Keystone
4. Ruční překlad knihovny Capstone
5. První kroky s knihovnou Keystone – zavolání assembleru s výpisem přeloženého kódu
6. 32bitový režim mikroprocesorů x86
7. 64bitový režim mikroprocesorů x86–64
8. Zápis instrukcí a operandů s využitím AT&T syntaxe
9. Návěští (labels), skoky a programové smyčky
11. Překlad do souboru s binárním kódem
12. Program typu „Hello world“ napsaný v assembleru procesorů x86 určený pro Linux
13. Vygenerování binárního souboru se strojovým kódem aplikace „Hello world“
14. Umístění řetězce (dat) před programový kód
15. Složitější kód zapsaný v assembleru založený na použití podprogramů (subrutin)
16. Překlad kódu zapsaného v assembleru se zpětným překladem
17. Seznam podporovaných architektur a režimů
18. Obsah navazujícího článku – framework Capstone
19. Repositář s demonstračními příklady
1. Frameworky Capstone a Keystone: základ pro tvorbu assemblerů a disassemblerů
V dnešním článku se ve stručnosti seznámíme s frameworky nazvanými Capstone a Keystone. Jedná se o „univerzální“ assembler a disassembler, které však nejsou přímo volatelné z příkazového řádku tak, jako klasické assemblery (jmenujme například GAS nebo NASM). Namísto toho je jak assembler, tak i disassembler dostupný ve formě nativní knihovny, kterou je možné přímo volat z C i C++ (nebo z jiného jazyka přes FFI). Navíc ovšem existují i rozhraní pro další programovací jazyky, abecedně pro D, Clojure, F#, Common Lisp, Visual Basic, PHP, PowerShell, Haskell, Perl, Python, Ruby, C#, NodeJS, Java, Go, C++, OCaml, Lua, Rust, Delphi, Free Pascal a pro Valu.
Z jakého důvodu se však jedná o „univerzální“ assembler a disassembler? Cílem autorů těchto dvou frameworků je nabídnout uživatelům podporu pro mnoho procesorových architektur. Samozřejmě se v první řadě jedná o x86 (s podporou 16bitového, 32bitového i 64bitového režimu) a o AArch64. Podporovány jsou ovšem i další procesorové architektury, například 32bitový ARM, MIPS, PowerPC, SPARC a SystemZ, přičemž další architektury je možné díky „pluginovatelnosti“ Capstone i Keystone relativně snadno přidat (takto byl například disassembler rozšířen o podporu historického osmibitového mikroprocesoru MOS 6502 atd.).
2. Proč je vlastně zapotřebí „nový“ assembler a disassembler?
Na tomto místě si možná čtenáři tohoto článku kladou otázku, proč je vlastně zapotřebí vytvářet nový assembler a disassembler. Autoři frameworků Capstone a Keystone pracují mj. na nástrojích z oblasti kyberbezpečnosti, analýzy kódů atd. A v těchto oblastech je někdy zapotřebí provést zpětný překlad krátké sekvence bajtů (disassembling), úpravu takto získaného kódu v assembleru následovaného překladem do nativního kódu (assembling). Pro tyto účely se klasické assemblery a disassemblery ovládané z příkazového řádku příliš nehodí, takže může být vhodnější použít k tomuto účelu vytvořené nativní knihovny. Navíc v současnosti vlastně ani neexistuje skutečně univerzální assembler a disassembler s podporou různých způsobů zápisu instrukcí (GAS s Intel syntaxí, GAS s AT&T syntaxí, NASM, MASM, atd.). Keystone a Capstone se snaží (i když prozatím jen s částečným úspěchem) o dosažení plné kompatibility s těmito syntaxemi a současně o podporu co největšího množství procesorových architektur.
3. Instalace rozhraní mezi Pythonem a frameworky Capstone a Keystone
V této kapitole si ukážeme instalaci balíčků pro Python, které zajišťují rozhraní mezi skripty napsanými v Pythonu na jedné straně s frameworky Capstone a Keystone na straně druhé.
Vzhledem k tomu, že rozhraní mezi Pythonem a frameworky Capstone a Keystone je dostupné ve formě klasických Pythonovských balíčků na PyPi (https://pypi.org/project/capstone/, https://pypi.org/project/keystone-engine/), je samotná instalace zcela bezproblémová. Nejdříve nainstalujeme jak vlastní Capstone (tedy disassembler), tak i příslušné rozhraní pro Python:
$ pip3 install --user capstone Collecting capstone Downloading capstone-4.0.2-py2.py3-none-manylinux1_x86_64.whl (2.1 MB) |████████████████████████████████| 2.1 MB 1.4 MB/s Installing collected packages: capstone Successfully installed capstone-4.0.2
Následně nainstalujeme i Keystone (tedy assembler), pochopitelně opět s rozhraním pro Python:
$ pip3 install --user keystone-engine Collecting keystone-engine Downloading keystone_engine-0.9.2-py2.py3-none-manylinux1_x86_64.whl (1.8 MB) |████████████████████████████████| 1.8 MB 1.6 MB/s Installing collected packages: keystone-engine Successfully installed keystone-engine-0.9.2
. ├── capstone │ ├── include │ │ └── capstone │ ├── lib │ └── __pycache__ ├── capstone-4.0.2.dist-info ├── keystone │ └── __pycache__ ├── keystone_engine-0.9.2.dist-info ├── pep517 │ ├── in_process │ │ └── __pycache__ │ └── __pycache__ └── pep517-0.12.0.dist-info
4. Ruční překlad knihovny Capstone
V případě, že budete chtít využít nástroje Capstone a Keystone z programovacího jazyka C (popř. pochopitelně z C++) a nikoli z Pythonu, je možné si nechat tyto knihovny přeložit přímo překladačem jazyka (Capstone) nebo C++ (Keystone), a to ze zdrojových kódů dostupných na GitHubu. Ukažme si pro ilustraci postup při překladu knihovny Capstone, která vyžaduje pouze překladač jazyka C.
Naklonujeme repositář se zdrojovými kódy této knihovny:
$ git clone git@github.com:capstone-engine/capstone.git Cloning into 'capstone'... remote: Enumerating objects: 30861, done. remote: Counting objects: 100% (1604/1604), done. remote: Compressing objects: 100% (1045/1045), done. remote: Total 30861 (delta 631), reused 1367 (delta 535), pack-reused 29257 Receiving objects: 100% (30861/30861), 48.27 MiB | 2.03 MiB/s, done. Resolving deltas: 100% (21692/21692), done.
Ve druhém kroku se přepneme do větve next. To je velmi důležité, protože výchozí větev master se v tomto projektu nepoužívá:
$ cd capstone/ ✔ /tmp/ramdisk/capstone [master|✔] $ git checkout next Branch 'next' set up to track remote branch 'next' from 'origin'. Switched to a new branch 'next'
Vlastní překlad je založen na klasickém Makefile:
$ make CC cs.o CC utils.o CC SStream.o CC MCInstrDesc.o CC MCRegisterInfo.o CC arch/ARM/ARMModule.o CC arch/ARM/ARMMapping.o CC arch/ARM/ARMInstPrinter.o CC arch/ARM/ARMDisassembler.o CC arch/AArch64/AArch64Disassembler.o CC arch/AArch64/AArch64Module.o CC arch/AArch64/AArch64Mapping.o CC arch/AArch64/AArch64InstPrinter.o CC arch/AArch64/AArch64BaseInfo.o CC arch/M68K/M68KInstPrinter.o ... ... ... LINK test_m680x.static LINK test_evm LINK test_evm.static LINK test_riscv LINK test_riscv.static LINK test_wasm LINK test_wasm.static LINK test_mos65xx LINK test_mos65xx.static LINK test_bpf LINK test_bpf.static make[1]: Leaving directory '/tmp/ramdisk/capstone/tests' install -m0755 ./libcapstone.so.5 ./tests/ cd ./tests/ && rm -f libcapstone.so && ln -s libcapstone.so.5 libcapstone.so
Výsledkem bude především nativní knihovna libcapstone.so, kterou bude možné využít způsobem popsaným v navazujícím článku.
5. První kroky s knihovnou Keystone – zavolání assembleru s výpisem přeloženého kódu
Nyní se můžeme podívat na základní způsob použití knihovny Keystone, která implementuje assembler (jenž je do značné míry nezávislý na architektuře procesorů). Pokusíme se z Pythonu přeložit tři jednoduché instrukce ve starobylém šestnáctibitovém režimu mikroprocesorů s architekturou x86. Instrukce jsou zapsány v bytovém poli a oddělené jsou středníkem. Před překladem je nutné zvolit jak architekturu (KS_ARCH_X86), tak i režim (zde konkrétně KS_MODE16). Výsledkem překladu je buď výjimka (která nese základní informaci o tom, k jaké chybě došlo) nebo sekvence bajtů představujících binární strojový kód:
# import všech symbolů použitých ve skriptu from keystone import Ks, KS_ARCH_X86, KS_MODE_16, KsError # instrukce, které se mají assemblerem přeložit CODE = b"MOV AX, 100; INC AX; MOV BX, AX" try: # inicializace assembleru se specifikací architektury a popř. i režimu ks = Ks(KS_ARCH_X86, KS_MODE_16) # vlastní překlad (assembling) encoding, count = ks.asm(CODE) # tisk výsledku činnosti assembleru print("%s = %s (number of statements: %u)" % (CODE, encoding, count)) except KsError as e: print("ERROR: %s" % e)
Výsledek získaný po spuštění tohoto skriptu by měl vypadat následovně:
b'MOV AX, 100; INC AX; MOV BX, AX' = [184, 100, 0, 64, 137, 195] (number of statements: 3)
Alternativně je možné každou instrukci zapsat na samostatném řádku, tj. namísto středníků se použije znak pro konec řádku. Současně namísto pole bajtů použijeme klasický řetězec. V Pythonu může upravený skript vypadat takto:
# import všech symbolů použitých ve skriptu from keystone import Ks, KS_ARCH_X86, KS_MODE_16, KsError # instrukce, které se mají assemblerem přeložit CODE = """ MOV AX, 100 INC AX MOV BX, AX """ try: # inicializace assembleru se specifikací architektury a popř. i režimu ks = Ks(KS_ARCH_X86, KS_MODE_16) # vlastní překlad (assembling) encoding, count = ks.asm(CODE) # tisk výsledku činnosti assembleru print("%s = %s (number of statements: %u)" % (CODE, encoding, count)) except KsError as e: print("ERROR: %s" % e)
Výsledek činnosti skriptu by měl být totožný se skriptem předchozím:
MOV AX, 100 INC AX MOV BX, AX = [184, 100, 0, 64, 137, 195] (number of statements: 4)
Konkrétně nyní překlad vypadá takto:
Offset | Decimal | Hexa | Instrukce |
---|---|---|---|
0 | 184 100 0 | b8 64 00 | mov ax,0×64 |
3 | 64 | 40 | inc ax |
4 | 137 195 | 89 c3 | mov bx,ax |
6. 32bitový režim mikroprocesorů x86
V případě, že se pokusíme o překlad tří výše zmíněných instrukcí v 32bitovém režimu mikroprocesorů, překlad se podaří, ovšem výsledný binární kód bude odlišný, a to z toho důvodu, že odlišné je i samotné kódování instrukcí:
# import všech symbolů použitých ve skriptu from keystone import Ks, KS_ARCH_X86, KS_MODE_32, KsError # instrukce, které se mají assemblerem přeložit CODE = """ MOV AX, 100 INC AX MOV BX, AX """ try: # inicializace assembleru se specifikací architektury a popř. i režimu ks = Ks(KS_ARCH_X86, KS_MODE_32) # vlastní překlad (assembling) encoding, count = ks.asm(CODE) # tisk výsledku činnosti assembleru print("%s = %s (number of statements: %u)" % (CODE, encoding, count)) except KsError as e: print("ERROR: %s" % e)
Povšimněte si, jak je výsledný binární kód dlouhý. Je tomu tak kvůli odlišným prefixům (102=0×66) u prakticky všech šestnáctibitových instrukcí:
MOV AX, 100 INC AX MOV BX, AX = [102, 184, 100, 0, 102, 64, 102, 137, 195] (number of statements: 4)
Konkrétně nyní překlad vypadá takto:
Offset | Decimal | Hexa | Instrukce |
---|---|---|---|
0 | 102 184 100 0 | 66 b8 64 00 | mov ax,0×64 |
4 | 102 64 | 66 40 | inc ax |
6 | 102 137 195 | 66 89 c3 | mov bx,ax |
7. 64bitový režim mikroprocesorů x86–64
Pro úplnost přeložíme ten samý kód, ovšem v 64bitovém režimu mikroprocesorů s architekturou x86–64. I v tomto režimu je totiž možné využít původní šestnáctibitové registry, když je to skutečně zapotřebí (většinou však není):
# import všech symbolů použitých ve skriptu from keystone import Ks, KS_ARCH_X86, KS_MODE_64, KsError # instrukce, které se mají assemblerem přeložit CODE = """ MOV AX, 100 INC AX MOV BX, AX """ try: # inicializace assembleru se specifikací architektury a popř. i režimu ks = Ks(KS_ARCH_X86, KS_MODE_64) # vlastní překlad (assembling) encoding, count = ks.asm(CODE) # tisk výsledku činnosti assembleru print("%s = %s (number of statements: %u)" % (CODE, encoding, count)) except KsError as e: print("ERROR: %s" % e)
S výsledkem:
MOV AX, 100 INC AX MOV BX, AX = [102, 184, 100, 0, 102, 255, 192, 102, 137, 195] (number of statements: 4)
Konkrétně nyní překlad vypadá takto (opět odlišně – viz prostřední instrukci!):
Offset | Decimal | Hexa | Instrukce |
---|---|---|---|
0 | 102 184 100 0 | 66 b8 64 00 | mov ax,0×64 |
4 | 102 255 192 | 66 ff c0 | inc ax |
7 | 102 137 195 | 66 89 c3 | mov bx,ax |
8. Zápis instrukcí a operandů s využitím AT&T syntaxe
Při použití různých assemblerů (zejména GNU Assembleru) na mikroprocesorech s architekturou i386 či x86–64 je možné použít dvě různé syntaxe zápisu programů. Proč ale vlastně k tomuto stavu došlo? Původní verze GNU Assembleru z historických důvodů používala zápis používaný v AT&T (resp. přesněji řečeno v Bell Labs při vývoji Unixu). Tento zápis je sice (samozřejmě jen do určité míry) konzistentní mezi různými platformami, ovšem pro mnoho programátorů pracujících na platformách s procesory s architekturou i386 je AT&T syntaxe velmi nezvyklá a taktéž nekompatibilní s dalšími typy assemblerů (dnes již historický Turbo Assembler – TASM, Microsoft Macro Assembler – MASM atd.). Proto mj. vznikl i projekt Netwide Assembler (NASM), který i na Linux (resp. přesněji řečeno do jeho toolchainu) přidal podporu pro zápis programů v assembleru podle zvyklostí z jiných systémů. Změny později nastaly i v GNU Assembleru, což mj. znamená, že od verze 2.10 je možné se jedinou direktivou přepnout do režimu částečně kompatibilního s TASM/MASM.
AT&T syntaxe je podporována i v Keystone, jen musíme explicitně specifikovat její použití (viz též zvýrazněnou část skriptu):
# import všech symbolů použitých ve skriptu from keystone import Ks, KS_ARCH_X86, KS_MODE_16, KS_OPT_SYNTAX_ATT, KsError # instrukce, které se mají assemblerem přeložit CODE = """ MOV %AX, 100 INC %AX MOV %BX, %AX """ try: # inicializace assembleru se specifikací architektury a popř. i režimu ks = Ks(KS_ARCH_X86, KS_MODE_16) # exlicitní specifikace použité syntaxe ks.syntax = KS_OPT_SYNTAX_ATT # vlastní překlad (assembling) encoding, count = ks.asm(CODE) # tisk výsledku činnosti assembleru print("%s = %s (number of statements: %u)" % (CODE, encoding, count)) except KsError as e: print("ERROR: %s" % e)
Ve skutečnosti ovšem tento skript neobsahuje korektní sekvenci instrukcí, takže i výsledek nebude korektní:
MOV %AX, 100 INC %AX MOV %BX, %AX = [163, 0, 1, 64, 137, 216] (number of statements: 4)
V AT&T syntaxi se totiž operandy instrukcí uvádí v opačném pořadí (zdroj, cíl). Navíc se odlišně zapisují i konstanty. Provedeme tedy malé úpravy do podoby:
# import všech symbolů použitých ve skriptu from keystone import Ks, KS_ARCH_X86, KS_MODE_16, KS_OPT_SYNTAX_ATT, KsError # instrukce, které se mají assemblerem přeložit CODE = """ MOV $64, %AX INC %AX MOV %AX, %BX """ try: # inicializace assembleru se specifikací architektury a popř. i režimu ks = Ks(KS_ARCH_X86, KS_MODE_16) # exlicitní specifikace použité syntaxe ks.syntax = KS_OPT_SYNTAX_ATT # vlastní překlad (assembling) encoding, count = ks.asm(CODE) # tisk výsledku činnosti assembleru print("%s = %s (number of statements: %u)" % (CODE, encoding, count)) except KsError as e: print("ERROR: %s" % e)
Nyní by již mělo dojít k překladu, a to do stejné sekvence bajtů, jako tomu bylo u příkladů uvedených v páté kapitole:
MOV $64, %AX INC %AX MOV %AX, %BX = [184, 100, 0, 64, 137, 195] (number of statements: 4)
9. Návěští (labels), skoky a programové smyčky
V prakticky všech programech zapsaných v assembleru se setkáme se skoky a programovými smyčkami (realizovanými opět skoky). Cíle skoků jsou přitom označeny pojmenovaným návěštím (label), přičemž za jméno návěští se zapisuje dvojtečka a samotné návěští typicky začíná na začátku assemblerovského řádku. Podívejme se nyní na realizaci velmi jednoduché programové smyčky, v níž je ve funkci počitadla použit 32bitový registr EAX, jehož hodnota se postupně snižuje od 100 k nule. Jakmile se dosáhne nulové hodnoty, skok JNZ (Jump if Not Zero) se již neprovede:
MOV EAX, 100 LOOP: DEC EAX JNZ LOOP
Tuto smyčku lze přeložit do sekvence strojových instrukcí takto:
# import všech symbolů použitých ve skriptu from keystone import Ks, KS_ARCH_X86, KS_MODE_64, KsError # instrukce, které se mají assemblerem přeložit CODE = """ MOV EAX, 100 LOOP: DEC EAX JNZ LOOP """ try: # inicializace assembleru se specifikací architektury a popř. i režimu ks = Ks(KS_ARCH_X86, KS_MODE_64) # vlastní překlad (assembling) encoding, count = ks.asm(CODE) # tisk výsledku činnosti assembleru print("%s = %s (number of statements: %u)" % (CODE, encoding, count)) except KsError as e: print("ERROR: %s" % e)
Podívejme se nyní na způsob překladu výše uvedené programové smyčky do assembleru:
MOV EAX, 100 LOOP: DEC EAX JNZ LOOP = [184, 100, 0, 0, 0, 255, 200, 117, 252] (number of statements: 5)
10. Vnořené programové smyčky
Ve strojovém kódu (a tedy i v assembleru) je možné realizovat skoky na prakticky libovolnou adresu v kódovém segmentu (i když některé architektury omezují cílové adresy na hodnoty dělitelné 4, 8 atd.). To mj. znamená, že je možné realizovat vnořené programové smyčky; ovšem fakt, že se jedná o strukturované vnořené smyčky a nikoli o „špagetový kód“ záleží jen na vývojáři.
Podívejme se nyní na triviální příklad s dvojicí vnořených smyček. Jako počitadlo vnější smyčky slouží pracovní registr EBX, pro vnitřní smyčku je jako počitadlo použit pracovní registr EAX. Počitadla jsou vždy snižována o jedničku a skok na začátek smyčky je proveden v případě, že hodnota počitadla ještě nedosáhla nuly:
MOV EBX, 10 OUTER_LOOP: MOV EAX, 100 INNER_LOOP: DEC EAX JNZ INNER_LOOP DEC EBX JNZ OUTER_LOOP
Výše uvedenou dvojici programových smyček je možné přeložit do sekvence strojových instrukcí následovně:
# import všech symbolů použitých ve skriptu from keystone import Ks, KS_ARCH_X86, KS_MODE_64, KsError # instrukce, které se mají assemblerem přeložit CODE = """ MOV EBX, 10 OUTER_LOOP: MOV EAX, 100 INNER_LOOP: DEC EAX JNZ INNER_LOOP DEC EBX JNZ OUTER_LOOP """ try: # inicializace assembleru se specifikací architektury a popř. i režimu ks = Ks(KS_ARCH_X86, KS_MODE_64) # vlastní překlad (assembling) encoding, count = ks.asm(CODE) # tisk výsledku činnosti assembleru print("%s = %s (number of statements: %u)" % (CODE, encoding, count)) except KsError as e: print("ERROR: %s" % e)
Opět si ukažme, jakým způsobem se předchozí kód přeloží do assembleru:
MOV EBX, 10 OUTER_LOOP: MOV EAX, 100 INNER_LOOP: DEC EAX JNZ INNER_LOOP DEC EBX JNZ OUTER_LOOP = [187, 10, 0, 0, 0, 184, 100, 0, 0, 0, 255, 200, 117, 252, 255, 203, 117, 243] (number of statements: 9)
11. Překlad do souboru s binárním kódem
Vzhledem k tomu, že výsledkem překladu je v případě frameworku Keystone sekvence bajtů, je možné velmi snadno realizovat překlad do souboru obsahujícího binární kód. Pozor ovšem na to, že se nebude jednat o spustitelný soubor ani o nativní knihovnu, ale skutečně „pouze“ o sekvenci bajtů se zakódovanými strojovými instrukcemi.
Kód s vnořenými smyčkami přeložíme do binárního souboru nazvaného „loops.bin“ takto:
# import všech symbolů použitých ve skriptu from keystone import Ks, KS_ARCH_X86, KS_MODE_64, KsError # instrukce, které se mají assemblerem přeložit CODE = """ MOV EBX, 10 OUTER_LOOP: MOV EAX, 100 INNER_LOOP: DEC EAX JNZ INNER_LOOP DEC EBX JNZ OUTER_LOOP """ try: # inicializace assembleru se specifikací architektury a popř. i režimu ks = Ks(KS_ARCH_X86, KS_MODE_64) # vlastní překlad (assembling) encoding, count = ks.asm(CODE) # uložení výsledného nativního kódu do souboru with open("loops.bin", "wb") as fout: fout.write(bytes(encoding)) print("Binary file written") except KsError as e: print("ERROR: %s" % e)
Vygenerovaný soubor „loops.bin“ bude mít délku osmnácti bajtů, jejichž obsah si můžeme snadno prohlédnout v hexadecimálním prohlížeči (nenechte se zmýlit tím, že od původně znamenalo octal dump):
$ od -tx1 loops.bin 0000000 bb 0a 00 00 00 b8 64 00 00 00 ff c8 75 fc ff cb 0000020 75 f3 0000022
Nástrojem objdump se můžeme pokusit o disassembling binárního souboru, tedy vlastně o obnovu původního kódu napsaného v assembleru a doplněného o konkrétní adresy:
$ objdump -b binary -D -m i386:x86-64 -M intel loops.bin > loops_dump.asm
S tímto výsledkem:
loops.bin: file format binary Disassembly of section .data: 0000000000000000 <.data>: 0: bb 0a 00 00 00 mov ebx,0xa 5: b8 64 00 00 00 mov eax,0x64 a: ff c8 dec eax c: 75 fc jne 0xa e: ff cb dec ebx 10: 75 f3 jne 0x5
12. Program typu „Hello world“ napsaný v assembleru procesorů x86 určený pro Linux
Podívejme se nyní, jak může vypadat zápis programu typu „Hello world!“ napsaný v GNU Assembleru a určený pro spuštění na architektuře i386 s Linuxem. Celý program vlastně volá jen dvě služby jádra: sys_write a sys_exit. V případě sys_exit je nutné nastavit tuto dvojici pracovních registrů:
Registr | Význam | Obsah |
---|---|---|
eax | číslo syscallu | sys_write=1 |
ebx | návratová hodnota | exit code = 0 |
U volání sys_write se naproti tomu nastaví pracovní registry takto:
Registr | Význam | Obsah |
---|---|---|
eax | číslo syscallu | sys_write=4 |
ebx | číslo deskriptoru | stdout=1 |
ecx | adresa řetězce/bajtů | nastaví se do .data segmentu |
edx | počet bajtů pro zápis | strlen(„Hello world!\n“)=13 |
Zajímavý je obsah pracovního registru ecx, protože ten musí obsahovat adresu řetězce (resp. bloku bajtů). V AT&T syntaxi to vypadá následovně:
mov $hello_lbl,%ecx
kdežto v syntaxi Intelu se namísto toho použije:
mov ecx, hello_lbl
Přičemž hello_lbl je návěští (label) neboli pojmenovaná adresa. Povšimněte si, že řetězec není ukončen znakem s ASCII kódem 0. To není nutné, protože systémová služba přesně zná délku řetězce (bloku bajtů):
# asmsyntax=as # Jednoducha aplikace typu "Hello world!" naprogramovana # v assembleru GNU as - pouzita je "Intel" syntaxe. # # Autor: Pavel Tisnovsky .intel_syntax noprefix # Linux kernel system call table sys_exit=1 sys_write=4 _start: mov eax, sys_write # cislo syscallu pro funkci "write" mov ebx, 1 # standardni vystup mov ecx, hello_lbl # adresa retezce, ktery se ma vytisknout mov edx, 13 # pocet znaku, ktere se maji vytisknout int 0x80 # volani Linuxoveho kernelu mov eax, sys_exit # cislo sycallu pro funkci "exit" mov ebx, 0 # exit code = 0 int 0x80 # volani Linuxoveho kernelu hello_lbl: .string "Hello World!\n" # string, ktery JE ukoncen nulou
13. Vygenerování binárního souboru se strojovým kódem aplikace „Hello world“
Zdrojový kód napsaný v assembleru, který byl uveden v předchozí kapitole, můžeme přímo přeložit frameworkem Keystone. Postup je v tomto případě velmi jednoduchý: nejprve obsah souboru načteme do řetězce (s oddělovači řádků atd.), provedeme překlad (assembling) a výsledný binární sekvenci instrukcí uložíme do binárního souboru:
# import všech symbolů použitých ve skriptu from keystone import Ks, KS_ARCH_X86, KS_MODE_64, KsError try: # načtení kódu v assembleru ze souboru with open("hello_world.asm", "r") as fin: code = fin.read() # kontrolní výpis, jaký kód budeme překládat print(code) # inicializace assembleru se specifikací architektury a popř. i režimu ks = Ks(KS_ARCH_X86, KS_MODE_64) # vlastní překlad (assembling) encoding, count = ks.asm(code) # uložení výsledného nativního kódu do souboru with open("hello.bin", "wb") as fout: fout.write(bytes(encoding)) print("Binary file written") except KsError as e: print("ERROR: %s" % e)
Po spuštění výše uvedeného skriptu by měl vzniknout soubor nazvaný „hello.bin“ s délkou přesně 48 bajtů. Můžeme se samozřejmě podívat na obsah tohoto souboru. Nejdříve použijeme standardní osmičkový/hexadecimální prohlížeč od neboli octal dump:
$ od -tx1 hello.bin 0000000 b8 04 00 00 00 bb 01 00 00 00 b9 22 00 00 00 ba 0000020 0d 00 00 00 cd 80 b8 01 00 00 00 bb 00 00 00 00 0000040 cd 80 48 65 6c 6c 6f 20 57 6f 72 6c 64 21 0a 00 0000060
Zajímavější je ovšem použít nástroj objdump, a to následujícím způsobem:
$ objdump -b binary -D -m i386:x86-64 -M intel hello.bin > hello_dump.asm
Výsledek bude vypadat takto:
hello.bin: file format binary Disassembly of section .data: 0000000000000000 <.data>: 0: b8 04 00 00 00 mov eax,0x4 5: bb 01 00 00 00 mov ebx,0x1 a: b9 22 00 00 00 mov ecx,0x22 f: ba 0d 00 00 00 mov edx,0xd 14: cd 80 int 0x80 16: b8 01 00 00 00 mov eax,0x1 1b: bb 00 00 00 00 mov ebx,0x0 20: cd 80 int 0x80 22: 48 rex.W 23: 65 6c gs ins BYTE PTR es:[rdi],dx 25: 6c ins BYTE PTR es:[rdi],dx 26: 6f outs dx,DWORD PTR ds:[rsi] 27: 20 57 6f and BYTE PTR [rdi+0x6f],dl 2a: 72 6c jb 0x98 2c: 64 21 0a and DWORD PTR fs:[rdx],ecx ...
$ strings hello.bin Hello World!
14. Umístění řetězce (dat) před programový kód
Pokusme se nyní zdrojový kód napsaný v assembleru a uvedený ve dvanácté kapitole upravit tak, že definici řetězce vložíme před vlastní sekvenci instrukcí. Vzhledem k tomu, že v kódu není uvedena definice sekcí (segmentů) a vlastně ani nepoužíváme linker, bude se výsledný nativní binární kód lišit:
# asmsyntax=as # Jednoducha aplikace typu "Hello world!" naprogramovana # v assembleru GNU as - pouzita je "Intel" syntaxe. # # Autor: Pavel Tisnovsky .intel_syntax noprefix # Linux kernel system call table sys_exit=1 sys_write=4 hello_lbl: .string "Hello World!\n" # string, ktery JE ukoncen nulou _start: mov eax, sys_write # cislo syscallu pro funkci "write" mov ebx, 1 # standardni vystup mov ecx, hello_lbl # adresa retezce, ktery se ma vytisknout mov edx, 13 # pocet znaku, ktere se maji vytisknout int 0x80 # volani Linuxoveho kernelu mov eax, sys_exit # cislo sycallu pro funkci "exit" mov ebx, 0 # exit code = 0 int 0x80 # volani Linuxoveho kernelu
Pro překlad použijeme skript:
# import všech symbolů použitých ve skriptu from keystone import Ks, KS_ARCH_X86, KS_MODE_64, KsError try: # načtení kódu v assembleru ze souboru with open("hello_world_2.asm", "r") as fin: code = fin.read() # kontrolní výpis, jaký kód budeme překládat print(code) # inicializace assembleru se specifikací architektury a popř. i režimu ks = Ks(KS_ARCH_X86, KS_MODE_64) # vlastní překlad (assembling) encoding, count = ks.asm(code) # uložení výsledného nativního kódu do souboru with open("hello_2.bin", "wb") as fout: fout.write(bytes(encoding)) print("Binary file written") except KsError as e: print("ERROR: %s" % e)
Výsledný binární soubor bude mít opět délku 48 bajtů, ovšem odlišný obsah:
$ od -tx1 hello_2.bin 0000000 48 65 6c 6c 6f 20 57 6f 72 6c 64 21 0a 00 b8 04 0000020 00 00 00 bb 01 00 00 00 b9 00 00 00 00 ba 0d 00 0000040 00 00 cd 80 b8 01 00 00 00 bb 00 00 00 00 cd 80 0000060
I z výstupu získaného nástrojem objdump je patrné, že v souboru se nejdříve nachází samotný řetězec a teprve od offsetu 0×13 sekvence instrukcí:
hello_2.bin: file format binary Disassembly of section .data: 0000000000000000 <.data>: 0: 48 rex.W 1: 65 6c gs ins BYTE PTR es:[rdi],dx 3: 6c ins BYTE PTR es:[rdi],dx 4: 6f outs dx,DWORD PTR ds:[rsi] 5: 20 57 6f and BYTE PTR [rdi+0x6f],dl 8: 72 6c jb 0x76 a: 64 21 0a and DWORD PTR fs:[rdx],ecx d: 00 b8 04 00 00 00 add BYTE PTR [rax+0x4],bh 13: bb 01 00 00 00 mov ebx,0x1 18: b9 00 00 00 00 mov ecx,0x0 1d: ba 0d 00 00 00 mov edx,0xd 22: cd 80 int 0x80 24: b8 01 00 00 00 mov eax,0x1 29: bb 00 00 00 00 mov ebx,0x0 2e: cd 80 int 0x80
15. Složitější kód zapsaný v assembleru založený na použití podprogramů (subrutin)
Nástroj Keystone dokáže přeložit i složitější programové kódy zapsané v assembleru. Může se jednat například o programy, v nichž se používají podprogramy neboli subrutiny. Povšimněte si, že subrutiny voláme instrukcí call ještě před jejich definicí, takže assembler nemůže při prvním průchodu znát jejich adresy (navíc ze subrutin voláme další subrutiny). Ty bude znát až na začátku druhého průchodu:
# asmsyntax=as # Program pro otestovani chovani instrukci CALL a RET # - pouzita je "Intel" syntaxe. # # Autor: Pavel Tisnovsky .intel_syntax noprefix # Linux kernel system call table sys_exit = 1 sys_write = 4 # Dalsi konstanty pouzite v programu - standardni streamy std_input = 0 std_output = 1 message1: # adresa prvni zpravy .string "Hello World\n" message1len = 13 # delka prvni zpravy message2: # adresa druhe zpravy .string "Assembler je fajn\n" message2len = 18 # delka druhe zpravy _start: call writeFirstMessage # zavolani podprogramu pro vytisteni prvni zpravy call writeSecondMessage # zavolani podprogramu pro vytisteni druhe zpravy call exit # zavolani podprogramu pro ukonceni procesu # Podprogram pro vytisteni prvni zpravy writeFirstMessage: mov ecx, message1 # adresa retezce, ktery se ma vytisknout mov edx, message1len # pocet znaku, ktere se maji vytisknout call writeMessage # zavolani podprogramu pro vytisteni zpravy ret # navrat z podprogramu # Podprogram pro vytisteni druhe zpravy writeSecondMessage: mov ecx, message2 # adresa retezce, ktery se ma vytisknout mov edx, message2len # pocet znaku, ktere se maji vytisknout call writeMessage # zavolani podprogramu pro vytisteni zpravy ret # navrat z podprogramu # Podprogram pro vytisteni zpravy na standardni vystup # Ocekava se, ze v ecx bude adresa zpravy a v edx jeji delka writeMessage: mov eax, sys_write # cislo syscallu pro funkci "write" mov ebx, std_output # standardni vystup int 0x80 # volani Linuxoveho kernelu ret # navrat z podprogramu # Podprogram pro ukonceni procesu zavolanim syscallu exit: mov eax, sys_exit # cislo sycallu pro funkci "exit" mov ebx, 0 # exit code = 0 int 0x80 # volani Linuxoveho kernelu # finito
16. Překlad kódu zapsaného v assembleru se zpětným překladem
Výše uvedený kód zapsaný v assembleru přeložíme následujícím skriptem:
from keystone import * try: with open("subroutines.asm", "r") as fin: code = fin.read() print(code) ks = Ks(KS_ARCH_X86, KS_MODE_32) encoding, count = ks.asm(code) with open("subroutines.bin", "wb") as fout: fout.write(bytes(encoding)) except KsError as e: print("ERROR: %s" %e)
Výsledkem překladu bude binární soubor o délce 104 bajtů, jehož obsah si můžeme prohlédnout nástrojem objdump. Povšimněte si, že vlastní instrukce začínají až od offsetu 0×20, protože v první části binárního souboru jsou uloženy řetězce:
subroutines.bin: file format binary Disassembly of section .data: 0000000000000000 <.data>: 0: 48 rex.W 1: 65 6c gs ins BYTE PTR es:[rdi],dx 3: 6c ins BYTE PTR es:[rdi],dx 4: 6f outs dx,DWORD PTR ds:[rsi] 5: 20 57 6f and BYTE PTR [rdi+0x6f],dl 8: 72 6c jb 0x76 a: 64 0a 00 or al,BYTE PTR fs:[rax] d: 41 73 73 rex.B jae 0x83 10: 65 6d gs ins DWORD PTR es:[rdi],dx 12: 62 (bad) 13: 6c ins BYTE PTR es:[rdi],dx 14: 65 72 20 gs jb 0x37 17: 6a 65 push 0x65 19: 20 66 61 and BYTE PTR [rsi+0x61],ah 1c: 6a 6e push 0x6e 1e: 0a 00 or al,BYTE PTR [rax] 20: e8 0a 00 00 00 call 0x2f 25: e8 15 00 00 00 call 0x3f 2a: e8 2d 00 00 00 call 0x5c 2f: b9 00 00 00 00 mov ecx,0x0 34: ba 0d 00 00 00 mov edx,0xd 39: e8 11 00 00 00 call 0x4f 3e: c3 ret 3f: b9 0d 00 00 00 mov ecx,0xd 44: ba 12 00 00 00 mov edx,0x12 49: e8 01 00 00 00 call 0x4f 4e: c3 ret 4f: b8 04 00 00 00 mov eax,0x4 54: bb 01 00 00 00 mov ebx,0x1 59: cd 80 int 0x80 5b: c3 ret 5c: b8 01 00 00 00 mov eax,0x1 61: bb 00 00 00 00 mov ebx,0x0 66: cd 80 int 0x80
17. Seznam podporovaných architektur a režimů
Na závěr si ještě pro úplnost uveďme seznam podporovaných architektur a jejich režimů, a to při použití kombinace Keystone + rozhraní pro Python. Může se totiž stát, že samotný Keystone bude v budoucnu podporovat i další architektury, které se do rozhraní pro Python zařadí s určitým zpožděním.
Seznam podporovaných architektur:
Konstanta | Význam |
---|---|
KS_ARCH_ARM | 32bitový ARM |
KS_ARCH_ARM64 | 64bitový ARM (AArch64) |
KS_ARCH_MIPS | MIPS |
KS_ARCH_X86 | x86(64) |
KS_ARCH_PPC | PowerPC |
KS_ARCH_SPARC | SPARC |
KS_ARCH_SYSTEMZ | SystemZ |
KS_ARCH_HEXAGON | Qualcomm Hexagon |
KS_ARCH_EVM | EVM (Texas Instruments) |
Režimy:
Konstanta | Význam |
---|---|
KS_MODE_LITTLE_ENDIAN | LE režim pro některé architektury |
KS_MODE_BIG_ENDIAN | BE režim pro některé architektury |
KS_MODE_ARM | klasický 32bitový ARM |
KS_MODE_THUMB | 32bitový ARM s instrukcemi Thumb a Thumb2 |
KS_MODE_V8 | ARM v8 |
KS_MODE_V9 | ARM v9 |
KS_MODE_MIPS3 | režimy pro MIPS |
KS_MODE_MIPS32R6 | režimy pro MIPS |
KS_MODE_MIPS32 | režimy pro MIPS |
KS_MODE_MIPS64 | režimy pro MIPS |
KS_MODE16 | režimy pro x86(64) |
KS_MODE32 | režimy pro x86(64) |
KS_MODE64 | režimy pro x86(64) |
KS_MODE_PPC32 | režimy pro PowerPC |
KS_MODE_PPC64 | režimy pro PowerPC |
KS_MODE_SPARC32 | režimy pro SPARC |
KS_MODE_SPARC64 | režimy pro SPARC |
18. Obsah navazujícího článku – framework Capstone
V navazujícím článku se seznámíme s frameworkem Capstone, jenž se velmi často používá společně s dnes popisovaným frameworkem Keystone. Zatímco výše zmíněný framework Keystone je univerzálním assemblerem, jedná se v případě frameworku Capstone o univerzální disassembler, který podporuje všechny v současnosti mainstreamové architektury (x86, x86–64, 32bitový ARM, AArch64 atd.) a je implementován formou nativní knihovny, kterou je možné volat z různých programovacích jazyků. Oba zmíněné frameworky, tedy jak Capstone, tak i Keystone, lze přitom používat společně. Příkladem mohou být systémy, které nejprve provedou disassembling vybraného binárního bloku dat s úpravou kódu v assembleru a s jeho následným překladem zpět do nativního binárního kódu.
Ukažme si tedy alespoň základní způsob zavolání disassembleru Capstone. Použijeme ho pro zpětný překlad nativních instrukcí uložených v souboru „loops.bin“, jenž vznikl překladem provedeným v rámci předchozích kapitol:
from capstone import * with open("loops.bin", "rb") as fin: code = fin.read() md = Cs(CS_ARCH_X86, CS_MODE_64) for i in md.disasm(code, 0x0000): print("0x%x:\t%s\t%s" %(i.address, i.mnemonic, i.op_str))
Výsledek se do určité míry podobá výsledku získaného nástrojem objdump:
0x0: mov ebx, 0xa 0x5: mov eax, 0x64 0xa: dec eax 0xc: jne 5 0xe: dec ebx 0x10: jne 0
19. Repositář s demonstračními příklady
Zdrojové kódy všech prozatím popsaných demonstračních příkladů určených pro programovací jazyk Python 3 (nikoli ovšem pro starší verze Pythonu 2!) byly uloženy do Git repositáře dostupného na adrese https://github.com/tisnik/most-popular-python-libs. V případě, že nebudete chtít klonovat celý repositář (ten je ovšem stále velmi malý, dnes má velikost zhruba několik desítek kilobajtů), můžete namísto toho použít odkazy na jednotlivé příklady, které naleznete v následující tabulce:
20. Odkazy na Internetu
- Keystone Engine na GitHubu
https://github.com/keystone-engine/keystone - Keystone: The Ultimate Assembler
https://www.keystone-engine.org/ - The Ultimate Disassembler
http://www.capstone-engine.org/ - Tutorial for Keystone
https://www.keystone-engine.org/docs/tutorial.html - Rozhraní pro Capstone na PyPi
https://pypi.org/project/capstone/ - Rozhraní pro Keystone na PyPi
https://pypi.org/project/keystone-engine/ - KEYSTONE: Next Generation Assembler Framework
https://www.keystone-engine.org/docs/BHUSA2016-keystone.pdf - AT&T Syntax versus Intel Syntax
http://web.mit.edu/rhel-doc/3/rhel-as-en-3/i386-syntax.html - AT&T assembly syntax and IA-32 instructions
https://gist.github.com/mishurov/6bcf04df329973c15044 - ARM GCC Inline Assembler Cookbook
http://www.ethernut.de/en/documents/arm-inline-asm.html - Extended Asm – Assembler Instructions with C Expression Operands
https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html - ARM inline asm secrets
http://hardwarebug.org/2010/07/06/arm-inline-asm-secrets/ - How to Use Inline Assembly Language in C Code
https://gcc.gnu.org/onlinedocs/gcc/Using-Assembly-Language-with-C.html#Using-Assembly-Language-with-C - GCC-Inline-Assembly-HOWTO
http://ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html - A Brief Tutorial on GCC inline asm (x86 biased)
http://www.osdever.net/tutorials/view/a-brief-tutorial-on-gcc-inline-asm - GCC Inline ASM
http://locklessinc.com/articles/gcc_asm/ - GNU Assembler Examples
http://cs.lmu.edu/~ray/notes/gasexamples/ - X86 Assembly/Arithmetic
https://en.wikibooks.org/wiki/X86_Assembly/Arithmetic - Art of Assembly – Arithmetic Instructions
http://oopweb.com/Assembly/Documents/ArtOfAssembly/Volume/Chapter6/CH06–2.html - The GNU Assembler Tutorial
http://tigcc.ticalc.org/doc/gnuasm.html - The GNU Assembler – macros
http://tigcc.ticalc.org/doc/gnuasm.html#SEC109 - ARM subroutines & program stack
http://www.toves.org/books/armsub/ - Generating Mixed Source and Assembly List using GCC
http://www.systutorials.com/240/generate-a-mixed-source-and-assembly-listing-using-gcc/ - Calling subroutines
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.kui0100a/armasm_cihcfigg.htm - Linux assemblers: A comparison of GAS and NASM
http://www.ibm.com/developerworks/library/l-gas-nasm/index.html - Programovani v assembleru na OS Linux
http://www.cs.vsb.cz/grygarek/asm/asmlinux.html - Is it worthwhile to learn x86 assembly language today?
https://www.quora.com/Is-it-worthwhile-to-learn-x86-assembly-language-today?share=1 - Why Learn Assembly Language?
http://www.codeproject.com/Articles/89460/Why-Learn-Assembly-Language - Is Assembly still relevant?
http://programmers.stackexchange.com/questions/95836/is-assembly-still-relevant - Why Learning Assembly Language Is Still a Good Idea
http://www.onlamp.com/pub/a/onlamp/2004/05/06/writegreatcode.html - Assembly language today
http://beust.com/weblog/2004/06/23/assembly-language-today/ - Assembler: Význam assembleru dnes
http://www.builder.cz/rubriky/assembler/vyznam-assembleru-dnes-155960cz - Assembler pod Linuxem
http://phoenix.inf.upol.cz/linux/prog/asm.html - Online x86 / x64 Assembler and Disassembler
https://defuse.ca/online-x86-assembler.htm#disassembly