Sec24 – Introduktion till Intel X86

Av: Oscar Andersson, Sec24,
Rev 1.0 2013-07-01

OBS:  Följande guide är för utbildningssyfte enbart och får absolut inte användas för olagliga ändamål.

Intel har varit en ledande processortillverkare i över 30 år och att förstå vad som egentligen händer när kod körs ger inte bara bättre förståelse för hårdvara utan gör det också möjligt att gå vidare med reverse engineering, operativsystemsdesign, kodoptimering och att kunna utnyttja systems sårbarheter.

Processorarkitekturen vi kommer gå igenom är Intel X86 som är den vanligaste på marknaden. För att citera Wikipedia igen: “Termen x86 används som samlingsnamn för en familj av binärkompatibla CPU:er som kontinuerligt vidareutvecklats sedan den ursprungliga 8086 från 1978. Det är den idag vanligaste processorarkitekturen för persondatorer och har dominerat den marknaden sedan IBM PC och dess kloner blev vanliga i mitten av 1980-talet… Intels processorer efter 8086 och 8088 hade beteckningarna 801868028680386 och 80486, därav begreppet x86 (“något + 86″).”

assembler (assemblyspråk). Från Wikipedia: “Assembler eller assemblyspråk är ett sätt att uttrycka maskinkoden för en dators processor på ett sätt som människor kan läsa och skriva. Programmet som översätter assemblyspråk till maskinkod (binärkod) kallas en assemblator. Maskinkod består av mönster av ettor och nollor och är i allmänhet svår för programmerare att använda. Assembler tillåter att bitmönstren istället skrivs med bokstäver och siffror, så kallade mnemnotekniska symboler, vilket väsentligt underlättar programmerarens arbete. Vidare tillhandahåller assembler möjligheten att använda symboliska namn för minnesadresser.

Assembler är inte ett enda enhetligt språk. Olika processorfamiljer erbjuder olika instruktioner, och olika assemblatorer erbjuder olika syntax för till exempel adressering och makron. Detta gör att det i allmänhet inte går att använda ett assemblerprogram skrivet för en processor på en annan typ av processor. För att göra det möjligt att flytta program mellan olika processortyper används i stället ett högnivåspråk.”

Inför den här guiden är det bra att känna till lite om C-programmering. Det finns bra guider för detta på Youtube och länkat är ett exempel som börjar med de absoluta grunderna. IDE-programmet, integrated development environment, jag använder är CodeLite som fungerar på de flesta plattformar. För att göra en C-applikation laddar du ner programmet, går till fliken Workspace, väljer New Project. Nu kommer en ny ruta upp och här trycker du på Console följt av Simple executable (gcc). Skriv sedan in ett Project name och Project path vart projektet ska sparas. Klicka sedan OK.

Sec24-sec-24-hur-hackar-man-IDA Pro Intel X86 C programmering disassembly reverse engineering 4

Vi börjar nu med ett enkelt projekt med följande kod:

#include <stdio.h>

int main(){    
	printf("Hello World!\n");
	printf ("%d", 0x1234);
	getch();
}

Kompilerar och kör vi programmet ser vi följande på skärmen:

Sec24-sec-24-hur-hackar-man-IDA Pro Intel X86 C programmering disassembly reverse engineering 5

Skriver vi om koden att istället returnera ett värde ser koden ut enligt följande:

#include <stdio.h>

int main(){    
	printf("Hello World!\n");
	return 0x1234;
}

Nedanstående information är hämtad från “Introductory Intel x86: Architecture, Assembly, Applications, & Alliteration” av Open Security Training.

När denna kod körs händer följande på datorn

.text:00401730 main
 .text:00401730              push    ebp
 .text:00401731              mov     ebp, esp
 .text:00401733              push    offset aHelloWorld ; "Hello world\n"
 .text:00401738              call    ds:__imp__printf
 .text:0040173E              add     esp, 4
 .text:00401741              mov     eax, 1234h
 .text:00401746              pop     ebp
 .text:00401747              retn

(Windows Visual C++ 2005, /GS (buffer overflow protection) option turned off. Disassembled with IDA Pro 4.9 Free Version)

08048374 <main>:
 8048374:       8d 4c 24 04              lea    0x4(%esp),%ecx
 8048378:       83 e4 f0                 and    $0xfffffff0,%esp
 804837b:       ff 71 fc                  pushl  -0x4(%ecx)
 804837e:       55                        push   %ebp
 804837f:       89 e5                     mov    %esp,%ebp
 8048381:       51                        push   %ecx
 8048382:       83 ec 04                  sub    $0x4,%esp
 8048385:       c7 04 24 60 84 04 08      movl   $0x8048460,(%esp)
 804838c:       e8 43 ff ff ff            call   80482d4 <puts@plt>
 8048391:       b8 2a 00 00 00            mov    $0x1234,%eax
 8048396:       83 c4 04                  add    $0x4,%esp
 8048399:       59                        pop    %ecx
 804839a:       5d                        pop    %ebp
 804839b:       8d 61 fc                 lea    -0x4(%ecx),%esp
 804839e:       c3                        ret
 804839f:       90                        nop

