Bekijk online

Afschermen van Bibliotheekinterfaces in Windows
tegen Aanvallen
Bert Abrath
Promotor: prof. dr. ir. Bjorn De Sutter
Begeleiders: ir. Stijn Volckaert, dr. Jonas Maebe, dr. Bart Coppens
Masterproef ingediend tot het behalen van de academische graad van
Master of Science in de ingenieurswetenschappen: computerwetenschappen
Vakgroep Elektronica en Informatiesystemen
Voorzitter: prof. dr. ir. Jan Van Campenhout
Faculteit Ingenieurswetenschappen en Architectuur
Academiejaar 2013-2014
Voorwoord
In de eerste plaats wil ik mijn promotor, prof. dr. ir. Bjorn De Sutter, bedanken voor zijn ondersteuning en uitstekende feedback op deze scriptie. Ook wil ik mijn begeleiders, ir. Stijn Volckaert, dr. Bart Coppens en dr. Jonas Maebe, bedanken voor de hulp en inzicht die ze mij
hebben geboden. Specifiek bedank ik Stijn voor het delen van zijn kennis over de interne werking van Windows, en Bart en Jonas voor de hulp die zij geboden hebben toen ik worstelde
met problemen in Diablo. Naast mijn promotor en begeleiders zijn er nog twee mensen die op
een directe wijze een invloed hebben gehad op deze scriptie door ze uit vrije wil na te lezen:
Ronald en mijn zus, Margo.
Op indirecte wijze hebben er natuurlijk veel meer mensen bijgedragen aan deze scriptie. Zo
was er altijd een stimulerende werkomgeving met een goede sfeer en interessante verhalen
beschikbaar. Het afgelopen academiejaar heb ik dan ook veel bijgeleerd over AllyDBG, XOR
linked lists, GoT, en the A-Team, en heb ik een gezonde paranoia ten opzichte van willekeurige toetsaanslagen aangeleerd. Hiervoor dank ik Bart, Ronald, Stijn, Jens, Jonas en Sander.
Graag wil ik ook de mensen van de Werkgroep Ethical Hacking (van wie ik velen reeds vernoemd heb) bedanken voor het aanwakkeren van mijn interesse in beveiliging.
Een goede leefomgeving was natuurlijk ook belangrijk. Daarvoor dank ik mijn ouders, mijn
zussen, en mijn bijna 1-jarige neefje, Mathiz, dat altijd een interessante afleiding vormde en
er in zijn talloze pogingen nooit is in geslaagd mijn laptop, boeken of papieren, schade aan te
doen. Ook in Gent kon ik mij altijd thuis voelen. Ik wil mijn ganggenoten op kot danken voor
de vele jaren aan plezier. De gezelschapsspelletjes, andere spelletjes, zang- en danssessies,
kooksessies en veel meer zullen mij nog lang bijblijven. Naast mijn vele vrienden op kot wil
ik ook mijn andere vrienden doorheen de jaren bedanken voor de vele onvergetelijke verhalen
en het algemeen jolijt.
Bert Abrath
10 juni 2014
II
Toelating tot bruikleen
De auteur geeft de toelating deze scriptie voor consultatie beschikbaar te stellen en delen van
de scriptie te kopi¨eren voor persoonlijk gebruik. Elk ander gebruik valt onder de beperkingen
van het auteursrecht, in het bijzonder met betrekking tot de verplichting de bron uitdrukkelijk te vermelden bij het aanhalen van resultaten uit deze scriptie.
The author gives permission to make this master dissertation available for consultation and
to copy parts of this master dissertation for personal use. In the case of any other use, the
limitations of the copyright have to be respected, in particular with regard to the obligation
to state expressly the source when quoting results from this master dissertation.
Bert Abrath
10 juni 2014
III
Afschermen van Bibliotheekinterfaces
in Windows tegen Aanvallen
door
Bert Abrath
Promotor: prof. dr. ir. Bjorn De Sutter
Begeleiders: ir. Stijn Volckaert, dr. Jonas Maebe, dr. Bart Coppens
Masterproef ingediend tot het behalen van de academische graad van
Master of Science in de ingenieurswetenschappen: computerwetenschappen
Vakgroep Elektronica en Informatiesystemen
Voorzitter: prof. dr. ir. Jan Van Campenhout
Faculteit Ingenieurswetenschappen en Architectuur
Academiejaar 2013–2014
Samenvatting
Een dynamisch gelinkte applicatie bestaat uit een uitvoerbaar bestand en een aantal dynamische bibliotheken (die ook door andere applicaties gebruikt kunnen worden). Dynamisch
gelinkte applicaties hebben echter een beveiligingsprobleem: de interfaces tussen de verschillende componenten zijn perfecte aanknopingspunten voor reverse engineering. Om reverse
engineering tegen te gaan willen we het gebruik van deze interfaces afschermen. Het gebruik
van bibliotheekinterfaces door het uitvoerbaar bestand wordt in een eerste methode verborgen d.m.v. van encryptie.
In een tweede methode wordt de applicatie volledig statisch gelinkt, door alle gebruikte delen
uit de dynamische bibliotheken toe te voegen aan het uitvoerbaar bestand. Volledig statisch
gelinkte applicaties zijn echter niet compatibel met meerdere versies van Windows omdat
de interface die de Windows-kernel aanbiedt niet stabiel is. Daarom werd er in de tweede
methode voor gezorgd dat deze statisch gelinkte applicaties zichzelf kunnen aanpassen aan
de versie van Windows waarop ze uitgevoerd worden.
Beide methodes zijn als proof-of-concept ge¨ımplementeerd en kunnen nog niet gebruikt worden om realistische applicaties te herschrijven. Ze werden ge¨ımplementeerd op Diablo, een
raamwerk ontwikkeld binnen CSL om applicaties te herschrijven.
Trefwoorden: Beveiliging, bibliotheekinterfaces, reverse engineering, Windows, Diablo
IV
Protecting Library Interfaces in Windows
against Attacks
Bert Abrath
Supervisor(s): prof. dr. ir. Bjorn De Sutter, ir. Stijn Volckaert, dr. Jonas Maebe, dr. Bart Coppens
Abstract— Dynamically linked applications consist of an executable, and a number of dynamic libraries that can be used by
other executables. The benefits of this approach to creating applications notwithstanding, the interfaces between these components make the application more susceptible to reverse engineering. Consequently, from a security perspective it is preferable to
build statically linked applications. Fully statically linked applications however are not compatible with multiple versions of Windows. In this dissertation two methods are presented that protect the use of library interfaces from reverse engineering. These
methods have been implemented on the Diablo framework.
Keywords— security, protection, library interfaces, reverse engineering, Windows, Diablo
I. I NTRODUCTION
W
HEN a dynamic library is used in multiple applications, only one copy of the library has to be
present in memory and on disk. This can result in significant savings in memory usage and disk space. The
most important advantage however is in the area of
software engineering. Subdividing an application into
an executable and dynamic libraries makes it easier to
reuse existing functionality from other applications and
allows the components to be updated independently, because the interfaces between the components are fixed.
These fixed interfaces are good targets for reverse engineering however, making it preferable to build statically linked applications. In a fully statically linked application the – required parts of the – system libraries
are also a part of the executable and thus the application interacts directly with the kernel. Unfortunately,
the interface provided by the Windows kernel differs
between subsequent versions of Windows (and even between service packs). Consequently, applications use
the interface provided by the system libraries (the Windows API) in stead, as this interface is guaranteed to be
stable. As a result of the instability of the kernel interface, all Windows applications are dynamically linked
and thus more susceptible to reverse engineering.
II. R EVERSE ENGINEERING
Reverse engineering of software is the process of trying to understand an application at a higher level of
abstraction from lower level information such as executable code. There are numerous reasons for reverse
engineering an application, but here we will assume the
role of the reverse engineer being filled by an attacker
with malicious goals. In general there are two techniques a reverse engineer can use: static analysis and
dynamic analysis.
Static analysis is the process of analyzing an application without executing it. The executable file will be
analyzed by disassembling the binary code it contains,
constructing a control flow graph (CFG) and using this
information to gain a deeper understanding of the application. In analyzing dynamically linked application the
meta-information contained within the components can
especially be of use. Meta-information is what we call
the information present in these components that allow
them to dynamically link with – and use functionality
from – other components.
Dynamic analysis on the other hand involves executing the application and observing its dynamic behavior
in order to analyze it. A technique often used to analyze
dynamically linked applications is hooking. Dynamically linked applications depend on components being
found and used through their interfaces at runtime. A
potential attacker can thus build a fake component that
offers the same interface as a real component, and trick
the application into using his component in stead of the
real one. The fake component can then forward all function calls to the real component so the application continues executing normally and is unaware of any danger.
However, the attacker would then be able to intercept all
communication between the components, allowing him
to reverse engineer the application more effectively.
III. D IABLO
Diablo is a link-time binary rewriting framework developed within CSL. Applications can be build on top of
this framework, in our case we implemented two methods to protect a binary from reverse engineering on top
of it. Diablo works at linker-level, which means that it
takes the place of the linker in the build process. The
object files generated by the compiler are taken by Diablo which uses it to emulate the linker process. This
results in a binary that is the same as the original binary,
but some information that is used by the linker and unrecoverable from the resulting binary will be kept. This
information allows Diablo to build an accurate representation of the application upon which transformations
and analysis can be applied.
IV. E NCRYPTION OF META - INFORMATION
The first implemented method is aimed at obstructing the use of meta-information during static analysis.
As this information has to be present in the components in order to allow them dynamically link with other
components, we can’t simply remove it. In stead Diablo rewrites the executable so the meta-information
is still present, but in an encrypted form. On disk
the executable will seem to have no interface with any
other component, and there will be no meta-information
present to use in static analysis. At runtime the application will use the encrypted meta-information to adjust
itself and reconstruct the interfaces. As the interfaces
are still present at runtime the application will still be
vulnerable to dynamic analysis through hooking.
V. S TATIC LINKING
From a security eye-point it would be preferable
to make applications statically linked, as no metainformation would be present in the executable and
hooking would be impossible. Therefore a second
method was implemented that transforms a dynamically
linked application into a statically linked one by linking
the relevant portions of the dynamic libraries into the
executable. To overcome the instability of the Windows
kernel interface, the application adapts itself to the interface of the kernel on which it is running. This way
we can enjoy the security advantages of static linking
while retaining compatibility across different versions
of Windows.
VI. E VALUATION
The methods were implemented as proof-of-concept
and there are several limitations in the implementation
that hinder the rewriting of realistic applications. The
overhead introduced by the methods was evaluated and
reckoned to be unnoticeable except in extreme cases.
VII. C ONCLUSION
Dynamically linked applications are at an increased
risk to reverse engineering. In this dissertation two
methods to protect the use library interfaces between
dynamically linked components against revere engineering were proposed and implemented. These methods are only proof-of-concept and can’t yet be used to
rewrite applications of a realistic scale, but the overhead
they introduce is limited.
Inhoudsopgave
1 Inleiding
1.1
1
Probleemstelling
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1
1.1.1
De dynamisch gelinkte softwarestapel . . . . . . . . . . . . . . . . . .
1
1.1.2
Voor- en nadelen . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2
1.1.3
Statisch gelinkte softwarestapel . . . . . . . . . . . . . . . . . . . . .
3
1.2
Doelstellingen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4
1.3
Overzicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5
2 Gerelateerd werk
2.1
2.2
2.3
2.4
2.5
6
Linken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
2.1.1
Het buildproces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
2.1.2
Objectbestanden . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7
2.1.3
Het linkerproces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8
2.1.4
Statisch linken versus dynamisch linken . . . . . . . . . . . . . . . . .
9
Diablo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
10
2.2.1
Werking . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
10
2.2.2
Structuur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
12
2.2.3
Toepassingen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
12
Disassembleren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
13
2.3.1
Lineair disassembleren . . . . . . . . . . . . . . . . . . . . . . . . . .
13
2.3.2
Recursief disassembleren . . . . . . . . . . . . . . . . . . . . . . . . .
14
2.3.3
IDA Pro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
15
Reverse engineering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
16
2.4.1
Statische analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
16
2.4.2
Dynamische analyse . . . . . . . . . . . . . . . . . . . . . . . . . . .
17
2.4.3
Bestaande oplossingen . . . . . . . . . . . . . . . . . . . . . . . . . .
17
Het PE-formaat . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
18
2.5.1
18
Dynamisch linken . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
VII
2.6
2.7
2.5.2
Laden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
20
2.5.3
Rebasing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
21
De Windows-systeembibliotheken . . . . . . . . . . . . . . . . . . . . . . . .
22
2.6.1
De verschillende API’s . . . . . . . . . . . . . . . . . . . . . . . . . .
22
2.6.2
Het API-set-schema . . . . . . . . . . . . . . . . . . . . . . . . . . . .
24
De kernel-interface in Windows . . . . . . . . . . . . . . . . . . . . . . . . .
25
2.7.1
Systeemoproepen . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
25
2.7.2
Syscall en sysenter . . . . . . . . . . . . . . . . . . . . . . . . . . . .
27
2.7.3
Instabiliteit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
27
2.7.4
Systeemoproepen en systeembibliotheken . . . . . . . . . . . . . . . .
28
2.7.5
WoW64 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
28
3 Encrypteren van meta-informatie
30
3.1
De algemene werking . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
30
3.2
Het encrypteren van de meta-informatie . . . . . . . . . . . . . . . . . . . .
31
3.3
De glue code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
33
3.3.1
Aanpassingen in Diablo
. . . . . . . . . . . . . . . . . . . . . . . . .
33
3.3.2
De initialisatiefunctie . . . . . . . . . . . . . . . . . . . . . . . . . . .
34
3.3.3
De laadfunctie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
36
Beperkingen en mogelijke uitbreidingen . . . . . . . . . . . . . . . . . . . . .
37
3.4.1
De eenrichtingsfuncties . . . . . . . . . . . . . . . . . . . . . . . . . .
37
3.4.2
Uitbreiden ondersteuning uitvoerbare bestanden . . . . . . . . . . . .
38
3.4.3
Dynamische aanvallen . . . . . . . . . . . . . . . . . . . . . . . . . .
38
3.4
4 Statisch linken
4.1
40
Toevoegen van DLL’s . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
40
4.1.1
Bepalen van de benodigde DLL’s . . . . . . . . . . . . . . . . . . . .
40
4.1.2
Reconstrueren van relocaties . . . . . . . . . . . . . . . . . . . . . . .
41
Disassembleren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
41
4.2.1
Implementatie van recursief disassembleren . . . . . . . . . . . . . . .
42
4.2.2
Sprongtabellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
43
4.3
Statisch linken van dynamisch gelinkte bestanden . . . . . . . . . . . . . . .
43
4.4
Verwijderen van overbodige code en data . . . . . . . . . . . . . . . . . . . .
45
4.5
Initialisatieroutines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
46
4.6
Partieel statisch linken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
47
4.7
Beperkingen en mogelijke uitbreidingen . . . . . . . . . . . . . . . . . . . . .
47
4.7.1
47
4.2
Iteratief bepalen van benodigde DLL’s . . . . . . . . . . . . . . . . .
VIII
4.7.2
Opsplitsen data-secties . . . . . . . . . . . . . . . . . . . . . . . . . .
49
4.7.3
Onderscheid code en data . . . . . . . . . . . . . . . . . . . . . . . .
49
4.7.4
Initialisatie van de systeembibliotheken . . . . . . . . . . . . . . . . .
50
5 Compatibiliteit
51
5.1
Algemene werking . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
51
5.2
Glue code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
51
5.2.1
Aanpassingen in Diablo
. . . . . . . . . . . . . . . . . . . . . . . . .
51
5.2.2
Initialisatiefunctie . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
52
5.2.3
Laadfunctie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
53
5.3
Windows 7 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
53
5.4
Beperkingen en mogelijk uitbreidingen . . . . . . . . . . . . . . . . . . . . .
54
5.4.1
Grafische systeemoproepen . . . . . . . . . . . . . . . . . . . . . . . .
54
5.4.2
Verandering structuur SCW . . . . . . . . . . . . . . . . . . . . . . .
54
5.4.3
Veranderingen van system services . . . . . . . . . . . . . . . . . . .
55
5.4.4
32-bits Windows . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
55
6 Implementatie
57
6.1
Inwerken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
57
6.2
Encrypteren van meta-informatie . . . . . . . . . . . . . . . . . . . . . . . .
57
6.3
Statisch linken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
58
6.4
Compatibiliteit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
59
6.5
SVN-statistieken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
59
7 Evaluatie
61
7.1
Encrypteren van meta-informatie . . . . . . . . . . . . . . . . . . . . . . . .
61
7.2
Statisch linken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
62
7.2.1
Beperkingen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
62
7.2.2
Nutteloze code en data . . . . . . . . . . . . . . . . . . . . . . . . . .
62
7.2.3
Evaluatie overhead . . . . . . . . . . . . . . . . . . . . . . . . . . . .
64
8 Conclusie
65
Bibliografie
66
IX
Hoofdstuk 1
Inleiding
In dit inleidend hoofdstuk wordt het probleem dat deze scriptie poogt op te lossen in algemene
bewoordingen uitgelegd. Er worden een aantal methodes voorgesteld om dit probleem op te
lossen, en er wordt een overzicht gegeven van de rest van de scriptie.
1.1
1.1.1
Probleemstelling
De dynamisch gelinkte softwarestapel
We beginnen met een algemeen beeld te schetsen van een dynamisch gelinkte softwarestapel
(zie Figuur 1.1). Onderaan vinden we de kernel, de belangrijkste component in de stack. De
kernel beheert de machine en de bronnen waarover de machine beschikt. Hij zorgt ervoor
dat meerdere applicaties tegelijkertijd op de machine kunnen uitvoeren zonder met elkaar te
interfereren. De kernel voert uit in kernel-modus, dit houdt in dat hij de totale controle over
de machine heeft. Applicaties voeren uit in user-modus, in deze modus zijn ze beperkt in
hun acties. Als een applicatie een geprivilegieerde actie wil ondernemen (invoer van een muis
krijgen, meer geheugenpagina’s laten alloceren, uitvoer naar een scherm printen etc.) moet
deze aan de kernel vragen om dit voor haar te doen d.m.v. systeemoproepen (system calls).
Net boven de kernel vinden we de systeembibliotheken. Deze maken – net als de kernel – deel
uit van het besturingssysteem en bieden functionaliteit aan die door de andere componenten
uit de softwarestapel gebruikt kunnen worden. Zo bieden ze een abstractie aan van de systeemoproepen, zodat deze op een gemakkelijkere manier door andere componenten gebruikt
kunnen worden. Naast deze abstractie bezitten ze ook eigen functionaliteit voor gebruik door
andere componenten.
Een applicatie (bv. een tekstverwerker, een kalender of een internet browser) bestaat uit
een uitvoerbaar bestand (“APP” op de figuur) en de dynamische bibliotheken waarvan dit
bestand afhankelijk is. Deze bibliotheken kunnen zowel systeembibliotheken als andere dynamische bibliotheken (ontwikkeld door de ontwikkelaar(s) van de applicatie of door een derde
partij, “LIB” op de figuur) zijn. Om hun functionaliteit ten dienste van andere componenten
te stellen bieden deze dynamische bibliotheken een interface aan die aan bepaalde conventies
voldoet en stabiel (t.t.z. niet veranderd) is.
Als laatste bespreken we de stuurprogramma’s (drivers). Een stuurprogramma stuurt een spe-
1
Figuur 1.1: Een algemeen beeld van een dynamisch gelinkte softwarestapel.
cifiek stuk hardware (zoals een muis, een toetsenbord of een beeldscherm) aan, en kan invoer
ophalen van dit stuk hardware of uitvoer ernaartoe sturen. Stuurprogramma’s kunnen zowel
in kernel- als in user-modus uitvoeren. Dit hangt af van de situatie en het besturingssysteem.
1.1.2
Voor- en nadelen
Een dynamisch gelinkte softwarestapel heeft een aantal voordelen. Doordat de interface tussen dynamisch gelinkte componenten vast staat, kan ´e´en van deze componenten gemakkelijk
vervangen worden door een andere component met dezelfde interface. Een dynamische bibliotheek kan bijvoorbeeld vervangen worden door een nieuwere versie van deze bibliotheek
waarin een aantal bugfixes zijn gebeurd, zonder dat de andere componenten die er afhankelijk
van zijn aangepast moeten worden. Doordat meerdere applicaties van eenzelfde dynamische
bibliotheek gebruik kunnen maken, wordt er ook uitgespaard op vlak van opslagruimte en
geheugengebruik. Een goed voorbeeld hiervan zijn de systeembibliotheken. Deze worden door
quasi elke applicatie op het systeem gebruikt, maar er is slechts ´e´en kopie van aanwezig in
het geheugen en op de harde schijf.
Het gebruik van dynamisch linken heeft echter ook een aantal nadelen. In deze scriptie zullen
we vooral aandacht besteden aan nadelen op vlak van beveiliging. Zo zijn de interfaces van
dynamische bibliotheken perfecte aanknopingspunten om aan reverse engineering te doen.
Deze aanknopingspunten zijn bij verschillende vormen van reverse engineering nuttig. We
onderscheiden twee vormen van reverse engineering: bij statische analyse wordt de applicatie geanalyseerd zonder ze (ook echt) uit te voeren, bij dynamische analyse wordt ze wel
uitgevoerd. Om het gebruik van elkaars interfaces toe te laten, bezitten de componenten
meta-informatie die gebruikt kan worden tijdens statische analyse.
Aangezien de interface van een component gekend is, kan een potenti¨ele aanvaller een valse
2
Figuur 1.2: Een voorbeeld van een hook.
Figuur 1.3: Een algemeen beeld van een statisch gelinkte softwarestapel.
component construeren die dezelfde interface aanbiedt als de echte component, en andere
componenten deze valse component laten gebruiken in plaats van de echte component. De
valse component kan alle functieoproepen doorsturen naar de echte component. Op deze
manier blijft de applicatie werken en lijkt het alsof er niets mis is, maar de aanvaller is nu
goed geplaatst om de communicatie tussen de twee echte componenten af te luisteren. Deze
techniek wordt vaak gebruikt bij dynamisch analyse. We noemen dit hooking. Een voorbeeld
hiervan valt te zien op Figuur 1.2. De hook – aanwezig tussen applicatie en bibliotheek – is
rood gekleurd.
1.1.3
Statisch gelinkte softwarestapel
Een mogelijke oplossing is om de applicatie volledig statisch te linken (zie Figuur 1.3). Alle
componenten waaruit de applicatie bestaat zijn nu samengevoegd tot ´e´en grote component.
Deze is enkel afhankelijk van de kernel zelf, en maakt geen gebruik van dynamische interfaces.
De beveiligingsnadelen van dynamisch linken zijn dus niet meer aanwezig.
Nu we een applicatie hebben die rechtstreeks afhankelijk is van de kernel, moeten we ons wel
3
Figuur 1.4: De instabiele kernel-interface op Windows.
vragen beginnen stellen over de stabiliteit van de interface tussen de applicatie en de kernel
(de kernel-interface). Een ontwikkelaar wil natuurlijk dat zijn applicatie op verschillende
versies van het besturingssysteem werkt. Als er een nieuwe versie van het besturingssysteem
uitkomt – met een nieuwe kernel – dan willen we dat de applicatie ook op deze nieuwe versie
werkt. Omdat een volledig statisch gelinkte applicatie rechtstreeks gebruik maakt van de
kernel-interface, willen we dus ook dat de kernel-interface achterwaarts-compatibel is. Op
Linux is dit het geval [1]. De kernel-interface kan wel uitbreiden, maar verandert voor de rest
niet.
Op Windows daarentegen is de kernel-interface niet stabiel (zie Figuur 1.4). Een volledig statisch gelinkte applicatie zal dus enkel werken op die versie van Windows waarvoor ze gelinkt
is. Applicaties die met meerdere versies van Windows compatibel willen zijn, moeten daarom
de systeembibliotheken gebruiken. Aangezien deze deel uitmaken van het besturingssysteem
zijn ze aangepast om gebruik te maken van de specifieke versie van de kernel-interface, en
de interface die de systeembibliotheken aanbieden is wel stabiel. Door het gebruik van bibliotheekinterfaces is de applicatie wel kwetsbaarder voor reverse engineering, wat we eigenlijk
wilden voorkomen.
1.2
Doelstellingen
In deze scriptie worden twee methodes gepresenteerd om uitvoerbare bestanden te herschrijven met als doel hun gebruik van bibliotheekinterfaces af te schermen tegen reverse engineering. De uitvoerbare bestanden die worden herschreven zijn meer bepaald 32-bits uitvoerbare
bestanden geschreven voor de x86-versie van Windows.
In de eerste methode wordt de meta-informatie ge¨encrypteerd. Het gebruik van bibliotheekinterfaces wordt zo verborgen en statische analyse wordt tegengegaan. Omdat de interfaces
tijdens de uitvoering nog steeds gebruikt worden, blijft de applicatie wel kwetsbaar voor hooking. In de tweede methode wordt de applicatie volledig statisch gelinkt. Dit gebeurt door
die delen van de dynamische bibliotheken die de applicatie gebruikt toe te voegen aan het
uitvoerbaar bestand. De bibliotheekinterfaces zijn dan verdwenen en het uitvoerbaar bestand
is enkel afhankelijk van de kernel. Om met de instabiliteit van de kernel-interface overweg
te kunnen, is de applicatie in staat zichzelf tijdens de uitvoering aan te passen aan de specifieke versie van de kernel waarop ze uitgevoerd wordt. Zo wordt ook dynamische analyse
tegengegaan.
4
Om uitvoerbare bestanden te herschrijven maken we gebruik van het Diablo-raamwerk. Dit
binnen CSL ontwikkelde raamwerk maakt het mogelijk om uitvoerbare bestanden op het
niveau van de linker te herschrijven. Bij de op Diablo ge¨ımplementeerde toepassingen beschikt
men normaal gezien over de broncode van de applicatie of over de objectbestanden waaruit de
applicatie is opgebouwd. We beschikken echter over broncode noch objectbestanden voor de
dynamische bibliotheken die we in de tweede methode willen toevoegen aan het uitvoerbaar
bestand. Deze fundamentele beperking leidt tot een aantal problemen in de implementatie.
1.3
Overzicht
In Hoofdstuk 2 wordt het gerelateerd werk besproken en wordt de probleemstelling uitgediept.
Vervolgens wordt in Hoofdstuk 3 de eerste eerder besproken methode voorgesteld. De tweede
methode wordt in twee hoofdstukken uitgelegd: Hoofdstuk 4 gaat over het eigenlijke statisch
linken, en Hoofdstuk 5 over het verzorgen van compatibiliteit met verschillende versies van
Windows. In Hoofdstuk 6 wordt het werk geleverd tijdens de implementatie besproken. De
twee methoden worden ge¨evalueerd in Hoofdstuk 7 en de scriptie wordt besloten met een
conclusie in Hoofdstuk 8.
5
Hoofdstuk 2
Gerelateerd werk
Dit hoofdstuk bestaat uit twee grote delen. In het eerste deel wordt gerelateerd werk besproken dat meer algemeen is, het tweede deel is daarentegen Windows-specifiek. Van een aantal
onderwerpen uit het eerste deel wordt toegelicht hoe deze op Windows werken, en er wordt
meer Windows-specifieke informatie gegeven waarvan gebruik gemaakt wordt in de rest van
de scriptie.
2.1
Linken
Aangezien we bestanden op linkerniveau gaan herschrijven, krijgen we te maken met een
proces waar in hedendaagse opleidingen snel aan voorbijgegaan wordt: dat van het linken. In
deze sectie bespreken we kort de manier waarop een uitvoerbaar bestand gegenereerd wordt
en de rol die de linker daarin speelt in Figuur 2.1. Het merendeel van de sectie is gebaseerd
op Levine’s boek ‘Linkers & Loaders’ [2].
2.1.1
Het buildproces
Het genereren van een uitvoerbaar bestand op basis van broncode in een bepaalde programmeertaal – het buildproces – gebeurt in een aantal stappen. Twee belangrijke stappen zijn die
van het compileren (uitgevoerd door de compiler) en het linken (uitgevoerd door de linker).
Als voorbeeld beschouwen we een op Windows uitvoerbaar bestand dat gegenereerd wordt
op basis van C-broncode (zie Figuur 2.1). Per bronbestand wordt er door de compiler een
objectbestand gegenereerd. Dit bestand bevat de instructies en plaats voor de – al dan niet
ge¨ınitialiseerde – statische variabelen.
De objectbestanden die de compiler gegenereerd heeft dienen vervolgens als invoer voor
de linker, die deze combineert tot het uitvoerbaar bestand. Niet enkel de net gegenereerde
objectbestanden dienen als invoer. Er wordt namelijk ook gebruik gemaakt van bibliotheken.
Een bibliotheek is een verzameling reeds gecompileerde code die hergebruikt kan worden door
meerdere applicaties. Ons voorbeeld maakt – zoals quasi alle applicaties geschreven in C –
gebruik van de C-standaardbibliotheek, waarin men de functies printf, malloc, abs etc. vindt.
6
Figuur 2.1: Een compiler zet bronbestanden om in objectbestanden, die de linker combineert
tot een uitvoerbaar bestand.
2.1.2
Objectbestanden
Een objectbestand bestaat uit headers (die de rest van het bestand beschrijven), een aantal
secties met verschillende eigenschappen, en informatie die enkel gebruikt wordt bij het linken.
Er bestaan verschillende secties:
• de .text-sectie: in deze sectie vinden we de uitvoerbare code.
• de .data-sectie: deze sectie bevat data die zowel leesbaar als schrijfbaar is.
• de .rdata-sectie: de data in deze sectie is leesbaar, maar niet schrijfbaar. De inhoud van
de sectie blijft dus hetzelfde doorheen de hele uitvoering van de applicatie.
• de .bss-sectie: in tegenstelling tot andere secties is deze sectie niet echt aanwezig in
het bestand. De sectie bevat data waarvan de waarde op nul ge¨ınitialiseerd wordt en
is zowel leesbaar als schrijfbaar. Aangezien geweten is dat de sectie enkel nullen bevat,
houden we in de headers enkel de grootte ervan bij zonder de sectie zelf te includeren in
het bestand. Als een uitvoerbaar bestand met een .bss-sectie in het geheugen geladen
wordt zal er een sectie met de juiste grootte gecre¨eerd en op nul ge¨ınitialiseerd worden.
Alle secties die enkel doorgaans enkel data bevat (zoals .data, .rdata en .bss) noemen we
data-secties.
Een objectbestand bevat twee soorten informatie waarvan de linker gebruik maakt: symboolinformatie en relocatie-informatie. Een symbool is de naam van een functie of een variabele
7
Figuur 2.2: Een voorbeeld van een aantal relocaties.
die gedefinieerd wordt in een objectbestand. Een objectbestand bevat enerzijds informatie
over symbolen die in het objectbestand zelf gedefinieerd worden en anderzijds over symbolen die ge¨ımporteerd moeten worden uit andere objectbestanden (of die gedefinieerd worden
door de linker). Stel bijvoorbeeld dat de functie foo ge¨ımplementeerd wordt in foo.obj, en
aangeroepen wordt vanuit main.obj. Dan is foo een symbool dat gedefinieerd is in foo.obj,
en ge¨ımporteerd wordt in main.obj.
Binnenin een objectbestand zijn er plaatsen in de ene sectie die plaatsen in een andere sectie refereren. Als we bijvoorbeeld een functie hebben die een statisch gealloceerde variabele
incrementeert, leidt dit tot een instructie in de .text-sectie waarvan het operand een adres
in de .data-sectie is. Deze referenties worden in het uiteindelijke uitvoerbaar bestand voorgesteld als adressen. Op het moment dat de compiler het objectbestand genereert kunnen deze
adressen nog niet berekend worden. Dit gebeurt later door de linker in een proces genaamd
relocatie. Om deze adressen te kunnen berekenen wordt er relocatie-informatie bijgehouden,
deze bestaat uit een lijst van relocaties die moeten gebeuren. Het voorbeeld met de statisch
gealloceerde variabele leidt tot een relocatie die voorgesteld wordt op Figuur 2.2. Het adres
gebruikt door de inc-instructie is nog niet berekend en staat in het rood. Er wijst wel een
relocatie van deze operand naar de offset (offsetNaar1 ) binnen de .data-sectie waarop de
variabele zich bevindt. Zodra het uiteindelijke adres van de .data-sectie gekend is, wordt
het adres van de variabele berekend en weggeschreven op de positie van de operand. In de
relocatie-informatie wordt de positie van deze operand ook voorgesteld als een offset binnen
een sectie. Relocaties komen niet enkel voor tussen secties, een relocatie kan ook wijzen van
een plaats binnen een sectie naar een symbool. Het adres van het symbool – dat gevonden
moet worden door de linker – wordt dan weggeschreven op de plaats binnen de sectie. Op
Figuur 2.2 wijst de onderste relocatie naar een symbool.
2.1.3
Het linkerproces
In deze sectie wordt het linkerproces voorgesteld zoals het verloopt indien er statisch gelinkt
wordt. Tijdens het linkerproces worden alle objectbestanden samengevoegd tot een uitvoerbaar bestand, en worden de uiteindelijke adressen berekend. De eerste fase in dit proces is die
van symboolresolutie. Voor alle in objectbestanden ge¨ımporteerde symbolen moeten er definities gevonden worden. Een definitie specificeert een bepaalde offset binnen een bepaalde
sectie waarop een symbool gevonden kan worden. Indien een symbool door een bepaald objectbestand ge¨ımporteerd wordt maar nergens – of meerdere keren – gedefinieerd wordt, leidt
8
dit tot een linkerfout.
Alle gelijknamige secties uit de objectbestanden worden samengevoegd zodat er nog maar
´e´en .text-sectie, ´e´en .data-sectie etc. overblijft. Naast het uitvoerbaar bestand kan de linker
ook nog de zogenaamde linker map genereren. Hierin valt onder meer te vinden op welk adres
binnen de resulterende secties de oorspronkelijke secties (of subsecties) uit de objectbestanden
geplaatst zijn.
Daarna worden de eigenlijke relocaties uitgevoerd. De adressen worden berekend en weggeschreven, ook voor ge¨ımporteerde symbolen. Tijdens de symboolresolutie zijn alle relocaties
die naar een symbool wijzen namelijk al vertaald naar relocaties die naar een offset binnen
een sectie wijzen.
Het eindresultaat van dit proces is een uitvoerbaar bestand waarin alle symbooladressen
berekend zijn en waaruit de symboolinformatie dus kan verwijderd worden. Zoals gezegd is
dit alles enkel van toepassing indien we volledig statisch linken. Indien we dynamisch linken
is het namelijk nog mogelijk om tijdens de uitvoering symbolen op te zoeken.
2.1.4
Statisch linken versus dynamisch linken
De procedure die in Sectie 2.1.3 werd besproken is die van het statisch linken. Een volledig
statisch gelinkt uitvoerbaar bestand moet geen symboolinformatie meer bevatten omdat alle
symbolen reeds gevonden zijn, en het is niet afhankelijk van andere componenten (uitgezonderd de kernel). Het uitvoerbaar bestand kan echter ook dynamisch gelinkt zijn. In dit geval
bevat het wel nog symboolinformatie – de zogeheten meta-informatie – die het de applicatie
mogelijk maakt om tijdens de uitvoering (of “dynamisch”) symbolen op te zoeken in dynamische bibliotheken en deze te gebruiken. Deze dynamische bibliotheken worden bij het
opstarten van de applicatie in de adresruimte van de applicatie geladen. Als er andere applicaties opstarten die gebruik maken van eenzelfde bibliotheek wordt deze in alle gerelateerde
adresruimtes geladen op copy-on-write pagina’s. Dit wil zeggen dat alle processen eenzelfde
fysieke geheugenpagina delen, tenzij ze er zelf aanpassingen op aanbrengen. Zodra een proces
een copy-on-write pagina beschrijft, wordt deze gekopieerd naar een pagina die enkel door
dat specifieke proces gebruikt wordt.
Het gebruik van dynamisch linken heeft zijn voordelen. Het voornaamste voordeel is op
vlak van software engineering. Delen van een applicatie kunnen op een gemakkelijke wijze
hergebruikt worden in een andere applicatie (zelfs delen die door iemand anders ontwikkeld
zijn). Als meerdere applicaties eenzelfde dynamisch bibliotheek gebruiken is er maar ´e´en
fysieke kopie van die bibliotheek aanwezig op de harde schijf, en bij het uitvoeren meestal
maar ´e´en kopie in het fysieke geheugen. Het eigenlijke uitvoerbaar bestand is ook kleiner
aangezien er meer code en data aanwezig is in dynamische bibliotheken, en deze bibliotheken
kunnen ge¨
updatet worden zonder dat er iets moet veranderen aan het uitvoerbaar bestand.
Er zijn echter ook nadelen aan het gebruik van dynamisch linken verbonden. Dynamische
gelinkte applicaties presteren iets slechter vanwege indirectie (voor het specifieke geval van
9
Windows wordt dit uitgelegd in Sectie 2.5.2, en verschillende versies van eenzelfde dynamische
bibliotheek kunnen leiden tot compatibiliteitsproblemen (e.g. DLL hell [3]). Daarnaast is er nu
meta-informatie aanwezig in de bestanden die gebruikt kan worden bij statische analyse van
de applicatie. Bovendien is het vertrouwen op componenten die dynamisch gevonden worden
gevaarlijk. Dit maakt namelijk een nieuwe klasse aan aanvallen mogelijk die gebruikt kunnen
worden bij dynamische analyse. Voor deze scriptie zijn vooral deze twee laatste nadelen van
belang.
2.2
Diablo
De linker is de eerste component in het buildproces van een applicatie die het overzicht
heeft over het volledige uitvoerbare bestand. In het bestand dat de linker produceert is de
relocatie-informatie die in het linkerproces gebruikt wordt niet meer aanwezig. De symboolinformatie kan die niet met dynamisch symbolen te maken heeft kan ook verwijderd zijn. Als
we uitvoerbare bestanden willen herschrijven door enkel gebruik te maken van de uitvoerbare
bestanden zelf, beschikken we dus over minder informatie. Bijgevolg is een tool die op linkerniveau werkt in de beste positie om over de grenzen van de oorspronkelijke objectbestanden
heen optimalisaties toe te passen en uitvoerbare bestanden te herschrijven. Een voorbeeld
hiervan is het Diablo-raamwerk, dat in deze scriptie gebruikt wordt [4].
2.2.1
Werking
De eerste stap die Diablo onderneemt bij het herschrijven van een uitvoerbaar bestand is
het linkerproces emuleren. Hiervoor wordt gebruik gemaakt van de objectbestanden die als
invoer dienden bij het oorspronkelijke linkerproces en van de linker map die bij dit proces
gegenereerd werd (zie Sectie 2.1.3). Tijdens het emuleren wordt er een parent-object (dat
een voorstelling van het uitvoerbaar bestand vormt) opgebouwd uit de objectbestanden. Dit
parent-object bestaat uit parent-secties die opgebouwd zijn uit secties afkomstig van de verschillende objectbestanden (zogenaamde subsecties). De symbool- en relocatie-informatie uit
de objectbestanden wordt gebruikt om de adressen binnen het parent-object te berekenen, en
vervolgens door Diablo in datastructuren bijgehouden. Uiteindelijk wordt er gecontroleerd of
het resultaat van het ge¨emuleerde linkerproces overeenkomt met het te herschrijven bestand.
Vervolgens worden alle .text-secties in het parent-object gedisassembleerd en wordt er een
controleverloopgraaf opgesteld op basis van de gedisassembleerde instructies en de relocatieinformatie. Deze controleverloopgraaf is een gerichte graaf met knopen en pijlen. Aan de hand
van de relocatie-informatie wordt er ook een gerichte graaf opgebouwd die de afhankelijkheden tussen de verschillende data-secties voorstelt. Deze twee grafen samen noemen we de
aangevulde controleverloopgraaf of ACVG, die als een representatie van de applicatie dient.
Het is deze representatie die we zullen aanpassen om de applicatie te herschrijven. Diablo
zal dan op basis van de aangepaste representatie een nieuw, herschreven uitvoerbaar bestand
genereren.
De controleverloopgraaf opgebouwd door Diablo bevat als knopen basisblokken of BBL’s.
Een BBL is een blok van instructies die altijd samen uitgevoerd worden. Dit impliceert dat
10
Figuur 2.3: Een voorbeeld van een deel van een controleverloopgraaf.
enkel de eerste instructie in het blok het doel kan zijn van een controleverloopinstructie, en
dat enkel de laatste instructie in het blok een controleverloopinstructie kan zijn. Het BBL
dat het beginpunt van de applicatie vormt, wordt de beginknoop van de controleverloopgraaf
genoemd.
De pijlen in de graaf stellen het controleverloop voor. Zo zijn er sprong-pijlen die een sprong
van ´e´en BBL naar een andere BBL voorstellen. Een conditionele sprong wordt voorgesteld
door twee pijlen die vertrekken vanuit het BBL: een sprong-pijl voor het geval waarin de
sprong genomen wordt en een doorval-pijl voor het geval waarin de sprong niet genomen
wordt (het doorvalpad). Een functieoproep wordt voorgesteld door een call-pijl die van een
BBL met als laatste instructie een functieoproep naar het eerste BBL van de opgeroepen
functie gaat. Deze call-pijl heeft een corresponderende return-pijl die van het laatste BBL
van de functie naar het BBL volgende op die met de functieoproep gaat. Figuur 2.3 toont
een deel van een controleverloopgraaf. De groene pijlen zijn sprong-pijlen, de zwarte zijn
doorval-pijlen en de rode is een call-pijl.
Indien het doel van een controleverloopinstructie een register of een adres opgeslagen in het
geheugen is, dan spreken we over indirect controleverloop. Het uiteindelijke doeladres (en
bijhorende BBL) kan in dat geval niet altijd door Diablo bepaald worden. Dit wordt binnen
de controleverloopgraaf gemodelleerd d.m.v. een helleknoop. In deze knoop komen pijlen
toe vanuit alle BBL’s die resulteren in indirect controleverloop, en er gaan pijlen vanuit de
helleknoop naar alle BBL’s die een doel kunnen zijn van indirect controleverloop.
11
Eens de controleverloopgraaf opgebouwd is, wordt deze onderverdeeld in functies. Deze functies zijn groepen van BBL’s die ruwweg overeenkomen met de oorspronkelijke procedures in
de applicaties. Het onderverdelen in functies gebeurt door de controleverloopgraaf te overlopen (beginnende vanaf het beginpunt van de applicatie) en te zoeken naar call-pijlen.
2.2.2
Structuur
Diablo kan bestanden herschrijven die bedoeld zijn voor verschillende platformen en verschillende architecturen. Om dit mogelijk te maken bestaat het raamwerk uit een algemeen
gedeelte en een aantal platform- en architectuur-specifieke backends. Er bestaan verschillende
formaten voor uitvoerbare bestanden en objectbestanden, zoals het PE-formaat gebruikt op
Windows en het ELF-formaat gebruikt op Linux. Voor het inlezen van deze bestanden bezit
Diablo dus een PE-backend en een ELF-backend. Het emuleren van het linkerproces gebeurt
gebeurt bijvoorbeeld grotendeels in algemene code, maar om de .text-secties te disassembleren wordt er gebruik gemaakt van architectuur-specifieke backends. In deze scriptie wordt er
gebruik gemaakt van de i386 -backend (i386 is een andere naam voor x86).
Op dit raamwerk kunnen er applicaties gebouwd worden. Een voorstelling van zo’n Diablogebaseerde applicatie met het Diablo-raamwerk en zijn backends valt te zien op Figuur 2.4.
In het kader van deze masterproef heb ik een Diablo-gebaseerde applicatie geschreven.
2.2.3
Toepassingen
Diablo is doorheen de jaren voor veel verschillende toepassingen gebruikt, enkele hiervan
worden kort besproken.
Een eerste toepassing is compactie na het linken, waarbij Diablo gebruikt wordt om uitvoerbare bestanden te verkleinen [4][5][6]. Voor een doorsnee uitvoerbaar bestand zijn er in
de door Diablo opgebouwde ACVG knopen aanwezig die niet bereikbaar zijn vanaf de beginknoop. Elke onbereikbare knoop die een BBL is, bestaat uit instructies die niet bereikbaar
zijn vanuit het beginpunt en dus nooit uitgevoerd kunnen worden. Elke onbereikbare knoop
die een subsectie is, bestaat uit data die tijdens de uitvoering van de applicatie onmogelijk gebruikt kan worden. Onbereikbare knopen kunnen dus zonder problemen door Diablo
verwijderd worden om een kleiner uitvoerbaar bestand te bekomen. We noemen dit het elimineren van onbereikbare code en data. Er zijn in Diablo nog verdere optimalisaties en analyses
ge¨ımplementeerd die het mogelijk maken een uitvoerbaar bestand nog meer te verkleinen,
maar die zijn voor deze scriptie niet van belang [5][6].
Uitvoerbare bestanden kunnen ook herschreven worden met het oog op beveiliging. Zo is
Diablo gebruikt om herschreven uitvoerbare bestanden te genereren die zichzelf dynamisch
aanpassen [7]. Deze techniek zorgt ervoor dat een bepaald geheugenbereik meerdere, verschillende code-sequenties bevat doorheen de uitvoering van de applicatie. Op deze manier wordt
het moeilijker voor een potenti¨ele aanvaller om aan reverse engineering te doen. Diablo wordt
ook gebruikt om aan code-diversificatie te doen [8]. Het doel is hier om patch-gebaseerde exploits tegen te gaan door – op basis van eenzelfde broncode – uitvoerbare bestanden te
genereren die op vlak van uitvoerbare code sterk verschillen. Om dit bereiken wordt on12
Figuur 2.4: De structuur van een Diablo-gebaseerde applicatie[4].
der meer de controleverloopgraaf sterk aangepast en wordt direct controleverloop verborgen
achter indirect controleverloop.
2.3
Disassembleren
Als we willen weten welke instructies er in een .text-sectie zitten moeten we deze disassembleren. Dit kan op twee manieren: lineair of recursief, met elk zijn eigen voor- en nadelen [9].
2.3.1
Lineair disassembleren
Een eerste manier om een .text-sectie te disassembleren is lineair disassembleren. Dit houdt
in dat er, vanaf het beginadres van de sectie, instructie per instructie gedisassembleerd wordt.
Op het beginadres van de sectie disassembleren we de eerste instructie en determineren zijn
lengte. Zo krijgen we het adres van de volgende instructie, die op zijn beurt disassembleren
enzoverder. De aard van de gedisassembleerde instructies is hierbij niet van belang.
13
Figuur 2.5: Een voorbeeld van het lineair disassembleren van een .text-sectie waarin data
aanwezig is.
Deze manier van disassembleren is gemakkelijk te implementeren, maar komt in de problemen
indien er data aanwezig is in de .text-sectie. In Figuur 2.5 bijvoorbeeld wordt de functie
GeefVijfTerug ge¨exporteerd. Deze maakt gebruik van de interne functie GeefEenterug en
de gebruikte variabelen zijn als data tussen de twee functies in opgeslagen. Indien we hier
vanaf het begin van de sectie lineair beginnen te disassembleren, dan worden de data bytes als
instructiebytes aanzien en tot valse instructies gedisassembleerd. Dit zou geen groot probleem
vormen in het geval van een RISC-instructieset, waar alle instructies dezelfde lengte hebben.
De x86-architectuur heeft echter een CISC-instructieset waarbij instructies een verschillende
lengte hebben. Bij het disassembleren van de databytes kan het gebeuren dat er een instructie
gedisassembleerd wordt die zowel data- als echte instructiebytes bevat, met als gevolg dat de
daaropvolgende gedisassembleerde instructies niet overeenkomen met de eigenlijke instructies.
2.3.2
Recursief disassembleren
Bij recursief disassembleren beginnen we met disassembleren op adressen waarvan we zeker
weten dat er instructies op te vinden zijn, zoals het beginadres van een applicatie, het adres
van de initialisatieroutine van een bibliotheek, en de adressen van ge¨exporteerde functies.
Van op elk van deze adressen beginnen we met instructies lineair te disassembleren, met dat
verschil dat in het geval dat de net gedisassembleerde instructie een controleverloopinstructie
is, we het controleverloop zullen volgen. Op deze manier proberen we te voorkomen dat data
verkeerdelijk gedisassembleerd wordt tot instructies. In Figuur 2.6 zien we een voorbeeld van
recursief disassembleren. De enige ge¨exporteerde functie is GeefVijfTerug. Er wordt begonnen
met disassembleren op het adres van deze functie, het controleverloop wordt gevolgd, alle
instructies worden gedisassembleerd en de data niet.
Indien de uitvoerbare code geobfusceerd is, is het wel mogelijk dat er data verkeerdelijk gedisassembleerd wordt [10]. Er kunnen bijvoorbeeld conditionele sprongen ge¨ıntroduceerd worden
die eigenlijk altijd genomen of nooit genomen worden. Als het beginadres van het pad dat
eigenlijk nooit genomen wordt data bevat, blijft de applicatie correct uitvoeren maar wordt er
14
Figuur 2.6: Een voorbeeld van het recursief disassembleren van een .text-sectie, met stappen
genummerd in volgorde.
verkeerdelijk data gedisassembleerd. Net zoals bij het opstellen van een controleverloopgraaf
vormt indirect controleverloop een struikelblok voor het recursief disassembleren. Indien we
bijvoorbeeld een ‘call eax’-instructie tegenkomen dan kan het controleverloop meestal niet
gevolgd worden (tenzij eventueel via constantenpropagatie). Van alle adressen waarop er geen
instructies gedisassembleerd zijn, wordt er verwacht dat deze data bevatten. Vanwege indirect
controleverloop worden er bij recursief disassembleren dus veel instructies niet gedisassembleerd en beschouwd als data. In principe zou een methode die voor alle mogelijke .text-secties
de aanwezige code en data perfect kan onderscheiden ook in staat zijn het stopprobleem op
te lossen. Bijgevolg kunnen we onmogelijk zo’n methode construeren [11].
2.3.3
IDA Pro
IDA Pro (Interactive Disassembler Professional ) is een populaire, recursieve disassembler die
m.b.v. een aantal bijkomende technieken een zeer goede nauwkeurigheid in het onderscheiden
van code en data haalt [12]. E´en van deze technieken is Fast Library Identification and
Recognition Technology of FLIRT. Dit houdt in dat statisch gelinkte code die afkomstig is
uit bibliotheken gemakkelijk herkend wordt a.d.h.v. een databank met signaturen van gekende
functies. Het herkennen van deze reeds gekende functies beperkt het werk dat IDA levert bij
het disassembleren van een uitvoerbaar bestand, en vergemakkelijkt ook het werk van een
reverse engineer. De werking van deze gekende functies is namelijk duidelijk uitgelegd in
specificaties en handleidingen. Reverse engineering is dus niet meer nodig. Daarnaast wordt
er ook gebruik gemaakt van heuristieken om pointers naar code te herkennen [12]. Op deze
wijze kunnen ook bepaalde instructies die enkel via indirect controleverloop bereikbaar zijn
gedisassembleerd worden.
IDA Pro maakt ook gebruik van PDB -bestanden (Program DataBase). Een PDB-bestand
bevat debugging-informatie voor een specifiek PE-bestand, zoals onge¨exporteerde symbolen.
Voor DLL’s van Microsoft-makelij zijn de bijhorende PDB-bestanden online te vinden. IDA
download deze automatisch en gebruikt ze om tot een beter resultaat te komen.
15
2.4
Reverse engineering
Reverse engineering van software is het proberen te begrijpen hoe een applicatie op een hoger niveau werkt [13]. Om dit te doen beschikt een reverse engineer doorgaans enkel over
het uitvoerbaar bestand en de verwante dynamische bibliotheken, beiden bestaande uit uitvoerbare code en data. Er zijn verschillende redenen om aan reverse engineering te doen,
zowel goedaardig als kwaadaardig. De ontwikkelaars van de applicatie kunnen proberen hun
eigen verloren gegane broncode te reconstrueren, of een aanvaller kan een poging wagen om
beschermde algoritmes of datastructuren aanwezig in een uitvoerbaar bestand te kopi¨eren
en te gebruiken voor zijn eigen doeleinden. Antivirus-software kan proberen te achterhalen
of een uitvoerbaar bestand kwaadaardige doeleinden heeft. Verder zijn er tools die bugs in
applicaties opsporen zodat deze gefixed kunnen worden, maar aanvallers kunnen deze zwakheden (en een verbeterd begrip van de werking van de applicatie) omzetten in een succesvolle
exploit.
Reverse engineering kan op twee manieren: statische analyse en dynamische analyse.
2.4.1
Statische analyse
Bij statische analyse wordt een applicatie geanalyseerd zonder deze ook daadwerkelijk uit
te voeren. Er wordt dus gekeken naar de uitvoerbare code in het uitvoerbaar bestand. Deze
wordt gedisassembleerd en er wordt een controleverloopgraaf opgesteld. De resulterende statische representatie van de applicatie wordt gebruikt om – met de hand of automatisch –
de applicatie te analyseren. Hierbij komt symboolinformatie aanwezig in het bestand (om
dynamische symbolen te gebruiken of om debuggen gemakkelijker te maken) van pas. Het
gebruik van dynamische symbolen (meta-informatie) in een functie helpt een reverse engineer
deze functie te begrijpen indien de betekenis van dit symbool gekend is. Aangezien symbolen
namen van functies of variabelen zijn, kan symboolinformatie op zich al veelzeggend zijn.
Het is bijvoorbeeld duidelijk wat een oproep naar de functie ‘CreateFile’ doet. Doordat de
interface aangeboden door een dynamische bibliotheek vast staat, zal een oproep naar deze
functie vanuit eender welk bestand in de toekomst ook altijd hetzelfde blijven doen.
Statische analyse heeft echter zijn beperkingen als het gaat om begrijpen wat een bepaald
stuk code doet. Virusscanners kunnen malware bijvoorbeeld detecteren door in uitvoerbare
code patronen te herkennen die geassocieerd zijn met gekende malware [14], maar deze methode kan niet overweg met polymorfisme. Polymorfisme houdt in dat er vele verschillende
versies van ´e´en virus worden gegenereerd die allen hetzelfde gedrag hebben, maar syntactisch
verschillen [15]. Daarnaast gaat statische analyse er ook van uit dat het statische en dynamische beeld van een applicatie overeenkomen, wat niet per se zo is. De uitvoerbare code in
een uitvoerbaar bestand kan dynamisch aangepast worden [7], en zowel het disassembleren
van uitvoerbare code als het opstellen van een controleverloopgraaf kan bemoeilijkt worden
d.m.v. obfuscatie [16] [10]. Een goede reverse engineer maakt dus ook gebruik van dynamische
analyse.
16
2.4.2
Dynamische analyse
Dynamische analyse houdt in dat we een applicatie analyseren terwijl ze uitvoert. Dit kan
nieuwe inzichten verschaffen en een aantal problemen waar statische analyse last van heeft
oplossen, maar het is in het algemeen ook meer werk. Malware kan bijvoorbeeld statische
analyse bemoeilijken, maar door ze in een beschermde omgeving uit te voeren kan de malware
wel gemakkelijk dynamisch geanalyseerd worden [17][18]. In tegenstelling tot statische analyse
is de vergaarde informatie wel enkel geldig voor een specifieke uitvoering van de applicatie,
en ze levert dus geen volledig beeld van de applicatie [19].
Een techniek die vaak gebruikt wordt tijdens dynamische analyse is API-hooking [20]. Omdat
de interface aangeboden door een dynamische bibliotheek gekend is, kan een reverse engineer
zelf een bibliotheek maken die – een deel van – dezelfde interface aanbiedt. De reverse engineer
kan er dan voor zorgen dat zijn eigen bibliotheek gebruikt wordt in plaats van de oorspronkelijke bibliotheek, en kan zo op een gemakkelijk manier zijn eigen code laten uitvoeren in
een applicatie. De geplaatste ‘hooks’ laten ook toe dynamisch het gebruik van functies uit
bibliotheken waar te nemen (op het einde van de geplaatste hook moeten de echte functies
dan ook opgeroepen worden om een correct verloop van de applicatie te verzekeren).
2.4.3
Bestaande oplossingen
Als we een applicatie willen beschermen tegen reverse engineering kunnen we gebruik maken
van obfuscatie [21]. Een ontwikkelaar kan echter enkel die componenten die hij zelf levert
obfusceren. Obfuscatie is dus geen volledige oplossing voor een dynamisch gelinkte applicatie,
doordat deze nog steeds kwetsbaar is aan de interfaces (vanwege de meta-informatie en APIhooking). Een logische oplossing is dus om het aantal interfaces te verkleinen, door minder
dynamische bibliotheken te gebruiken en meer statisch te linken. Het liefst zouden we volledig
statisch gelinkte applicaties gebruiken, maar die zijn niet compatibel met meerdere versies
van Windows (zie Sectie 2.7).
Prelinking is een proces op Linux waarbij de indeling van de adresruimte van een proces al
statisch (voor de uitvoering) vastgelegd wordt [22]. De adressen van de dynamische bibliotheken (en hun ge¨exporteerde symbolen) binnen de adresruimte van het proces liggen al
vast en kunnen dus op de juiste plaatsen weggeschreven worden. De applicatie zal bijgevolg
sneller opstarten omdat deze adressen niet meer dynamisch berekend moeten worden. Indien
sommige adressen niet up-to-date zijn (omdat een dynamische bibliotheek veranderd is t.o.v.
diegene waartegen er gelinkt werd) worden deze opnieuw berekend. Import Binding is een
gelijkaardig proces op Windows [23]. Deze processen zorgen ervoor dat dynamisch gelinkte
applicaties sneller kunnen opstarten. Ze vormen wel geen beveiliging tegen reverse engineering
aangezien de adressen toch nog dynamisch berekend worden indien een bepaalde bibliotheek
veranderd is.
Slinky is een systeem voorgesteld door Collberg et al. dat de voordelen van statisch en
dynamisch linken met elkaar combineert [24]. Alle uitvoerbare bestanden zijn volledig statisch
gelinkt, maar gemeenschappelijke delen worden op de schijf en in het geheugen gedeeld. Dit
gebeurt door voor elke codepagina een digest te genereren die deze pagina uniek identificeert.
Aan de hand van deze digest wordt bij het installeren van een nieuwe applicatie gekeken of
17
deze codepagina reeds op het systeem aanwezig is, en bij het opstarten van de applicatie of
de codepagina al in het geheugen geladen is. Aangezien de applicatie volledig statisch gelinkt
is, is het beveiligingsprobleem dat deze scriptie poogt op te lossen ook verdwenen. Slinky is
echter enkel een oplossing op Linux. Volledig statisch gelinkte applicaties zijn namelijk niet
compatibel met alle versies van Windows wegens de instabiele kernel-interface. Daarnaast
vereist Slinky ook een aantal aanpassingen in de kernel, iets wat in het geval van Windows
om duidelijke redenen niet mogelijk is in deze scriptie.
Er bestaan een aantal tools voor Windows zoals PEBundle [25], MoleBox [26] en DLLPackager [27] die simpelweg de dynamische bibliotheken waarvan het uitvoerbaar bestand
afhankelijk is samen met dit bestand mee inpakken. Er wordt dan nog extra code aan het
uitvoerbaar bestand toegevoegd om dynamisch deze bibliotheken weer uit te pakken en de
nodige symbolen op te zoeken. De resulterende applicatie is niet echt statisch gelinkt en
hoewel ze minder meta-informatie bevat is ze nog steeds vatbaar voor API-hooking. Deze
oplossingen zullen ook nooit de de systeembibliotheken aan het uitvoerbaar bestand toevoegen, en de interface tussen de systeembibliotheken en de rest van de applicatie zal dus blijven
bestaan.
2.5
Het PE-formaat
Windows gebruikt voor uitvoerbare bestanden het PE-formaat, waarbij de PE staat voor
Portable Executable. In deze sectie worden een aantal eigenschappen van het PE-formaat
uitgelegd die van belang zijn in deze scriptie. Net zoals objectbestanden bestaat een bestand
van dit formaat uit headers die de rest van het bestand beschrijven en uit een aantal secties.
De secties die gebruikt worden, zijn doorgaans dezelfde als diegene die in objectbestanden
gebruikt worden (met dezelfde eigenschappen). Er zijn ook een aantal extra mogelijke secties zoals .edata (voor ge¨exporteerde symbolen), .idata (voor ge¨ımporteerde symbolen) en
.reloc. Voor meer informatie dan in deze sectie gegeven wordt, zie de ‘Microsoft PE/COFF
Specification’ [28].
2.5.1
Dynamisch linken
Het PE-formaat biedt ondersteuning voor dynamisch linken. Het formaat wordt namelijk niet
enkel gebruikt voor uitvoerbare bestanden (exe’s) maar ook voor dynamische bibliotheken
(Dynamic-Link Libraries of DLL’s). Een PE-bestand kan symbolen exporteren en importeren, het exporteert symbolen die in het bestand gedefinieerd zijn zodat andere bestanden
ze kunnen gebruiken, en importeert symbolen uit andere PE-bestanden die deze exporteren. Zowel uitvoerbare bestanden als DLL’s importeren meestal symbolen, maar enkel DLL’s
exporteren doorgaans symbolen.
De meta-informatie aanwezig in het formaat die het mogelijk maakt om dynamisch te linken
vinden we in de export- en importtabellen. De exporttabellen bevatten de symbolen die de
DLL exporteert met hun adres relatief t.o.v. de base (dit adres noemen we een Relative Virtual
Address, of RVA). De base (of het basisadres) van een PE-bestand is het adres waarop het
in het geheugen geladen wordt. Dit adres is niet altijd hetzelfde. De importtabellen bevatten
18
Figuur 2.7: Een voorbeeld van de importtabellen van een PE-bestand.
Figuur 2.8: Een voorbeeld van de exporttabellen van een PE-bestand.
de namen van de DLL’s waaruit er symbolen ge¨ımporteerd worden (de exporterende DLL),
en per ge¨ımporteerde DLL is er een lijst van symbolen die er uit ge¨ımporteerd worden.
Een deel van de importtabellen valt te zien in het voorbeeld op Figuur 2.7. Veel van de voor
deze uitleg niet relevante velden zijn niet aanwezig op de figuur. Per ge¨ımporteerde DLL is
er een Import Descriptor aanwezig in de tabellen. In het voorbeeld wordt enkel de Import
Descriptor voor user32.dll getoond. De Import Lookup Table (of ILT ) bestaat uit RVA’s naar
de informatie die we nodig hebben om symbolen te importeren. De uit user32 ge¨ımporteerde
symbolen zijn GetMessage, LoadIcon en TranslateMessage. Op de figuur is ook de Import
Address Table (of IAT ) aanwezig, waarvan we het nut in Sectie 2.5.2 wordt uitgelegd.
Het importeren van een symbool kan op twee manieren: via naam (zoals in het voorbeeld)
of via ordinaal. Indien een symbool ge¨ımporteerd wordt via naam wordt deze naam in de
importtabellen bijgehouden. Om het adres van een symbool te berekenen wordt deze naam
opgezocht binnen de Export Name Table of ENT van de juiste DLL. Deze tabel bevat een
alfabetisch gesorteerde lijst van ge¨exporteerde symboolnamen. Eens deze naam gevonden
is, wordt de index ervan binnen de ENT gebruikt als een index in de ordinaaltabel. Op deze
index in de ordinaaltabel vinden we dan de ordinaal die gebruikt wordt als index in de Export
Address Table of EAT. Deze tabel bestaat uit een reeks RVA’s (t.o.v. het basisadres van de
exporterende DLL), ´e´en voor elk ge¨exporteerd symbool. De gevonden RVA, opgeteld met het
basisadres van de exporterende DLL levert het symbooladres. Een voorbeeld van een deel
van de exporttabellen valt te zien op Figuur 2.8. De procedure om het adres van het symbool
LoadIcon te berekenen wordt hier voorgesteld a.d.h.v. de rode genummerde pijlen.
19
Figuur 2.9: Een voorbeeld van een dynamisch gelinkt PE-bestand.
Bij import via ordinaal dient de ordinaal (die in de importtabellen bijgehouden wordt) direct
als index in de EAT. Op deze manier wordt het symbooladres dus iets sneller gevonden. Als
er echter een nieuwe versie van de DLL gemaakt wordt, kan het zijn dat de index van het
symbool in de EAT verandert. Import via ordinaal is dus niet even toekomstbestendig als
import via naam en wordt nauwelijks gebruikt (voornamelijk in de systeembibliotheken).
Een andere eigenschap van het PE-formaat die quasi enkel in de systeembibliotheken gebruikt
wordt is export forwarding (of export-doorverwijzing). Als een DLL een export doorverwijst
wil dit zeggen dat het symbool wel ge¨exporteerd wordt door de DLL, maar niet echt aanwezig is in de DLL. Het symbool is een alias voor een ander symbool in een andere DLL,
en de informatie om dit symbool in de andere DLL op te zoeken is aanwezig in de eerste DLL in plaats van het symbool zelf. Deze informatie vind men dan in een string van
de vorm “DLLNAAM.FunctieNaam”. Het door kernel32 ge¨exporteerde symbool HeapAlloc
wordt bijvoorbeeld doorverwezen naar “NTDLL.RtlAllocateHeap”.
2.5.2
Laden
PE-bestanden worden in het geheugen geladen door de loader, die deel uitmaakt van het
besturingssysteem. Niet alleen het PE-bestand zelf maar ook de DLL’s waaruit het symbolen
importeert, worden in het geheugen geladen en de adressen van de ge¨ımporteerde symbolen
worden berekend. De ge¨ımporteerde DLL’s kunnen natuurlijk zelf ook symbolen importeren
zodat nog andere DLL’s waarvan zij afhankelijk zijn ook in het geheugen geladen zullen
worden, enzoverder. Eens een PE-bestand geladen is wordt het beginpunt (indien aanwezig)
aangeroepen. Bij een uitvoerbaar bestand is dit het begin van de uitvoering, bij een DLL is
dit een initialisatieroutine.
De adressen van de ge¨ımporteerde symbolen worden bij het laden van het PE-bestand gevonden en weggeschreven in de IAT (zie Figuur 2.7). De IAT bevatte van tevoren dezelfde RVA’s
als de ILT (zie Sectie 2.5.1). Deze worden dus overschreven met de eigenlijke adressen van de
symbolen. Alle instructies die gebruik maken van een ge¨ımporteerd symbool hebben als ´e´en
van hun operanden een locatie in de IAT (zie als voorbeeld Figuur 2.9). Een operand kan op
20
indirecte wijze gebruikt worden. Het operand zelf wordt dan niet als een waarde beschouwd,
maar als het adres van een waarde. Het is deze waarde die door de instructie gebruikt wordt.
Alle instructies die een ge¨ımporteerd symbool gebruiken, maken dus indirect gebruik van een
operand.
De IAT is een veelgebruikte vector voor aanvallen. De adressen van ge¨ımporteerde functies
kunnen namelijk gemakkelijk overschreven worden. Deze techniek staat bekend als IAThooking, wat een variatie is op API-hooking [29]. Het adres van de echte ge¨ımporteerde
functie in de IAT wordt overschreven met het adres van een door een aanvaller geschreven
functie. Het overschrijven gebeurt door ge¨ınjecteerde code (bv. via DLL injection [30]) of
vanuit een ander proces.
2.5.3
Rebasing
Een laatste eigenschap van het PE-formaat die we moeten aanhalen, is dat bestanden in
het formaat rebaseable zijn. PE-bestanden bevatten absolute geheugenadressen die berekend
zijn met de veronderstelling dat het bestand op een specifiek adres in het geheugen geladen
wordt (de base, of het basisadres). Deze absolute adressen worden onder meer gebruikt om
naar data op een bepaald adres te refereren. Bij een uitvoerbaar bestand kan ervan uitgegaan
worden dat het basisadres waarop het bestand berekend is ook datgene is waarop het geladen
wordt (meestal 0x400000). Bij een DLL is dit niet per se het geval.
Het is gemakkelijk mogelijk dat in ´e´en van de adresruimtes van de applicaties waarin de DLL
gebruikt wordt het basisadres reeds gebruikt wordt door een andere DLL. In dit geval moeten
alle absolute adressen in de DLL herberekend worden. Dit proces heet rebasing. Om dit te
kunnen doen wordt er een lijst met RVA’s voor de locaties van alle absolute adressen binnen
het bestand bijgehouden in de .reloc-sectie (zogenaamde base relocaties).
In het voorbeeld op Figuur 2.10 maakt een inc-instructie (gelegen in de .text-sectie) gebruik
van een statisch gealloceerde variabele (gelegen in de .data-sectie). De DLL is berekend op
0x6000000 als basisadres. De inc-instructie maakt gebruik van een absoluut adres voor de
variabele (0x6002008) voor rebasing), en er is een base relocatie die naar het adres van
dit absoluut adres wijst. Indien het adres 0x6000000 bezet is en de daarom op basisadres
0x7000000 geladen wordt, moet er aan rebasing gedaan worden. Het absoluut adres van de
variabele wordt dan herberekend (als 0x7002008) en het oude adres in de inc-instructie wordt
overschreven.
DLL’s kunnen – na rebasing – op eender welk geheugenadres geladen worden. De mogelijkheid om een component (ook uitvoerbare bestanden) op eender welk geheugenadres te laden
heeft ook een voordeel op beveiligingsvlak. Als namelijk de adresruimte van een proces willekeurig ingedeeld is dan wordt het voor een aanvaller moeilijker om een exploit te schrijven
die het proces op een effectieve manier aanvalt [31][32]. Met een willekeurige indeling willen
we zeggen dat de DLL’s en het uitvoerbaar bestand op een willekeurig basisadres geladen
worden voor elke instantie van de applicatie. We noemen deze techniek Address Space Layout
Randomization of ASLR en base relocaties maken deze techniek mogelijk op Windows [33].
21
Figuur 2.10: Een voorbeeld van rebasing.
Om op andere platformen zoals Linux (waarop ASLR ook gebruik wordt) het laden van
een component op een willekeurig geheugenadres toe te laten wordt er gebruik gemaakt van
Position Independent Code of PIC [2]. Door een extra indirectie toe te voegen kunnen uitvoerbare bestanden en dynamische bibliotheken daar op eender welk geheugenadres geladen
worden zonder dat er een extra proces zoals rebasing aan te pas komt. De code voert zonder
probleem uit op elk adres, maar presteert iets slechter vanwege de indirectie.
2.6
De Windows-systeembibliotheken
De systeembibliotheken zijn een onderdeel van het Windows-besturingssysteem en verschillen
qua inhoud (en soms ook qua structuur) tussen de verschillende versies. Ze bieden wel een
stabiele interface aan voor gebruik door applicaties, maar weinig van wat zich achter deze
interface bevindt is officieel gedocumenteerd en/of gegarandeerd stabiel. We zullen kort de
structuur en verschillende interfaces van de systeembibliotheken bespreken. Hierbij baseren
we ons op Windows 8/8.1.
De belangrijkste systeembibliotheken met hun onderlinge afhankelijkheden zijn te zien op
Figuur 2.11. Een bibliotheek is afhankelijk van een andere bibliotheek als ze symbolen importeert uit de andere bibliotheek. Dit wordt aangeduid met een pijl van de eerste bibliotheek
naar de tweede.
2.6.1
De verschillende API’s
In de eerste versie van Windows NT waren er subsystemen aanwezig voor drie gebruiksomgevingen: Windows, POSIX en OS/2. Dit gebruik van subsystemen maakte het mogelijk om
applicaties die bedoeld zijn voor verschillende gebruiksomgevingen op eenzelfde besturingssysteem uit te voeren. Hoewel er nog altijd een subsysteem voor Unix-gebaseerde applicaties
(SUA) bestaat is het Windows-subsysteem overduidelijk het belangrijkste. Bepaalde functionaliteit is enkel in het Windows-subsysteem ge¨ımplementeerd zodat andere subsystemen
deze enkel kunnen aanroepen via het Windows-subsysteem, en quasi alle applicaties voor het
22
Figuur 2.11: De belangrijkste Windows-systeembibliotheken met onderlinge afhankelijkheden.
Windows-besturingssysteem worden voor dit subsysteem geschreven. De API aangeboden
door dit subsysteem staat bekend als de Windows API (specifiek voor 32-bit applicaties de
Win32 API ), en is goed gedocumenteerd [33].
De symbolen ge¨exporteerd door kernel32, user32 en gdi32 zijn een onderdeel van de Windows API. Naast deze DLL’s zijn er nog veel meer (minder belangrijke) systeembibliotheken
wiens interfaces deel zijn van de Windows API. kernel32 biedt functionaliteit aan om gebruik te maken van typische kerneldiensten die te maken hebben met het bestandssysteem,
het beheren van processen en threads, geheugenbeheer et cetera. Het manipuleren van de
Windows gebruikersinterface (i.e. van menu’s, vensters e.d.) gebeurt via user32, terwijl gdi32
functionaliteit aanbiedt om uitvoer te genereren op grafische hardware (bv. een lijn tekenen).
Op een lager niveau vinden we de grotendeels ongedocumenteerde Native API [33][34][35].
Deze wordt gebruikt door de verschillende subsystemen om de kernel aan te spreken, en
door zogenaamde native applicaties die niet binnen een subsysteem uitvoeren (zoals bv.
drivers). De Native API kan aangeroepen worden vanuit zowel kernel- als user-modus. In
kernel-modus bestaat deze uit functies ge¨exporteerd door ntoskrnl.exe (de kernel-image), in
user-modus uit gelijknamige door ntdll ge¨exporteerde functies die eenvoudige wrappers zijn
rond systeemroepen naar de eigenlijke functies (ge¨ımplementeerd in de kernel). Naast de
Native API bevat ntdll loaderfunctionaliteit en algemene functionaliteit die door de subsystemen gebruikt wordt zoals een aantal functies uit de C-standaardbibliotheek en functies die
communicatie tussen de subsystemen mogelijk maakt. Aangezien ntdll zich architecturaal net
boven de kernel bevindt, importeert ze geen symbolen. Communicatie met de kernel verloopt
23
Figuur 2.12: Een aantal voorbeelden van het API-set-schema.
via systeemoproepen.
2.6.2
Het API-set-schema
Bij de introductie van Windows 7 en Windows 8 vond er een heuse reorganisatie van de
systeembibliotheken plaats. Omdat de door de applicaties gebruikte API stabiel moet blijven
is dit allemaal onder de oppervlakte gebeurd, en is dit niet echt door Microsoft gedocumenteerd. Er werd functionaliteit verplaatst tussen DLL’s, er kwamen DLL’s bij (zoals KernelBase, die functionaliteit bevat die eerder in kernel32 zat), en het API-set-schema werd
ge¨ıntroduceerd [36][37][38][39][40].
Het API-set-schema is een mechanisme dat voor een ontkoppeling zorgt tussen de plaats
waar een API gedefinieerd wordt en de plaats waar deze ge¨ımplementeerd wordt. Er wordt
gebruik gemaakt van API-sets, zogenaamde groepen van ge¨exporteerde functies met een
gemeenschappelijke functionaliteit (bv. alle functionaliteit die met tijd en data te maken
heeft). Voor elke API-set bestaat er een virtuele DLL die de functies waaruit de set bestaat
exporteert. Deze virtuele DLL’s hebben een naam die altijd begint met “api-” (bv. apims-win-core-datetime-l1-1-0 ). Deze virtuele DLL’s bestaan niet echt, en a.d.h.v. het APIset-schema wordt er dynamisch bepaald in welke DLL de eigenlijke implementatie van alle
functies uit de API-set aanwezig is (de implementatie-DLL of logische DLL). Het API-setschema zelf is aanwezig in een sectie van ApiSetSchema.dll, .apiset genaamd. In Figuur 2.12
links valt een voorbeeld te zien van een DLL die importeert uit api-ms-win-core-datetimel1-1-0, waarvan de bijhorende logische DLL kernel32 is. De echte DLL’s worden voorgesteld
met rechthoeken, de virtuele met ruiten. Een gestreepte pijl duidt het gebruik van het APIset-schema aan, een volle pijl de feitelijke afhankelijkheid die daaruit volgt.
Er is echter een uitzondering. In het geval dat DLL importeert uit een virtuele DLL waarvoor
de normale logische DLL dezelfde is als de importerende DLL, bevindt de implementatie zich
niet in de normale logische DLL maar in een andere DLL. Zo is er bijvoorbeeld de APIset api-ms-win-core-file-l1-1-0.dll waarvoor kernel32 normaliter de logische DLL is. Indien
kernel32 zelf echter uit deze API-set importeert, is de bijhorende logische DLL KernelBase.
Deze twee voorbeelden zijn ook te zien op Figuur 2.12.
24
Het gebruik van het API-set-schema (door te importeren uit virtuele DLL’s) gebeurt doorgaans enkel door DLL’s van Microsoft-makelij (de systeembibliotheken en anderen die hierop
bouwen).
2.7
De kernel-interface in Windows
De interface die de kernel aan applicaties aanbiedt noemen we de kernel-interface. Naast
deze interface biedt de kernel natuurlijk ook nog interfaces aan voor gebruik door andere
componenten uit de kernel-modus (zoals drivers), maar deze zijn niet van belang voor deze
scriptie. Net zoals een bibliotheekinterface bestaat uit ge¨exporteerde symbolen, bestaat de
kernel-interface uit de systeemoproepen die mogelijk zijn.
2.7.1
Systeemoproepen
Als een applicatie een geprivilegieerde actie wil ondernemen, moet deze aan de kernel vragen
dit voor haar te doen. Dit gebeurt via een systeemoproep. De systeembibliotheken bieden
simpele wrapper-functies aan als abstractie voor systeemoproepen. We noemen deze System
Call Wrappers of SCW’s. We nemen als voorbeeld van een geprivilegieerde actie het alloceren
van meer virtueel geheugen. In dit voorbeeld is de naam van de gebruikte SCW NtAllocateVirtualMemory. Het aanroepen van een SCW (en dus het uitvoeren van een systeemoproep)
leidt ertoe dat er in de kernel-modus een functie zal opgeroepen worden om de gevraagde
actie te ondernemen. Deze opgeroepen functie noemen we de system service [41][42]. System
services hebben dezelfde namen als de SCW’s die hun user-modus abstractie vormen, de in
het voorbeeld opgeroepen system service heet dus ook NtAllocateVirtualMemory.
Aangezien kernel en applicatie in verschillende modi uitvoeren, moet er bij een systeemoproep
van modus gewisseld worden. In vroegere tijden gebeurde dit op x86 d.m.v. een interruptinstructie, maar Intel en AMD hebben voor dit doeleind allebei een gespecialiseerde instructie
ontwikkeld (respectievelijk sysenter en syscall ) die gebruikt wordt op modernere versies van
Windows [33]. Welke manier er ook gebruikt wordt om van modus te wisselen, op een zeker
moment wordt de routine KiFastCallEntry in kernel-modus opgeroepen [43][44]. Deze routine
handelt de systeemoproep af door te beslissen welke system service er gevraagd is, en deze
uit te voeren.
Om ervoor te zorgen dat KiFastCallEntry kan beslissen welke system service er exact gevraagd wordt, wordt er net voor het uitvoeren van een systeemoproep een waarde in het
eax-register geplaatst (de System Call Ordinal of SCO). Dit is een 32-bits waarde, maar niet
al deze bits worden echt gebruikt. Figuur 2.13 toont de indeling van de bits in de SCO. Om
uit te kunnen leggen hoe een systeemoproep afgehandeld wordt (en hoe de SCO hierbij van
pas komt), leggen we eerst een aantal kernel-structuren uit.
Een System Service Dispatch Table of SSDT is een tabel bestaande uit adressen van system
services. De System Service Number of SSN (zie Figuur 2.13) wordt gebruikt als 12-bits index
in deze tabel om de gevraagde system service op te roepen [45][46]. Er zijn minstens twee
SSDT’s aanwezig in de kernel: ´e´en voor gewone system services en ´e´en voor grafische system
25
Figuur 2.13: De indeling van de SCO.
Figuur 2.14: De verschillende structuren die gebruikt worden bij het afhandelen van een
systeemoproep.
services. Deze laatste SSDT noemen we de Shadow SSDT, en de bijhorende system services
zijn niet ge¨ımplementeerd in de kernel-image maar in win32k.sys. Daarnaast is elke thread
geassocieerd aan een Service Descriptor Table of SDT. Deze bestaan uit (maximaal) vier
System Service Tables of SST’s. Een SST bestaat uit het adres van een SSDT en wat gerelateerde informatie (onder meer over de argumenten voor elke system service). Op Figuur 2.14
wordt een beeld van al deze structuren weergegeven.
Er zijn slechts twee mogelijke SDT’s: KeServiceDescriptorTable en KeServiceDescriptorTableShadow. De eerste wordt gebruikt door gewone threads (systeemthreads), de tweede door
threads die grafische functionaliteit gebruiken (GUI-threads). Van zodra een systeemthread
probeert grafische functionaliteit te gebruiken, wordt deze omgezet naar een GUI-thread en
wordt de KeServiceDescriptorTableShadow zijn SDT. KeServiceDescriptorTable heeft slechts
´e´en SST, deze is voor de gewone SSDT. KeServiceDescriptorTableShadow heeft er twee: ´e´en
voor de gewone SSDT en ´e´en voor de Shadow-SSDT. De keuze voor een specifieke SST (en
dus SSDT) gebeurt a.d.h.v. een index van 2 bits in de SCO (de SST-index, zie Figuur 2.13).
Samengevat kunnen we zeggen dat de SDT thread-afhankelijk is, de SST bepaald wordt
a.d.h.v. de SDT en de SST-index, en de system service opgeroepen wordt op basis van de
SST en de SSN.
26
2.7.2
Syscall en sysenter
Er bestaan twee gespecialiseerde instructies om op een x86-processor een systeemoproep uit
te voeren. Indien de processor in 32-bits modus uitvoert, moet de sysenter-instructie uitgevoerd indien het om een Intel-processor gaat, en de syscall-instructie indien het om een
AMD-processor gaat (deze ondersteunen elkaars instructies niet). Deze instructies hebben
een gelijkaardige werking maar verschillen enigszins en zijn niet zomaar verwisselbaar. Alvorens sysenter uit te voeren moet het adres van de top van de stapel opgeslagen worden (dit
gebeurt in het edx-register). Bij de syscall-instructie is dit niet nodig [47][33]. Uit compatibiliteitsoverwegingen maken de systeembibliotheken geen direct gebruik van deze instructies.
In plaats daarvan bevatten de SCW’s een indirecte oproep naar een kleine routine die door
de kernel wordt ingesteld bij het opstarten: SystemCallStub. SystemCallStub zal de juiste
instructie bevatten om een systeemoproep uit te voeren, en indien nodig het adres van de
top van de stapel in het edx-register plaatsen. In het algemeen ziet een van deze indirectie
gebruik makende SCW er als volgt uit:
mov
mov
call
ret
eax, SCO
edx, offset SharedUserData!SystemCallStub
dword ptr [edx]
In 64-bits modus ondersteunen Intel-processors de syscall-instructie wel. Alle systeemoproepen vanuit 64-bits modus gebeuren dan ook via syscall, zonder indirectie:
mov
syscall
ret
2.7.3
eax, SCO
Instabiliteit
Zoals vermeld in de inleiding is de interface aangeboden door de Windows-kernel niet stabiel
over verschillende versies. Het probleem is niet dat de system services die aangeboden worden
en de argumenten die deze gebruiken veranderen. Deze zijn inderdaad niet gegarandeerd om
hetzelfde te blijven, maar in de praktijk veranderen ze nauwelijks. De SCO verbonden aan een
specifieke system service daarentegen verandert wel tussen verschillende versies van Windows.
Deze kan zelfs veranderen bij het uitbrengen van een nieuwe service pack, en de SCO’s voor
dezelfde system services verschillen bv. ook tussen Windows 8 en Windows 8.1. Een tabel met
SCO’s voor alle systeemoproepen op alle recente versies van Windows kan online gevonden
worden [48].
Om een volledig statisch gelinkte applicatie compatibel te maken met verschillende versies
van Windows is het dus voornamelijk van belang om ervoor te zorgen dat de applicatie op
elke versie de gepaste SCO’s gebruikt.
27
Figuur 2.15: De architectuur van de WoW64-laag.
2.7.4
Systeemoproepen en systeembibliotheken
Er zijn een aantal systeembibliotheken die SCW’s bevatten. Zo is er ntdll dat de Native
API bevat (zie Sectie 2.6.1). Deze API bestaat uit ge¨exporteerde SCW’s waarvan de namen
allemaal beginnen met een “Nt” zoals NtAllocateVirtualMemory, NtWritefile en NtCreateProcess. Deze SCW’s vormen een abstractie voor de gelijknamige system services, allen
ge¨ımplementeerd in de kernel-image. De adressen van deze system services zijn te vinden in
de gewone SSDT, en de SCO voor deze systeemoproepen bevat dus als SST-index altijd 0.
Daarnaast zijn er ook (voornamelijk onge¨exporteerde) SCW’s in user32 (namen beginnende
met “NtUser”) en gdi32 (namen beginnende met “NtGdi”). De bijhorende system services
zijn ge¨ımplementeerd in win32k.sys en hun adressen zijn te vinden in de Shadow SSDT.
Bijgevolg bevat de SCO voor deze systeemoproepen altijd een 1 als SST-index.
2.7.5
WoW64
WoW64 is de afkorting voor Win32 on Windows 64-bit, de emulator die het mogelijk maakt
om Win32-applicaties (bedoeld voor een 32-bits Windows) uit te voeren op een 64-bits Windows [49]. Deze emulator is eigenlijk een laag tussen de 32-bits applicatie en het 64-bits
besturingssysteem, bestaande uit drie DLL’s: wow64, wow64cpu en wow64win [50]. In de
adresruimte van elke 32-bits applicatie zijn dus naast de 32-bits systeembibliotheken ook nog
de 64-bits WoW64-bibliotheken en een 64-bits versie van ntdll aanwezig, hoewel de applicatie
van deze laatste twee geen weet heeft (zie Figuur 2.15). De 32-bits systeembibliotheken op
een 64-bits Windows zijn zeer gelijkaardig aan die van een 32-bits Windows, maar er is een
belangrijk verschil: ze doen zelf geen systeemoproepen [51].
Als we een 32-bits systeembibliotheek op een 64-bits Windows 8/8.1 beschouwen, dan ziet
een SCW er als volgt uit (vergelijk met Sectie 2.7.2):
mov
call
ret
eax, SCO
large dword ptr fs:0xC0
De SCO ziet er in dit geval ook net iets anders uit (zie Figuur 2.16). De bovenste 16 bits
vormen een index die gebruikt wordt door de WoW64-laag (de WoW64-index ), en er schieten
28
Figuur 2.16: De indeling van de SCO voor WoW64.
nog maar twee bits over die ongebruikt zijn (aangeduid met “ONG.”).
De indirecte call-instructie die de sysenter- of syscall-instructie vervangt in de 32-bits systeembibliotheken heeft als doeladres een ‘far jump’-instructie in wow64cpu. Dit soort instructie
springt naar een specifiek adres in een specifiek segment. In dit geval springt de instructie
naar de functie CpupReturnFromSimulatedCode binnen wow64cpu, in een 64-bits segment.
Dit is dus het punt waarop de CPU overschakelt naar 64-bits modus. Alvorens de syscallinstructie uit te voeren, moeten de argumenten (die nu op een 32-bits stapel staan) aangepast
worden om correct gebruikt te worden door de 64-bits kernel. De exacte manier waarop dit
gebeurt, hangt af van de WoW64-index. Eens de systeemoproep uitgevoerd is, wordt het
resultaat aangepast tot hetgeen een 32-bits kernel zou hebben teruggeven. Hierna wordt er
terug overgeschakeld naar 32-bits modus en wordt de WoW64-laag verlaten.
De gebruikte SCO wordt niet aangepast door de WoW64-laag. Dit impliceert dat op een
64-bits Windows de SCO’s die de 32-bits en 64-bits systeembibliotheken gebruiken voor een
specifieke system service dezelfde zijn. De SCO’s die de 32-bits systeembibliotheken op een
32-bits en een 64-bits Windows gebruiken verschillen wel.
29
Hoofdstuk 3
Encrypteren van meta-informatie
In dit hoofdstuk wordt de methode voorgesteld die – door het encrypteren van meta-informatie
– als doel heeft om de interfaces tussen een uitvoerbaar bestand en alle gebruikte bibliotheken
te verbergen. Dit is voordelig op vlak van beveiliging. De meta-informatie aanwezig in dynamisch gelinkte uitvoerbare bestanden kan namelijk gebruikt worden bij het statisch analyseren van de applicatie. In de besproken methode wordt de meta-informatie in ge¨encrypteerde
vorm opgeslagen zodat dynamisch linken nog mogelijk blijft, maar statische analyse sterk bemoeilijkt wordt. Eerst wordt de algemene werking besproken, vervolgens wordt de methode
in meer detail uitgewerkt en tot slot komen de beperkingen en mogelijke uitbreidingen van
de methode aan bod.
3.1
De algemene werking
Het doel van de methode is om op basis van het oorspronkelijk uitvoerbaar bestand door Diablo een herschreven bestand te laten genereren waarin de meta-informatie in ge¨encrypteerde
vorm is opgeslagen. Aan de hand van deze ge¨encrypteerde meta-informatie zal de applicatie tijdens het uitvoeren de benodigde symbolen vinden (op het moment dat ze nodig zijn).
De meta-informatie wordt niet simpelweg dynamisch gedecrypteerd naar zijn oorspronkelijke vorm, deze zou namelijk bij het uitvoeren van de applicatie gemakkelijk uit het geheugen gehaald kunnen worden om vervolgens te gebruiken in een statische analyse. In plaats
daarvan worden de ge¨ımporteerde symbolen gevonden op basis van de ge¨encrypteerde metainformatie, zonder deze te decrypteren.
Om een herschreven bestand te genereren dat hiertoe in staat is, maakt Diablo twee grote
aanpassingen aan het uitvoerbaar bestand. Ten eerste worden de importtabellen (die de
meta-informatie bevatten) uit het oorspronkelijke bestand verwijderd en in ge¨encrypteerde
vorm toegevoegd aan het nieuwe bestand. Ten tweede wordt er extra code aan het bestand
toegevoegd. Deze code – de zogenaamde glue code – staat in voor het dynamisch vinden van
de benodigde symbolen op basis van de ge¨encrypteerde meta-informatie.
De glue code bevat – naast een aantal hulpfuncties – twee hoofdfuncties. De eerste is de initialisatiefunctie die het nieuwe beginpunt van de applicatie wordt. Deze initialiseert een aantal
variabelen die door de rest van de glue code gebruikt worden, waarna ze naar het oorspronkelijke beginpunt van de applicatie springt. Daarnaast is er ook de laadfunctie die instaat
voor het dynamisch opzoeken van de ge¨ımporteerde symbolen op basis van de ge¨encrypteerde
30
Figuur 3.1: De algemene werking van de methode.
meta-informatie. Diablo vervangt elke instructie die gebruik maakt van ge¨ımporteerde symbolen (dit is een indirecte instructie die gebruik maakt van de IAT, zie Sectie 2.5.2) door een
oproep naar de laadfunctie. Eens de laadfunctie opgeroepen zal deze de instructie (die nu een
call-instructie is) terug herschrijven naar de instructie die er eigenlijk moet staan, namelijk
de directe variant van de (indirecte) oorspronkelijke instructie. De instructie die er eigenlijk
moet staan noemen we de eigenlijke instructie.
De algemene werking wordt nog eens voorgesteld op Figuur 3.1. Deze figuur toont ook een
voorbeeld van het controleverloop van de oorspronkelijke en de herschreven applicatie, voorgesteld door de nummering.
3.2
Het encrypteren van de meta-informatie
Voor het encrypteren wordt er gebruik gemaakt van eenrichtingsfuncties. Dit zijn functies
waarvan de uitvoer gemakkelijk te berekenen is gegeven de invoer, maar waarvoor het zeer
moeilijk is de invoer terug te vinden gegeven de uitvoer. Het is deze uitvoer die bijgehouden
wordt in het herschreven bestand en gebruikt zal worden om toch nog de ge¨ımporteerde symbolen te kunnen vinden. De specifieke vorm van eenrichtingsfuncties die in de implementatie
gebruikt wordt is die van een hashfunctie, met als uitvoer een hashwaarde.
De informatie die we zeker nodig hebben om dynamisch ge¨ımporteerde symbolen te kunnen
vinden bestaat uit de namen van de DLL’s waaruit we symbolen importeren en de namen
(of ordinalen) van deze symbolen zelf. In de huidige implementatie is er enkel ondersteuning
voor import via naam en niet via ordinaal, omdat er niet veel uitvoerbare bestanden zijn die
nog van het laatste gebruik maken. Zo’n uitvoerbare bestanden bestaan echter wel en om
alle uitvoerbare bestanden te kunnen herschrijven zou ook ondersteuning voor import via
ordinaal voorzien moeten worden. Er wordt in de glue code een tabel voorzien die de hash
bevat voor de naam van elke gebruikte DLL, zodat de glue code deze kan laden. Deze tabel
noemen we de DLL-tabel.
31
Figuur 3.2: De structuur van een element in de DYNIMP-tabel. De groottes van de velden
zijn niet op schaal.
Om elke oproep naar de laadfunctie te kunnen herschrijven naar de eigenlijke instructie wordt
er ook een tabel voorzien – de DYNIMP-tabel – die een element bevat voor elke instructie die
een ge¨ımporteerd symbool gebruikt. Dit element bevat alle informatie die nodig is voor het
herschrijven. Figuur 3.2 toont de structuur van dit element. De hash van het terugkeeradres en
de salt worden gebruikt door de laadfunctie om te identificeren welk element uit de DYNIMPtabel ze moet gebruiken. De hash van de symboolnaam en de index in de DLL-tabel worden
gebruikt om het symbooladres dynamisch te berekenen, en alle andere velden worden gebruikt
om de ‘call laadfunctie’-instructie te herschrijven.
De laadfunctie moet als ze aangeroepen wordt in staat zijn te identificeren welk element
uit de tabel te gebruiken. Een mogelijkheid zou zijn om Diablo net voor de oproep naar de
laadfunctie een instructie te laten toevoegen die een bepaalde waarde in een register plaatst
op basis waarvan deze identificatie kan gebeuren. We verkiezen echter zo weinig mogelijk
aanpassingen in de oorspronkelijke code aan te laten brengen door Diablo, omdat we bij het
dynamisch herschrijven deze aanpassingen weer ongedaan moeten maken. De identificatie
gebeurt daarom op basis van het terugkeeradres, dat bij een functieoproep automatisch op
de stapel geplaatst wordt.
Als we de terugkeeradressen rechtstreeks in de tabel zouden opslaan, zou het mogelijk zijn om
gewoon door naar de tabel te kijken af te leiden op welke locaties in de code er ge¨ımporteerde
symbolen gebruikt worden. Om dit ietwat te bemoeilijken zijn ook de terugkeeradressen
ge¨encrypteerd d.m.v. een eenrichtingsfunctie. Aangezien we met 32-bits adressen werken en
de uitvoerwaarden ook een grootte van 32 bits hebben, zijn de invoerruimte en de uitvoerruimte van de functie even groot. Dit is negatief voor de kwaliteit van een eenrichtingsfunctie
en daarom wordt er gebruik gemaakt van een salt [52]. Een salt bestaat uit extra – willekeurig
gegenereerde – bytes die aan de invoer toegevoegd worden waardoor de invoerruimte vergroot
wordt. De formule voor de oorspronkelijke eenrichtingsfunctie is h(x), met x als invoer en
het resultaat van functie als uitvoer (of hash). Indien we gebruik maken van een salt wordt
deze formule h(x, r()). Hierbij is r() een functie die geen invoer heeft maar een willekeurige
waarde als uitvoer heeft. De uitvoer van deze functie dient als extra invoer voor de eenrichtingsfunctie. Het gebruik van salts maakt het ook mogelijk om, in geval van botsingen tussen
de uitvoerwaarden, nieuwe uitvoerwaarden te genereren op basis van dezelfde (echte) invoer,
maar met nieuwe salts.
32
3.3
3.3.1
De glue code
Aanpassingen in Diablo
Het toevoegen van de glue code gebeurt in Diablo na het emuleren van het linkerproces,
maar voor het disassembleren van de .text-secties en het opbouwen van de ACVG. Op dit
punt voegen we de objectbestanden die de glue code bevatten toe aan het parent-object,
zodat we veranderingen kunnen maken in de ACVG en verbindingen kunnen maken tussen
de oorspronkelijke applicatie en de glue code.
Na het opbouwen van de controleverloopgraaf wordt deze door Diablo onderverdeeld in functies. Knopen die niet bereikbaar zijn vanaf de beginknoop worden geen onderdeel van een
functie. Indien geen van de BBL’s waaruit een functie bestaat bereikbaar is vanuit de beginknoop, zal deze groep BBL’s niet als een functie herkend worden door Diablo. Dit is
natuurlijk het geval voor de knopen uit de glue code, er zijn namelijk nog geen verbindingen
tussen de glue code en de oorspronkelijke code. Bijgevolg zullen de initialisatiefunctie noch
de laadfunctie (noch enige hulpfuncties) herkend worden. Omdat het ons voordelig uitkomt
als functies als zodanig herkend worden maken we gebruik van de force reachable-vlag die
Diablo dwingt een specifiek symbool als bereikbaar te beschouwen, waardoor de verwante
functie herkend wordt.
Er moeten een aantal veranderingen gebeuren in de controleverloopgraaf om de glue code
echt aan de applicatie toe te voegen. Zo moet de initialisatiefunctie het beginpunt worden en
op het einde naar het oorspronkelijke beginpunt springen. Ten tweede moet elke instructie
die gebruik maakt van ge¨ımporteerde symbolen vervangen worden door een oproep naar de
laadfunctie. Deze instructies worden bepaald door alle bestaande relocaties te overlopen en
die relocaties te zoeken die van een instructie naar een locatie binnen de IAT wijzen. Voor
elke instructie die gebruik maakt van een ge¨ımporteerd symbool wordt er een element aan
de DYNIMP-tabel toegevoegd alvorens de instructie te vervangen.
Er zijn twee gevallen, afhankelijk van wat voor soort instructie we vervangen. Indien het een
controleverloopinstructie is, zal deze zich aan het einde van zijn BBL bevinden. In dit geval
vervangen we de uitgaande pijl door een call-pijl met als eindknoop het eerste BBL van de
laadfunctie. De oorspronkelijke eindknoop van deze pijl is een helleknoop. Het operand is
namelijk een adres dat opgeslagen ligt binnen de IAT, en er is dus sprake van indirect controleverloop. Een voorbeeld waarin de controleverloopinstructie in kwestie een call-instructie
is, is voorgesteld in Figuur 3.3.
In het tweede geval hebben we te maken met een instructie die geen controleverloopinstructie is, en dus niet noodzakelijk de laatste instructie binnen zijn BBL is. Mochten we deze
instructie vervangen door een call naar de laadfunctie dan zou dit resulteren in een call in
het midden van een BBL, wat niet toegestaan is. Daarom zullen we in dit geval het BBL net
na de instructie opsplitsen in twee BBL’s en de nodige aanpassingen maken. Een voorbeeld
van dit geval waarin de te vervangen instructie een mov-instructie is, valt eveneens te zien in
Figuur 3.3.
33
Figuur 3.3: Voorbeelden voor het vervangen van instructies die ge¨ımporteerde symbolen gebruiken.
Bij het vervangen van de oorspronkelijke instructie door een call-instructie moet er ook
rekening gehouden worden met een eventueel verschil in lengte tussen de call-instructie (5
bytes) en de eigenlijke instructie die na het oproepen van de laadfunctie de call-instructie zal
vervangen. Indien de eigenlijke instructie langer is dan 5 bytes wordt er extra plaats voorzien
door middel van padding bytes (NOP-instructies).
3.3.2
De initialisatiefunctie
De initialisatiefunctie bootst de initialisatie na die bij het opstarten van een nieuwe applicatie plaatsvindt. De DLL’s waarvan de applicatie afhankelijk is, worden in de adresruimte
van het proces geladen en hun initialisatieroutines worden uitgevoerd. In tegenstelling tot
de normale initialisatie worden de adressen van de ge¨ımporteerde symbolen nog niet door
de initialisatiefunctie berekend en op de juiste plaats weggeschreven. Dit zal op een later
moment (voor elk afzonderlijk gebruik van een symbool) gebeuren bij het aanroepen van de
laadfunctie.
Het volledige proces om een DLL te laden is een ingewikkelde taak waarvoor ook medewerking van de kernel vereist is. We kunnen dit volledige proces laten uitvoeren d.m.v. ´e´en
functie-aanroep, namelijk die van de ge¨exporteerde functie LoadLibrary[53] uit een van de
systeembibliotheken, kernel32. LoadLibrary zal ons het adres teruggeven waarop de DLL is
geladen, wat we voor later gebruik zullen bijhouden.
Er zijn nog meer plaatsen in de glue code waar het ons voordelig uitkomt om gebruik te
maken van de functionaliteit die kernel32 aanbiedt, zoals bijvoorbeeld bij het herschrijven
van instructies. Het vinden van de symbolen die de glue code nodig heeft uit kernel32 gebeurt
op dezelfde wijze als waarop het vinden van de ge¨ımporteerde symbolen voor de eigenlijke
applicatie gebeurt, namelijk op basis van gehashte symboolnamen. Rest wel de vraag hoe
we kernel32 kunnen laden als we nog niet beschikken over de functionaliteit om een DLL te
34
laden.
In feite is kernel32 reeds aanwezig in de adresruimte van het proces. Bij het opstarten van
een applicatie zal Windows namelijk een aantal systeembibliotheken automatisch laden, zelfs
als de applicatie hieruit geen symbolen importeert (en zelfs als de applicatie in het geheel
geen symbolen importeert). E´en van deze systeembibliotheken is kernel32. kernel32 is dus wel
degelijk aanwezig in de adresruimte van ons proces, het enige wat ons nog rest is het basisadres ervan te vinden. Dit is een probleem dat men ook tegenkomt bij Windows-shellcodes.
Op het moment dat een shellcode aan zijn uitvoering begint, is kernel32 reeds aanwezig in de
adresruimte van het aangevallen proces, maar kent de shellcode het adres ervan niet. Aangezien de shellcode de functionaliteit van kernel32 wil gebruiken voor zijn eigen doeleinden
is de eerste stap in de uitvoering dus dit adres te vinden. Er bestaat een algemene manier
om dit te doen die gebruik maakt van het Process Environment Block (PEB) [54] [55]. Deze
methode wordt ook in de initialisatiefunctie gebruikt.
Eens de initialisatiefunctie over het adres van kernel32 beschikt worden de adressen berekend van alle symbolen die de glue code hieruit gebruikt. kernel32 exporteert de functie
GetProcAddress [56] die gegeven het adres van een DLL en een symboolnaam het adres van
het symbool berekent, maar doordat we met hashes werken in plaats van de eigenlijke namen
kunnen we deze functie niet gebruiken. Daarom is er in de glue code een functie aanwezig
met dezelfde functionaliteit als GetProcAddress, maar dan op basis van de hashwaarde van
een symboolnaam. Deze functie overloopt alle namen in de ENT van de exporterende DLL en
vergelijkt per naam de resulterende hashwaarde met de hashwaarde van de symboolnaam die
we zoeken. Indien deze twee gelijk zijn hebben we het gezochte symbool (en bijhorend adres)
gevonden. Het is echter ook mogelijk dat we te maken hebben met een ge¨exporteerd symbool
dat doorverwezen wordt (zie Sectie 2.5.1). In dit geval is het bijhorend adres niet dat van het
symbool zelf – gezien het niet aanwezig is in de DLL – maar het adres van een string van
de vorm “DLLNAAM.FunctieNaam” [28]. We kunnen deze informatie nu gebruiken om de
eigenlijke DLL te laden via LoadLibrary – als de DLL reeds geladen is zal LoadLibrary gewoon het adres teruggeven zonder de DLL nog eens te laden – en vervolgens GetProcAddress
gebruiken om het adres te vinden van het symbool waarnaar er doorverwezen wordt.
Nu we beschikken over de nodige functionaliteit uit kernel32 kunnen we beginnen de DLL’s
waarvan de applicatie afhankelijk is te vinden en te laden. Aangezien we weer enkel de
gehashte namen hebben en niet de namen zelf, kunnen we niet meteen de functie LoadLibrary
gebruiken. Er zijn twee plaatsen van waarop DLL’s geladen kunnen worden bij het opstarten
van een applicatie: de system32 -map en de map waarin de applicatie zelf aanwezig is. Eerst
lopen we door de system32-map en vergelijken per DLL de hash van hun naam met de
hash van de gezochte naam. Indien er geen match gevonden wordt doen we hetzelfde bij
applicatiemap. Vervolgens gebruiken we de gevonden naam als argument in LoadLibrary en
overschrijven in de DLL-tabel de hash van de naam met het adres van de DLL.
Op het einde van de initialisatiefunctie springen we naar het oorspronkelijke beginpunt van
de applicatie en wordt er aan de uitvoering van de echte applicatie begonnen.
35
Figuur 3.4: Voorbeelden van het herschrijven van instructies die ge¨ımporteerde symbolen
gebruiken.
3.3.3
De laadfunctie
De laadfunctie zal, eens ze opgeroepen wordt, de call-instructie vanaf waar ze is opgeroepen
herschrijven naar de eigenlijke instructie aan de hand van de informatie in de DYNIMP-tabel.
Zie Figuur 3.4 voor een aantal voorbeelden hiervan.
In het begin van de laadfunctie worden alle caller-saved registers op de stapel geplaatst en
deze registers krijgen terug hun oorspronkelijke waarde op het einde van de laadfunctie.
Normaal gezien worden deze registers (indien nodig) op de stapel geplaatst net voor een callinstructie. De ‘call laadfunctie’-instructie is echter door Diablo aan de applicatie toegevoegd
en de oorspronkelijke instructie was niet per se een call-instructie. Daarom zal de laadfunctie
voor alle zekerheid de caller-saved registers veilig stellen.
De eerste, echte stap is bepalen welk element uit de DYNIMP-tabel benodigd is. Dit gebeurt
aan de hand van het terugkeeradres, zoals beschreven in Sectie 3.2. Nadat het juiste element
gevonden is, zoeken we (m.b.v. de index in het element) het basisadres op van de DLL waaruit
het symbool ge¨ımporteerd wordt. Dit adres wordt samen met de hash van de symboolnaam
gebruikt om het adres van het symbool te berekenen.
Om de ‘call laadfunctie’-instructie te kunnen herschrijven moet eerst het geheugenbereik
waarop we willen schrijven schrijfbaar gemaakt worden. Hiervoor gebruiken we de VirtualProtect [57] functie uit kernel32 die de geheugenbescherming aanpast voor alle geheugenpagina’s met minstens ´e´en byte in het gevraagde bereik. Het gevraagde geheugenbereik begint
op het adres van de ‘call laadfunctie’-instructie en heeft de lengte van de instructie waarmee we deze willen vervangen. Deze lengte valt te vinden in het gevonden element uit de
tabel en is minstens vijf bytes (minstens ´e´en byte opcode en altijd vier voor een adres) en
hoogstens vijftien bytes (de maximale lengte van een x86-instructie [47]). Het geheugenbereik
wordt leesbaar, schrijfbaar ´en uitvoerbaar gemaakt. Dit geheugen moet ook uitvoerbaar zijn
in het geval dat we een ‘call laadfunctie’-instructie willen overschrijven die zich per toeval op
eenzelfde pagina bevindt als stukken van de glue code die ook uitgevoerd moeten worden.
Nu het geheugen schrijfbaar is kunnen we de eigenlijke instructie wegschrijven en op de juiste
plaats binnen de instructie het adres van het symbool – dat als operand dient – wegschrijven.
36
Alle informatie om dit te doen is aanwezig in het element uit de DYNIMP-tabel. Indien de
eigenlijke instructie een controleverloopinstructie is (valt te vinden in de tabel) moet niet het
adres van het symbool maar een EIP-relatieve offset naar dit adres weggeschreven worden.
Eens de instructie aangepast is, wordt VirtualProtect nogmaals opgeroepen om het geheugenbereik weer de oude geheugenbescherming te geven. Omdat zelf-aanpassende code tot
problemen met de instructiecaches kan leiden, doen we een oproep naar FlushInstructionCache [58]. Uiteindelijk wordt het terugkeeradres van de stapel gehaald, aangepast naar het
adres van de herschreven instructie, en terug op de stapel geplaatst. De laadfunctie keert dus
terug naar de herschreven instructie zodat de normale uitvoering kan hervatten.
Elke ‘call laadfunctie’-instructie wordt dus hoogstens ´e´en keer uitgevoerd, en het uitvoeren
resulteert in het overschrijven van deze instructie met de eigenlijke instructie. Op deze manier
wordt de overhead ge¨ıntroduceerd door de methode geminimaliseerd. De herschreven instructie maakt ook direct gebruik van het adres van ge¨ımporteerd symbool. Dit in tegenstelling
tot de oorspronkelijke, indirecte variant uit de oorspronkelijke applicatie die dit adres uit de
IAT haalde. Het verwijderen van deze indirectie heeft een positief effect op de prestatie van
de applicatie.
3.4
3.4.1
Beperkingen en mogelijke uitbreidingen
De eenrichtingsfuncties
Een eerste beperking in de huidige implementatie heeft te maken met het gebruik van eenrichtingsfuncties om de meta-informatie te encrypteren. We vertrouwen erop dat deze functies
sterk genoeg zijn zodat het niet mogelijk is om op basis van de uitvoer de invoer af te leiden.
De huidige gebruikte eenrichtingsfuncties zijn eerder van functioneel nut dan dat ze cryptografisch sterk zijn. Zelfs indien we gebruik maken van ingewikkeldere eenrichtingsfuncties zal
het niet onmogelijk zijn om de invoer te achterhalen aan de hand van de uitvoer doordat de
invoerruimte in de praktijk zeer klein is. Er zijn maar een beperkt aantal namen van DLL’s
die de applicatie kan gebruiken en per DLL is er een beperkt aantal namen van ge¨exporteerde
symbolen.
In een normaal gebruiksgeval is het berekenen van eenrichtingsfunctie voor al zijn invoerwaarden een uitdaging die een grote hoeveelheid rekenkracht en tijd vereist. Omdat de feitelijke
invoerruimte in dit geval veel kleiner is dan de theoretische invoerruimte – bestaande uit
alle mogelijke strings – kan een aanvaller wel binnen een redelijke termijn voor alle mogelijk
invoerwaarden de hashwaarde berekenen. Het zou dus mogelijk zijn voor een aanvaller om
alle mogelijk hashwaardes te berekenen, en zo de hashwaardes uit de DYNIMP-tabel en de
DLL-tabel te vertalen naar de bijhorende symboolnamen respectievelijk DLL-namen. Het
gebruik van salts zou hier ook niet zoveel helpen als men zou verwachten. Stel dat er 3000
DLL’s aanwezig zijn in de systeemmap en de applicatiemap tezamen waarvan een applicatie
mogelijk gebruik maakt. Als we salts gebruiken moet de aanvaller voor elke gebruikte DLL
afzonderlijk de namen van al deze DLL’s hashen en vergelijken. Stel dat de applicatie gebruik
maakt van 100 DLL’s – voor het merendeel van de applicaties al een grove overschatting –
dan komt dit neer op de hashfunctie 300000 keer uitvoeren, wat met de rekenkracht van de
37
huidige processoren binnen de tijdspanne van enkele seconden gebeurd is. Eens de gebruikte
DLL’s gekend zijn kan de aanvaller beginnen met de gebruikte symbolen te achterhalen. Deze
stap zou via een gelijkaardige procedure gebeuren binnen een gelijkaardig tijdsbestek. Het
toevoegen van salts kan wel helpen om van eventuele botsingen tussen de hashwaardes van
de DLL-namen en symboolnamen binnen een DLL te voorkomen.
Het is dus mogelijk om met enige moeite de meta-informatie toch nog te reconstrueren
en aan statische analyse te doen. Een mogelijke uitbreiding om dit te vermijden is om de
ge¨encrypteerde meta-informatie nogmaals te encrypteren d.m.v. white-box cryptografie [59].
De ge¨encrypteerde meta-informatie zal dan nooit volledig zichtbaar zijn in het geheugen, en
deze observeren gaat enkel via dynamische analyse.
3.4.2
Uitbreiden ondersteuning uitvoerbare bestanden
In de huidige implementatie is er nog geen ondersteuning voor uitvoerbare bestanden die
aan import via ordinaal doen of die variabelen importeren, omdat dit eigenschappen zijn van
het PE-formaat die niet vaak gebruikt worden. Om alle uitvoerbare bestanden te kunnen
herschrijven, zouden deze eigenschappen wel ondersteund moeten worden. Lichte aanpassingen in DYNIMP-tabel en de glue code zouden volstaan om ook import via ordinaal te
ondersteunen.
Momenteel wordt enkel het importeren van functies ondersteund. Het adres van een ge¨ımporteerde
functie kan maar in drie instructies gebruikt worden: een call, een jmp, of een mov. In het
laatste geval wordt het adres in een register geplaatst voor snellere toegang, wat voordelig is
als de functie vaker opgeroepen zal worden. Een ge¨ımporteerde variabele kan echter in zeer
veel instructies gebruikt worden: inc, dec, add, sub, mul, imul, div, idiv, or, xor, cmp, et
cetera. Voor al deze gevallen zou nog ondersteuning geschreven moeten worden.
3.4.3
Dynamische aanvallen
De applicatie is natuurlijk nog altijd kwetsbaar voor dynamische analyse via hooking. Daarnaast zijn er ook een aantal dynamische aanvallen die het mogelijk maken de meta-informatie
te reconstrueren. Als de applicatie lang genoeg aan het uitvoeren is, zullen de adressen
van – het merendeel van – de ge¨ımporteerde symbolen berekend zijn. Een geheugendump
van de applicatie op dit moment stelt een aanvaller dus – mits enige moeite – in staat de
symbooladressen te achterhalen uit de herschreven oproepen naar de laadfunctie. Van deze
symbooladressen worden dan vervolgens de bijhorende symbolen afgeleid, voor gebruik bij
statische analyse. Een mogelijke manier om dit tegen te gaan zou zijn om een mechanisme
te implementeren waarbij de instructies die gebruik maken van ge¨ımporteerde symbolen tijdens de uitvoering periodiek terug herschreven worden naar oproepen naar de laadfunctie.
In het extreme geval worden deze instructies zelfs nooit herschreven door de laadfunctie. De
laadfunctie wordt dan elke keer opgeroepen en zal dan instaan voor het uitvoeren van de
benodigde instructie alvorens terug te keren. Dit alles komt de prestatie van de applicatie
echter niet ten goede.
Het is ook mogelijk voor een aanvaller om de ge¨ımporteerde symbolen te achterhalen zonder
38
de applicatie volledig uit te voeren. Door middel van reverse engineering kan het adres van
de laadfunctie achterhaald worden, en via statische analyse kunnen alle oproepen naar de
laadfunctie (en hun terugkeeradres) gevonden worden. Als een aanvaller eerst de initialisatiefunctie laat uitvoeren en dan de laadfunctie voor elk terugkeeradres laat oproepen, wordt de
laadfunctie gebruikt om de ge¨ımporteerde symbolen voor de aanvaller te vinden. Dit vereist
natuurlijk wel dat er een aantal aanpassingen aan het uitvoerbaar bestand gebeuren.
Om ook de kwetsbaarheid ten opzichte van deze aanvallen (en dynamische analyse) te verminderen, moeten we in het geheel af van het gebruik van dynamische linken en een applicatie
cre¨eren die slechts uit ´e´en component bestaat. Dit is dan ook wat we zullen doen in Hoofdstuk 4.
39
Hoofdstuk 4
Statisch linken
Om dynamische analyse tegen te gaan willen we uitvoerbare bestanden zo herschrijven dat
ze niet meer afhankelijk zijn van andere componenten (behalve de kernel). In dit hoofdstuk
bespreken we de methode van het statisch linken, die exact dat als doel heeft. De herschreven uitvoerbare bestanden zijn slechts compatibel met ´e´en versie van Windows, namelijk
die versie waarop ze door Diablo gemaakt zijn. Een methode om ze compatibel te maken
met meerdere versies van Windows wordt later besproken in Hoofdstuk 5. Eerst worden de
verschillende stappen in de methode van het statisch linken overlopen, vervolgens bespreken
we partieel statisch linken, en we eindigen met de beperkingen en mogelijke uitbreidingen
van de methode.
4.1
Toevoegen van DLL’s
We willen een herschreven uitvoerbaar bestand cre¨eren dat enkel (rechtstreeks) interageert
met de kernel en niet afhankelijk is van DLL’s. Aangezien het oorspronkelijk uitvoerbaar
bestand natuurlijk wel afhankelijk is van DLL’s moeten we een manier vinden om de delen
die we nodig hebben uit deze DLL’s aan het uitvoerbaar bestand toe te voegen. Een DLL toevoegen gebeurt in Diablo door bij het parent-object (dat het uitvoerbaar bestand voorstelt)
een extra objectbestand (dat de DLL voorstelt) bij te linken. De eerste stap is natuurlijk te
bepalen welke DLL’s aan het uitvoerbaar bestand toe te voegen.
4.1.1
Bepalen van de benodigde DLL’s
De importtabellen van het uitvoerbaar bestand worden bekeken en de DLL’s waarvan het
uitvoerbaar bestand rechtstreeks afhankelijk is worden bepaald. Deze DLL’s worden aan een
lijst van benodigde DLL’s toegevoegd, de DLL’s uit deze lijst worden vervolgens ´e´en voor
´e´en als objectbestand ingelezen en aan het parent-object toegevoegd. Voor elke toegevoegde
DLL worden de importtabellen ook bekeken en de DLL’s waarvan het afhankelijk is (en
waarvan het uitvoerbaar bestand dus eventueel indirect afhankelijk is) bepaald en aan de
lijst van benodigde DLL’s toegevoegd. Het is mogelijk dat de delen die we nodig hebben
uit een bepaalde DLL (de delen die geassocieerd zijn met de uit die DLL ge¨ımporteerde
symbolen) geen uit andere DLL’s ge¨ımporteerde symbolen gebruiken, en dus eigenlijk niet
afhankelijk zijn van enige andere DLL. Dit is echter nog niet geweten op het moment dat we
proberen te bepalen van welke DLL’s het uitvoerbaar bestand (direct of indirect) afhankelijk
is. Daarom worden alle DLL’s waarvan het uitvoerbaar bestand eventueel afhankelijk is aan
40
het bestand toegevoegd, en de lijst van benodigde DLL’s bestaat dus niet per se uit DLL’s
die echt benodigd zijn, maar eerder uit DLL’s die eventueel benodigd zijn.
Bij het bepalen van de benodigde DLL’s moet natuurlijk ook rekening gehouden worden met
export forwarding (zie Sectie 2.5.1) en het API-set-schema (zie Sectie 2.6.2). Indien we een
symbool uit een DLL willen importeren dat eigenlijk doorverwezen wordt, dan is de DLL
waarnaar het doorverwezen wordt ook benodigd. Indien we een een symbool importeren uit
een virtuele DLL, dan is de bijhorende logische DLL benodigd.
In het geval dat het uitvoerbaar bestand afhankelijk is van kernel32 – zie Figuur 2.11 – zullen
naast kernel32 ook KernelBase en ntdll aan het bestand worden toegevoegd. Is het bestand
afhankelijk van bijvoorbeeld user32 dan zullen alle op de figuur aanwezige DLL’s aan het
bestand worden toegevoegd. Deze DLL’s bevatten veel code en data die in de applicatie
eigenlijk niet gebruikt wordt, omdat ze geassocieerd is aan symbolen die het oorspronkelijk
uitvoerbaar bestand niet importeerde. Het is zelfs mogelijk dat er DLL’s toegevoegd worden
waaruit de applicatie niets nodig heeft. In Sectie 4.4 wordt er besproken hoe we zo veel
mogelijk van deze overbodige code en data verwijderen.
4.1.2
Reconstrueren van relocaties
Alvorens het objectbestand dat de DLL voorstelt bij het parent-object bij gelinkt wordt,
wordt er nog een extra bewerking uitgevoerd. Er bestaan namelijk referenties die van ´e´en
sectie naar een andere sectie binnen een DLL gaan (voor een voorbeeld van zo’n referentie
zie Figuur 2.10). Als een DLL (of delen ervan) aan een uitvoerbaar bestand toegevoegd
wordt, moeten de referenties tussen de toegevoegde secties behouden blijven om te verzekeren
dat de toegevoegde delen correct blijven functioneren. Daarom moeten we een beeld van
deze referenties opbouwen. De enige informatie uit het PE-formaat die we hiervoor kunnen
gebruiken zijn de base relocaties (zie Sectie 2.5.3).
Een base relocatie wijst steeds naar een locatie binnen een sectie van een PE-bestand waarop
er een absoluut adres te vinden is. Deze absolute adressen wijzen naar een adres in een
tweede sectie van het bestand. Deze twee secties zijn meestal verschillend maar ze kunnen
ook dezelfde zijn, in welk geval het een interne referentie is. Voor elke base relocatie bepalen
we in Diablo deze twee secties en voegen aan de relocatie-informatie die Diablo bijhoudt een
relocatie toe die van het juiste adres in de eerste sectie naar het juiste adres in de tweede
sectie wijst. Alle relocaties tussen secties (en ook sommigen binnen secties) worden op deze
manier gereconstrueerd.
4.2
Disassembleren
Eens alle eventueel benodigde DLL’s aan het uitvoerbaar bestand zijn toegevoegd, begint Diablo de .text-secties te disassembleren en de ACVG te construeren. Om de .text-secties van
systeembibliotheken op een correcte manier te disassembleren moesten er echter een aantal
aanpassingen gebeuren in Diablo. In alle systeembibliotheken (behalve kernel32) is het namelijk zo dat de data die gewoonlijk aanwezig is in de .rdata-sectie, in de .text-sectie is geplaatst.
41
Deze data staat niet gewoon op het einde van de .text-sectie, maar is door de hele sectie verspreid. Daarnaast plaatst Visual Studio (de IDE waarmee de meeste Windows-applicaties
en ook de systeembibliotheken gemaakt worden) ook alle sprongtabellen (zie verder) in de
.text-sectie, in tegenstelling tot andere compilers die deze in de .rdata-sectie plaatsen.
Om correct met al deze data in de .text-secties om te gaan, moeten deze recursief gedisassembleerd worden (zie Sectie 2.3.2). Diablo bezat echter enkel functionaliteit om lineair te
disassembleren, en daarom heb ik ondersteuning geschreven voor recursief disassembleren.
Enkel .text-secties afkomstig uit DLL’s worden recursief gedisassembleerd. Zoals besproken
in Sectie 2.3.2 kan er niet altijd een perfect onderscheid tussen code en data gemaakt worden. Tenzij een bestand geobfusceerd is met als specifiek doel het belemmeren van recursief
disassembleren kunnen we er wel van uitgaan dat er geen data verkeerdelijk als code herkend
is. Vanwege indirect controleverloop zal er wel veel code als data herkend worden, dit veroorzaakt een probleem dat we in Sectie 4.3 zullen bespreken. Een mogelijke oplossing wordt
besproken in Sectie 4.7.3.
4.2.1
Implementatie van recursief disassembleren
Het is mogelijk dat een PE-bestand meerdere .text-secties bevat. In Diablo worden secties
afzonderlijk gedisassembleerd. Bij het recursief disassembleren is dit echter niet wenselijk,
omdat we bij het volgen van het controleverloop mogelijk bij een instructie in een andere .textsectie terecht zouden komen. Omdat secties afzonderlijk worden gedisassembleerd zouden we
het controleverloop niet kunnen volgen naar een andere sectie en zou de code in deze sectie
dus mogelijkerwijs niet gedisassembleerd worden. Om dit te vermijden wordt er (voor het
disassembleren) voor gezorgd dat er maar ´e´en .text-sectie meer aanwezig is in de DLL, dit
door alle .text-secties (indien er meerdere zijn) samen te voegen tot ´e´en sectie.
De adressen waarop we beginnen te disassembleren zijn die van de initialisatieroutine en de
ge¨exporteerde functies. Het is niet mogelijk om aan de hand van enkel het PE-bestand te
beslissen welke ge¨exporteerde symbolen functies zijn en welke variabelen zijn. Daarom wordt
er in Diablo een lijst gebruikt met alle door systeembibliotheken ge¨exporteerde symbolen
waarvan we uit ervaring weten dat ze variabelen zijn. Bij het disassembleren wordt er in deze
lijst opgezocht of een ge¨exporteerd symbool een variabele is of niet. Momenteel bevat deze
lijst nog maar ´e´en symbool.
Voor elk adres van een ge¨exporteerde functie of van de initialisatieroutine roepen we een functie op die lineair begint te disassembleren tot ze een reeds gedisassembleerde instructie tegenkomt, of een controleverloopinstructie disassembleert. Indien de controleverloopinstructie een
return-instructie is of een instructie die de uitvoering laat stoppen (zoals een interrupt), dan
stopt de disassembleerfunctie met disassembleren. Indien het een controleverloopinstructie
is die naar een andere plaats in de applicatie kan gaan, dan volgen we deze door de functie
recursief op te roepen met het doeladres als argument. Dit laatste doen we enkel in geval
van direct controleverloop aangezien het doel van indirect controleverloop niet gekend is. Het
doel zou eventueel nog via constantenpropagatie berekend kunnen worden, maar dit is niet
ge¨ımplementeerd.
42
Nadat alle instructies – in de mate van het mogelijke – gedisassembleerd zijn, wordt de rest
van de sectie in Diablo gekenmerkt als data. Voor elke locatie waarop er geen instructie
herkend is, zal Diablo een data-instructie genereren. Zo’n instructie is ´e´en byte groot en geeft
aan dat er op dat adres data aanwezig is. Als er op een later moment BBL’s gemaakt worden,
vormt elk contigu bereik van data-instructies ´e´en data-BBL.
4.2.2
Sprongtabellen
Indirect controleverloop kan doorgaans niet gevolgd worden bij het recursief disassembleren. Er is echter ´e´en vorm van indirect controleverloop dat we wel kunnen volgen, namelijk
hetgeen dat te maken heeft met sprongtabellen. Zo’n tabel bestaat uit een aantal adressen
waarheen gesprongen kan worden vanaf een indirecte jmp-instructie. Dit wordt gebruikt bij
het implementeren van switch-statements. Een voor een switch-statement gegenereerde, indirecte jmp-instructie kan er bijvoorbeeld als volgt uitzien: jmp [sprongtabel + 4 · case]. Hier
is sprongtabel het adres van de sprongtabel, 4 de lengte van een adres in bytes en case de
index in de tabel die overeenkomt met de specifieke case.
Sprongtabellen worden gevonden door mogelijke instructiepatronen die wijzen op de implementatie van een switch-statement te herkennen. Deze patronen eindigen altijd in een
indirecte jmp-instructie. Eens gevonden, wordt de disassembleerfunctie voor elk adres binnen de sprongtabel opgeroepen. Zo slagen we erin ook dit indirect controleverloop te volgen.
Doordat het vinden van sprongtabellen steunt op het herkennen van mogelijke patronen worden sommige sprongtabellen niet gevonden (bv. diegenen die geassocieerd zijn met ongekende
patronen en andere compilers).
De sprongtabellen zelf worden herkend als zijnde data in de .text-sectie en komen dus terecht
in data-BBL’s. Voor het verwijderen van overbodige code en data in Sectie 4.4 is het voordelig
als een data-BBL dat een sprongtabel bevat enkel die sprongtabel bevat. Daarom worden
de nodige aanpassingen gemaakt zodat eventuele data gelegen voor de sprongtabel in een
afzonderlijk data-BBL terechtkomt, en hetzelfde gebeurt voor eventuele data gelegen na de
sprongtabel.
4.3
Statisch linken van dynamisch gelinkte bestanden
In de vorige secties werd besproken hoe de benodigde DLL’s als objectbestanden aan het
parent-object werden toegevoegd en vervolgens de .text-secties van de toegevoegde DLL’s
en het uitvoerbaar bestand werden gedisassembleerd. Eens dat gebeurd is bouwt Diablo
een ACVG op die we gaan aanpassen. Omdat functies die niet bereikbaar zijn vanaf het beginknoop van de applicatie niet door Diablo herkend worden, gebruiken we de force reachablevlag op elke ge¨exporteerde functie die we nodig hebben (net als in Sectie 3.3.1). Omdat het
uitvoerbaar bestand en de DLL’s van elkaars functionaliteit gebruik maken via dynamisch
linken zijn er geen relocaties tussen secties afkomstig uit verschillende bestanden, en bevat de
opgebouwde ACVG geen verbindingen tussen de deelgrafen die deze verschillende bestanden
voorstellen. Om verbindingen te cre¨eren tussen deze niet onderling verbonden deelgrafen –
en alle bestanden feitelijk statisch te linken – moet elk dynamisch gebruik van een symbool
43
Figuur 4.1: Een voorbeeld van een relocatie op een data-instructie.
aangepast worden naar een statisch gebruik van het symbool.
Het bepalen van alle instructies die een dynamisch symbool gebruiken gebeurt analoog aan
de manier waarop dit bij het encrypteren van de meta-informatie in Sectie 3.3.1 plaatsvindt.
De relocatie-informatie bijgehouden in Diablo bestaat op dit moment niet enkel uit relocaties
binnen het oorspronkelijke uitvoerbare bestand maar ook uit relocaties binnen de toegevoegde
DLL’s. We lopen over alle relocaties en zoeken die relocaties die naar ´e´en van de IAT’s (elke
toegevoegde DLL kan ook een IAT hebben) wijzen. Deze relocaties zijn afkomstig van de
gezochte instructies, maar aangezien bij het recursief disassembleren niet alle instructies
herkend werden, is het mogelijk dat sommige van deze instructies data-instructies zijn.
Elke gevonden instructie (met als ´e´en van de operanden een adres in een IAT) zal aangepast worden om direct gebruik te maken van het adres van het corresponderende symbool,
behalve wanneer het een data-instructie is. Op Figuur 4.1 zien we een voorbeeld van zo’n
data-instructie waarvan een relocatie komt die naar de IAT wijst. Deze instructie is de eerste
van vier data-instructies die eigenlijk een adres binnen de IAT bevatten (0x00404018 in het
voorbeeld). De opcode-bytes van de eigenlijke instructie waarvan dit adres een operand bestaan uit data-instructies gelegen voor de instructie met de relocatie. Gezien deze eigenlijke
instructie niet gedisassembleerd is, kennen we zijn type noch de lengte van de opcode. De
instructie op dit moment in het proces nog proberen te disassembleren is niet gegarandeerd
om het juiste resultaat te geven. We weten namelijk niet of we de opcode van de eigenlijke
instructie bestaat uit ´e´en, twee of meerdere bytes. We kunnen de eigenlijke instructie dus niet
aanpassen, en relocaties komende van data-instructies worden daarom in de huidige implementatie gewoon verwijderd. Mocht deze instructie tijdens de uitvoering van de herschreven
applicatie toch uitgevoerd worden zou dit tot een fout leiden. Een mogelijke oplossing voor
dit probleem wordt voorgesteld in Sectie 4.7.3.
Elke echte instructie die gevonden wordt, wordt wel aangepast. Eerst wordt het gebruikte
ge¨ımporteerde symbool gevonden aan de hand van de locatie in de IAT waar de relocatie naar wijst. Met behulp van het ge¨ımporteerde symbool zoeken we het overeenkomstige
ge¨exporteerde symbool (waarvan de definitie aanwezig is in een toegevoegde DLL). Hierbij
wordt natuurlijk rekening gehouden met export forwarding en het API-set-schema, en zowel
44
import via naam als via ordinaal worden ondersteund. Eens het ge¨exporteerde symbool gevonden is, wordt er een statische verbinding gemaakt tussen het adres van het symbool en
de instructie die het gebruikt.
Er zijn twee manieren waarop deze verbinding gemaakt kan worden. Indien de instructie een
call of een jmp is, wordt de controleverloopgraaf aangepast zodat er een passende pijl van het
instructie-BBL naar het BBL geassocieerd aan het ge¨exporteerd symbool gaat. Dit zorgt er
ook voor dat de instructie aangepast wordt om direct in plaats van indirect gebruik te maken
van het operand. In de andere gevallen (indien het functie-adres als data gebruikt wordt, of
het symbool een variabele is) worden er geen aanpassingen gemaakt in de controleverloopgraaf. De relocatie zelf wordt aangepast om rechtstreeks naar het adres van het ge¨exporteerd
symbool te wijzen (in plaats van naar de IAT), en de instructie wordt aangepast van een
indirect naar een direct gebruik van het operand.
Op het einde van deze fase zijn de symbolen die van tevoren dynamisch gevonden werden nu
al gevonden, en zijn de nodige verbindingen tussen het oorspronkelijk uitvoerbaar bestand en
de toegevoegde DLL’s (onderling) gemaakt. De indirectie die aanwezig was om het gebruik
van dynamische symbolen mogelijk te maken is verdwenen, en er zijn geen relocaties meer
aanwezig die naar een IAT wijzen. Dit laatste is enkel zo omdat ook relocaties die van datainstructies kwamen verwijderd zijn.
4.4
Verwijderen van overbodige code en data
Het verwijderen van de overbodige uit DLL’s afkomstige code en data komt eigenlijk neer
op het elimineren van onbereikbare code en data (zie Sectie 2.2.3). Nadat alle nodige verbindingen gemaakt zijn, maken we dan ook gebruik van deze functionaliteit om de knopen
die niet verbonden zijn met de beginknoop (en dus niet gebruikt worden) uit de ACVG te
verwijderen. Na deze operatie zal er echter nog steeds veel overbodige code en data aanwezig
zijn.
Een DLL is – net als een uitvoerbaar bestand – linker-uitvoer. Alle adressen van symbolen
binnen het bestand zijn reeds berekend, met als gevolg dat er symboolinformatie (behalve over
dynamische symbolen) noch relocatie-informatie (behalve base relocaties) in afzonderlijke
structuren aanwezig is. Deze informatie wordt normaal gezien door Diablo gereconstrueerd
door het linkerproces te emuleren, en vervolgens gebruikt om een nauwkeurige representatie
van de interne afhankelijkheden van een bestand op te bouwen (zie Sectie 2.2.1). Omdat
we niet beschikken over de objectbestanden waaruit de toegevoegde DLL’s zijn opgebouwd
(laat staan de bijhorende linker maps), kunnen we het linkerproces voor deze DLL’s niet
emuleren. We kunnen enkel de relevante informatie die aanwezig is in het PE-formaat (de
base relocaties en de informatie over dynamische symbolen) gebruiken, en bijgevolg is de
opgebouwde representatie veel minder nauwkeurig dan normaal gezien. Het is dit gebrek aan
nauwkeurigheid in de representatie van de toegevoegde DLL’s die ons parten speelt bij het
verwijderen van overbodige code en data.
Van de data-secties afkomstig uit het uitvoerbaar bestand is geweten uit welke subsecties
45
(afkomstig uit de objectbestanden) deze zijn opgebouwd. Er wordt dus voor al deze subsecties
afzonderlijk gekeken of deze verbonden zijn met de beginknoop (en dus of ze verwijderd
kunnen worden). Voor de data-secties afkomstig uit toegevoegde DLL’s is dit niet het geval.
Deze bevatten slechts ´e´en subsectie (die qua inhoud gelijk is aan zijn parent-sectie) en er is
niets geweten over de oorspronkelijke subsecties waaruit ze zijn opgebouwd. Bijgevolg is het
zo dat er in realiteit subsecties zijn die niet verbonden zijn met de beginknoop (omdat deze
bijvoorbeeld bestaan uit data geassocieerd aan een niet-gebruikte ge¨exporteerde functie) en
dus eigenlijk verwijderd zouden kunnen worden, maar waarvan we niet weten dat ze bestaan.
Deze subsecties kunnen ook – als enige – verbonden zijn met andere subsecties of BBL’s
(in geval van functie-pointers in een data-sectie) die eigenlijk ook verwijderd zouden kunnen
worden, maar ook dit is niet mogelijk wegens het gebrek aan nauwkeurigheid.
Daarnaast leidt het recursief disassembleren ook tot onnauwkeurigheid. Alle bytes in een
.text-sectie die niet als onderdeel van een instructie zijn herkend, zijn gekenmerkt als data
en zitten in data-BBL’s. Deze data-BBL’s bestaan uit contigue geheugenbereiken die normaliter begrensd worden door echte BBL’s. Ze kunnen dus bestaan uit meerdere niet gedisassembleerde functies (of delen ervan) en data-subsecties die samengevoegd zijn. Om deze
nauwkeurigheid – enigszins – te verbeteren zijn alle sprongtabellen (waarvan we weten dat
ze eigenlijk afzonderlijke subsecties zijn) in afzonderlijke data-BBL’s geplaatst.
4.5
Initialisatieroutines
Eens een DLL in het geheugen geladen is wordt er – indien aanwezig – een initialisatieroutine
uitgevoerd [60][61]. Deze routine wordt niet alleen opgeroepen als een DLL aan een proces
wordt toegevoegd maar ook als ze eruit verwijderd wordt. Dit verwijderen gebeurt als het
proces eindigt, maar kan ook gebeuren tijdens de uitvoering (de DLL wordt dan gelost). Ook
voor elke nieuwe thread die start en elke oude thread die eindigt wordt de initialisatieroutine
opgeroepen. De context waarin ze opgeroepen wordt kan afgeleid worden van de argumenten,
en voor elk van deze situaties kan er verschillende code uitgevoerd worden. Op deze manier
wordt er aan initialisatie en finalisatie van een DLL gedaan, en is het mogelijk om Thread
Local Storage (TLS) te voorzien [62].
Er moet bij het inlijven van delen van een DLL bij het uitvoerbaar bestand voor gezorgd
worden dat de bijhorende initialisatieroutine op de juiste momenten aangeroepen wordt. In
de huidige implementatie wordt deze enkel aangeroepen bij het opstarten van de applicatie.
Er wordt voor gezorgd dat alle initialisatieroutines gevonden worden als functie door Diablo
(m.b.v. de force reachable-vlag), en aan de lijst met uit te voeren initialisatieroutines toegevoegd worden. Er wordt extra code toegevoegd die als nieuw beginpunt van de applicatie zal
dienen en deze initialisatieroutines met de juiste argumenten zal aanroepen.
De initialisatieroutine voor een DLL zou idealiter enkel aangeroepen worden indien de data
die ge¨ınitialiseerd wordt ook daadwerkelijk gebruikt wordt in de delen van de DLL die we
willen bijhouden. Het is echter moeilijk om dit te bepalen en daarnaast is het mogelijk dat de
routine functies oproept die een neveneffect hebben. Daarom verkiezen we om – zodra er ook
maar iets uit een DLL gebruikt wordt in het uiteindelijke bestand – haar initialisatieroutine bij
46
te houden, met als gevolg dat ook alle code en data geassocieerd aan die initialisatieroutine
niet verwijderd worden. We hebben echter in Sectie 4.1.1 gezien dat we niet van tevoren
weten of een DLL echt nodig is of niet, en in de huidige implementatie worden dus alle
initialisatieroutines (en verwante code en data) bijgehouden en uitgevoerd. Een mogelijke
uitbreiding die dit probleem oplost wordt besproken in Sectie 4.7.1.
4.6
Partieel statisch linken
Een volledig statisch gelinkt uitvoerbaar bestand dat gebruik maakt van systeemoproepen
zal normaal gezien enkel werken op de versie van Windows waarvoor het gelinkt is. Hoofdstuk 5 presenteert een methode die een volledig statisch applicatie compatibel maakt met
meerdere versies van Windows, maar in deze sectie bespreken we een tussenoplossing die
ge¨ımplementeerd werd: partieel statisch linken. Dit houdt in dat Diablo een uitvoerbaar
bestand genereert waarin alle niet-systeemspecifieke DLL’s zijn toegevoegd, waardoor het
bestand enkel nog maar afhankelijk is van de Windows API (t.t.z. de Win32-bibliotheken).
Het gebruik van deze API wordt dan verborgen via het encrypteren van meta-informatie.
Het bepalen van de nog uit Win32-bibliotheken te importeren symbolen gebeurt door (na
het statisch linken) alle relocaties te overlopen en diegene te zoeken die nog steeds naar een
IAT wijzen.
Als we geen Win32-bibliotheken toevoegen aan het uitvoerbaar bestand zijn er een aantal
moeilijkheden die we – meestal – kunnen vermijden: het API-set-schema, export forwarding,
de aanwezigheid van read-only data in de .text-sectie, en natuurlijk de aanwezigheid van systeemoproepen in het uitvoerbaar bestand. Om de meta-informatie te encrypteren gebruiken
we de methode voorgesteld in Hoofdstuk 3. Deze voegt glue code toe aan het bestand, we
gebruiken de initialisatiefunctie uit deze glue code om de mogelijke initialisatieroutines van
de toegevoegde DLL’s op te roepen.
Indien het oorspronkelijk uitvoerbaar bestand enkel gebruik maakt van Win32-bibliotheken is
er geen verschil tussen het encrypteren van meta-informatie en partieel statisch linken. Deze
tussenoplossing is dan ook enkel van nut indien er een DLL gebruikt wordt in de applicatie die
ontwikkeld werd door een derde partij. Delen van een zelf-ontwikkelde DLL zouden namelijk
beter op broncode-niveau aan het uitvoerbaar bestand toegevoegd kunnen worden.
4.7
4.7.1
Beperkingen en mogelijke uitbreidingen
Iteratief bepalen van benodigde DLL’s
In de huidige implementatie worden alle DLL’s toegevoegd waarvan het te herschrijven uitvoerbaar bestand eventueel afhankelijk is alvorens een ACVG op te bouwen en te bepalen
welke delen uit deze DLL’s we eigenlijk nodig hebben. Op deze manier worden er mogelijkerwijs DLL’s toegevoegd die uiteindelijk helemaal niet gebruikt blijken te worden. Dit is al bij
een aantal eenvoudige voorbeeldapplicaties gebleken, en is ook mogelijk bij meer ingewikkelde
applicaties. Indien een onnodige DLL een initialisatieroutine heeft, zal deze (samen met geassocieerde code en data) echter wel aanwezig zijn in het uiteindelijk uitvoerbaar bestand. Een
mogelijke uitbreiding van de huidige implementatie die het toevoegen van onnodige DLL’s
47
Figuur 4.2: Een voorbeeld van het gebruik van een afhankelijkheidsgraaf.
(op enkele uitzonderingen na) vermijdt, is om ze ´e´en voor ´e´en toe te voegen en voor elke DLL
afzonderlijk te bepalen welke delen eruit eigenlijk benodigd zijn. Dit zal het probleem met
de initialisatieroutines oplossen en de prestatie van de implementatie verbeteren.
Om de DLL’s iteratief toe te kunnen voegen moet er eerst een afhankelijkheidsgraaf opgesteld
worden die de afhankelijkheden tussen DLL’s voorstelt. Het opstellen van deze graaf wordt
dan de nieuwe eerste stap van de methode en wordt uitgevoerd in plaats van het bepalen
van de benodigde DLL’s. Dit opstellen gebeurt aan de hand van de importtabellen van de
DLL’s, en houdt rekening met export forwarding en het API-set-schema. Het uitvoerbaar
bestand wordt de beginknoop van de graaf, en elke DLL is bereikbaar vanaf deze knoop
(voor een voorbeeld van zo’n afhankelijkheidsgraaf, zie Figuur 4.2). Eens de graaf opgesteld is,
beginnen we met het toevoegen van DLL’s. We kiezen een DLL waarvan enkel het uitvoerbaar
bestand afhankelijk is, voegen deze toe, bouwen een ACVG op en verwijderen de onnodige
delen van de DLL. Alvorens de ACVG terug om te zetten naar gewone secties, passen we
de afhankelijkheidsgraaf aan. De net toegevoegde DLL wordt uit de graaf verwijderd en
nieuwe afhankelijkheden tussen het uitvoerbaar bestand en DLL’s (afkomstig van de nieuw
toegevoegde delen) worden aangebracht. Indien een knoop niet meer bereikbaar is vanaf de
beginknoop wordt deze uit de graaf verwijderd. Als de hele procedure doorlopen is en we
terug over gewone secties beschikken, kiezen we weer een DLL waarvan enkel het uitvoerbaar
bestand afhankelijk is, voegen deze toe etc. tot er geen DLL’s meer overblijven in de graaf.
In het voorbeeld op Figuur 4.2 wordt eerst DLL1 toegevoegd. Hierdoor wordt het uitvoerbaar
bestand afhankelijk van DLL2, dat vervolgens toegevoegd wordt. Beide DLL’s zijn afhankelijk
van DLL3, maar geen van de toegevoegde delen uit de eerste twee DLL’s is eigenlijk afhankelijk van DLL3. Bijgevolg wordt deze DLL niet toegevoegd, en belandt zijn initialisatieroutine
dus niet in het herschreven uitvoerbaar bestand.
Twee of meer DLL’s die van elkaar afhankelijk zijn (bv. user32 en gdi32 op Figuur 2.11)
vormen een uitzonderingsgeval. Deze vormen een cyclische deelgraaf die wel als ´e´en knoop
48
beschouwen en moeten aan het uitvoerbaar bestand tegelijkertijd toegevoegd worden. Deze
gevallen kunnen weer leiden tot initialisatieroutines die bijgehouden worden zonder dat de
bijhorende DLL eigenlijk gebruikt wordt, maar in de praktijk verwachten we geen al te complexe afhankelijkheidsgrafen omdat dit tegengesteld is aan de best practices op vlak van
softwarearchitectuur. Een blik op de architectuur van de systeembibliotheken en de architectuur van een aantal applicaties (VLC Media Player, Internet Explorer en Word) heeft deze
verwachtingen bevestigd.
4.7.2
Opsplitsen data-secties
We zouden liefst een nauwkeurigere representatie van de data-secties hebben. Een data-sectie
kan namelijk opgedeeld worden in de subsecties waaruit ze oorspronkelijk is opgebouwd, maar
bij de toegevoegde DLL’s ontbreekt de informatie om dit te doen. Indien er structuren in deze
secties aanwezig zijn die we kennen (bv. export- en importtabellen) kunnen we deze secties
wel in drie opsplitsen: ´e´en deel voor de gekende structuur, ´e´en deel erna, en de structuur zelf.
Export- of importtabellen kunnen gewoon verwijderd worden omdat deze niet meer gebruikt
worden.
4.7.3
Onderscheid code en data
Zoals gezegd in Sectie 2.3.2 kan er in principe geen perfect onderscheid gemaakt worden
tussen code en data. Onder bepaalde voorwaarden is dit echter wel mogelijk. Indien we er
bijvoorbeeld van uit kunnen gaan dat de gebruikte toolchain geen data in de .text-secties
van het bestand zal plaatsen moet er geen onderscheid gemaakt worden tussen code en data
en volstaat lineair disassembleren. In het geval van Visual Studio weten we dat de enige
data die normaal gezien in de .text-secties geplaatst wordt, bestaat uit sprongtabellen. Een
verbeterde versie van lineair disassembleren die alle sprongtabellen kan herkennen is ook in
staat om een perfect onderscheid te maken tussen code en data. De implementatie van lineair
disassembleren aanwezig in Diablo was hier niet toe in staat omdat ervan uitgegaan werd dat
sprongtabellen zich in .rdata-secties bevonden. Deze werd in het kader van deze masterproef
wel uitgebreid om sprongtabellen in de .text-secties te herkennen en kan dus – behalve indien
er gebruik gemaakt wordt van een onbekend sprongtabel-patroon – een perfect onderscheid
maken.
Bij de systeembibliotheken is er – per uitzondering – in de .text-secties ook read-only data
aanwezig naast de sprongtabellen. Een implementatie van recursief disassembleren werd geschreven om zo goed mogelijk code en data te onderscheiden, maar deze is niet perfect. Een
garantie op het dynamisch maken van een perfect onderscheid kan in dit geval niet gegeven
worden. Het zou wel mogelijk zijn om de .text-secties van de systeembibliotheken eenmalig
te laten disassembleren door IDA Pro (zie Sectie 2.3.3). IDA is namelijk beter in staat is om
code en data te onderscheiden dan de huidige disassembler in Diablo. Het gemaakte onderscheid zou wel nog niet perfect zijn. Eventuele fouten moeten handmatig verbeterd worden
om tot een perfect onderscheid te komen zodat het resultaat opgeslagen kan worden voor
gebruik door Diablo.
IDA Pro zou dus instaan voor het disassembleren van de .text-secties afkomstig uit de sys49
teembibliotheken, en Diablo voor de .text-secties afkomstig uit andere DLL’s en het uitvoerbaar bestand. Voor dit laatste volstaat lineair disassembleren aangevuld met sprongtabelherkenning, behalve voor bestanden die geobfusceerd zijn met als specifiek doel het disassembleren te belemmeren. Dit soort bestanden kan dan ook niet ondersteund worden.
4.7.4
Initialisatie van de systeembibliotheken
Er zijn drie systeembibliotheken die we speciaal behandelen op vlak van initialisatieroutines:
kernel32, KernelBase en ntdll (zie Sectie 2.6). Deze drie DLL’s worden in de adresruimte van
elke applicatie geladen, zelfs indien de applicatie ze niet gebruikt. Het herschreven uitvoerbaar bestand is van geen enkele DLL afhankelijk, maar deze drie DLL’s zullen toch geladen
en ge¨ınitialiseerd worden alvorens de applicatie begint uit te voeren. Er zal nooit gebruik
gemaakt worden van deze geladen DLL’s aangezien de benodigde delen uit deze DLL’s in
het uitvoerbaar bestand zelf aanwezig zijn. De uit deze DLL’s afkomstige delen die aan het
uitvoerbaar bestand zijn toegevoegd moeten natuurlijk wel ge¨ınitialiseerd worden.
De initialisatieroutines moeten in een specifieke volgorde aangeroepen worden: eerst die van
ntdll, dan KernelBase en tot slot kernel32. De initialisatieroutine van kernel32 maakt namelijk gebruik van functies ge¨ımporteerd uit KernelBase en ntdll, en die van KernelBase maakt
gebruik van functies ge¨ımporteerd uit ntdll. In tegenstelling tot bij andere DLL’s (zoals kernel32 en KernelBase) heeft ntdll geen initialisatieroutine die ingesteld staat als beginpunt in
de headers. ntdll exporteert een functie – LdrInitializeThunk – die ntdll initialiseert, maar
deze functie doet eigenlijk veel meer dan enkel ntdll initialiseren [63]. LdrInitializeThunk is
eigenlijk het beginpunt voor alle user-mode threads, en staat dus ook in voor het initialiseren van deze threads, alsook voor het initialiseren van het proces. Al deze initialisatiecode
(voor ntdll, de thread en het proces) is niet duidelijk gescheiden. LdrInitializeThunk in zijn
volledigheid uitvoeren is niet mogelijk vanwege de andere initialisatie die hij probeert uit
te voeren. Het is om deze reden dat deze drie DLL’s niet ge¨ınitialiseerd worden in de huidige implementatie. Een aantal door deze DLL’s ge¨exporteerde functies die vertrouwen op
ge¨ınitialiseerde data werken daarom nog niet.
Idealiter zouden we de stukken uit LdrInitializeThunk (en de functies die deze oproept)
vinden die instaan voor het initialiseren van ntdll en enkel deze stukken uitvoeren, alvorens
de initialisatieroutines van KernelBase en kernel32 uit te voeren. Om deze stukken te vinden
kan de broncode van ReactOS een goede referentie zijn [64]. ReactOS is een open-source
project dat als doel heeft om als vervanging te kunnen dienen voor de Windows NT familie
van besturingssystemen (met compatibiliteit voor applicaties en drivers). Ik heb de broncode
voor de ReactOS implementatie van ntdll en LdrInitializeThunk reeds bekeken en deze kan
zeker als referentie dienen om de Windows-versie beter te begrijpen.
50
Hoofdstuk 5
Compatibiliteit
Een volledig statisch gelinkte applicatie is enkel compatibel met die versie van Windows
waarop ze gelinkt is. Daarom wordt er in dit hoofdstuk een methode voorgesteld om deze
toch compatibel te maken met meerdere versies van Windows. Dit gebeurt door de applicatie
zichzelf dynamisch te laten herschrijven. De applicatie zal zichzelf aanpassen aan de specifieke
kernel die op het systeem aanwezig is. Ten eerste wordt de algemene werking van de methode
voorgesteld, vervolgens wordt er in meer detail gegaan, en als laatste worden de beperkingen
en mogelijke uitbreidingen van de methode besproken.
5.1
Algemene werking
In de huidige implementatie gaan we ervan uit dat de applicatie altijd uitgevoerd wordt in de
WoW64-omgeving (zie Sectie 2.7.5). Eerst ontwikkelen we een methode om compatibiliteit
met zowel Windows 8 als Windows 8.1 te voorzien. Het enige verschil tussen deze twee
versies is dat de SCO’s veranderd zijn, de systeemoproepen gebeuren op dezelfde wijze (zie
Sectie 2.7.1). In Sectie 5.3 wordt de methode uitgebreid om ook compatibiliteit met Windows
7 (waarop systeemoproepen enigszins anders gebeuren) te voorzien.
Om ervoor te zorgen dat een statisch gelinkt uitvoerbaar bestand op meerdere versies van
Windows werkt, moeten de SCW’s in het bestand zichzelf dynamisch kunnen aanpassen
aan een specifieke versie van Windows. Om dit mogelijk te maken wordt er net zoals in
Sectie 3.1 glue code aan het bestand toegevoegd. Deze glue code is gelijkaardig aan die bij
het encrypteren van meta-informatie en bevat ook een initialisatiefunctie en een laadfunctie.
De initialisatiefunctie zal het nieuwe beginpunt voor de applicatie worden, en elke SCW
zal aangepast worden om de laadfunctie op te roepen in plaats van een systeemoproep (of
vervangende instructie) uit te voeren. De laadfunctie past dan de SCW waarvan ze opgeroepen
werd aan, zodat deze nu een systeemoproep doet met de juiste SCO. Hoe we deze SCO bepalen
wordt uitgelegd in Sectie 5.2.2.
5.2
5.2.1
Glue code
Aanpassingen in Diablo
Het toevoegen van de glue code gebeurt op dezelfde wijze als in Sectie 3.3.1. De ACVG
wordt aangepast zodat de initialisatiefunctie het nieuwe beginpunt van de applicatie wordt,
alvorens naar het oorspronkelijke beginpunt van de applicatie te springen. Alle SCW’s worden
51
gevonden door alle door Diablo herkende functies te overlopen en die functies te zoeken
waarvan de naam begint met “Nt”. Op Windows 8/8.1 ziet een SCW uit een WoW64systeembibliotheek er als volgt uit:
mov
call
ret
eax, SCO
large dword ptr fs:0xC0
Na aanpassing door Diablo ziet een SCW er dan als volgt uit:
mov
call
nop
nop
ret
eax, SCW Hash
laadfunctie
SCW Hash is hierin de hash van de naam van de SCW. Deze wordt door de laadfunctie
gebruikt om de bijhorende SCO te vinden. De ‘call laadfunctie’-instructie zal dynamisch
door de laadfunctie terug herschreven worden naar de oorspronkelijke indirecte call-instructie.
Doordat deze instructie gebruik maakt van het fs-segment is ze twee bytes groter. Daarom
worden er na de call-instructie twee padding bytes (NOP-instructies) toegevoegd.
5.2.2
Initialisatiefunctie
De initialisatiefunctie zal – net als bij het encrypteren van meta-informatie – een aantal
variabelen initialiseren die door de rest van de glue code gebruikt worden. Om bijvoorbeeld
de SCW’s dynamisch te kunnen herschrijven moet de geheugenbescherming van een specifiek
geheugenbereik aangepast kunnen worden. Dit kan enkel via een systeemoproep naar de
NtProtectVirtualMemory system service. De bijhorende SCW maakt deel uit van de glue
code, maar om deze uit te kunnen voeren moet eerst de juiste SCO bepaald worden.
Om SCO’s te bepalen maken we gebruik van een hulpfunctie uit de glue code die als argument een gehashte SCW-naam neemt. De huidige implementatie ondersteunt enkel SCW’s
die deel uitmaken van de Native API, en die men dus kan vinden in ntdll. In Sectie 3.3.2
werd er verwezen naar een methode om het adres van kernel32 te vinden. Deze methode
kan ook gebruikt worden om het adres van ntdll te vinden. De hulpfunctie zal – gegeven de
gehashte naam en het adres van ntdll – alle namen in de ENT van ntdll hashen en deze vergelijken met de gehashte naam. Indien deze twee overeenkomen is de gezochte SCW gevonden.
We weten hoe een SCW er uitziet qua instructies, en weten dus dat de SCO (bestaande
uit vier bytes) te vinden valt op het adres van de SCW vermeerderd met ´e´en (de opcode
van de mov-instructie). Er worden dus nooit instructies uit de op het systeem aanwezige
systeembibliotheken uitgevoerd, enkel de SCO’s uit deze bibliotheken worden gebruikt.
52
Naast het initialiseren van een aantal variabelen staat de initialisatiefunctie ook in voor het
oproepen van de initialisatieroutines van de bibliotheken die aan het uitvoerbaar bestand
zijn toegevoegd (zie Sectie 4.5).
5.2.3
Laadfunctie
Om de SCW waarvan ze is opgeroepen te herschrijven, begint de laadfunctie met de juiste
SCO te bepalen m.b.v. de net besproken hulpfunctie en de hash van de SCW-naam. Deze
hashwaarde is net voor de oproep naar de laadfunctie in het eax-register geplaatst. Vervolgens
wordt NtProtectVirtualMemory opgeroepen om het geheugenbereik waarop er geschreven
gaat worden schrijfbaar, leesbaar een uitvoerbaar te maken. De nodige aanpassingen worden
gemaakt en NtProtectVirtualMemory wordt nogmaals opgeroepen om het geheugenbereik
zijn oude geheugenbescherming terug te geven. Aan het einde van de laadfunctie wordt er
terug naar de eerste instructie van de SCW gesprongen, zodat de normale uitvoering van de
applicatie kan hervatten.
Bij het encrypteren van de meta-informatie werd er na het herschrijven de functie FlushInstructionCache uitgevoerd (zie Sectie 3.3.3) omdat zelf-aanpassende code tot problemen met
de instructiecaches kan leiden. Omdat we geen functies willen importeren uit andere DLL’s
zullen we hier geen gebruik maken van deze functie. Op de x86-architectuur moeten de instructiecaches eigenlijk niet geflushed worden na het aanpassen van uitvoerbare code, maar
er moet wel rekening gehouden worden met het feit dat de CPU speculatief code uitvoert [47].
Het uitvoeren van een jmp-instructie alvorens het uitvoeren van herschreven instructies is voldoende om problemen met deze speculatieve uitvoering te vermijden. Aan deze voorwaarde
is voldaan aangezien we op het einde van de laadfunctie naar de eerste instructie van de SCW
springen.
5.3
Windows 7
Op Windows 7 zien SCW’s in een WoW64-omgeving er net iets anders uit. De WoW64-index
maakt geen deel uit van de SCO zoals op Windows 8/8.1, maar wordt afzonderlijk in het
ecx-register geplaatst. Daarnaast wordt het adres van het eerste argument op de stapel in
het edx-register geplaatst, en wordt er een waarde van de stapel gehaald na het uitvoeren
van de systeemoproep:
mov
mov
lea
call
add
ret
eax, SCO
ecx, WoW64 index
edx, [esp+4]
large dword ptr fs:0xC0
esp, 4
Om met deze verschillende manier om de WoW64-laag aan te roepen om te gaan, is de methode op een aantal punten aangepast. Ten eerste wordt op het moment dat Diablo de SCW’s
53
herschrijft ervoor gezorgd dat er voldoende NOP-instructies toegevoegd worden. Zo zal de
laadfunctie in staat zijn de SCW ook in deze stijl te herschrijven indien nodig. Daarnaast is
de initialisatiefunctie aangepast om vast te stellen of de applicatie wordt uitgevoerd op Windows 7. De hulpfunctie voor het bepalen van de SCO’s staat nu ook in voor het afzonderlijk
bepalen van de WoW64-index voor een specifieke SCW.
5.4
5.4.1
Beperkingen en mogelijk uitbreidingen
Grafische systeemoproepen
Momenteel worden enkel systeemoproepen naar system services uit de gewone SSDT ondersteund. Daarnaast bevat de Shadow SSDT de adressen voor grafische system services, en
de gelijknamige SCW’s zijn aanwezig in user32 en gdi32 (zie Sectie 2.7.4). Deze SCW’s zijn
echter moeilijker terug te vinden dan die uit ntdll. Sommigen worden wel ge¨exporteerd, maar
onder een andere naam. Als we user32 disassembleren met IDA Pro blijkt er bijvoorbeeld een
SCW te bestaan met interne naam NtGdiD3dContextCreate, die ge¨exporteerd wordt onder
een andere naam, namelijk DdEntry1. Andere SCW’s worden helemaal niet ge¨exporteerd, en
zijn dus interne functies die enkel vanuit andere ge¨exporteerde functies opgeroepen worden.
Er zijn twee uitbreidingen die moeten gebeuren worden om grafische systeemoproepen te
ondersteunen. Ten eerste moeten de bijhorende SCW’s in Diablo herkend worden zodat we
ze kunnen aanpassen. In de huidige implementatie gebeurt dit door te zoeken naar functies
waarvan de naam begint met ‘Nt’. Enkel functies waarop er een symbool staat krijgen namen in Diablo en de enige symboolinformatie waarover Diablo beschikt bij een toegevoegde
DLL heeft te maken met dynamische symbolen. Dit zorgt er voor dat we momenteel enkel
ge¨exporteerde SCW’s kunnen herkennen. Er zijn twee oplossingen voor dit probleem: het
patroon van een SCW herkennen, of gebruik maken van de onge¨exporteerde symbolen van de
systeembibliotheken via PDB-bestanden. Dit laatste wordt ook gedaan door IDA Pro, mochten we dus IDA gebruiken om de systeembibliotheken te disassembleren (zoals voorgesteld
in Sectie 4.7.3) dan zouden we al over deze symbolen beschikken.
De tweede uitbreiding die moet gebeuren is dat de glue code in staat moet zijn om de SCW’s
terug te vinden in de systeembibliotheken, zelfs als ze niet ge¨exporteerd worden. Een mogelijke
oplossing zou zijn om deze SCW’s op te zoeken via functie die deze SCW’s oproepen, die
wel ge¨exporteerd worden. We zouden in de glue code deze ge¨exporteerde functies kunnen
analyseren en het controleverloop kunnen volgen tot het patroon van een SCW herkend
wordt. In de huidige glue code is er reeds ondersteuning aanwezig om uitvoerbare code te
analyseren.
5.4.2
Verandering structuur SCW
De structuur van SCW’s is veranderd tussen Windows 7 en Windows 8. Eventueel toekomstige
veranderingen in deze structuur kunnen niet voorspeld worden en de glue code kan hier niet
op voorzien worden. Met deze methode kan dus geen compatibiliteit met toekomstige versies
van Windows verzekerd worden.
54
De uitvoerbare bestanden zijn zo herschreven dat er nooit code uitgevoerd wordt die zich niet
in het bestand zelf bevindt. Indien er code uitgevoerd zou worden uit een DLL die dynamisch
gevonden moet worden zou de applicatie kwetsbaar zijn voor hooking. Om compatibiliteit te
voorzien wordt er wel gebruik gemaakt van de SCO’s afkomstig uit die DLL’s. Een mogelijke
aanpassing om betere compatibiliteit met toekomstige versies van Windows te voorzien zou
zijn om in plaats van enkel van deze dynamisch gevonden SCO’s te gebruiken, de volledige
SCW’s te gebruiken. Dit komt weer neer op dynamisch linken met de systeembibliotheken
aanwezig op het systeem waarop de applicatie uitgevoerd wordt. Het gebruik van deze SCW’s
zou gemakkelijk verborgen kunnen worden via het encrypteren van meta-informatie, maar
hooking zou terug een mogelijkheid zijn. De geplaatste hooks zouden zich wel op het laagste niveau van de applicatie bevinden, net boven de kernel. Enkel de communicatie tussen
applicatie en kernel zou dus onderschept kunnen worden.
5.4.3
Veranderingen van system services
Het is mogelijk dat er tussen verschillende versies van Windows definities van system services
(qua argumenten) veranderen, of dat er system services vervangen worden door of samengenomen worden tot andere system services. Dit soort veranderingen vinden niet vaak plaats,
maar met dit soort uitzonderingen moet er wel rekening gehouden worden in de glue code.
Een voorbeeld van zo’n uitzondering vinden we in de functionaliteit die te maken heeft met
consolevensters.
kernel32 exporteert een aantal functies zoals GetConsoleMode, WriteConsoleA en AttachConsole die met consolevensters te maken hebben. In een WoW64-omgeving op Windows 7
zijn die functies eigenlijk SCW’s of roepen ze SCW’s op. Deze SCW’s gebruiken een SCO met
als SST-index 2, wat ons doet vermoeden dat deze gebruik maken van een derde, onbekende
SSDT (waarover ook op het internet niets valt te vinden). Op een 32-bits Windows 7 – net als
in een WoW64-omgeving op Windows 8/8.1 – worden deze functies ge¨ımplementeerd m.b.v.
van een functie (ConsoleClientCallServer genaamd op Windows 7 en ConsoleCallServer genaamd op Windows 8/8.1) die een oproep doet naar de Native API in ntdll.
Nog een voorbeeld van zo’n verandering heeft te maken met het schrijven naar een consolevenster. Hiervoor kan de functie WriteFile uit kernel32 opgeroepen worden met een argument dat aanduid dat er naar het consolevenster geschreven moet worden. Op Windows 7
zal er in de implementatie van WriteFile aan de hand van dit argument beslist worden om
een sprong te maken naar WriteConsoleA dan wel de NtWriteFile-SCW op te roepen. Op
Windows 8/8.1 wordt er geen onderscheid gemaakt tussen deze twee gevallen en gebeurt er
altijd een oproep naar NtWriteFile. Omdat deze functionaliteit gebruikt wordt door de ‘Hello
World!’-testapplicatie, is er in de glue code een oplossing aanwezig voor deze uitzondering.
Op Windows 8/8.1 zal de NtWriteFile-SCW opgeroepen worden en op Windows 7 de SCW
waar WriteConsoleA gebruik van maakt.
5.4.4
32-bits Windows
We gaan ervan uit dat de 32-bits applicatie altijd in een WoW64-omgeving (en dus op een
64-bits Windows) uitgevoerd wordt. Om er voor te zorgen dat de applicatie ook op een 3255
bits Windows zou kunnen uitvoeren, moeten er een aantal aanpassingen gebeuren. De glue
code moet aangepast worden om te bepalen of de applicatie uitgevoerd wordt in een WoW64omgeving, en indien niet of ze uitgevoerd wordt op een Intel-processor of een AMD-processor
(zie Sectie 2.7.2). Deze informatie wordt dan gebruikt om de SCW’s op de correcte wijze te
herschrijven.
56
Hoofdstuk 6
Implementatie
In dit hoofdstuk wordt het werk geleverd bij de implementatie besproken. Deze bespreking
wordt ingedeeld naargelang de verschillende fasen die er in mijn masterproef waren: eerst
het inwerken, vervolgens het implementeren van het encrypteren van meta-informatie, dan
de implementatie van statisch linken en uiteindelijk het implementeren van de methode om
compatibiliteit te voorzien. We be¨eindigen het hoofdstuk met een aantal statistieken gegenereerd met behulp van SVN, het versiebeheersysteem gebruikt voor Diablo. Alle functionaliteit
beschreven in dit hoofdstuk werd door mij ge¨ımplementeerd, tenzij anders vermeld.
6.1
Inwerken
Alvorens aan de eigenlijke implementatie te beginnen heb ik aanverwante literatuur (over
linken, het PE-formaat, Diablo etc.) gelezen en mijzelf vertrouwd gemaakt met het Diabloraamwerk. Omdat Diablo een redelijk steile leercurve heeft, heb ik veel tijd besteed aan
het lezen en debuggen van de broncode. Diablo had een PE-backend (zie Sectie 2.2.2) maar
deze bevond zich nog niet in een werkende staat. Er was ondersteuning voor het inlezen
van bestanden maar nog niet om deze ook weer weg te schrijven. Het implementeren van
deze functionaliteit vormde een relatief eenvoudige instap tot programmeren in Diablo. Bij
het emuleren van het linkerproces moest er voor gezorgd worden dat de importtabellen op
correcte wijze opgebouwd werden, en op het einde door Diablo ook correct weggeschreven
werden. De PE-backend is doorheen de rest van de masterproef nog verder uitgebreid en
verbeterd door zowel mij als mijn begeleider, Stijn Volckaert.
6.2
Encrypteren van meta-informatie
Opdat het uitvoerbaar bestand in staat zou zijn zichzelf aan te passen en de benodigde
symbolen te importeren wordt er gebruik gemaakt van glue code. Deze bestaat uit de initialisatiefunctie, de laadfunctie en een aantal hulpfuncties (waaronder hashfuncties). Al deze
code werd – op de hashfuncties na – volledig in assembler geschreven. Op een later moment
werd deze assembler herschreven naar C-broncode gemengd met inline assembler voor een
betere onderhoudbaarheid. In Diablo werd er code geschreven om de importtabellen van
een uitvoerbaar bestand te lokaliseren, te lezen en nadien te verwijderen. Er werden datastructuren voorzien om bij te houden welke symbolen ge¨ımporteerd werden uit welke DLL’s.
Daarna werd er uitgezocht hoe de glue code aan het parent-object toegevoegd kon worden,
en vervolgens werd de code geschreven die instaat voor het aanpassen van de ACVG.
57
Bij het aanpassen van de ACVG worden alle instructies die ge¨ımporteerde symbolen gebruiken
opgezocht, en ook deze informatie werd aan de datastructuren toegevoegd. Er werd code
geschreven die op basis van de informatie in de datastructuren de DYNIMP-tabel en DLLtabel genereert en als data-subsecties aan een data-sectie van de glue code toevoegt, zodat
de glue code deze kan vinden. De DYNIMP-tabel bevat de hashes van de terugkeeradressen,
maar deze zijn op het moment dat de DYNIMP-tabel in Diablo gecre¨eerd wordt nog niet
gekend. Pas nadat de ACVG weer omgezet is naar secties zijn deze terugkeeradressen gekend.
Daarom werd er op het moment van het toevoegen van de DYNIMP-tabel in Diablo een broker
call ge¨ınstalleerd. Deze broker call wordt opgeroepen op het moment dat de terugkeeradressen
gekend zijn en berekent dan deze hashes. Al deze code in Diablo was oorspronkelijk in C
geschreven, maar werd op een later moment omgezet naar C++. Op deze manier konden
de zelf geschreven datastructuren vervangen worden door de Standard Template Library
containers.
6.3
Statisch linken
In het begin van deze fase werd de glue code uitgebreid met de Hacker Disassembler Engine
of HDE [65]. Deze tool wordt gebruikt om dynamisch door DLL’s ge¨exporteerde functies
te analyseren en te kopi¨eren naar geheugen dat door de glue code gealloceerd wordt. Het
plan was om dit te gebruiken om de functies die door het uitvoerbaar bestand ge¨ımporteerd
worden dynamisch naar nieuwe geheugenpagina’s te kopi¨eren en vanaf daar uit te voeren. Op
deze manier kon tijdens de uitvoering een dynamisch gelinkt uitvoerbaar bestand omgezet
worden naar een statisch gelinkt uitvoerbaar bestand. Omdat de uitvoerbare code gekopieerd
werd uit componenten die dynamisch gevonden werden, was hooking hier echter nog altijd
een mogelijkheid. Er werd dus beslist om deze functies – en alle andere benodigde delen
uit DLL’s – reeds door Diablo aan het uitvoerbaar bestand toe te laten voegen. Tijdens dit
verkennend werk (dat ongeveer een week heeft geduurd) heb ik wel al veel naar de structuur
van de systeembibliotheken gekeken, wat bij de uiteindelijke implementatie ook een hulp was.
Om ervoor te zorgen dat de toevoegde DLL’s correct bleven functioneren moest er in Diablo code geschreven worden om de relocaties tussen de secties te reconstrueren uit de base
relocaties. Vervolgens werd er code geschreven om de ACVG aan te passen zodat symbolen uit de toegevoegde DLL’s statisch gebruikt werden in plaats van dynamisch. Hiervoor
werd verder gebouwd op de datastructuren die reeds aanwezig waren om informatie over
ge¨ımporteerde symbolen etc. bij te houden. Er werd eerst gewerkt met eenvoudige DLL’s.
Eens er ook systeembibliotheken werden toegevoegd, kreeg ik te maken met een aantal problemen: sprongtabellen en andere data in de .text-sectie, het API-set-schema, export forwarding
en import via ordinaal. De implementatie van lineair disassembleren die reeds aanwezig was
in de i386-backend werd door Stijn Volckaert uitgebreid om met sprongtabellen om te gaan.
Deze code heb ik op sommige punten nog aangepast om meer patronen te herkennen. Omdat
er ook andere data naast sprongtabellen aanwezig bleken te zijn in de .text-secties van de systeembibliotheken heb ik vervolgens code geschreven om recursief te kunnen disassembleren.
Om ondersteuning te voorzien voor export forwarding en import via ordinaal moesten er geen
grote aanpassingen gebeuren. Het API-set-schema daarentegen was niet gedocumenteerd en
de enige bron van informatie hierover bestond uit artikels geschreven door reverse engineers
op het internet. Om ondersteuning voor het API-set-schema te kunnen bieden moesten er
58
UTF-16 strings gelezen worden uit een 64-bits DLL. De PE-backend van Diablo biedt echter
nog geen ondersteuning voor 64-bits PE-bestanden. Er werd dus geen gebruik gemaakt van
een bepaalde backend in Diablo voor het inlezen van bestanden. In plaats daarvan werd er een
niet-herbruikbare en tijdelijke oplossing geschreven. Alvorens aan de ondersteuning voor het
API-set-schema te beginnen heb ik alle functionaliteit die ik op het Diablo-raamwerk geschreven had, omgezet van C-broncode naar C++. Vervolgens heb ik de recursieve disassembler
ge¨ımplementeerd en de code geschreven om de .text-secties van de systeembibliotheken samen te voegen. Voor de recursieve disassembler was er functionaliteit nodig om te kunnen
beslissen of een bepaald ge¨exporteerd symbool een functie of een variabele is. Tot slot werd
er code geschreven in zowel Diablo als de glue code om het uitvoeren van initialisatieroutines mogelijk te maken. Hiervoor werd ook functionaliteit ge¨ımplementeerd om te kunnen
beslissen of een bepaalde DLL een systeembibliotheek is of niet.
6.4
Compatibiliteit
Om een methode te kunnen implementeren die compatibiliteit met meerdere versies van
Windows voorziet, moest ik eerst begrijpen hoe het hele gebeuren van een systeemoproep
verloopt. Dit is niet volledig gedocumenteerd door Microsoft zelf. Er bestaan wel artikels
geschreven door reverse engineers op het internet, maar deze artikels zijn niet volledig in hun
omschrijvingen en vaak tegenstrijdig. Ik heb dus ook zelf met IDA Pro de gedisassembleerde
SCW’s, WoW64-laag en kernel-image bekeken.
De glue code werd geschreven in C. De glue code voor het encrypteren van meta-informatie
werd toen ook herschreven naar C om hergebruik van code toe te laten, maar dit hergebruik
was redelijk beperkt. Er werd ook code geschreven in Diablo die de nodige aanpassingen
maakt in de ACVG om verbindingen te maken met deze toegevoegde glue code.
6.5
SVN-statistieken
Al het werk voor deze masterproef gebeurde in een afzonderlijke SVN-branch van de Diablo
repository waarnaar enkel ik en Stijn Volckaert commits hebben gedaan. We kunnen SVNstatistieken genereren in een poging de hoeveelheid werk geleverd tijdens deze masterproef
te kwantificeren. Een eerste mogelijke maat voor het geleverde werk is het aantal lijnen code
dat aangepast of toegevoegd werd. Doorheen de looptijd van de masterproef heb ik 21534
lijnen code aangepast of toegevoegd. Deze cijfers geven echter geen volledig beeld. Als in
een eerste commit een bepaalde lijn code toegevoegd wordt en deze lijn vervolgens tien keer
wordt aangepast in tien afzonderlijke commits dan telt dit als elf aangepaste lijnen.
Een andere maat die we kunnen gebruiken is de toename in het totaal aantal lijnen code in
de branch, maar bij deze maat wordt er niet gedifferentieerd op auteurschap. Deze toename
komt neer op ongeveer 7500 lijnen code. Een laatste maat voor het geleverde werk bekomen
we aan de hand van het SVN-commando ‘blame’. Dit commando gaat voor elke lijn code
in een bestand na wie deze het laatste heeft aangepast, wat kan gebruikt worden te tellen
hoeveel lijnen code in een map met broncodebestanden het laatste zijn aangepast door een
bepaalde auteur. Kijken naar de laatste persoon die een bepaalde lijn heeft aangepast geeft
59
Onderdeel
Lijnen code
Diablo-gebaseerde applicatie
2251
Glue code
1866
Algemene Diablo-code
746
i386-backend
97
PE-backend
∼800
Tabel 6.1: De groottes van de herschreven uitvoerbare bestanden.
natuurlijk ook geen volledig beeld aangezien iemand anders deze lijn kan hebben geschreven
en meerdere malen aangepast alvorens de laatste persoon ze aanpaste. In Tabel 6.1 wordt
deze maat voor door mij geschreven code weergegeven voor verschillende onderdelen.
Voor de PE-backend kon geen nauwkeurige meting gedaan worden. De line endings in deze
bestanden werden tijdens de masterproef veranderd van Windows-stijl naar Unix-stijl met
als gevolg dat bijna alle lijnen code verkeerdelijk als door mij aangepast worden beschouwd.
Handmatig bekijken van de geschreven functies uit deze bestanden leidt tot een schatting
van ongeveer 800 van de 4843 lijnen die door mij geschreven werden. Onder ‘Glue code’
valt zowel de glue code voor het verzorgen van compatibiliteit als de glue code voor het
encrypteren van meta-informatie. Bij het laatste werd zowel de versie geschreven in assembler
als de herschreven C-versie geteld. Met ‘Diablo-gebaseerde applicatie’ wordt de functionaliteit
bedoeld die op het Diablo-raamwerk werd ge¨ımplementeerd om de applicaties te herschrijven
volgens de methodes van het encrypteren van meta-informatie en statisch linken.
60
Hoofdstuk 7
Evaluatie
De in de vorige hoofdstukken voorgestelde methodes worden in dit hoofdstuk ge¨evalueerd.
Aangezien deze methodes proof-of-concept zijn en realistische applicaties herschrijven nog
niet mogelijk is, bestaat deze evaluatie vooral uit het bespreken van de beperkingen van de
methodes en het schatten van de overhead ge¨ıntroduceerd door het gebruik ervan.
7.1
Encrypteren van meta-informatie
Momenteel zijn de belangrijkste beperkingen bij het encrypteren van meta-informatie dat het
importeren van variabelen noch import via ordinaal ondersteund worden. Niet alle mogelijke
uitvoerbare bestanden kunnen dus herschreven worden. De overhead die de methode introduceert bestaat enkel uit de tijd die gespendeerd wordt aan het uitvoeren van de glue code.
Daarom worden de gemiddelde uitvoeringstijden van de initialisatiefunctie en de laadfunctie
gemeten. Deze metingen werden uitgevoerd op een 64-bits Windows 8.1 met een Intel Core
i7 processor.
De uitvoeringstijd van de initialisatiefunctie werd gemeten voor een realistisch scenario waarin
de DLL-tabel de gehashte namen van acht DLL’s (groot en klein) bevat. Het laden van deze
acht DLL’s is de taak waar de initialisatiefunctie het meeste tijd aan besteed. De totale
uitvoeringstijd kwam voor dit scenario na meerdere testen uit op ongeveer 17ms. De initialisatiefunctie neemt door het laden van deze DLL’s eigenlijk de taak van de loader over (zie
Sectie 2.5.2) en deze uitvoeringstijd is dus geen pure overhead.
Voor de laadfunctie werd er een scenario opgezet met een reeks oproepen naar de laadfunctie. Voor elke oproep wordt er een ‘call laadfunctie’-instructie herschreven naar een ‘call
kernel32.symbool’-instructie. Omdat de laadfunctie de namen in de ENT overloopt en hasht
tot de juiste naam gevonden is, verwachten we dat de laadfunctie gemiddeld langer uitvoert
bij DLL’s met een grote ENT. We nemen kernel32 als gebruiksgeval omdat deze een ENT
heeft met een groot aantal namen, namelijk 1551. Er werden drie mogelijke symbolen uit
kernel32 gebruikt: ´e´en dat zich aan het begin van de ENT bevindt, ´e´en dat zich aan het
einde bevindt, en ´e´en dat zich ongeveer in het midden bevindt. Een symbool zal sneller gevonden worden indien het in het begin van de ENT staat, en op deze manier middelen we de
resultaten dus uit.
Hoe meer elementen er aanwezig zijn in de DYNIMP-tabel, hoe langer het gemiddeld duurt
61
voor de laadfunctie om deze te overlopen. We verwachten dus dat de uitvoeringstijd van de
laadfunctie toeneemt naarmate deze tabel groter wordt. Bij een reeks van 99 oproepen kostte
elke oproep gemiddeld 49µs. Bij een reeks van 300 oproepen was dit gemiddeld 51µs, bij
900 was dit gemiddeld 52µs en bij 9000 83µs. Voor een uitvoerbaar bestand waarin er meer
instructies gebruik maken van een ge¨ımporteerd symbool voert de laadfunctie dus iets trager
uit. De totale overhead veroorzaakt door de laadfunctie is beperkt omdat de laadfunctie
hoogstens even veel opgeroepen kan worden als dat er instructies zijn die ge¨ımporteerde
symbolen gebruiken. Uit het disassembleren van een aantal uitvoerbare bestanden blijkt dat
het bij grote, ingewikkelde applicaties mogelijk is dat er duizenden zo’n instructies zijn.
Indien dit het geval is zou de totale overhead nog steeds beperkt blijven tot minder dan een
seconde, verspreid over de uitvoertijd van de applicatie. Enkel indien er op een bepaald punt
van de applicatie – zoals het opstarten – een ongewone concentratie aan oproepen naar de
laadfunctie is, is deze overhead merkbaar.
7.2
Statisch linken
Deze sectie gaat over de methode van het statisch linken die uitgelegd is geweest in Hoofdstuk 4 en Hoofdstuk 5. Deze bestaat eigenlijk uit twee deelmethodes (elk uitgelegd in zijn
eigen hoofdstuk), maar in deze sectie wordt de volledige methode ge¨evalueerd.
7.2.1
Beperkingen
Er zijn een aantal belangrijke beperkingen aan de huidige implementatie van de methode:
• de toegevoegde delen uit kernel32, KernelBase en ntdll worden nog niet ge¨ınitialiseerd.
• er is enkel compatibiliteit met de 64-bits versies van Windows 7, Windows 8 en Windows
8.1.
• er is geen ondersteuning voor grafische systeemoproepen op meerdere versies van Windows.
• eventuele veranderingen in de system services worden niet opgevangen.
• niet alle instructies uit de .text-secties van de systeembibliotheken worden gedisassembleerd, met mogelijke fouten in de applicatie tot gevolg.
• er blijft teveel nutteloze code en data over in het herschreven uitvoerbaar bestand.
7.2.2
Nutteloze code en data
We gaan nog even in op de laatste beperking, namelijk die van de nutteloze code en data.
Deze zorgt er namelijk voor dat de herschreven uitvoerbare bestanden een stuk groter zijn
dan de uitvoerbare bestanden uit de oorspronkelijke, dynamisch gelinkte applicaties. Om
dit probleem te verduidelijken bevat Tabel 7.1 voor een aantal kleine voorbeeldapplicaties
de grootte van het herschreven uitvoerbaar bestand. De uitvoerbare bestanden voor deze
voorbeeldapplicaties hebben allemaal een grootte van 2560 bytes.
62
Applicatie
Grootte (in KB)
HeapAlloc
387
VirtualAlloc
421
CreateThread
435
GetTickCount
435
OpenFile
1025
HelloWorld
2169
HelloWorldExtra
2177
Tabel 7.1: De groottes van de herschreven uitvoerbare bestanden.
In HeapAlloc wordt er geheugen op de heap gealloceerd en beschreven. In VirtualAlloc wordt
hetzelfde gedaan, waarna er een extra geheugenpagina wordt gealloceerd en beschreven. CreateThread doet hetzelfde als VirtualAlloc maar cre¨eert een nieuwe thread om naar de geheugenpagina te schrijven. GetTickCount zal al het voorgaande doen, maar de nieuwe thread zal
nu de – binaire representatie van de – systeemtijd naar de geheugenpagina schrijven. OpenFile
is hetzelfde als GetTickCount maar opent ook een bestand. Tot slot schrijven HelloWorld
en HelloWorldExtra allebei de tekst “Hello World!” naar een consolevenster, maar doet de
tweede versie daarnaast ook alles wat OpenFile doet.
We zien dat ingewikkeldere applicaties in het algemeen groter zijn, maar dat er bepaalde
drempels zijn. Zodra er een functie ge¨ımporteerd wordt die gebruik maakt van de .datasectie van een DLL, wordt deze sectie (samen met geassocieerde code en data) bijgehouden
en is het herschreven uitvoerbaar bestand weer een stuk groter. Als er daarnaast een andere
functie uit dezelfde DLL ge¨ımporteerd wordt die gebruik maakt van deze sectie, dan is de
uiteindelijke kost (qua grootte van het bestand) voor deze ge¨ımporteerde functie kleiner dan
voor de eerste.
De eerste vier bestanden bevatten geen toegevoegde data-secties. De toename in grootte
tussen de eerste drie bestanden is redelijk klein omdat er enkel extra code toegevoegd wordt.
De derde en vierde herschreven bestanden zijn zelfs even groot. De functionaliteit om de
systeemtijd op te vragen was namelijk al aanwezig in de derde applicatie, hoewel deze daar
niet gebruikt werd. Bij de overgang van het vierde naar het vijfde bestand wordt er een
drempel overschreven. Het vijfde bestand is veel groter dan het vierde omdat er in het vijfde
bestand wel een data-sectie aanwezig is. HelloWorld en HelloWorldExtra verschillen niet
veel qua grootte maar wel qua ge¨ımporteerde functies. Het merendeel van de code en data
geassocieerd aan de door HelloWorldExtra ge¨ımporteerde functies was dus reeds aanwezig in
HelloWorld zonder dat het gebruikt werd.
Om meer nutteloze code en data te kunnen verwijderen zou het dus vooral beter zijn om
data-secties beter onder te verdelen in subsecties. Er is natuurlijk ook een bovengrens aan de
63
grootte van het herschreven uitvoerbaar bestand, namelijk de som van de groottes van het
oorspronkelijk uitvoerbaar bestand en de toegevoegde DLL’s.
7.2.3
Evaluatie overhead
Net zoals in Sectie 7.1 bestaat de overhead van deze methode uit de tijd die gespendeerd
wordt aan het uitvoeren van de glue code. Deze metingen werden uitgevoerd op een 64-bits
Windows 8.1 met een Intel Core i7 processor.
Bij het evalueren van de initialisatiefunctie werd er geen bepaald scenario gebruikt omdat
deze functie in elk geval hetzelfde gedrag vertoont. De uitvoeringstijd bleek gemiddeld 26µs
te zijn. De enige variabele factor in de uitvoeringstijd van de laadfunctie bestaat uit de tijd
die het kost om in ntdll’s ENT de naam van de SCW op te zoeken. Dit gebeurt door alle
namen in de ENT te hashen tot de hashes overeenkomen. Namen die in het begin van de ENT
staan worden dus sneller gevonden dan die op het einde. In het scenario dat we gebruiken
wordt de laadfunctie 20 keer opgeroepen om de SCW waarvan de naam het laatst staat in de
ENT te herschrijven. De gemiddelde uitvoeringstijd voor de laadfunctie bleek 35µs te zijn.
De laadfunctie wordt hoogstens ´e´en keer opgeroepen per SCW aanwezig in het uitvoerbaar
bestand en in alle systeembibliotheken tezamen zijn er slechts een paar duizend SCW’s.
De totale uitvoeringstijd van de glue code is dus beperkt tot minder dan een seconde. De
laadfunctie zal op verschillende momenten tijdens de uitvoering van de applicatie opgeroepen
worden, hoewel het natuurlijk te verwachten valt dat er tijdens het opstarten van de applicatie
het vaakst SCW’s voor het eerst opgeroepen zullen worden. Zelfs indien er 1000 SCW’s voor
het eerst opgeroepen zouden worden tijdens het opstarten – een extreem scenario – dan zou
dit opstarten in totaal maar 35ms langer duren. De overhead ge¨ıntroduceerd door de methode
is dus zeer klein.
64
Hoofdstuk 8
Conclusie
In deze scriptie werden twee methodes besproken om het gebruik van bibliotheekinterfaces
af te schermen tegen reverse engineering, door uitvoerbare bestanden te herschrijven. Vooral
de methode van het statisch linken is beloftevol. In deze methode worden er dynamische
bibliotheken waarvoor we de broncode noch de objectbestanden bezitten aan het uitvoerbaar
bestand toegevoegd. Dit gebrek aan informatie zorgt ervoor dat er veel nutteloze code en data
aanwezig is in het uitvoerbaar bestand, waardoor dit bestand onnodig groot is. Hiernaast
zijn er nog een aantal beperkingen in de huidige implementatie van de methode. Bij beide
methodes is het nog niet mogelijk om realistische applicaties te herschrijven, hoewel dit met
de nodige uitbreidingen wel mogelijk zou moeten zijn.
Met het toevoegen van de dynamische bibliotheken waarvan het uitvoerbaar bestand afhankelijk was, worden er ook nieuwe mogelijkheden gecre¨eerd. De code en data uit deze
bibliotheken wordt nu meegeleverd door de ontwikkelaar van applicatie, zodat deze code en
data naar believen aangepast kan worden. Deze kan aangepast worden met het oog op beveiliging, zo kan bijvoorbeeld de code uit de bibliotheken – ook uit de systeembibliotheken –
geobfusceerd worden om reverse engineering tegen te gaan.
65
Bibliografie
[1] Linux, “Linux Documentation: The kernel syscall interface.” [Online]. Available: http://www.cs.fsu.edu/∼baker/devices/lxr/http/source/linux/Documentation/
ABI/stable/syscalls
[2] J. R. Levine, Linkers & Loaders. Morgan Kaufmann Publishers, 2000.
[3] Wikipedia, “DLL Hell.” [Online]. Available: http://en.wikipedia.org/wiki/DLL Hell
[4] L. Van Put, D. Chanet, B. De Bus, B. De Sutter, and K. De Bosschere, “DIABLO: a
reliable, retargetable and extensible link-time rewriting framework,” Proceedings of the
Fifth IEEE International Symposium on Signal Processing and Information Technology,
2005., 2005.
[5] B. De Sutter, B. De Bus, and K. De Bosschere, “Link-time binary rewriting techniques
for program compaction,” ACM Transactions on Programming Languages and Systems
(TOPLAS), vol. 27, no. 5, pp. 882–945, 2005.
[6] B. De Sutter, L. Van Put, D. Chanet, B. De Bus, and K. De Bosschere, “Link-time
compaction and optimization of ARM executables,” ACM Transactions on Embedded
Computing Systems (TECS), vol. 6, no. 1, p. 5, 2007.
[7] M. Madou, B. Anckaert, P. Moseley, S. Debray, B. De Sutter, and K. De Bosschere,
“Software protection through dynamic code mutation,” in International Workshop on
Information Security Applications (WISA), 2005, pp. 194–206.
[8] B. Coppens, B. De Sutter, and K. De Bosschere, “Protecting your software updates,”
pp. 1–1, 2012.
[9] B. Schwarz, S. Debray, and G. Andrews, “Disassembly of executable code revisited,”
Ninth Working Conference on Reverse Engineering, 2002. Proceedings., 2002.
[10] C. Linn and S. Debray, “Obfuscation of executable code to improve resistance to static
disassembly,” Proceedings of the 10th ACM conference on Computer and communication
security - CCS ’03, p. 290, 2003.
[11] R. N. Horspool and N. Marovac, “An approach to the problem of detranslation of computer programs,” The Computer Journal, vol. 23, no. 3, pp. 223–229, 1980.
[12] C. Eagle, The IDA pro book: the unofficial guide to the world’s most popular disassembler.
No Starch Press, 2008.
[13] C. Cifuentes and K. J. Gough, “Decompilation of Binary Programs,” Software Practice
and Experience, vol. 25, pp. 811–829, 1995.
66
[14] M. Christodorescu and S. Jha, “Static analysis of executables to detect malicious patterns,” SSYM’03 Proceedings of the 12th conference on USENIX Security Symposium,
vol. 12, pp. 12–12, 2003.
[15] CLET Team, “Polymorphic Shellcode Engine,” Phrack, vol. 11, no. 61, 2003. [Online].
Available: http://phrack.org/issues/61/9.html
[16] A. Moser, C. Kruegel, and E. Kirda, “Limits of Static Analysis for Malware Detection,”
Twenty-Third Annual Computer Security Applications Conference (ACSAC 2007), pp.
421–430, Dec. 2007.
[17] U. Bayer, A. Moser, C. Kruegel, and E. Kirda, “Dynamic Analysis of Malicious Code,”
pp. 67–77, 2006.
[18] M. Egele, C. Kruegel, E. Kirda, and D. Song, “Dynamic Spyware Analysis,” Analysis,
pp. 233–246, 2007.
[19] T. Bell, “The concept of dynamic analysis,” pp. 216–234, 1999.
[20] I. Ivanov, “API hooking revealed,” The Code Project, 2002.
[21] C. Collberg and C. Thomborson, “Watermarking, tamper-proofing, and obfuscation tools for software protection,” IEEE Transactions on Software Engineering, vol. 28, 2002.
[22] J. Crasta, “ELF Prelinking and what it can do for you.” [Online]. Available:
http://crast.us/james/articles/prelink.php
[23] M. Pietrek, “An In-Depth Look into the Win32 Portable Executable File Format.”
[Online]. Available: http://msdn.microsoft.com/en-us/magazine/cc301805.aspx
[24] C. S. Collberg, J. H. Hartman, S. Babu, and S. K. Udupa, “SLINKY: Static Linking
Reloaded.” in USENIX Annual Technical Conference, General Track, 2005, pp. 309–322.
[25] PEBundle, “PEBundle.” [Online]. Available: http://bitsum.com/pebundle.asp
[26] MoleBox, “MoleBox.” [Online]. Available: http://www.molebox.com/
[27] ReWolf, “DLLPackager.” [Online]. Available: https://code.google.com/p/dllpackager/
[28] Microsoft, “Microsoft Portable Executable and Common Object File Format Specification,” 2013.
[29] J. Leitch, “IAT Hooking Revisited.” [Online]. Available: http://www.autosectools.com/
IAT-Hooking-Revisited.pdf
[30] A. Malik, “DLL Injection and Hooking.” [Online]. Available: http://securityxploded.
com/dll-injection-and-hooking.php
[31] PaX, “ASLR.” [Online]. Available: http://pax.grsecurity.net/docs/aslr.txt
[32] H. Shacham, M. Page, B. Pfaff, E.-J. Goh, N. Modadugu, and D. Boneh, “On the
effectiveness of address-space randomization,” in Proceedings of the 11th ACM conference
on Computer and communications security, 2004, pp. 298–307.
67
[33] M. E. Russinovich, D. A. Solomon, and A. Ionescu, Windows Internals: Covering Windows Server 2008 and Windows Vista, 2009.
[34] Wikipedia, “Native API.” [Online]. Available: http://en.wikipedia.org/wiki/Native API
[35] M. E. Russinovich, “Inside the Native API.” [Online]. Available: http://netcode.cz/
img/83/nativeapi.html
[36] Microsoft, “New Low-Level Binaries.” [Online]. Available: http://msdn.microsoft.com/
library/dd371752.aspx
[37] ——, “Windows API Sets.” [Online]. Available: http://msdn.microsoft.com/en-us/
library/hh802935(v=vs.85).aspx
[38] NirSoft, “Windows 7 Kernel Architecture Changes.” [Online]. Available: http:
//www.nirsoft.net/articles/windows 7 kernel architecture changes.html
[39] Quarkslab, “Runtime DLL name resolution: ApiSetSchema.” [Online]. Available:
http://blog.quarkslab.com/runtime-dll-name-resolution-apisetschema-part-i.html
[40] G. Chapell, “The API Set Schema.” [Online]. Available: http://www.geoffchappell.
com/studies/windows/win32/apisetschema/index.htm
[41] J. Gulbrandsen, “How Do Windows NT System Calls REALLY Work?” [Online]. Available: http://www.codeguru.com/cpp/w-p/system/devicedriverdevelopment/
article.php/c8035/How-Do-Windows-NT-System-Calls-REALLY-Work.htm
[42] G. Hoglund, “A *REAL* NT Rootkit, patching the NT Kernel,” Phrack, vol. 9, no. 55,
1999. [Online]. Available: http://phrack.org/issues/55/5.html#article
[43] Shift32, “Inside KiSystemService.” [Online]. Available: http://shift32.wordpress.com/
2011/10/14/inside-kisystemservice/
[44] Trapframe, “Just enough kernel to get by (part 2) : Syscall & SSDT.” [Online].
Available: http://trapframe.org/just-enough-kernel-to-get-by-2/
[45] D. Lukan, “Hooking the System Service Dispatch Table.” [Online]. Available:
http://resources.infosecinstitute.com/hooking-system-service-dispatch-table-ssdt/
[46] The Honeynet Project, “Get system call address from SSDT.” [Online]. Available:
http://www.honeynet.org/node/438
R 64 and IA-32 Architectures Software Developer’s Manual,”
[47] Intel Corporation, “Intel 2014.
[48] M. Jurczyk, “Windows X86 System Call Table (NT/2000/XP/2003/Vista/2008/7/8).”
[Online]. Available: http://j00ru.vexillium.org/ntapi/
[49] Microsoft, “Running 32-bit Applications.” [Online]. Available: http://msdn.microsoft.
com/en-us/library/windows/desktop/aa384249(v=vs.85).aspx
68
[50] ——, “WoW64 Implementation Details.” [Online]. Available: http://msdn.microsoft.
com/en-us/library/windows/desktop/aa384274(v=vs.85).aspx
[51] K. Johnson, “What’s the difference between the Wow64 and native x86 versions of a
DLL?” [Online]. Available: http://www.nynaeve.net/?p=131
[52] M. Naor and M. Yung, “Universal one-way hash functions and their cryptographic applications,” Proceedings of the twenty-first annual ACM . . . , pp. 33–43, 1989.
[53] Microsoft, “LoadLibrary function.” [Online]. Available: http://msdn.microsoft.com/
en-us/library/windows/desktop/ms684175(v=vs.85).aspx
[54] Wikipedia, “Process Environment Block.” [Online]. Available: http://en.wikipedia.org/
wiki/Process Environment Block
[55] Harmony Security, “Retrieving Kernel32’s Base Address.” [Online]. Available:
http://blog.harmonysecurity.com/2009/06/retrieving-kernel32s-base-address.html
[56] Microsoft, “GetProcAddress function.” [Online]. Available: http://msdn.microsoft.
com/en-us/library/windows/desktop/ms683212(v=vs.85).aspx
[57] ——, “VirtualProtect function.” [Online]. Available: http://msdn.microsoft.com/en-us/
library/windows/desktop/aa366898(v=vs.85).aspx
[58] ——, “FlushInstructionCache function.” [Online]. Available: http://msdn.microsoft.
com/en-us/library/windows/desktop/ms679350(v=vs.85).aspx
[59] S. Chow, P. A. Eisen, H. Johnson, and P. C. van Oorschot, “White-Box Cryptography
and an AES Implementation,” in Revised Papers from the 9th Annual International
Workshop on Selected Areas in Cryptography, 2003, pp. 250–270.
[60] Microsoft, “DllMain entry point.” [Online]. Available: http://msdn.microsoft.com/
en-us/library/windows/desktop/ms682583(v=vs.85).aspx
[61] ——, “Dynamic-Link Library Entry-Point Function.” [Online]. Available: http:
//msdn.microsoft.com/en-us/library/windows/desktop/ms682596(v=vs.85).aspx
[62] ——, “Using Thread Local Storage in a Dynamic-Link Library.” [Online]. Available:
http://msdn.microsoft.com/en-us/library/ms686997(v=vs.85).aspx
[63] K. Johnson, “A catalog of NTDLL kernel mode to user mode callbacks, part 6:
LdrInitializeThunk.” [Online]. Available: http://www.nynaeve.net/?p=205
[64] ReactOS, “ReactOS Project.” [Online]. Available: https://www.reactos.org/
[65] V. Patkov, “Hacker Disassembler Engine.” [Online]. Available: http://vxheavens.com/
vx.php?id=eh04
69