Tråd bedømmelse:
  • 1 Stemmer - 5 Gennemsnit
  • 1
  • 2
  • 3
  • 4
  • 5
Linux assembler - En kort introduktion
02-10-2013, 10:17 (Denne besked var sidst ændret: 12-04-2015, 16:32 af iTick.)
#1
Linux assembler - En kort introduktion
Assembler er det sprog der kommer nærmest maskinkode, uden at være det. Og jeg vil lige starte med en advarsel om,
man er nød til at læse lidt, før man kan begynde at skrive kode.
Det er sjældent man bruger assembler til at lave et helt program med, med mindre det har meget begrænset funktionalitet.
F.eks. bruges det til bootloaderen som initieres af BIOS, da man kun har 512 bytes tilgængelig.
Oftest vil man lave små moduler i assembler, hvor hastigheden er meget vigtig, og så kalde disse moduler fra et 3. generationssprog.
Det er mest set i grafik drivere og sikkert spil engines osv.

Selve assembler koden, som egentlig bare er tekst, vil først blive compilet til OP koder, eller rigtige maskinkode instruktioner.
Her efter linkes det sammen med eventuelle andre compilede assembler filer, som f.eks. libraries. Når det linkes, vil de faktiske adresser blive fastlagt.
Når forskellige stykker assembler kode bliver compilet seperat, ved compileren ikke, hvilke adresser, funktioner, variabler osv. vil få,
før det hele er linket sammen, til een fil.

Denne lille teaser vil ikke være dybdegående, men forhåbentlig give nogen af jer lidt blod på tanden, og komme i gang.
Der er ikke andre måder at lære assembler på, end at læse og kode.
Tro det eller ej, men i dagens danmark, og resten af verden, er assembler faktisk blevet en del nemmere.
Man skal ikke længere holde øje med, hvilket segment man er i, med mindre du vil kode dit eget operativsystem. Og man kan slippe afsted med at lave systemkald.
Altså kald til funktioner i kernen. Det er også den primære årsag til, der er forskel på assembler til Linux og Windows.
Det vil jeg dog ikke gå i detaljer med.

Der er primært to assembler stilarter til CPUer, som understøtter IA32 (Intel Architecture) og amd64, og disse er Intel-style og AT&T style.
Instruktionerne er ikke alle ens og AT&T-style flytter fra venstre til højre og Intel-style, flytter fra højre til venstre.
For mig, som har kodet en del forskellige sprog, er det underligt at "flytte mod højre", så jeg bruger altid Intel-style,
og det er det, vi vil bruge i dette eksempel.

Uden jeg vil forklare selve koden, er her et eksempel på forskellen:

Kode:
Intel                   AT&T
MOV EAX, [0100]         movl 0x0100, %eax

flyt EAX <-- [0100]    flyt 0x0100 --> %EAX

Jeg kan ikke forene mig selv med tanken om at skulle flytte mod højre, så denne guide dækker kun Intel-style assembler.

Inde i CPUen er der nogle registre, som vi ofte fylder værdier i, når vi skal regne på dem. Man bruger ikke helt så meget variabler,
som man kender det fra andre sprog. Disse registre har nogle faste formål, men kan i dag, også bruges mere generelt.
Et af disse registre er det nye 64 bit RAX, som så selvfølgelig indeholder 64 bits. Det er ikke unormalt at tænke på disse som binære registre.
Et tomt RAX register vil se således ud:

RAX: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000

Hvis man flytter et tal til RAX, gøres det med "mov(e)" kommandoen, som naturligt nok flytter data.

mov eax, 8

Her efter vil RAX se således ud:
RAX: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00001000

Selv om der med 64 bit CPUen, blev introduceret nye "General Purpose" registre, er det ikke fordi, der er så mange,
at man kan fråse med dem.
Man kan derfor arbejde i de 32 bits til højre i RAX, ved at bruge EAX. Så EAX og RAX er det samme register, og hvis du flytter noget til det ene,
vil det overskrive indholdet i det andet.

Her er et overblik:
Kode:
RAX: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00001000
EAX:                                     00000000 00000000 00000000 00001000
Man kan ligeledes referere til de 16 bits til højre i EAX, ved at bruge AX.

Kode:
RAX: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00001000
EAX:                                     00000000 00000000 00000000 00001000
AX:                                                        00000000 00001000

AX registret kan deles op i to, og man kan flytte data til de 8 bits til højre og de 8 bits til venstre, seperat.


Kode:
RAX: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00001000
EAX:                                     00000000 00000000 00000000 00001000
AX:                                                        00000000 00001000
                                                           AH       AL
dvs:

Kode:
RAX:                                EAX              |AH      |AL       |
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00001000
                                                     |        AX        |