(Ubuntu 8.04, GCC 4.2.4. Disassembled with “objdump -d”)

_main:
 00001fca  pushl  %ebp
 00001fcb  movl  %esp,%ebp
 00001fcd  pushl  %ebx
 00001fce  subl  $0x14,%esp
 00001fd1  calll  0x00001fd6
 00001fd6  popl  %ebx
 00001fd7  leal  0x0000001a(%ebx),%eax
 00001fdd  movl  %eax,(%esp)
 00001fe0  calll  0x00003005  ; symbol stub for: _puts
 00001fe5  movl  $0x00001234,%eax
 00001fea  addl  $0x14,%esp
 00001fed  popl  %ebx
 00001fee  leave
 00001fef  ret

(Mac OS 10.5.6, GCC 4.0.1. Disassembled from command line with “otool -tV”)

Optimerar vi koden blir det följande:

.text:00401000 main
 .text:00401000 push offset aHelloWorld ; "Hello world\n"
 .text:00401005 call ds:__imp__printf
 .text:0040100B pop ecx
 .text:0040100C mov eax, 1234h
 .text:00401011 retn

(Windows Visual C++ 2005, /GS (buffer overflow protection) option turned off. Optimize for minimum size (/O1) turned on. Disassembled with IDA Pro 4.9 Free Version)

Det vi ser ovan är assembler instruktioner för X86 (push, call, pop, mov, retn). Enligt vissa analyser står 14 assembler instruktioner för 90% av koden. Kan du 20-30 så kommer det räcka för att slippa gå tillbaka till manualen hela tiden. Ovan visas 11 olika instruktioner i de olika “Hello World!” exemplen.

Datatyper – Data types

Det är även viktigt att känna till olika datatyper. Bilden nedan är från Intels mjukvaruutvecklingsmanual på sidan 99. Anledningen till att short kallas ord (word) är på grund av att Intels originalprocessor 8086 var 16-bitars och inte 32-bitars. Därför räknades ett ord av data som 16-bitar. När sedan 32-bit introducerades blev dessa kallade dubbelord eller Doubleword. Om du ser dword eller qword i Windowsprogrammering står det för Doubleword och Quadword.

Sec24-sec-24-hur-hackar-man-IDA Pro Intel X86 C programmering disassembly reverse engineering 6

Talsystem

För att fortare kunna gå igenom assembler är det bra att memorera det decimala-, binära– och hexadecimala talsystemet så gott det går. Tabellen nedan presenterar det viktigaste att memorera. I tabellen är värdena 0-15 representerade i en “nibble” som motsvarar en enkel hexadecimal symbol (en nibble är en grupp av 4 bitar, alltså en halv byte som har 8 bitar och består av antigen nollar eller ettor). Det decimala går att memorera upp till 4 eller 8 bitar men efter det behövs endast binära- och hexadecimala tal. Exempelvis är F i Hex alltid 1111 binärt och det  kan användas inom bitvis operation (eng. bitwise operation). Ett kort exempel nedan.

0x0000000E
and
0x00000003
Ger följande:
1110
and
0011
=0010
0x0000000E
and
0x00000003
=
0x00000002

För att läsa om hur bitwise operation fungerar i C kan du göra det här.

Decimal (base 10) Binary (base 2) Hex (base 16)
0 0000b 0x00
1 0001b 0x01
2 0010b 0x02
3 0011b 0x03
4 0100b 0x04
5 0101b 0x05
6 0110b 0x06
7 0111b 0x07
8 1000b 0x08
9 1001b 0x09
10 1010b 0x0A
11 1011b 0x0B
12 1100b 0x0C
13 1101b 0x0D
14 1110b 0x0E
15 1111b 0x0F
Negativa nummer

Det är även viktigt att ha lite kunskap om negativa tal inom binärkod. Negativa tal var ett hett diskussionsämne när datorer var något helt nytt och kan enkelt delas in i tre kategorier:

Ones’ complement = Vänd alla bitar. 0 -> 1 och 1 -> 0.

Two’s complement = Ones’ complement + 1.

Sign-and-magnitude = Den sista biten står för positivt eller negativt tal. Ofta är det 0 för positivt och 1 för negativa nummer (Tänk på att binärt räknas från höger till vänster. I exemplet 01111111 räknas 0 som sista biten).

De som visas i tabellen nedan är Ones’ complement och Two’s complement.

