Hier eine kleine Demonstration, wie man mit Assembler innerhalb eines Programms nicht nur Variablen, sondern auch den Programmcode verschlüsseln kann, und somit Programme hat, die sich erst zur Laufzeit decodieren.
Schritt 1:
Wir erstellen zunächst ein handelsübliches Programm, zum Beispiel das allseits beliebte "Hello World".
Code: Alles auswählen
; 64-bit "Hello World!" uncodiert... 1 von 3
; by myself
; nasm -f elf64 hello-cod01.asm
; ld -s -o hello-cod01 hello-cod01.o
[bits 64]
global _start ; global entry point export for ld
section .data
_start:
jmp decode
db 90h ; opcode für den nop-Befehl
db 90h ; also "no operation"
db 90h
db 90h
sss:
mov rax,4 ; schreiben
mov rbx,1 ; an der Console
mov rcx,msg
mov rdx,msg_size
int 0x80
mov rax,1
xor rbx,rbx
int 0x80
msg: db 'Hello world!',0xA
msg_size: equ $-msg
decode:
jmp sss
db 90h ; wieder 4 mal opcodes für den nop-Befehl,
db 90h ; also "no operation"
db 90h
db 90h
Vor und nach dem eigentlichen Programm habe ich jeweils 4 mal den nop-Befehl verwendet, bzw. den zugehörigen opcode, um später beim debuggen, oder beim hexdump leichter die wichtigen Stellen dazwischen identifizieren zu können.
OK, dann wie im Quellcode beschrieben assemblieren und linken, fertig ist der erste Schritt.
Schritt 2:
Wir nehmen uns die ausführbare Version des obigen Programms und schauen es uns an.
Linux liefert normalerweise ein hexdump-Programm mit, ansonsten geht das auch mit jedem beliebigen Hexeditor.
Ich selbst habe mit "hexdump -C hello-cod01" folgende Ausgabe erhalten:
Code: Alles auswählen
00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............|
00000010 02 00 3e 00 01 00 00 00 78 00 60 00 00 00 00 00 |..>.....x.`.....|
00000020 40 00 00 00 00 00 00 00 c8 00 00 00 00 00 00 00 |@...............|
00000030 00 00 00 00 40 00 38 00 01 00 40 00 03 00 02 00 |....@.8...@.....|
00000040 01 00 00 00 06 00 00 00 00 00 00 00 00 00 00 00 |................|
00000050 00 00 60 00 00 00 00 00 00 00 60 00 00 00 00 00 |..`.......`.....|
00000060 b6 00 00 00 00 00 00 00 b6 00 00 00 00 00 00 00 |................|
00000070 00 00 20 00 00 00 00 00 eb 36 90 90 90 90 b8 04 |.. ......6......|
00000080 00 00 00 bb 01 00 00 00 48 b9 a3 00 60 00 00 00 |........H...`...|
00000090 00 00 ba 0d 00 00 00 cd 80 b8 01 00 00 00 48 31 |..............H1|
000000a0 db cd 80 48 65 6c 6c 6f 20 77 6f 72 6c 64 21 0a |...Hello world!.|
000000b0 eb cc 90 90 90 90 00 2e 73 68 73 74 72 74 61 62 |........shstrtab|
000000c0 00 2e 64 61 74 61 00 00 00 00 00 00 00 00 00 00 |..data..........|
000000d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00000100 00 00 00 00 00 00 00 00 0b 00 00 00 01 00 00 00 |................|
00000110 03 00 00 00 00 00 00 00 78 00 60 00 00 00 00 00 |........x.`.....|
00000120 78 00 00 00 00 00 00 00 3e 00 00 00 00 00 00 00 |x.......>.......|
00000130 00 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 |................|
00000140 00 00 00 00 00 00 00 00 01 00 00 00 03 00 00 00 |................|
00000150 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000160 b6 00 00 00 00 00 00 00 11 00 00 00 00 00 00 00 |................|
00000170 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 |................|
*
00000188
In der Zeile 00000070 finden wir nach kurzer Suche unsere opcodes mit "90" wieder, welche wir vorhin zur besseren Identifikation mit ein programmiert haben. Und in der Zeile 000000b0 finden wir die anderen "90"-er Opcodes, die wir am Ende unseres kleinen Programms mit eingebaut haben. Das heißt, alles was dazwischen ist, ist jeweils der opcode der Befehle des eigentlichen Programms, weches "Hello World!" schreibt, und sich dann selbt beendet.
Schritt 3:
Wir erstellen eine Kopie des ersten asm-Programms und ersetzen die Befehle für den Schreibbefehl und den Befehl für das Programmende durch ihre Opcodes. Opcods kann man wie Variablen ohne Namen eingeben.
Es geht also wie gesgat los nach den 4 mal 90h, die für die4 "nop"-Befehle stehen und beginnt somit mit "b8" "04" "00" "00" usw...
bis wir eben wieder bei den unteren 90h Opcodes angelangt sind.
Faktisch gehen wir also zu dem sss:-Label und beginnen dort mit
db b8h
db 04h
db 00h
db 00h
usw...
bis wir alle Befehle durch Opcodes ersetzt haben. Im Programm selbst habe lasse ich jene Opcodes, die mit Buchstaben beginnen noch mit einer Null anführen, sozusagen aus ästhetischen Gründen.

Nach getaner Arbeit haben wir dann einen Assembler-Quellcode, der so aussieht:
Code: Alles auswählen
; 64-bit "Hello World!" uncodiert, aber als opcodes... 2 von 3
; by myself
; nasm -f elf64 hello-cod02.asm
; ld -s -o hello-cod02 hello-cod02.o
[bits 64]
global _start ; global entry point export for ld
section .data
_start:
jmp decode
db 90h
db 90h
db 90h
db 90h
sss:
db 0b8h
db 04h
db 00h
db 00h
db 00h
db 0bbh
db 01h
db 00h
db 00h
db 00h
db 48h
db 0b9h
db 0a3h
db 00h
db 60h
db 00h
db 00h
db 00h
db 00h
db 00h
db 0bah
db 0dh
db 00h
db 00h
db 00h
db 0cdh
db 80h
db 0b8h
db 01h
db 00h
db 00h
db 00h
db 48h
db 31h
db 0dbh
db 0cdh
db 80h
db 48h
db 65h
db 6ch
db 6ch
db 6fh
db 20h
db 77h
db 6fh
db 72h
db 6ch
db 64h
db 21h
db 0ah
decode:
jmp sss
db 90h
db 90h
db 90h
db 90h
Wenn wir die Datei assemblieren und linken, sollte eine exakte Kopie des ersten Programms entstanden sein, die auch exakt die gleiche Ausgabe produziert.
Womit wir auch schon zum dritten Schritt kommen.
Schritt 3:
Wir erstellen uns vom zweiten Programm ebenfalls eine Kopie und bearbeiten diese im Editor wie folgt:
Zu allen vorhin eingegebenen Opcodes (die ja letztlich nur eine hexadezimale Zahl darstellen) addieren wir 18h, so dass wir beginnen mit
Code: Alles auswählen
sss:
zeugs db 0b8h + 18h
db 04h + 18h
db 00h + 18h
db 00h + 18h
db 00h + 18h
db 0bbh + 18h
...und das ganze fortführen bis zu den altbekannten "90h"-Opcodes. Den Variablen-Namen "Zeugs habe ich ebenfalls eingeführt, weil wir für die Decodierroutine einen Offset brauchen, um fest zu legen, wo mit der Decodierung angefangen werden soll.
Praktischerweise startet das Programm ja bereits mit einem Sprung zu "decode:" (als ob wir's geahnt hätten...) so dass wir genau dort sofort mit der Decodierung anfangen können.
Code: Alles auswählen
decode:
sub byte [zeugs],18h
sub byte [zeugs+1],18h
sub byte [zeugs+2],18h
sub byte [zeugs+3],18h
sub byte [zeugs+4],18h
sub byte [zeugs+5],18h
usw...
Wir sagen also dem Programm, dass es ab dem Offset "zeugs" bei jedem folgenden byte 18h abziehen soll, nämlich genau jene, die wir vorhin händisch hinzu addiert haben. Und das machen wir bei allen einzelnen Opcodes, die wir vorhin ersetzt haben und dann 18h addiert haben.
Danach springen wir zu dem eigentlich aus zu führenden Programmteil, den wir bis dahin decodiert ahben, und alles sollte wieder funktionieren wie beim allerersten "Hello World!"-Programm.
Das fertige asm-Programm sollte also zum Schluss so aussehen:
Code: Alles auswählen
; 64-bit "Hello World!" codiert, als original opcodes + 18h... 3 von 3
; by myself
; nasm -f elf64 hello-cod03.asm
; ld -s -o hello-cod03 hello-cod03.o
[bits 64]
global _start ; global entry point export for ld
section .data
_start:
jmp decode
db 90h
db 90h
db 90h
db 90h
sss:
zeugs db 0b8h + 18h
db 04h + 18h
db 00h + 18h
db 00h + 18h
db 00h + 18h
db 0bbh + 18h
db 01h + 18h
db 00h + 18h
db 00h + 18h
db 00h + 18h
db 48h + 18h
db 0b9h + 18h
db 0a3h + 18h
db 00h + 18h
db 60h + 18h
db 00h + 18h
db 00h + 18h
db 00h + 18h
db 00h + 18h
db 00h + 18h
db 0bah + 18h
db 0dh + 18h
db 00h + 18h
db 00h + 18h
db 00h + 18h
db 0cdh + 18h
db 80h + 18h
db 0b8h + 18h
db 01h + 18h
db 00h + 18h
db 00h + 18h
db 00h + 18h
db 48h + 18h
db 31h + 18h
db 0dbh + 18h
db 0cdh + 18h
db 80h + 18h
db 48h + 18h
db 65h + 18h
db 6ch + 18h
db 6ch + 18h
db 6fh + 18h
db 20h + 18h
db 77h + 18h
db 6fh + 18h
db 72h + 18h
db 6ch + 18h
db 64h + 18h
db 21h + 18h
db 0ah + 18h
decode:
sub byte [zeugs],18h
sub byte [zeugs+1],18h
sub byte [zeugs+2],18h
sub byte [zeugs+3],18h
sub byte [zeugs+4],18h
sub byte [zeugs+5],18h
sub byte [zeugs+6],18h
sub byte [zeugs+7],18h
sub byte [zeugs+8],18h
sub byte [zeugs+9],18h
sub byte [zeugs+10],18h
sub byte [zeugs+11],18h
sub byte [zeugs+12],18h
sub byte [zeugs+13],18h
sub byte [zeugs+14],18h
sub byte [zeugs+15],18h
sub byte [zeugs+16],18h
sub byte [zeugs+17],18h
sub byte [zeugs+18],18h
sub byte [zeugs+19],18h
sub byte [zeugs+20],18h
sub byte [zeugs+21],18h
sub byte [zeugs+22],18h
sub byte [zeugs+23],18h
sub byte [zeugs+24],18h
sub byte [zeugs+25],18h
sub byte [zeugs+26],18h
sub byte [zeugs+27],18h
sub byte [zeugs+28],18h
sub byte [zeugs+29],18h
sub byte [zeugs+30],18h
sub byte [zeugs+31],18h
sub byte [zeugs+32],18h
sub byte [zeugs+33],18h
sub byte [zeugs+34],18h
sub byte [zeugs+35],18h
sub byte [zeugs+36],18h
sub byte [zeugs+37],18h
sub byte [zeugs+38],18h
sub byte [zeugs+39],18h
sub byte [zeugs+40],18h
sub byte [zeugs+41],18h
sub byte [zeugs+42],18h
sub byte [zeugs+43],18h
sub byte [zeugs+44],18h
sub byte [zeugs+45],18h
sub byte [zeugs+46],18h
sub byte [zeugs+47],18h
sub byte [zeugs+48],18h
sub byte [zeugs+49],18h
sub byte [zeugs+50],18h
jmp sss
db 90h
db 90h
db 90h
Man könnte an dieser Stelle natürlich noch einen 4. Schritt anhängen und die Decodierroutine als Schleife formulieren, aber das gehört nicht mehr zu der Sache, die ich erläutern wollte. Wichtig bei allem ist auf jeden Fall, dass die ganze section als "data" definiert wird, denn das standardmäßige "text" ließe eine Veränderung der Opcodes nicht zu, da schreibgeschützt.
Wenn wir die erstellten 3 Programme jetzt mit einem Hexeditor anschauen, können wir also sehen, dass die ersten beiden die "Hello-World"-Meldung im Klartext vorliegen haben, so dass sie von jedem Hinterhof-Hacker verändert werden können, während das im dritten Programm nicht mehr der Fall ist.