Det samme gælder for registre som RBX, RCX, RDX osv.
Dette kommer sig af, at man i gamle dage, kun havde de små registre, og som tingene udviklede sig, blev registrene større og større.
Men for at holde bagud kompabiliteten, har man udvidet de eksisterende registre, men stadig beholdt de gamle referencer.
Andre Interessante registre som i gamle dage havde mere bestemte formål, men nu også kan bruges til andre ting:
EAX 32 GP register (General Purpose/Accumulator register)
EBX 32 GP register (General Purpose/Base register)
ECX 32 GP register (General Purpose/Counter register)
EDX 32 GP register (General Purpose/Data)
ESP 32 Stack Pointer (Pejer på det element i stacken, som vil blive fjernet næste gang der fjernes. Toppen af stacken)
EBP 32 Base Pointer (Bruges til at bygge en stack frame med)
EIP 32 Instruction Pointer (Offset)
ESI 32 Source Index
EDI 32 Destination Index
Ovenstående er de mest interessante 32 bit registre, og de findes også med forskellige bit længder. Her under deres 64 bit versioner som f.eks. RAX, RBX osv.

Før vi ser på et par instruktioner, vil jeg præsentere et lille skelet jeg bruger, når jeg starter på noget nyt:

Kode:
BITS 32        ; Dette er et 32 bit eksempel. IA32 er Intel Architecture 32 bit.
GLOBAL _start    ; Entry point til ELF linkeren (Det er her, vir fortæller compileren hvor i .text sektionen, eksekveringen starter)

SECTION .data    ; Start af data area, hvor vi har vores initialiserede data. Variabler ol.

SECTION    .bss    ; Her lægger vi vores uninitialized data. Buffers osv.

SECTION .text    ; Start af code block. Selve assembler koden skal placeres her under
_start:
    ; Test din kode her...

    mov    eax,1        ; Systemkaldet til exit()
    mov    ebx,0        ; Returkoden til operativsystemet
    int    0x80        ; Udfør systemkaldet exit(). Eksekveringen mÃ¥ ikke fortsætte længere end her til

Det mest interessante i skellettet, er de tre SECTION dele.
.data bruger vi til vores initialiserede data.
Tilsvarende pseudo kode kunne være:
string besked = "Hello World!"
Altså variabler som får en værdi fra starten.

Modsat har vi .bss sektionen, hvor vi har vores data som ikke er initialiseret.
F.eks. en buffer til at læse data ind i.

.text sektionen er den del som indeholder vores kode.
Dette kommer også fra de gamle dage, da man havde meget begrænset ram og bagudkompabilitet.
F.eks. kunne man i de gamle 80386 CPUer ikke have et segment som var større end 64K. Hvis man kunne presse både data,
stack og kode, ind i eet segment, havde man et .com program.
Ofte var det ikke nok, så stacken og heapen havde deres egne segmenter, og man havde måske flere kode segmenter.
Og man havde så en .exe fil.
Det slipper vi heldigvis for i dag, og man slipper for at holde øje med hvilket segment man er i, og hvor man skal hen.
Bemærk at heapen er ram, allokeret af operativ systemet, og ikke en del af de tre sektioner.

Som i kan se, bruger man ; til kommentarer.

De tre kode linier i skabelonen gør faktisk ikke andet, end at fortælle kernen at programmet er færdig med at køre.
At eksekveringen ikke længere skal fortsætte. Forunderligt nok, kræver det tre instruktioner, bare at afslutte.

Jeg starter med at forklare den sidste instruktion først, for så giver de andre to god mening.

int 0x80

Int(errupt) kommandoen prikker til CPUen og siger, den har noget, kernen skal udføre. Eller, det gjorde den i gamle dage, i realiteten, er det bare en form for hop (jump) ind i kernel space.
Under 0x80 findes en stak system kald og dette bruges af alt software der kører i userspace.
For at kernen ved hvad, INT instruktionen skal udføre, kigger den i eax registrret. Det er i rax registret i 64 bit systemer

mov eax,1

Vi har med mov(e) kommandoen, lagt et 1-tal over i EAX og det betyder vi ønsker at udføre en exit().
Jeg kunne sådan set godt have udført en mov al,1 kommando. Det ville have givet samme resultat,
men det er god skik at sørge for, hele registret er tomt, og der ikke er nogen gamle data, højere oppe i EAX.
Eller længere til venstre om man vil.

Hvis EAX ser således ud inden mov kommandoen:
00010100 00001000 01010010 00001000
vil mov al,1 ikke rense ud og eax vil se således ud:
00010100 00001000 01010010 00000001
Hvis jeg bruger eax i stedet for al, vil jeg i hvert fald få tømt alle 32 bits i registret, så det kun indeholder 1.
Eller jeg kunne bruge RAX i stedet. Men jeg vil gerne have, i skal kunne rette eksemplet til at kunne køre 32 bit, uden de store ændringer.

Den midterste kommando som flytter 0 over i ebx registret, fortæller bare INT, at returkoden til operativ systemet er 0.

For hver af disse int 0x80 instruktioner, er der krav til, og beskrivelser af, hvilke data der skal være i de andre registre,
før man kalder int 0x80.
Man kan se alle 32 bit system kald i filen unistd_32.h, og alle 64 bit system kald i unistd_64.h
Disse findes normalt i /usr/include/x86_64-linux-gnu/asm i Kali Linux. Og sikkert noget nær samme sted på f.ek.s Debian og Ububtu.

Kode:
    mov    eax,1        ; Systemkaldet til exit()
    mov    ebx,0        ; Returkoden til operativsystemet
    int    0x80        ; Udfør systemkaldet exit(). Eksekveringen mÃ¥ ikke fortsætte længere end her til