Negativa nummer är definierade som Two’s complement av det positiva numret.

Binary: Hex: Decimal One’s Comp. Two’s Comp. (negative)
00000001b : 0x01 : 1 11111110b : 0xFE 11111111b : 0xFF : -1
00000100b : 0x04 : 4 11111011b : 0xFB 11111100b : 0xFC : -4
00011010b : 0x1A : 26 11100101b : 0xE5 11100110b : 0xE6 : -26
  • 0x01 till 0x7F är en positiv byte, 0x80 till 0xFF är en negativ byte
  • 0x00000001 till 0x7FFFFFFF positivt dword
  • 0x80000000 till 0xFFFFFFFF negativt dword
Processorarkitektur – CISC och RISC

Det är även bra att känna till hur processorarkitekturen ser ut. Intel X86 använder något som heter CISC, Complex instruction set computing. Kort och gott innebär det att en instruktion (eng. instruction) kan utföra flera low level operations såsom att ladda något från minnet, en aritmetiskt operation och spara något i minnet. Motsatsen är RISC, Reduced instruction set computing som går ut på att enklare och optimerade set av instruktioner ger bättre prestanda. Ett annat känt drag för RISC är att minnet bara kan nås via ladda och spara operationer (Load/store architecture). RISC används bland annat av PowerPC, SPARC, ARM och MIPS.

Endian

Nästa steg är att lära sig hur byteordningen ser ut i datorminnet. Inom datoranvändning kallas detta för Endian och delas in i Big-Endian och Little-Endian. Definition från Wikipedia nedan:

“Processorer från bland annat Motorola använder rak byteordning, vilket innebär att den högsta byten kommer först och den lägsta kommer sist. Detta kan jämföras med ett decimalt system där hundratalen kommer först, sedan tiotal och sist ental. Detta format kallas Big-Endian (big-endian).

Intel använder omvänd byteordning, vilket innebär att den lägsta byten kommer först och den högsta byten kommer sist (som om vi skulle skriva ental först, följt av tiotal etc.). Detta kallas Little-Endian (little-endian). Alla typer av programkod som direkt skriver ett heltal över flera byte på nätverk eller som fil måste hantera byteordningsproblematiken, för att man skall kunna uppnå kompatibilitet mellan de två systemen.

Många binära protokoll på Internet använder Big-Endian, vilket därför ibland har kallats “Network Byte Order” (främst på system som själva har omvänd byteordning).

Termen big-endian kommer ursprungligen från Jonathan Swifts satiriska roman Gullivers resor från 1726. Dataingenjören Danny Cohen införde det 1980 som ett begrepp inom datorvärlden. I sin roman beskrev Swift spänningarna i de båda rikena Lilliput och Blefuscu; medan kungliga påbud i Lilliput krävde att man skulle knacka sitt (löskokta) ägg i den smalare änden, var invånarna i det rivaliserande kungadömet Blefuscu tillsagda att knacka sina ägg i den tjocka änden. Detta gav de senare deras benämning som Big-endians – “storändianer”.”

Exempel på Little Endian: 0x12345678 sparas i RAM som 0x78563412.

Följande bild ger ett bra grafiskt exempel.

Sec24-sec-24-hur-hackar-man-IDA Pro Intel X86 C programmering disassembly reverse engineering 7

Processorregister

Processorn innehåller även ett processorregister. Processorregistret är ett litet men snabbt datorminne som lagras i processorn. Det finns åtta olika register i X86 för “generella ändamål” (general purpose register) samt en instruktionspekare. X86-32 har register som är 32-bitar och X86-64 har 64-bitar.

Nedan är Intels förslag för processorregistret till utvecklare av kompilatorer.

• EAX – Stores function return values
• EBX – Base pointer to the data section
• ECX – Counter for string and loop operations
• EDX – I/O pointer
• ESP – Stack pointer
• EBP – Stack frame base pointer
• ESI – Source pointer for string operations
• EDI – Destination pointer for string operations

• EIP – Pointer to next instruction to execute (“instruction pointer”)

Vi kommer att se exempel på alla ovanstående förutom EBX och EDX i den här guiden.

Intel var från början 16-bitars och då fanns det endast AX, BX, CX, DX, SP, BP, SI, DI och IP för generella ändamål. AX, BX, CX och DX har även High (H) och Low (L) byte, i AX fall blev det AH och AL, som kan kommas åt separat och X:et kommer av engelskans “pair” för att dessa även kan användas tillsammans. När de övergick till 32-bitars lades “E” till framför och det står för “Extended”.

A står för Accumulator register, ackumulatorregistret, och används för I/O portåtkomst, aritmetik och avbrutna anrop etc.

B står för Base register, basregistret, som fungerar som en baspekare för minnestillgång.

C är Counter register, räknarregistret, används som en loopräknare och för skiften.