Man kan også udføre systemkald i både 32 bit og 64 bit Linux med sysenter kommanden, men det er ret besværligt.
int 0x80 findes både i 32 bit og 43 bit instruktionssættet (ISA), men findes kun i 64 bit, på grund af bagud kompabilitet.
I 64 bit programmer, anbefales det at bruge syscall. Den eksekverer også hurtigere.
Nu til hello world eksemplet:


Kode:
;title    "hello"
;********************************************************
;* Filnavn: hello.asm                    *
;* Author: iTick                            *
;* Version: 1.0                        *
;* Kernel: 2.6-32-5-amd64                *
;* Dist: Debian (Squeeze)                *
;* Initial Date: 2. oktober 2013            *
;* Purpose: Assember hello world - ShellSec.org        *
;********************************************************
; Dette program compiles og linkes som nedenstående:
; nasm -f elf32 ./hello.asm -o ./hello.o -g -Z ./errors.log
; ld -o ./hello ./hello.o -melf_i386
; Notes:
; ELF er Excutable and Linkable Format til Linux.
;********************************************************

BITS 32
GLOBAL _start                ; Info om Entry point.

SECTION .data                ; Start af data area

msg:    db    'Hello world!', 0xa    ; Vores hello world string, 0xa er line feed. 0xd er carriage return.
len    equ    $-msg            ; Variablen len starter hvor vores streng msg slutter, så den kan bruges til at beregne længden af msg

SECTION    .bss            ; Her lægger vi vores uninitialized data.

SECTION .text            ; Start af code block. Selve assembler koden skal placeres her under
_start:
    mov    ebx,1        ; File descriptor til stdout
    mov    eax,4        ; Systemkaldet write()
    mov    ecx,msg        ; Sæt ecx registret til at peje på hello world strengen
    mov    edx,len        ; Flyt længden af beskeden ind i edx registret
    int    0x80        ; Udfør kernel kaldet write()

    mov    eax,1        ; Systemkaldet til exit()
    mov    ebx,0        ; Returkoden til operativsystemet
    int    0x80        ; Udfør systemkaldet exit(). Eksekveringen må ikke fortsætte længere end her til.

Som det fremgår af eksemplet, bruger man mange instruktioner på at forberede et kald til kernen.
Vi bruger i alt 7 linier på at skrive en besked ud på konsollen.

Nedenstende variabel msg, indeholder teksten Hello world!. Det efterfølgende 0xa er line feed, for at få et linieskift.
msg: db 'Hello world!', 0xa
db står for define byte, og det er det mest logiske at bruge bytes her. Et alternetiv kunne være define word, som er to bytes.

Kode:
Collective terms of memory:
Byte        1
Word        2
Double Word    4
Quad Word    8
Ten Byte    10
Paragraph    16
Page        256
Segment        65536
Enhver adresse som kan divideres lige med 16 kaldes en Paragraph Boundery.

Nedenstående udfører sådan set ikke noget i sig selv, men gør det lidt nemmere for os selv.
len equ $-msg
Man kunne godt hardkode længden af strengen til edx registret direkte, men hvis man så ændrer tekst længden,
passer dette ikke længere.
Det der sker er, at jeg sætte len til forskellen mellem msg starte adresse og len start adresse.
Da len kommer lige efter msg, kan vi trygt bruge det til at beregne længden af msg strengen.

Kode:
    mov    ebx,1        ; File descriptor til stdout
    mov    eax,4        ; Systemkaldet write()
    mov    ecx,msg        ; Sæt ecx registret til at peje på hello world strengen
    mov    edx,len        ; Flyt længden af beskeden ind i edx registret
    int    0x80        ; Udfør kernel kaldet write()
                ; al registret sættes til 0 hvis det gik godt. Men det er vi lige glad med i dette eksempel.

Ovenstående kode, samler de informationer som int skal bruge, for at udføre et write() systemkald.
Vi flytter værdien 1 over i ebx registret, da det fortæller int, hvilken filedescriptor, vi ønsker at skrive beskeden til.
mov ebx,1

I følge POSIX standarden, skal et operativ system have følgende file descriptors:
0 = standard input (stdin)
1 = standard output (stdout)
2 = standard error (stderr)

Dem kender i nok fra jeres arbejde i konsollen, når i bruger sådan noget som "echo 1 > /proc/...", som defaulter til file descriptor 1.

Vi flytter så værdien 4 over i eax, da det er system kaldet write().
mov eax,4

Så flytter vi start adressen på msg, over i ecx, så kernen ved, hvad der skal skrives ud på konsollen.

Det sidste vi udfører, inden vi kalder int, er at fortælle hvor lang strengen som skal skrives ud, er.
Ellers får vi måske skrevet for lidt ud, eller endnu værre skrevet for meget ud. Tilfældige data i ram.
mov edx,len

Så er vi klar til at udføre selve kaldet med int kommandoen.
int 0x80

Så meget for så lidt. Hvis vi har givet int, de rigtige informationer og kaldet går godt, returnerer int en returkode i eax.
Vi læser dog ikke i dette tilfælde, denne værdi.

Hvad gør vi så nu?! Vi gemmer koden i en fil, som vi gør når vi programmerer i andre sprog.
Gem koden i en fil: hello.asm