D är Data register, dataregistret och även detta används för I/O portåtkomst, aritmetik och vissa avbrutna anrop.

SP, Stack pointer register, håller toppadressen i stack.

BP, Stack Base pointer register, håller basadressen i stack.

SI, Source index register, används för sträng och minnesarrays kopiering

DI, Destination index register, används för sträng och minnesarrays kopiering samt inställning för långpekaradressering (far pointer addressing) med ES. (ES är ett segmentregister och står för Extra Segment).

IP, Index Pointer, håller offset av nästa instruktion och kan bara läsas.

Nedan är två bilder som visar hur många bitar de olika registren innehåller och hur de kan anropas.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 8

 

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 9

EAX, EDX och ECX tillhör Caller-save registers. Det innebär att om callern (anroparen) har något i registret den vill spara så ansvarar callern själv för att detta görs. Värdet sparas innan en funktion anropas och återställs efter att anropet returnerats. Kan också beskrivas som att callee (anropade parten) kan, och görs i de flesta fall, modifiera värden i caller-save registren.

Motsatsen är EBP, EBX, ESI och EDI som är Callee-save registers. Om callee behöver använda fler register än vad som är sparat av callern så är callee ansvarig för att dessa värden lagras och återställs. Kort och gott så får callee inte modifiera register som caller inte sparat om inte callee själv sparar och återställer existerande värden.

Segmentregister

Vi pratade kort innan om segmentregister (segment register) och här kommer en utförligare förklaring. Segmentregistret håller segmentadressen av olika objekt. De finns enbart tillgängliga i 16 olika värden och de kan endast ges ett värde genom generella register eller speciella instruktioner. De flesta applikationer i moderna operativsystem (Ex. Linux eller Microsoft Windows) använder en minnesmodell som pekar nästan alla segment till samma plats och använder istället paging. Detta har medfört att segmentregistren inte längre spelar någon större roll.

Stack Segment (SS). Pekare till stack.
Code Segment (CS). Pekare till kod.
Data Segment (DS). Pekare till data.
Extra Segment (ES). Pekare till extra data ('E' står för 'Extra').
F Segment (FS). Pekare till mer extra data ('F' kommer efter 'E').
G Segment (GS). Pekare till ännu mer extra data ('G' kommer efter 'F').
EFLAGS

Inom X86 arkitekturen finns även EFLAGS (‘E’ står som vanligt för Extended). EFLAGS-registret håller processorns nuvarande tillstånd. Det modifieras av många olika instruktioner och används för att jämföra parametrar, villkorade loopar (conditional loops) och villkorade hopp (conditional jumps). Varje bit håller tillståndet av en specifik parameter från den senaste instruktionen.

Zero Flag (ZF) är satt om resultatet av någon instruktion är noll, annars är den rensad. Bra att använda istället för att kolla alla register såsom eax, ebx, ecx etc om ett värde är 0.

Sign Flag (SF) Sätts lika med den mest signifikanta biten (Most significant bit) av resultatet, som är teckenbiten (sign bit) av ett signerat heltal (signed integer). 0 indikerar ett positivt värde och 1 ett negativt värde. Se positiva och negativa nummer ovan för mer info.

Nedan följer en lista av vilka EFLAGS som finns. De som inte är listade är reserverade av Intel.

Bit   Label    Desciption
---------------------------
0      CF      Carry flag
2      PF      Parity flag
4      AF      Auxiliary carry flag
6      ZF      Zero flag
7      SF      Sign flag
8      TF      Trap flag
9      IF      Interrupt enable flag
10     DF      Direction flag
11     OF      Overflow flag
12-13  IOPL    I/O Priviledge level
14     NT      Nested task flag
16     RF      Resume flag
17     VM      Virtual 8086 mode flag
18     AC      Alignment check flag (486+)
19     VIF     Virutal interrupt flag
20     VIP     Virtual interrupt pending flag
21     ID      ID flag
Calling Conventions – Anropspraxis

Hur kod anropar en subrutin/underprogram beror på kompilatorn och det är konfigurerbart. Vi går igenom cdecl och stdcall men det finns många fler.

cdecl

Står för C declaration och är den vanligaste anropspraxisen. Funktionsparametrar läggs på stacken från höger till vänster, i exemplet: “printf (“%d”, 0x1234);” läggs 0x1234 först på stacken, sedan stringen och sedan anropas funktionen. Caller och callee måste vara enade om anropspraxisen. Det första som händer är att den anropade funktionen sparar den gamla stackens EBP, Stack frame base pointer, och sätter sedan upp en ny stackram (stack frame). De 32 mest signifikanta bitarna sparas sedan i EDX och de 32 minst signifikanta bitarna sparas i EAX. Om det är ett 32-bitarsversion sparas allt i EAX. EAX eller EDX:EAX returnerar resultatet för primitiva datatyper. Glöm inte att värden sparas som Big Endian i register och enbart Little Endian i minnesenheter. Caller är ansvarig för att rensa upp i stack.