Så skal vi compile filen, og det gør vi med nedenstående kommando:
nasm -f elf32 ./hello.asm -o ./hello.o -g -Z ./errors.log

Der er selvfølgelig flere måder at gøre det på, men ovenstående er, som jeg gør det.
nasm er compileren og vi giver den argumentet ".f elf32", som fortæller at det er 32 bit Excutable and Linkable Format. elf og elf32 er det samme og begge argumenter kan bruges.
Argumentet "-o ./hello.o" fortæller at vores binære output fil skal hedde hello.o.
Argumentet "-g" gør at nasm genererer debug informationer, hvilket kan være rart, når først man begynder at kore, teste og fejlfinde
sine programmer i en debugger.
Argumentet "-Z ./errors.log" får nasm til at sende fejlbeskederne til filen errors.log. Det er en smagssag, om man vil det,
men prøv lidt forskelligt, og se hvad der passer til dig.

Selv om vi kun har en enkelt fil, i dette eksempel, skal den stadig linkes, og det gør vi med følgende:
ld -o ./hello ./hello.o -melf_i386

Vi fortæller linkeren ld, at vi skal linke en binære output fil ./hello.o over i en eksekverbar fil, som skal hedde ./hello.
Argumentet "-o" står bare for output.

Argumentet "-melf_i386" fortæller linkeren, den binære objekt fil, bruger 32 bit adresser osv. Man vil stadig kunne køre programmet på en 64 bit linux box.

Både compileren og linkeren har mange features og options, som er værd at kigge på.
Begge kan formegentlig hentes med et standard pakkesystem på den distro du nu bruger.

Så er der kun tilbage at køre programmet med ./hello og se, om der kommer det på skærmen, vi forventer.

Der er meget meget mere til assembler og maskinkode, end jeg har skrevet. Det er som i ved, et kæmpe område, så jeg vil efterlade jer, med lidt ekstra informationer.

Når man programmerer assembler, er der meget at se til. En ting man kan tænke over, er hvad der sker, hvis man har værdien 250 i et 8 bit register, værdien 200 i et andet 8 bit register, og lægger dem sammen. Så vil resultatet bliver større end 8 bit.
Her til, har vi nogle flag, som bliver sat. dvs. en bit bliver sat til 1, så programmet kan tage forbehold for disse situationer.
I ovenstående eksempel, vil et Overflow flag blive sat, så programmet kan vide at resultatet af beregningen blev større en resultetet.
Under 32 bit har vi et register med alle disse flag i, der hedder eflags, og 64 bit versionen hedder rflags.
Hvordan skælner man mellem positive og negative tal!? Der er mange små ting, som nok også er en af årsagerne til,
assembler kun bruges når optimal hurtig kode kræves eller begrænset plads er et problem.

Conditional branching:
CMP (Compare) sammen ligner to værdier, og bruges til forgreninger:
cmp eax,ebx ; Sammenligner eax med ebx registret.
I virkeligheden trækker den eax fra ebx og ser, om resultatet bliver 0. Hvis resultatet bliver 0, er de ens.
En anden ting, der sker er, at Zero flaget bliver sat. Det er bl.a. det, nedenstående Jcc kommandoer undersøger.
Zero flager bliver sat, hver gang en matematisk beregning gør, at et register bliver sat til 0.

Vi kan så bruge disse flag til at forgrene med.
Det er ofte en af følgende, men der findes mange:
JA Jump Above
JAE Jump Above or Equal
JB Jump Below
JBE Jump Belov or Equal
JE Jump Equal
JNE Jump Not Equal
JG Jump Greater
JGE Jump Greater or Equal
JL Jump Lower
JLE Jump Lower or Equal
Nogle er til signed værdier og nogle er til unsigned værdier. Altså om værdien kan være positiv eller negativ, eller det ikke har nogen betydning.
Hvis et 8 bit register indeholder 10000000 binært, vil det svare til værdien -1 eller 128 decimalt. Der er jo en del forskel.

Loops:
Nedenstående eksempel viser en simpel loop. Der er flere måder at gøre det på.
Kode:
    ...
    mov    eax,4        ; Læg 4 over i eax. Dette er vores tæller. I dette eksempel tæller vi nedad.
.loop:    nop            ; Instruktioner her fra, er med i løkken. nop er den velkendte nop-sled ven, No Operation. Og gør ikke noget.
    dec    eax        ; Så trækker vi en fra eax registret med dec (decrease).
    jnz    .loop        ; Så bruger vi en jnz (Jump Not Zero). Hvis ovenstående dec eax når 0, sættes Zero flaget (ZF) og så vil denne jump ikke hoppe til .loop.
    ...            ; Så hvis eax ikke er 0, så hopper vi til .loop

Der er tilsvarende en inc(rement) instruks som tæller opad.
Man kan også bruge loop instruktionen til at lave en løkke med:
Kode:
    ...
    mov    ecx,4        ; Læg 4 over i counter registret ecx. Dette er vores tæller. I dette eksempel tæller vi nedad.
.loop:    nop            ; Instruktioner her fra, er med i løkken. nop er den velkendte nop-sled ven, No Operation. Og gør ikke noget.
    loop    .loop            ; loop instruktionen læser altid sin tæller fra ecx og sørger selv for at tælle ecx ned for hver iteration

Kald til en subrutine:
Kode:
    ...
    call    _myfunc    ; Dette er et kald til en subrutine som hedder myfunc og er tilsvarende funktioner i andre sprog
    ...
_myfunc:                   ; Her starter funktionen.
    ...
    nop                  ; Denne funktion gør så ikke andet, end at flytte EIP (Instruktion pointeren) til næste instruktion. Instruktionspointeret for 64 bit programmer, hedder RIP
    ret                  ; Subrutinen afsluttes og forlades med instruksen ret(urn) og den hopper til instruktionen lige efter call.


Ud over at have sine variabler i .data og i .bss, er det muligt at overføre værdier ved at smide (pushe) værdierne på stacken inden
man bruger call instruksen.
De kan så poppes igen tilbage, når man er i subrutinen.
Man skal dog være lidt varsom, for når man bruger call, bliver sådan noget som IP, Instruktionspointeren også pushed på stacken,
og det sker efter, de værdier vi selv har lagt på. Så det er noget med at poppe af stacken, tage vores værdier, og pushe adressen,
til ip tilbage på stacken. Stacken er en LIFO. Der findes flere forskellige push og pop instrukser til dette. Man kan også læse data, ved at bruge esp registret, som er vores stack pointer.
esp pejer på toppen af stacken.

Overblik:
Kode:
General purpose registers:
|RAX|EAX|AX|AH-AL|
|RBX|EBX|BX|BH-BL|
|RCX|ECX|CX|CH-CL|
|RDX|EDX|DX|DH-DL|
|EBP|BP|    Basepointer.
|ESI|SI|    Source Index (Læsning)
|EDI|DI|    Destination Index (Skrivning)
|ESP|SP|    Stackpointer.
De nye 64 bit registre:
R8-R15
Der er også 8 nye 128 bit hurtige matematik SSE registre.

EIP: 32 bit instruction pointer.
RIP: 64 bit instruction pointer.

32 bit Flags register, EFLAGS:
Se bl.a. CF, DF, ZF og OF flagene.

Bonus:
De gamle 32 bit CPUer havde en begrænsning i, hvor meget ram den kunne addresere med 32 bit.
Nu, vi er gået over til 64 bit, er vi ude i overkill 16 exabytes, så man har valgt, ikke at bruge alle 64 bits som tidligere, men at dele CPUens allokering op i Virtuel og Fysisk ram.

Historik frem til nu, hvor nummer 4 er standarden:
Memory models:
1. Real Mode Flat Model.
2. Real Mode Segmented Model. (DOS)
3. Protected Mode Flat Model. (Linux & modern Windows) 80386+
4. Long Mode Flat Model. (64 bit)

Jeg kan kun anbefale at surfe lidt rundt efter noget godt materiale og se at få kodet noget assembler.
Det er på godt og ondt, normalt at bøger om assembler, først præsenterer kode og kommandoer,
når man har passeret de første 100-200 sider om den interne arkitektur. Det er desværre nødvendigt.
Man er også nød til at vide hvilke flag, der bliver sat hvornår.

EDIT:
Jeg vil lige tilføje at Intel godt nok har deres eget 64 bit instruktionssæt, og de faktisk var ude med en 64 bit processor før AMD,
men Intel understøtter og bruger nu primært AMDs instruktionssæt.
Problemet var at Intels 64 bit ISA ikke var sønderligt bagudkompatibelt. Somme tider kunne IA32 kode køre på en Intel 64 bit processor, ofte skulle det dog recompiles og i værste fald, skulle koden rettes til.

EDIT2: Jeg fik lige lyst til at lave et filter:
Kode:
;title    "cho"
;********************************************************
;* Filnavn: cho.asm                    *
;* Author: iTick                *
;* Version: 1.0                        *
;* Kernel: 2.6-32-5-amd64                *
;* Dist: Debian (Squeeze)                *
;* Initial Date: 2. Oktober 2013            *
;* Purpose: Filtrere alt andet en alfanummerisk fra    *
;********************************************************
; Dette program compiles og linkes som nedenstÃ¥ende:    *
; nasm -f elf64 ./cho.asm -o ./cho.o -g -Z ./errors.log    *
; ld -o ./cho ./cho.o                    *
; Notes:                        *
; mov ecx, buffer tager adressen, hvor            *
; cmp byte [buffer], 30h tager indholdet        *
;********************************************************

BITS 64        ; Brug 64 bits. Dette er et 64 bit eksempel. IA32 er Intel Architecture 32 bit og kan ogsÃ¥ bruges.
GLOBAL _start    ; Entry point til ELF linkeren (Det er her, eksekveringen starter)
SECTION .data    ; Start af data area
SECTION    .bss    ; Her lægger vi vores uninitialized data.
    buffer    resb 1

SECTION .text    ; Start af code block. Selve assembler koden skal placeres her under
_start:
    nop