stdcall

stdcall används nästan enbart av Microsoft C++ kod, till exempel Win32 API. Skiljer sig från cdecl genom att Callee är ansvarig för att rensa upp alla stackparametrar som denne berör. 

Stack

Tidigare i texten kunde vi även läsa om stack (The Stack). Stack kommer från engelskans stapla och är en struktur för organisering av data. Stacken är ett begreppsmässigt område i RAM som är utsett av operativsystemet när ett program startas (Olika operativsystem startar på olika adresser enligt en överenskommelse). Enligt praxis växer stacken mot de lägre minnesadresserna. Om något adderas till stacken betyder det att toppen av stacken nu är på de lägre minnesadresserna.

Från Wikipedia: “Stack eller LIFO (Last In, First Out), en linjär datastruktur med två operationerpush och pop (ibland kallas operationen pull). Push lägger in ett element överst på stacken, och pop tar bort det översta elementet.

Stacken kan liknas med en tallriksstapel som kan påträffas i en skolbespisning eller lunchrestaurang. På stapeln kan man endast lägga en tallrik eller ta bort den översta. Tallrikar inne i stapeln kan inte kommas åt utan att ovanpåliggande tallrikar först tas bort. För att t ex byta ut den tionde tallriken räknat uppifrån avlägsnar man alltså först var och en av de nio översta, så att tallrik tio ligger fri. Sen gör man tallriksbytet och avslutar genom att i ordning lägga tillbaka de nio man tog bort. Av effektivitetsskäl kan en stack berikas med stackpekare som medger direktåtkomst till tallrikar i stapelns mitt utan att ovanpåliggande behöver avlägsnas/återställas.

Stacken är en mycket vanlig datastruktur och används implicit i stort sett i alla datorprogram. Vid funktionsanrop i imperativa programspråk lagras anropsparametrarna och lokala variabler i en stackstruktur, så att de sedan kan hämtas tillbaka i rätt ordning när funktionen återvänder. Många processorer har en inbyggd stack för att hantera funktionsanrop och returadresser.” ESP pekar på toppen av Stacken, den lägsta adressen som används. Data existerar över toppen av stacken men denna anses vara odefinierad. Stacken håller reda på vilka funktioner som anropats (called) före den aktuella. Den håller även lokala variabler och används frekvent för att skicka argument till nästa funktion som ska anropas. Det är viktigt att förstå stacken för att förstå hur ett program fungerar.

Exempel på en Strack Frame funktion nedan. Stacken växer neråt precis som vi gått igenom tidigare (mot lägre minnesadresser). Vi räknar med att main() är det första som körs i vilket program som helst och det första som händer är att main() reserverar plats på Stacken för sina lokala variabler. Stacken ser ut enligt nedan om vi antar att main() har lokala variabler.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 12

När main() beslutar sig för att kalla en Subroutine blir main() anroparen (caller). Vi antar att main() har några register den vill behålla och kommer därför spara dem. Vi antar också att den anropades funktioner tar några input argument.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 13

När main() faktiskt skickas CALL-instruktionen blir returadressen sparad på Stacken. Eftersom instruktionen efter callen kommer vara början på den anropade funktionen, anser vi att framen har bytts till den anropade (callee).

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 14

När foo() startar så pekar fortfarande frame-pekaren (EBP) på main()’s frame (foo() är en Subroutine.). Det första som händer är därför att foo() sparar den gamla Frame-pekaren på stacken och sätter sedan sin egna Frames värde till EBP.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 15

I nästa steg antar vi att anropade foo() vill använda alla register och måste därför spara alla callee-save register ( EBP, EBX, ESI och EDI). Efter detta är gjort allokeras plats för dess lokala variabler.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 16

I det här steget vill foo() anropa bar(). foo() är fortfarande den anropade av main() men blir nu anroparen av bar(). Sparar nu alla caller-save registren (EAX, EDX och ECX) som måste sparas och sätter sedan funktionens argument på stacken. Det är sällan en frame blir större än så här.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 17

Efter att den sista funktionen körts och programmet ska avslutas är det bara att gå uppåt i listan för att se vad som händer. Börjar och slutar på samma ställe. Varje part av stackramen är egentligen valfri (du kan handkoda assemblerspråk utan att följa konventionerna), men kompilatorer genererar kod som använder delar om de behövs. Vilka delar som används kan ibland manipuleras med kompilatoralternativ. Exempel är att utelämna rampekare (frame pointers) eller att byta anropskonventionen att skicka argument i register etc. Det är även viktigt att känna till att stackramar är en sammanlänkad lista. EBP i den nuvarande ramen pekar på den sparade EBP av den föregående ramen.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 19

r/m32 adresseringsschema (Addressing Forms)

Varje gång du ser r/m32 innebär det att r/m32 kan ta ett värde från ett register eller en minnesadress. Det heter egentligen inte “r/m32-schema” utan instruktionerna ser ut på olika sätt i manualen. I Intelsyntax så innebär hakparenteser [] ofta att värdet ska behandlas som en minnesadress, för att sedan hämta värdet på den adressen (Likt “dereferencing a pointer“). r/m32-instruktionerna som finns i manualen nedan (Den mest komplicerade är [base + index*scale + disp], disp = displacement. Mer info finns i PDFn: Intel 64 and 32 Arch Sw Dev Man V2A, kap 2.1.5 Addressing-Mode Encoding of ModR/M and SIB Bytes).
– mov eax, ebx
– mov eax, [ebx] – mov eax, [ebx+ecx*X] (X=1, 2, 4, 8)
– mov eax, [ebx+ecx*X+Y] (Y= en byte, 0-255 eller 4 bytes, 0-2^32-1)

Kodexempel

Vi ska nu gå igenom ett enkelt kodexempel. Stackramar i nedanstående exempel kommer vara väldigt enkla. Det enda som händer i koden är att main() anropar subrutinen sub() som alltid returnerar 0xbeef. main() gör sedan ingenting av denna information utan returnerar 0xf00d istället.

int sub(){
    return 0xbeef;
}
int main(){
	sub();
	return 0xf00d;
}
sub:
00401000  push        ebp  
00401001  mov         ebp,esp 
00401003  mov         eax,0BEEFh 
00401008  pop         ebp  
00401009  ret 
main:
00401010  push        ebp  
00401011  mov         ebp,esp 
00401013  call        sub (401000h) 
00401018  mov         eax,0F00Dh 
0040101D  pop         ebp  
0040101E  ret

Såhär ser det ut innan någonting har körts. Startvärdena kommer från tidigare funktioner som körts.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 20

Det första som sker är att EBP blir pushat på stacken. Kompileraren skapar automatiskt instruktionerna push ebp följt av mov ebp,esp.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 21

I nästa steg kommer något vi inte gått igenom. Vad händer om det står  “någon instruktion” xxx,xxx. I Intels syntax så är det: instruktion destination, källa. Exempel mov ebx, esp. Kort och gott så kopieras värdet från källan (höger) till destinationen (vänster). När det gäller AT&T syntax på exempelvis Linux är det tvärtom.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 22

I nästa steg anropas (CALL) sub() funktionen. Anropet skickas sedan till sub()’s adress som slutar med 401000. Som vi gått igenom tidigare i X86-instruktioner är CALLs uppgift är att överlåta kontrollen till en annan funktion på ett sätt att kontrollen senare kan återupptas där den slutade. Detta görs genom att adressen för nästa instruktion pushas på stacken. I det här fallet pushas 0x00401018 dit som är nästa instruktion i main().

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 23

När vi påbörjar en ny frame så sparas alltid rampekaren, EBP, som vi gick igenom innan. Stackpekaren, ESP, ändras också till den nya adressen.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 24

Precis som i main() så körs mov ebp,esp efter push ebp för att uppdatera EBP till den nya ramen (frame).

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 25

En kort paus för att att återkoppla vilka ramar (frames) som finns representerade inom Stack Frame (stackram). Som vi kan se sparar sub() adressen från main()’s ram (I minnet

0x0012FF60 är 0x0012FF68 sparat). I minnet 0x0012FF68 i main’s ram finns en plats sparad som är utanför det vi ser i bilden från funktionen innan main().

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 26

I nästa bild ser vi hur det returnerade värdet 0xBEEF läggs in i EAX registret. Detta görs därför att så fort koden ska returnera något (I vårt fall return 0xbeef;) så läggs det returnerade värdet in i EAX-registret enligt konvention.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 27

I nästa steg ska vi nu börja “riva ner” vår ram. Detta görs genom kommandot POP som tar bort värde från stacken och adderar stackpekaren med 4. Därav blir ESP 0x0012FF64 istället för 0x0012FF60. Tack vare att vi tar bort EBP från sub()’s ram så pekar nu EBP på den föregående ramens sparade adress som är 0x0012FF68.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 28

I nästa steg är det RET som körs. I och med att det bara står RET är det en plain return som vi gick igenom tidigare. Kollar vi på bilden ovan ser vi att minnet 0x0012FF64 innehåller adressen 0x00401018. RET kommer nu att sätta instruktionspekaren, EIP, till 0x00401018-adressen och sedan POPa av värdet från Stacken och öka ESP med 4.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 29