read:    mov    eax, 3        ; Forbered sys_read() kald
    mov    ebx, 0        ; POSIX filedescriptor stdin
    mov    ecx, buffer    ; Kopier adressen pÃ¥ buffer til ecx
    mov    edx, 1        ; Læs en enkelt byte fra den valgte filedescriptor stdin
    int    80h        ; Overgiv kontrollen til kernen og lad den udføre sys_read()

    cmp    eax, 0        ; Læs returkoden fra sys_read() kaldet. Sammenlign returkoden med 0
    je    exit        ; Hvis de er ens, hoppes der til exit. 0 betyder EOF

    ; Kontroller om det er mellemrum. 20h
    cmp    byte [buffer], 20h    ; Kontroller om bufferen indeholder hex værdien for ascii tegnet mellemrum
    je    write            ; Hvis det er ascii 20h, er det mellemrum. SÃ¥ hop til write. je er Jump Equal

    ; Komtroller ascii værdierne 0-9
    cmp    byte [buffer],30h    ; Sammenlign den læste værdi med 30h (ascii tallet 0, som i 2, 1, 0)
    jb    read            ; Hvis den er mindre end 0 er det et speciel tegn. SÃ¥ springer vi til read og læser et nyt tegn
    cmp    byte [buffer],3Ah    ; Hvis vi er kommet her til og vi er laver end hex 3A, er det et tal. Det skriver vi ud
    jb    write            ; Brug jb Jump Below til at hoppe til write.

    ; kontroller ascii værdierne A-Z
    cmp    byte [buffer], 41h    ; Sammenlign den læste værdi med 41h (A), hvis den er mindre, er det ikke et bogstav
    jb    read            ; Hvis ascii tegnet var mindre, sÃ¥ hop til read
    cmp    byte [buffer], 5Bh    ; Hvis den er mindre end 5B har vi ramt i ascii range med store bogstaver. Det vil vi udskrive
    jb    write

    ; Kontroller ascii værdierne a-z
    cmp    byte [buffer], 61h    ; Hvis værdien er mindre end 61 sÃ¥ er det ikke et lille bogstav. SÃ¥ læser vi et nyt
    jb    read            ; Jumo tilread og læs et nyt tegn hvis ascii værdien er "below" 61h
    cmp    byte [buffer], 7Bh    ; Sammenlign. Hvis tegnet er mindre end 7Bh, vil vi skrive det ud
    jb    write            ; Hop til write, hvis bufferen indeholder et tegn som er mindre end 7Bh

    jmp    read        ; Hop til read og start forfra, lige meget hvad

write:
    mov    eax, 4        ; Forbered sys_write()
    mov    ebx, 1        ; Vi vil gerne skrive til filedescriptor 1, stdout
    mov    ecx, buffer    ; ecx skal kende adressen som bufferen har. Her er det tegn vi gerne vil skrive ud
    mov    edx, 1        ; Dette er antal bytes vi gerne vil skrive med sys_write()
    int    80h        ; Kald kernens 80h vector og lad den udføre sys_write()
    jmp    read        ; Hop til read lige meget hvad. Vi vil læse det næste tegn, eller kontrollere om der er mere input

exit:
    mov    eax,1        ; Systemkaldet til exit()
    mov    ebx,0        ; Returkoden til operativsystemet
    int    0x80        ; Udfør systemkaldet exit(). Eksekveringen mÃ¥ ikke fortsætte længere end her til

Som i kan se, bruger dette program uinitialiserede data.
Det læser et tegn fra stdin, kigger om det er mellemrum, 0-9, a-z eller A-Z. Hvis det er det, bliver det sendt til stdout.
I kan her se jeg bruger følgende til at reservere en byte med.
buffer resb 1
Her bliver brugt lidt forskellige jumps.

Man kan også se forskellen på hvordan følgende to kommandoer skælner mellem adressen og indholdet i buffer:
mov ecx, buffer
cmp byte [buffer], 30h


Programmet "cho" eller CharOnly filteret, kan bruges som følger:
"cat EnFilMedTekstOgJunkTegn.txt | ./cho > NyFil.txt"
Alt andet end mellemrum og de almindelige alfanumeriske tegn bliver fjernet.
Hvor anvendeligt det er i den virkelige verden, ved jeg ikke, men jeg synes det er et godt,
men simpelt eksempel.

EDIT: Bonuskode - Et ARM helloworld.s Assembly program:
Kode:
.data                                                   // Start af datasektion.
string: .asciz "Hello world"    // Vores Hello World string
null:   .byte 10                                // Brug Line Feed til linieskift.
stringend:                                              // Denne bruges til at beregne stringlen med

.set size, stringend - string   // Beregn stringlen
.align 4                                                // Vi skal have aligned kode segmentet til en 4 byte boundery
.text                                                   // Start af kodesegment
.globl main                                             // Vi linker med gcc. Eksporter main ellers kan den ikke kaldes udefra.

main:
        // Function Prologue                    // Entrypoint.
    push {r7, lr}                                       // Gem r7 og Link Registeret. Se AAPCS

        // Forbered systemkaldet til write()
    mov r0, #1                                                  // FLyt 1 over i r0. File descriptor 1 som er stdout
    ldr r1, stringaddr                                  // Load helloworld string adresse ind i r1 registret
    mov r2, #size                                               // Angiv strlen for helloworld string
    mov r7, #4                                                  // Flyt 4 over i r7. Dette er syskaldet write()
    swi #0                                                              // Lav en software interrupt. Dette starter systemkaldet

        // Function Epilogue
        mov     r0, #0                                          // Returkode til operativ systemet
        pop     {r7, lr}                                        // Genskab r7, vi har brugt og LR - Return Address
        bx lr                                                   // Branch til Return Address, vi lige har poppet fra stacken