Nu är vi åter i main()-funktionen på plats 0x00401018. Med tanke på att vi inte gör något med 0xBEEF byts nu värdet i EAX ut mot 0XF00D istället. Detta då det returnerade värdet sparas i EAX enligt praxis.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 30

Nu tar vi även bort main()’s rampekare från stacken vilket gör att EBP nu blir värdet av funktionen innan mains EBP och ESP ökar som vanligt med 4 efter POP.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 31

I sista steget körs RET som sätter EIP till värdet 0x004012E8 som var sparat i minnesadressen 0x0012FF6C. ESP ökar med fyra tack vare att 0x0012FF6C tas bort (POP) från stacken.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 32

Notera: sub() är dökod (deadcode). Värdet som returneras används inte till något, main() returnerar alltid 0xf00d. Om optimering varit påslaget i kompilatorn hade sub() tagits bort. Tack vare att det inte finns några indataparametrar för sub() så spelar det ingen roll om programmet kompileras med cdecl eller stdcall-anropspraxis (calling convention).

Kodexempel 2

Nästa steg är att gå igenom lite svårare kod, men fortfarande väldigt basic.

#include <stdlib.h>
int sub(int x, int y){
    return 2*x+y;
}
int main(int argc, char ** argv){
	int a;
	a = atoi(argv[1]);
	return sub(argc,a);
}

Det första vi ser är att main() nu har två parametrar, int argc, char ** argv (Det skulle lika gärna kunnat stå int main(int argc, char *argv[])). argc står för “argument count” och argv “argument vector”. argc kommer vara antalet strängar som pekas på av argv. En vektor är en endimensionell array och argv är en endimensionell array med strängar. Den första strängen som lagras i argv[0] är programmets namn, Example2.c, exempel2.exe eller något annat som programmet döps till och på plats [1] och efter blir vad som skrivs i kommandotolken.

Programmet har en lokalvariabel som är en int och kallas a. Koden a = atoi(argv[1]); tar det första värdet som skrivits i kommandotolken och parsar värdet från en sträng till en integer. Värdet från argc och a skickas sedan till sub() där argc = x och a = y. sub returnerar sedan vad resultatet av ekvationen 2*x+y blir och det är även vad main() sedermera också returnerar.

.text:00000000 _sub:           push    ebp
.text:00000001                 mov     ebp, esp
.text:00000003                 mov     eax, [ebp+8]
.text:00000006                 mov     ecx, [ebp+0Ch]
.text:00000009                 lea     eax, [ecx+eax*2]
.text:0000000C                 pop     ebp
.text:0000000D                 retn
.text:00000010 _main:          push    ebp
.text:00000011                 mov     ebp, esp
.text:00000013                 push    ecx
.text:00000014                 mov     eax, [ebp+0Ch]
.text:00000017                 mov     ecx, [eax+4]
.text:0000001A                 push    ecx
.text:0000001B                 call    dword ptr ds:__imp__atoi
.text:00000021                 add     esp, 4
.text:00000024                 mov     [ebp-4], eax
.text:00000027                 mov     edx, [ebp-4]
.text:0000002A                 push    edx
.text:0000002B                 mov     eax, [ebp+8]
.text:0000002E                 push    eax
.text:0000002F                 call    _sub
.text:00000034                 add     esp, 8
.text:00000037                 mov     esp, ebp
.text:00000039                 pop     ebp
.text:0000003A                 retn

När main() anropades (Exempel är att vi kör programmet “exempel2.exe 100” i bilden nedan) så PUSHade den anropande funktionen parametrarna för main() på stacken från höger till vänster, int main (int argc, char ** argv). CHAR * ARGV[0] och CHAR * ARGV[1] är teckenpekare (Character Pointers) och pekar i sin tur på en sträng. Hur det hänger ihop är sammansatt i en bild nedan. Det kommer bli tydligare när vi går igenom koden steg för steg men det är bra att förstå grunderna innan vi går in på djupet i koden.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 33

Det första som sker är som vanligt att EBP blir pushat på stacken och ESP ökar med 4. På stacken ser vi även 0x2 från int argc och 0x12FFB0 (minnesadress för strängarna) fråm char ** argv.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 34

Efter push ebp kommer mov ebp,esp.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 35

Tack vare att vi inte ser pop ecx så vet vi att ecx i detta fallet inte används som ett caller-save register. Istället är det Visual Studios sätt att att allokera plats på stacken för vår lokala variabel int a, minskar esp med 4. Om vi hade haft optimering på hade koden sätt annorlunda ut, förmodligen esp subtraherat med 4. Värdet i ecx spelar ingen roll utan det är platsen på stacken programmet vill åt. a är fortfarande oinitierad för att inget tilldelats den.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 36

Här är första gången vi ser r/m32-formen användas. [ebp+0Ch] innebär ebp-värdet + 0x0c (h står för hexadecimal). ebp är 0x0012FF24 och + 0x0000000C blir det 0x0012FF30. För att förstå räknesättet kan du gå tillbaka till rubriken “Talsystem” längre upp på sidan. Som vi kommer ihåg från r/m32 så hämtas en minnesadress och sedan hämtas vad som finns i minnet därifrån. Kollar vi på Stacken ser vi att 0x0012FF30 innehåller värdet 0x0012FFB0. Detta värde stoppas nu i eax och motsvarar pekaren för argv[0].

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 37

Vi vill åt argv[1] och måste därför öka eax med fyra. Precis som innan så ökar vi eax (0x12FFB0) med 4 (0x04) och adressen vi får fram är 0x12FFB4. Vi antar att minnesadressen 0x12FFB4 innehåller värdet 0x12FFD4 eftersom det är utanför stackens område vi tittar på.  0x12FFD4 läggs sedan in i ecx (Hade vi skrivit in “exempel2.exe 100” hade minnesplatsen 0x12FFD4 innehållit strängen 100 och avslutats med null-tecknet “”).

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 38

Sparar in några slides och slår samman några funktioner. Funktionen atoi vill ha en pekare till strängen som ska omvandlas och det är precis vad 0x12FFD4 är. Hade vi gått igenom alla funktioner bild för bild så hade “push ecx” inneburit att stackadressen 0x0012FF1C fått värdet 0x12FFD4. I nästa steg hade call skrivit returadressen 0x00000021 för nästa funktion, “add esp,4”, till stackadressen 0x0012FF18. call dword ptr ds:__imp__atoi behandlar vi som en black box som vi anropar utan att veta exakt hur den fungerar, bara vilka inparametrar som krävs och vad den returnerar. Returvärdet sparas i eax som i det här fallet är 0x100, motsvarande 256 i decimaltal. I cdecl är callern ansvarig för att rensa stacken och därför körs kommandot “add esp,4” som ökar esp med 4 och därigenom återställer värdet.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 39

Det första som händer är att värdet i eax sätts till “a”. Detta görs genom att värdet som sparats i eax, 0x100, flyttas till minnesadressen för ebp subtraherat med 4 som i vårt fall blir  0x0012FF20. Hade optimering varit igång hade nästa instruktion varit “push eax” som hade skrivit 0x100 till 0x0012FF1C men istället görs det genom att först flytta värdet från [ebp-4] till edx-registret och sedan pusha edx till stacken. Anledningen till att det inte står “mov  [ebp-8], [ebp-4]” är för att det inte är tillåtet att flytta från minne till minne i Intels x86 arkitektur. Vi sparar 0x100 två gånger för att spara int a i main() och int y i sub().

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 40

I nästa steg pushas argc som är den första parametern (int x) till sub() på stacken. Görs genom [ebp+8] som innebär  0x0012FF24 + 0x8 som blir 0x0012FF2C. Värdet från 0x0012FF2C, 0x2, skrivs sedan till eax som sedemera pushas på stacken.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 41

I nästa steg anropas sub(). Som vanligt sparas adressen för nästa instruktion på stacken av call och esp minskar med 4. Ritar pilar på bilden nedan men vid det här laget borde de inte krävas för att förstå var värdena kommer ifrån och därför kommer jag enbart rita ut pilar om något är oklart eller för att förtydliga en bild hädanefter. Anledningen till att adresserna är så låga (0x00000034 istället för mer normala 0×00401018) är för att programmet disassemblerats i Ida Pro och därför getts relativa adresser.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 42

Nästa steg är att spara den gamla rampekaren på stacken för att sedan uppdatera ebp, som vanligt.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 43

Vi flyttar sedan x till eax och y till ecx.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 45

Utför ekvationen 2*x+y och skriver värdet till eax. Ekvationen blir 0x2 * 2 + 0x100 = 0x104 (r/m32 exempel: eax, [ebx+ecx*X]).

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 46

 

Nu är det dags att börja riva ner stacken. Vi börjar med att ta port rampekaren, ebp, från sub()’s ram och sedan återgår vi till main()-funktionen genom returadressen 0x00000034.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 47

 

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 48

Tack vare att det står “add esp,8” vet vi att det är cdecl för att callern, anroparen, är ansvarig för att rensa upp stacken.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 49

Fortsätter riva stacken tills vi kan återgå till funktionen som från början anropade main().

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 50

Notera att eax inte har modifierats sedan sub() och därför kommer main() att returnera samma som sub(). “mov esp, ebp” är det effektivaste sättet att ta bort alla lokala variabler du har deklarerat.

Sec24-sec-24-hur-hackar-man-introduktion Intel X86 C programmering assembler reverse engineering 51