stringaddr : .word string                       // ARM kan underligt nok ikke se de andre segmenter
Compilet på en rPI 2 med GNU assembler:
$ as helloworld.s -o helloworld.o
$ gcc helloworld.o -o helloworld

Links:
Linux System Call Quick Reference - http://www.scribd.com/doc/50665838/LINUX...-Reference
Lidt windows system kald - http://en.wikipedia.org/wiki/INT_21H
Instruktionssæt - http://www.intel.com/content/www/us/en/p...nuals.html
ASCII tabel - http://www.asciitable.com/
NASM doc - http://www.nasm.us/doc/
Linux LD Linker - http://man7.org/linux/man-pages/man1/ld.1.html
---
Writing a shellcode decoder stub in assembly is like talking gibberish in such a way that it is still perfectly intelligible. - iTick
Besøg denne brugers hjemmeside Find alle beskeder fra denne bruger
Citer denne besked i et svar
02-10-2013, 10:41
#2
RE: Linux assembler - En kort introduktion
Super fed introduktion! Selvom det er Linux, er det stort set det samme i Windows :)
Find alle beskeder fra denne bruger
Citer denne besked i et svar
02-10-2013, 11:03 (Denne besked var sidst ændret: 02-10-2013, 11:15 af iTick.)
#3
RE: Linux assembler - En kort introduktion
(02-10-2013, 10:41)Crypt Skrev: Super fed introduktion! Selvom det er Linux, er det stort set det samme i Windows :)

Nej, der er ikke den store forskel.
Jeg tænkte bare at nu der er så mange guides, som viser brugen af debuggers, er det nok på sin plads, at få lidt fundamentalt på plads. :)

EDIT: Og så glæder vi os til din C eller C++ intro. :)
---
Writing a shellcode decoder stub in assembly is like talking gibberish in such a way that it is still perfectly intelligible. - iTick
Besøg denne brugers hjemmeside Find alle beskeder fra denne bruger
Citer denne besked i et svar
03-10-2013, 11:17
#4
RE: Linux assembler - En kort introduktion
Det var noget af en smørre, men spændende læsning. Kanon arbejde!
Don't learn to hack, hack to learn
Find alle beskeder fra denne bruger
Citer denne besked i et svar
03-10-2013, 14:47
#5
RE: Linux assembler - En kort introduktion
(03-10-2013, 11:17)Spagnum Skrev: Det var noget af en smørre, men spændende læsning. Kanon arbejde!

Hehe. Det har du ret i.
Der skal bare så meget til, før man skriver den første linie kode. :(
Vi må se om der er nogen som gider assembler, af dem som ikke allerede kan det der står i guiden. :)
---
Writing a shellcode decoder stub in assembly is like talking gibberish in such a way that it is still perfectly intelligible. - iTick
Besøg denne brugers hjemmeside Find alle beskeder fra denne bruger
Citer denne besked i et svar
09-10-2013, 11:15 (Denne besked var sidst ændret: 09-10-2013, 11:16 af BlimBlamBlar.)
#6
RE: Linux assembler - En kort introduktion
(02-10-2013, 10:41)Crypt Skrev: Super fed introduktion! Selvom det er Linux, er det stort set det samme i Windows :)

Mht. systemkald så er der da kæmpe forskel på Windows og Linux. Linux systemkald er faste og ændrer sig ikke mellem opgraderinger af kernen, derfor kan du lave en 'int 0x80' eller 'sysenter'. Det kan du ikke i Windows, der er du nødt til at bruge et 'call ...' til en funktion, som så implementerer det korrekte systemkald for den givne kerneversion...også samme grund til at Windows shellcode er en hel del større end Linux shellcode.
Find alle beskeder fra denne bruger
Citer denne besked i et svar
09-10-2013, 13:32
#7
RE: Linux assembler - En kort introduktion
Rigtig god introduktion. Meget præcist og let forståeligt.
Kvalitets ting fra dig gang på gang :)

Dog har jeg lige et spørgsmål.
Ved dit hello world eksempel benytter du DL registret til at opbevare længden af din tekststreng.
Men eftersom DL kun er et 8 bit register så har du vel en maksimal størrelse på 255 chars.
Hvad ville der ske hvis man smed en streng ind som var 256 lang ?
Nogen speciel grund til at du ikke blot benytter EDX når det nu alligevel ikke bruges ?
Følg mig på twitter: https://twitter.com/Morph3s
Find alle beskeder fra denne bruger
Citer denne besked i et svar
09-10-2013, 18:15 (Denne besked var sidst ændret: 09-10-2013, 18:25 af iTick.)
#8
RE: Linux assembler - En kort introduktion
(09-10-2013, 11:15)BlimBlamBlar Skrev: Mht. systemkald så er der da kæmpe forskel på Windows og Linux. Linux systemkald er faste og ændrer sig ikke mellem opgraderinger af kernen, derfor kan du lave en 'int 0x80' eller 'sysenter'. Det kan du ikke i Windows, der er du nødt til at bruge et 'call ...' til en funktion, som så implementerer det korrekte systemkald for den givne kerneversion...også samme grund til at Windows shellcode er en hel del større end Linux shellcode.

At kode assembler giver ikke den store forkel. Du håndterer ikke selv adresserne i dine kernel services call gate til dit interrupt vector table. Og det er fordi, de adresser ændrer sig, hver gang der rettes noget i koden. Hvis der fjernes eller tilføjes kode når der patches i kernens service rutiner.
Og heldigvis for det. :) Vi er ikke længere der hvor vi skal holde øje med, hvilket segment vi er i. :D Men jeg har ikke kodet assember til windows siden da. Instruktionssætter er dog magen til, da det er CPU afhængigt og ikke OS afhængigt. Så om man bruger call i windows nu, tør jeg ikke sige. Om man bruger nogle wrappers til at kalde kernen, aner jeg ikke. Det virker så mest som om, assembler på windows er ved at blive et 3g sprog. :)
Det er interrupt nummeret, der ikke ændrer sig, hvis det er det du mener?

(09-10-2013, 13:32)Morph3s Skrev: Rigtig god introduktion. Meget præcist og let forståeligt.
Kvalitets ting fra dig gang på gang :)

Dog har jeg lige et spørgsmål.
Ved dit hello world eksempel benytter du DL registret til at opbevare længden af din tekststreng.
Men eftersom DL kun er et 8 bit register så har du vel en maksimal størrelse på 255 chars.
Hvad ville der ske hvis man smed en streng ind som var 256 lang ?
Nogen speciel grund til at du ikke blot benytter EDX når det nu alligevel ikke bruges ?

Hvis du smider en længere streng ind, sker der ikke noget. For den pejer bare på starten af strengen og skriver skriv 255 tegn ud. Så vil det sidste bare ikke komme med. Jeg har mest skrevet DL i stedet for EDX, for at vise brugen af det. :) Der er beskrivelser af hvilke kald som skal have hvad i hvilket register, og hvor i registret de skal være. :)

Jeg kunne i farten ikke lige finde en komplet liste, men her er et udsnit:

Kode:
%eax    Name         %ebx                %ecx                   %edx     %esx %edi
4       sys_write    unsigned int        const char *           size_t      -      -
---
Writing a shellcode decoder stub in assembly is like talking gibberish in such a way that it is still perfectly intelligible. - iTick
Besøg denne brugers hjemmeside Find alle beskeder fra denne bruger
Citer denne besked i et svar
09-10-2013, 18:34
#9
RE: Linux assembler - En kort introduktion
Komplet syscall table for 32 bit Linux: http://docs.cs.up.ac.za/programming/asm/...calls.html
Og for 64 bit: http://blog.rchapman.org/post/3680103886...for-x86-64

På Linux kan du være sikker på at syscall 1 altid er sys_exit med en 32 bit kerne, og at syscall 60 er sys_exit på 64 bit. Og at syscall nummeret skal i eax/rax, og at første argument skal i ebx/rdi. Altid!

I Windows ved du ikke, hvilket syscall nummer noget som helst har, eller hvilke argumenter, der skal hvorhen, for det ændrer sig med de forskellige service packs. Derfor er du nødt til at finde wrapperen i kernel32.dll eller hvor det nu ligger. Hvis du koder et normalt program i assembly, så kan du bare lænke til de krævede libs, men laver du shellcode, så skal du selv resolve adresserne. Se evt. Skapes whitepaper om shellcoding til Windows.
Find alle beskeder fra denne bruger
Citer denne besked i et svar
09-10-2013, 18:40 (Denne besked var sidst ændret: 09-10-2013, 18:40 af iTick.)
#10
RE: Linux assembler - En kort introduktion
(09-10-2013, 18:34)BlimBlamBlar Skrev: Komplet syscall table for 32 bit Linux: http://docs.cs.up.ac.za/programming/asm/...calls.html
Og for 64 bit: http://blog.rchapman.org/post/3680103886...for-x86-64

På Linux kan du være sikker på at syscall 1 altid er sys_exit med en 32 bit kerne, og at syscall 60 er sys_exit på 64 bit. Og at syscall nummeret skal i eax/rax, og at første argument skal i ebx/rdi. Altid!

I Windows ved du ikke, hvilket syscall nummer noget som helst har, eller hvilke argumenter, der skal hvorhen, for det ændrer sig med de forskellige service packs. Derfor er du nødt til at finde wrapperen i kernel32.dll eller hvor det nu ligger. Hvis du koder et normalt program i assembly, så kan du bare lænke til de krævede libs, men laver du shellcode, så skal du selv resolve adresserne. Se evt. Skapes whitepaper om shellcoding til Windows.

Gode links. :)

Ok. Jeg var ikke klar over, at de ændrer på de forskellige inerrupts i windows. Weird. De har godt nok ikke gjort det nemmere for sig selv.

På den måde er windows god. Du kan lave indirekte jumps til din shellcode, ved at jumpe til et sted i user32 eller kernel32, hvor der er et jump til starten af din nopsled i din sandwich. :)
---
Writing a shellcode decoder stub in assembly is like talking gibberish in such a way that it is still perfectly intelligible. - iTick
Besøg denne brugers hjemmeside Find alle beskeder fra denne bruger
Citer denne besked i et svar
« Ældre | Nyere »




User(s) browsing this thread: 1 Gæst(er)