Posts filed under 'RE'
Putain, ça fait 1000 ans que j’ai pas bloggé, je suis surchargé en ce moment. Vous allez vous foutre de ma gueule mais je suis en train d’étudier des choses comme java/CPP/sql/xml/uml, disons que ce ne sont pas les choses les plus inintéressantes mais c’est très très chiant à bosser. J’ai tellement de projets et de rapports à rendre dessus que je n’ai même plus le temps de jouer avec mon Windows
Bref, je passe vite fait pour vous montrer une feature marrant de Windbg (d’ailleur la version 6.10.3.233 est sortie il y moins d’une semaine). Il s’agit d’une extension aidant au debugging de binaire VDM. Je crois qu’un exemple sera plus parlant qu’autre chose :
0:003> !vdmexts.load
0:003> !vdmexts.help
WOW commands are not currently available.
------------- VDMEXTS Debug Extension help:--------------
help [cmd] - Displays this list or gives details on command
ApiProfClr - Clears the api profiling table
ApiProfDmp [options] - Dumps the api profiling table
at 0xXXXX - shows name associated with hex atom #
bp <addr> - Sets a vdm breakpoint
bd/be <n> - Disables/enables vdm breakpoint 'n'
bl - Lists vdm breakpoints
chkheap - Checks WOW kernel's global heap
cia - Dump cursor/icon alias list
d<b|w|d> <addr> [len] - Dump vdm memory
ddemem - Dump dde memory thunks
ddte <addr> - Dump dispatch table entry pointed to by <addr>
denv <bProt> <selEnv> - Dump environment for current task or given selector/segment
df [vector] - Dump protect mode fault handler address
dfh [fh [pdb]] - Dump DOS file handles for current or given PDB
dg <sel> - Dump info on a selector
ddh [seg] - Dump DOS heap chain starting at <seg>:0000
dgh [sel|ownersel] - Dump WOW kernel's global heap
dhdib [@<address>] - Dump dib.drv support structures (DIBINFO)
di [vector] - Dump protect mode interrupt handler address
dma - Dump virtual DMA state
dpd - Dump DPMI DOS memory allocations
dpx - Dump DPMI extended memory allocations
dsft [sft] - Dump all or specified DOS system file tables
dt [-v] <addr> - Dump WOW Task Info
dwp <addr> - Dump WOWPORT structure pointed to by <addr>
e<b|w|d> <addr> <data> - Edit vdm memory
filter [options] - Manipulate logging filter
fs <text to find> - Find text in 16:16 memory (case insensitive)
glock <sel> - Increments the lock count on a moveable segment
gmem - Dumps Global/heap memory alloc'd by wow32
gunlock <sel> - Decrements the lock count on a moveable segment
hgdi16 [-v] <h16> - Returns 32-bit GDI handle for <h16>
hgdi32 [-v] <h32> - Returns 16-bit GDI handle for <h32>
ica - Dump Interrupt Controller state
k - Stack trace
kb - Stack trace with symbols
LastLog - Dumps Last Logged WOW APIs from Circular Buffer
lg [#num] [count] - Dumps NTVDM history log
lgr [#num] [count] - Dumps NTVDM history log (with regs)
lgt [1|2|3] - Sets NTVDM history log timer resolution
lm <sel|modname> - List loaded modules
ln [addr] - Determine near symbols
LogFile [path] - Create/close toggle for iloglevel capture to file
(path defaults to c:\ilog.log)
MsgProfClr - Clears the msg profiling table
MsgProfDmp [options] - Dumps the msg profiling table
ntsd - Gets an NTSD prompt from the VDM prompt
r - Dump registers
rmcb - Dumps dpmi real mode callbacks
SetLogLevel xx - Sets the WOW Logging Level
StepTrace - Toggles Single Step Tracing On/Off
sx - Displays debugging options
sx<d|e> <flag> - Disables/enables debugging options
timer - Display 8253 timer 0 information
u [addr] [len] - Unassemble vdm code with symbols
wc <hwnd16> - Dumps the window class structure of <hwnd16>
ww <hwnd16> - Dumps the window structure of <hwnd16>
x <symbol> - Get symbol's value
-------------- i386 specific commands
fpu - Dump 487 state
pdump - Dumps profile info to file \profile.out
pint - Sets the profile interval
pstart - Causes profiling to start
pstop - Causes profiling to stop
vdmtib [addr] - Dumps the register context in the vdmtib
where [options] can be displayed with 'help <cmd>'
Avec ça on peut voir facilement les exeptions et interrupt handlers mis en place par le binaire ntvdm.exe avec les couples (segment:offset) :
0:000> !vdmexts.df ; Dump protect mode fault handler address
00: 00C7:00001200
01: 00C7:00001205
02: 00C7:0000120A
03: 00C7:0000120F
04: 00C7:00001214
05: 00C7:00001219
06: 00C7:0000121E
07: 00C7:00001223
08: 00C7:00001228
09: 00C7:0000122D
0A: 00C7:00001232
0B: 00C7:00001237
0C: 00C7:0000123C
0D: 00C7:00001241
0E: 00C7:00001246
0F: 00C7:0000124B
10: 00C7:00001250
11: 00C7:00001255
12: 00C7:0000125A
13: 00C7:0000125F
14: 00C7:00001264
15: 00C7:00001269
16: 00C7:0000126E
17: 00C7:00001273
18: 00C7:00001278
19: 00C7:0000127D
1A: 00C7:00001282
1B: 00C7:00001287
1C: 00C7:0000128C
1D: 00C7:00001291
1E: 00C7:00001296
1F: 00C7:0000129B
0:000> !vdmexts.di ; Dump protect mode interrupt handler address
00: 00C7:00000D00
01: 00C7:00001307
02: 00C7:00000D0A
03: 00C7:00001307
04: 00C7:00000D14
05: 00C7:00000D19
06: 00C7:00000D1E
07: 00C7:00000D23
08: 00C7:00000D28
09: 00C7:00000D2D
0A: 00C7:00000D32
0B: 00C7:00000D37
0C: 00C7:00000D3C
0D: 00C7:00000D41
0E: 00C7:00000D46
0F: 00C7:00000D4B
10: 00C7:00001393
11: 00C7:00000D55
12: 00C7:00000D5A
13: 00C7:00002870
14: 00C7:00000D64
15: 00C7:000014A7
16: 00C7:00000D6E
17: 00C7:00000D73
18: 00C7:00000D78
19: 00C7:00001308
1A: 00C7:00000D82
1B: 00C7:00000D87
1C: 00C7:00000D8C
1D: 00C7:00000D91
1E: 00C7:00000D96
1F: 00C7:00000D9B
20: 00C7:00000DA0
21: 00C7:000004E6
22: 00C7:00000DAA
23: 00C7:00000DAF
24: 00C7:00000DB4
25: 00C7:000029CD
26: 00C7:00002A85
27: 00C7:00000DC3
28: 00C7:000012B8
29: 00C7:00000DCD
2A: 00C7:0000315E
2B: 00C7:00000DD7
2C: 00C7:00000DDC
2D: 00C7:00000DE1
2E: 00C7:00000DE6
2F: 00C7:00000DEB
30: 00C7:00001307
31: 00C7:000012DC
32: 00C7:00000DFA
33: 00C7:0000155D
34: 00C7:00000E04
35: 00C7:00000E09
36: 00C7:00000E0E
37: 00C7:00000E13
38: 00C7:00000E18
39: 00C7:00000E1D
3A: 00C7:00000E22
3B: 00C7:00000E27
3C: 00C7:00000E2C
3D: 00C7:00000E31
3E: 00C7:00000E36
3F: 00C7:00000E3B
40: 00C7:00000E40
41: 00C7:00001307
42: 00C7:00000E4A
43: 00C7:00000E4F
44: 00C7:00000E54
45: 00C7:00000E59
46: 00C7:00000E5E
47: 00C7:00000E63
48: 00C7:00000E68
49: 00C7:00000E6D
4A: 00C7:00000E72
4B: 00C7:00000E77
4C: 00C7:00000E7C
4D: 00C7:00000E81
4E: 00C7:00000E86
4F: 00C7:00000E8B
50: 00C7:00000E90
51: 00C7:00000E95
52: 00C7:00000E9A
53: 00C7:00000E9F
54: 00C7:00000EA4
55: 00C7:00000EA9
56: 00C7:00000EAE
57: 00C7:00000EB3
58: 00C7:00000EB8
59: 00C7:00000EBD
5A: 00C7:00000EC2
5B: 00C7:00000EC7
5C: 00C7:0000318B
5D: 00C7:00000ED1
5E: 00C7:00000ED6
5F: 00C7:00000EDB
60: 00C7:00000EE0
61: 00C7:00000EE5
62: 00C7:00000EEA
63: 00C7:00000EEF
64: 00C7:00000EF4
65: 00C7:00000EF9
66: 00C7:00000EFE
67: 00C7:00000F03
68: 00C7:00000F08
69: 00C7:00000F0D
6A: 00C7:00000F12
6B: 00C7:00000F17
6C: 00C7:00000F1C
6D: 00C7:00000F21
6E: 00C7:00000F26
6F: 00C7:00000F2B
70: 00C7:00000F30
71: 00C7:00000F35
72: 00C7:00000F3A
73: 00C7:00000F3F
74: 00C7:00000F44
75: 00C7:00000F49
76: 00C7:00000F4E
77: 00C7:00000F53
78: 00C7:00000F58
79: 00C7:00000F5D
7A: 00C7:00000F62
7B: 00C7:00000F67
7C: 00C7:00000F6C
7D: 00C7:00000F71
7E: 00C7:00000F76
7F: 00C7:00000F7B
80: 00C7:00000F80
81: 00C7:00000F85
82: 00C7:00000F8A
83: 00C7:00000F8F
84: 00C7:00000F94
85: 00C7:00000F99
86: 00C7:00000F9E
87: 00C7:00000FA3
88: 00C7:00000FA8
89: 00C7:00000FAD
8A: 00C7:00000FB2
8B: 00C7:00000FB7
8C: 00C7:00000FBC
8D: 00C7:00000FC1
8E: 00C7:00000FC6
8F: 00C7:00000FCB
90: 00C7:00000FD0
91: 00C7:00000FD5
92: 00C7:00000FDA
93: 00C7:00000FDF
94: 00C7:00000FE4
95: 00C7:00000FE9
96: 00C7:00000FEE
97: 00C7:00000FF3
98: 00C7:00000FF8
99: 00C7:00000FFD
9A: 00C7:00001002
9B: 00C7:00001007
9C: 00C7:0000100C
9D: 00C7:00001011
9E: 00C7:00001016
9F: 00C7:0000101B
A0: 00C7:00001020
A1: 00C7:00001025
A2: 00C7:0000102A
A3: 00C7:0000102F
A4: 00C7:00001034
A5: 00C7:00001039
A6: 00C7:0000103E
A7: 00C7:00001043
A8: 00C7:00001048
A9: 00C7:0000104D
AA: 00C7:00001052
AB: 00C7:00001057
AC: 00C7:0000105C
AD: 00C7:00001061
AE: 00C7:00001066
AF: 00C7:0000106B
B0: 00C7:00001070
B1: 00C7:00001075
B2: 00C7:0000107A
B3: 00C7:0000107F
B4: 00C7:00001084
B5: 00C7:00001089
B6: 00C7:0000108E
B7: 00C7:00001093
B8: 00C7:00001098
B9: 00C7:0000109D
BA: 00C7:000010A2
BB: 00C7:000010A7
BC: 00C7:000010AC
BD: 00C7:000010B1
BE: 00C7:000010B6
BF: 00C7:000010BB
C0: 00C7:000010C0
C1: 00C7:000010C5
C2: 00C7:000010CA
C3: 00C7:000010CF
C4: 00C7:000010D4
C5: 00C7:000010D9
C6: 00C7:000010DE
C7: 00C7:000010E3
C8: 00C7:000010E8
C9: 00C7:000010ED
CA: 00C7:000010F2
CB: 00C7:000010F7
CC: 00C7:000010FC
CD: 00C7:00001101
CE: 00C7:00001106
CF: 00C7:0000110B
D0: 00C7:00001110
D1: 00C7:00001115
D2: 00C7:0000111A
D3: 00C7:0000111F
D4: 00C7:00001124
D5: 00C7:00001129
D6: 00C7:0000112E
D7: 00C7:00001133
D8: 00C7:00001138
D9: 00C7:0000113D
DA: 00C7:00001142
DB: 00C7:00001147
DC: 00C7:0000114C
DD: 00C7:00001151
DE: 00C7:00001156
DF: 00C7:0000115B
E0: 00C7:00001160
E1: 00C7:00001165
E2: 00C7:0000116A
E3: 00C7:0000116F
E4: 00C7:00001174
E5: 00C7:00001179
E6: 00C7:0000117E
E7: 00C7:00001183
E8: 00C7:00001188
E9: 00C7:0000118D
EA: 00C7:00001192
EB: 00C7:00001197
EC: 00C7:0000119C
ED: 00C7:000011A1
EE: 00C7:000011A6
EF: 00C7:000011AB
F0: 00C7:000011B0
F1: 00C7:000011B5
F2: 00C7:000011BA
F3: 00C7:000011BF
F4: 00C7:000011C4
F5: 00C7:000011C9
F6: 00C7:000011CE
F7: 00C7:000011D3
F8: 00C7:000011D8
F9: 00C7:000011DD
FA: 00C7:000011E2
FB: 00C7:000011E7
FC: 00C7:000011EC
FD: 00C7:000011F1
FE: 00C7:000011F6
FF: 00C7:000011FB
Je sais, je balance tout cela sans explications mais je connais quelqu’un que ça intéressa au plus haut point. Je continuerais l’exploration de la machine virtuelle DOS prochainement, il faut juste que je trouve du temps pour le faire. Sinon pour ceux qui se le demande Abyss n’est pas mort, je projette aussi d’en faire quelque chose de marrant dans le futur :]
novembre 25th, 2008
J’avais dit que je coderais une version générique pour mon outil TaskPwdDmp, c’est fait ! Après quelques retours me signalant que mon programme ne marchait à cause de l’utilisation d’offsets harcodés pour retrouver la fonction schedsvc!DecryptCredentials je me suis décidé à faire quelque chose de propre. Au lieu de fournir une liste de tous les offets possible de Windows XP à 2003 avec toutes les versions de DLLs possibles j’ai préféré opter pour une autre méthode plus simple.
La fonction DecryptCredentials de schedsvc.dll n’est pas exportée, le seul moyen donc de connaître son adresse précise est d’utiliser les symbols fournit par Microsoft sur leur site. Ainsi des programmes comme IDA, WinDbg ou Process Explorer utilisent ses symbols pour rendre le debugging/dissasembling/tracing/monitoring de binaires Windows plus aisé.
L’idée est donc simple, rendre l’outil TaskPwdDmp capable d’utiliser lui aussi les symbols pour obtenir l’adresse d’une fonction précise. En utilisant le moteur d’aide au debugging qui se charge d’obtenir la bonne version de symbols de manière dynamique on évite de se prendre la tête avec la version de l’OS, du Service Pack, de la localisation et de schedsvc.dll.
Les symbols sont en général représentés par des fichiers d’extensions .pdb. Par défaut Visual Studio génère un fichier .pdb pour chaque fichier .obj, en fonction des options de compilations bien sur. Microsoft met à disposition ses fichiers .pdb pour presque toutes les versions des binaires de son OS x86 ou x64, allant de Windows 2000 jusqu’à Windows Server 2008.
L’outil va donc aller chercher sur le net les symbols pour la DLL schedsvc pour lire l’adresse de la fonction DecryptCredentials. Cela rajoute donc une contrainte qui demande d’avoir l’accès au net mais de l’autre coté nous obtenons plus de fiabilité.
Pour obtenir les informations d’aide au debugging on utilise la librairie DbgHelp.dll. Cette DLL dont la dernière version est fournie dans les Debugging Tools permet de manipuler toutes les APIs de gestion des symbols, pour être plus précis : l’application des fichiers .pdb aux modules chargés en mémoire, aux stackframes, aux threads ainsi que l’aide du parsing entre les fichiers sources et binaires associés. D’un autre coté, nous avons aussi la DLL symsrv.dll qui elle à pour rôle de gérer le symbol server et le symbol store, les endroits ou sont disponibles les symbols et ou les stocker.
Il faut d’abord initialiser le moteur de gestion des symbols avec l’API SymInitialize, cette API demande le handle du process auquel seront appliquées toutes les APIs suivante ainsi qu’un argument spécifiant ou non si on doit chargé tous les symbols de tous les modules. Dans notre cas nous n’avons besoin que des symbols d’un seul module, ceux de schedsvc.dll, nous allons donc éviter de demander à DbgHelp.dll de retrouver tous les symbols. Pour cela, il est possible de spécifier à l’aide de SymSetOptions le flag SYMOPT_DEFERRED_LOADS qui demande au gestionnaire de symbols de les charger quand ceux ci sont nécessaire, cela évite de consommer trop d’espace mémoire et d’attendre 1000 ans qu’ils soient tous mappés.
Ensuite avec SymLoadModuleEx nous demandons de charger les symbols pour le module schedsvc.dll du binaire svchost.exe qui l’héberge. C’est à ce moment qu’intervient le gestionnaire symsrv, voyant que le .pdb n’existe pas dans le symbol store montré par la variable d’environnement _NT_SYMBOL_PATH , il va directement télécharger le .pdb (s’il existe !) depuis le symbol server qui lui aussi peut être définit avec NT_SYMBOL_PATH. En général ce symbol server est ‘http://msdl.microsoft.com/download/symbols’, on peut d’ailleur voir les requêtes tentées par DbgHelp.dll (visiblent sous forme de messages de debug) avant de faire appel à symsrv.dll :
DBGHELP: _NT_SYMBOL_PATH: SRV*C:\WINDOWS\TEMP*http://msdl.microsoft.com/download/symbols
DBGHELP: Symbol Search Path: .;SRV*C:\WINDOWS\TEMP*http://msdl.microsoft.com/download/symbols
DBGHELP: .\schedsvc.pdb - file not found
DBGHELP: .\dll\schedsvc.pdb - file not found
DBGHELP: .\symbols\dll\schedsvc.pdb - file not found
SYMSRV: schedsvc.pdb from http://msdl.microsoft.com/download/symbols: 56991 bytes - 0 percent copied
DBGHELP: schedsvc - public symbols
C:\WINDOWS\TEMP\schedsvc.pdb\21D8A0C07CFF463CA338812BD32887191\schedsvc.pdb
Normalement symsrv nous demande dans une DialogBox d’accepter une licence, pour éviter cela on crée le fichier ‘symsrv.yes’ dans le même répertoire que symsrv.dll. Pour plus d’infos voir la ProcessEula dans symsrv.dll.
Après l’API SymFromName est capable à partir d’un nom de retrouver toutes les infos symboliques associées, on obtient une structure SYMBOL_INFO qui contient notamment l’adresse en mémoire.
typedef struct _SYMBOL_INFO
{
ULONG SizeOfStruct;
ULONG TypeIndex;
ULONG64 Reserved[2];
ULONG Index;
ULONG Size;
ULONG64 ModBase;
ULONG Flags;
ULONG64 Value;
ULONG64 Address;
ULONG Register;
ULONG Scope;
ULONG Tag;
ULONG NameLen;
ULONG MaxNameLen;
TCHAR Name[1];
} SYMBOL_INFO, *PSYMBOL_INFO;
A partir de là 2 choix s’offrent à nous.
- Le binaire qui injecte la DLL va télécharger les symbols sur le net et envoyer l’information à la DLL injectée dans svchost.exe. Cela demande de mettre un place un mécanisme d’IPC.
- La dll injectée retrouve elle même les symbols sur le net directement depuis le processus svchost.exe.
Au début j’ai opté pour la 2ème solution parce que c’était plus simple à implémenter, qui plus est,dans le cas ou il existe un firewall sur le b0x, une connexion extérieure depuis un processus svchost.exe est relativement plus « normale » que celle provenant d’un binaire inconnu.
Après de nombreux essais mon code dans la DLL injectée ne marchait pas alors qu’un binaire stand-alone avec le même code fonctionnait quand à lui. De ce que j’ai pu voir, il existe plusieurs classes dans symsrv.dll qui sont : StoreWinHTTP, StoreHTTP, StoreWinInet et StoreUNC. Ces classes définissent quelles DLLs seront utilisées pour accéder au net, par exemple StoreWinHTTP utilise la DLL winhttp.dll et StoreWinInet utilise wininet.dll. En regardant avec Process Monitor j’ai vu que mon binaire stand-alone utilisait la librairie wininet alors que le même code dans la DLL injectée tentait une connexion avec winhttp.dll. Bref, j’ai un bug quantique et je n’arrive pas à trouver comment symsrv choisit quelle DLL il va utiliser, c’est très bizzare.
Changement de direction donc, retour à la 1ère solution. Le binaire va retrouver les symbols sur le net et les communiquer à la DLL. La communication entre les 2 processes est réalisée avec un named pipe qu’on crée avec CreateNamedPipe, on se place en écoute avec ConnectNamedPipe. Le processus d’injection écrit dans le pipe l’adresse de DecryptCredentials avec un simple WriteFile. La DLL connaissant l’adresse peut maintenant hooker la fonction et le binaire principal faire appel à la fonction ITask::GetAccountInformation pour trigger le hook.
Au final on a donc le code suivant qui retrouve l’adresse de DecryptCredentials :
/*++
Routine Description:
RetrieveDecryptCredentialsAddress
Arguments:
Task Scheduler svchost'pid.
Return Value:
Address of DecryptCredentials. 0 otherwise.
--*/
DWORD RetrieveDecryptCredentialsAddress(DWORD TaskSrvPid)
{
PSYMBOL_INFO Symbol;
HANDLE hProcess;
ULONG64 SymBase;
ULONG64 Address=0;
SymSetOptions(SYMOPT_UNDNAME|SYMOPT_DEBUG|SYMOPT_DEFERRED_LOADS|SYMOPT_PUBLICS_ONLY);
hProcess=OpenProcess(PROCESS_ALL_ACCESS, FALSE, TaskSrvPid);
if(hProcess==NULL)
{
printf("Error with OpenProcess : %lu\n", GetLastError());
return 0;
}
//
// Set _NT_SYMBOL_PATH for the current process
//
SetEnvironmentVariable("_NT_SYMBOL_PATH", "SRV*C:\\WINDOWS\\TEMP*http://msdl.microsoft.com/download/symbols");
if(!SymInitialize(hProcess, NULL, TRUE))
{
printf("Error with SymInitialize : %lu\n", GetLastError());
CloseHandle(hProcess);
return 0;
}
//
// Load symbol module from database
//
SymBase=SymLoadModuleEx(hProcess, NULL, TaskSchedDll, NULL, (ULONG64)GetRemoteHandle(TaskSrvPid, TaskSchedDll), 0, NULL, 0);
//
// If the module is already loaded, the return value is zero and GetLastError returns ERROR_SUCCESS
//
if((SymBase==0) && (GetLastError()!=ERROR_SUCCESS))
{
printf("Error with SymLoadModuleEx : %lu\n", GetLastError());
SymCleanup(GetCurrentProcess());
CloseHandle(hProcess);
return 0;
}
//
// Allocate symbol struct
//
Symbol=(PSYMBOL_INFO)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(SYMBOL_INFO)+MAX_SYM_NAME);
if(Symbol==NULL)
{
printf("Cannot allocate Symbol struct\n");
SymCleanup(GetCurrentProcess());
CloseHandle(hProcess);
return 0;
}
RtlZeroMemory(Symbol, sizeof(SYMBOL_INFO)+MAX_SYM_NAME);
Symbol->SizeOfStruct=sizeof(SYMBOL_INFO);
Symbol->MaxNameLen=MAX_SYM_NAME;
//
// Retrieve address of DecryptCredentials
//
if(!SymFromName(hProcess, "DecryptCredentials", Symbol))
{
printf("Error with SymFromName : %lu\n", GetLastError());
CloseHandle(hProcess);
HeapFree(GetProcessHeap(), 0, Symbol);
SymCleanup(GetCurrentProcess());
return 0;
}
Address=Symbol->Address;
printf("DecryptCredentials function is at : 0x%I64x\n", Address);
//
// Clean
//
SymCleanup(hProcess);
HeapFree(GetProcessHeap(), 0, Symbol);
CloseHandle(hProcess);
return (DWORD)Address;
}
A noter que le 5ème paramètre de SymLoadModuleEx est l’ImageBase du module dans le process svchost.exe, d’ou ma fonction GetRemoteHandle qui va retrouver l’ImageBase de schedsvc.dll à distance avec CreateToolhelp32Snapshot (option TH32CS_SNAPMODULE).
Pour finir nous avons donc la sortie :
C:\sym>TaskPwdDmp.exe
Windows Task Scheduler credentials dumper
By Ivanlef0u, thanks to Nicolas Ruff
BE M4D!
Works on Windows XP and 2003
Retrieves schedsvc!DecryptCrendetials address with symbols
/|
\`O.o'
=(_|_)=
U
There is 1 tasks in queue
Current tasks are :
Calculatrice
schedsvc.dll found in process [svchost.exe:1744]
Task Scheduler svchost pid is : 1744
DecryptCredentials function is at : 0x76b22962
Injecting Dll TaskPwdDmpDll.dll in process : 1744
FullDllPath : C:\sym\TaskPwdDmpDll.dll
[Msg from PID 1744]-> Dll injected in process 1744
[Msg from PID 1744]-> schedsvc dll is at : 0x76B10000
[Msg from PID 1744]-> Listenning thread created with TID : 856
[Msg from PID 1744]-> schedsvc!DecryptCredentials is at: 0x76b22962
Dumping credentials ...
[Msg from PID 1744]-> Credentials fu:fr
Dll successfully unloaded
Vous trouverez le binaire ici :
http://ivanlef0u.fr/repo/TaskPwdDmpSymbols.rar
Voilà j’espère que cette solution vous plait, j’attends vos retours avec impatience.
Merci à 0vercl0k pour les beta-tests :p
octobre 27th, 2008
Aujourd’hui, on va jouer avec le virtual mode, plus précisément sur son implémentation sous Windows. Alors accrochez votre ceinture et préparez-vous pour un plongeon au coeur des CPU et de Windows.
Pour comprendre ce qu’est ce mode il faut se rappeler que les vieux CPUs x86 étaient en 16 bits et fonctionnaient uniquement en real mode comme le 8086, un mode dans lequel on pouvait adresser un peu plus de 1MB de RAM grâce à la segmentation, la gestion des interruptions et exceptions passait par l’IVT (Interrupt Vector Table), une table de couples segment:offset pour chaque handler. Bref c’était roots, pas de pagination, pas de protected mode, mais pour l’époque ca envoyait du lourd. En parlant d’envoyer du lourd je vous conseil d’écouter ce track de ‘I am God Songs’ de Black Sheep Wall :
Modest Machine
Plus tard est arrivé le 80386, qui pour des raisons de compatibilité supporte le real mode mais pose les bases du protected mode, en même temps il introduit le virtual mode, l’exécution d’une tâche real mode dans un environnement protégé. Permettant ainsi de faire tourner des binaires DOS (16 bits) depuis un Windows 9x jusqu’a un Vista. En fait c’est depuis que j’ai regardé les specs du SMM que j’ai commencé à m’intéresser au mode V8086 juste pour le fun, il est clair que plus personne ne code de binaire 16 bits mais retrouver l’implémentation de ce mode est quelque chose d’assez passionnant surtout que cela fait appel à un bon nombre de composants du système.
Avant tout, j’insiste sur le fait qu’il faut bien comprendre que le virtual mode est un fonctionnement prévu par le CPU, ce n’est une émulation purement software d’une tâche 16 bits fait par un programme user-land. D’ailleurs Intel fournit au chapitre 15 ’8086 emulation’ de son manual ‘Intel 64 and IA-32 Architectures Software Developer’s Manual Volume 3A: System Programming Guide‘ toutes les informations nécessaire au programmeur pour mettre en place ce mécanisme.
Cette fois, je vais changer un peu de méthode, au lieu de creuser en partant de l’implémentation sous Windows vers le CPU, je vais partir des docs Intel puis tenter de retrouver les composants Windows correspondant. Pourquoi ? Parce qu’il est plus simple de comprendre le rôle et la position d’un composant si on sait ce qu’il représente pour le CPU, spécialement dans le cas présent ! Qui plus est, l’utilisation de cette feature fait appel à un trop grand nombre de fonctionnalités pour être comprise en partant « du haut », on retrouve pelle mêle des programmes 32 bits, une api native, des interruptions, des librairies 16 bits tournant en ring3, des gestionnaires d’interruptions à la fois en virtual mode et protected mode, ainsi de suite.
C’est partit, on bachote la doc, section 15.2 ‘Virtual-8086 mode’. Le virtual mode est activé lorsque le VM flag (bit 17) de l’EFlags est à 1, attention on ne modifie pas ce bit avec ‘pushfd ; pop eax ; or eax, 0×20000; push eax; popfd’. Notez bien que l’EFlags est spécifique à un thread, chaque thread ayant un contexte (ensemble de registres) qui lui est propre.
Justement, le passage du VM flag à 1 peut se faire de plusieurs manières. En utilisant un TSS 32 bits dans lequel le VM flag du champ EFlag est à 1. Le TSS (Task State Segment) est juste une structure contenant le contexte d’une tâche comme le montre le schéma suivant :

Le task register (tr) manipulé avec les instructions LTR (load task register) et STR (store task register) et est en fait un sélecteur de segment référençant un TSS descriptor dans la GDT du core. La prise en compte d’un TSS se fait en accédant avec un FAR JMP ou FAR CALL sur une task gate (équivalent d’une callgate pour une tâche) pouvant se situé aussi bien dans la GDT que la LDT.
Sauf que sous Windows, ce n’est pas réalisé du tout de cette manière, j’ai été un peu vite :] mais ca fait toujours de revoir la doc sur les TSS et puis ça sera utile pour la suite.
Windows implémente le passage en V8086 sans utiliser de TSS, trop contraignant. L’API native NtVdmControl fournit le Service VdmStartExecution :
NTSTATUS
NtVdmControl(
IN VDMSERVICECLASS Service,
IN OUT PVOID ServiceData
)
/*++
Routine Description:
This routine is the entry point for controlling Vdms.
Arguments:
Service -- Specifies what service is to be performed
ServiceData -- Supplies a pointer to service specific data
Return Value:
--*/
typedef enum _VdmServiceClass {
VdmStartExecution,
VdmQueueInterrupt,
VdmDelayInterrupt,
VdmInitialize,
VdmFeatures,
VdmSetInt21Handler,
VdmQueryDir,
VdmPrinterDirectIoOpen,
VdmPrinterDirectIoClose,
VdmPrinterInitialize,
VdmSetLdtEntries,
VdmSetProcessLdtInfo,
VdmAdlibEmulation,
VdmPMCliControl,
VdmQueryVdmProcess
} VDMSERVICECLASS, *PVDMSERVICECLASS;
Ce service fait appel à la fonction du noyau VdmpStartExecution, comme son nom l’indique cette fonction va faire passer le thread en virtual mode, pour cela VdmSwapContext va mettre à jour la KTRAP_FRAME crée par KiFastCallEntry (elle même appelée par l’instruction SYSENTER de la fonction KiFastSystemCall fournit par ntdll.dll). En fait la KTRAP_FRAME est juste une structure contenant l’état de tous les registres lors du passage en kernel-land afin, permettant ainsi de les restaurer en revenant du syscall :
kd> dt nt!_KTRAP_FRAME
+0x000 DbgEbp : Uint4B
+0x004 DbgEip : Uint4B
+0x008 DbgArgMark : Uint4B
+0x00c DbgArgPointer : Uint4B
+0x010 TempSegCs : Uint4B
+0x014 TempEsp : Uint4B
+0x018 Dr0 : Uint4B
+0x01c Dr1 : Uint4B
+0x020 Dr2 : Uint4B
+0x024 Dr3 : Uint4B
+0x028 Dr6 : Uint4B
+0x02c Dr7 : Uint4B
+0x030 SegGs : Uint4B
+0x034 SegEs : Uint4B
+0x038 SegDs : Uint4B
+0x03c Edx : Uint4B
+0x040 Ecx : Uint4B
+0x044 Eax : Uint4B
+0x048 PreviousPreviousMode : Uint4B
+0x04c ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x050 SegFs : Uint4B
+0x054 Edi : Uint4B
+0x058 Esi : Uint4B
+0x05c Ebx : Uint4B
+0x060 Ebp : Uint4B
+0x064 ErrCode : Uint4B
+0x068 Eip : Uint4B
+0x06c SegCs : Uint4B
+0x070 EFlags : Uint4B
+0x074 HardwareEsp : Uint4B
+0x078 HardwareSegSs : Uint4B
+0x07c V86Es : Uint4B
+0x080 V86Ds : Uint4B
+0x084 V86Fs : Uint4B
+0x088 V86Gs : Uint4B
Une des fonctions de VdmpStartExecution est d’activer le VM Flag dans l’EFlags de la KTRAP_FRAME. Le problème c’est que la fonction qui est chargée de revenir en user-land, KiServiceExit, qui utilise donc l’instruction SYSEXIT va restaurer l’EFlags avec un simple popfd mais comme je l’ai dit auparavant, on ne peut mettre ce bit à 1 avec cette méthode. L’astuce est que KiServiceExit va tester le VM Flag dans la KTRAP_FRAME, si jamais il est à 1 alors elle effectue un passage du protected mode ring0 au virtual mode et en branchant sur une instruction IRET au lieu de sysexit ! Comme l’instruction IRET récupère sur la stack EIP, CS, EFlags, ESP, SS et que le CPU autorise le VM Flag à 1 dans un IRET, le passage en virtual mode est effectué.
A partir de là, on sait comment notre noyau gère le passage du protected mode au virtual mode, pour l’autre sens on verra plus tard.
Reste que savoir passer en virtual mode ne fait pas tout, en effet comme l’explique la doc, la gestion du virtual mode fait appel à 2 parties distinctes du système. D’abord en kernel-land il est nécessaire d’avoir un ‘virtual-8086 monitor’, un module noyau chargé de gérer l’initialisation du virtual mode comme je l’ai dit plus haut, puis la gestion de l’émulation des I/Os, interruptions et exceptions. Ce monitor se retrouve donc très proche des features de base de l’OS, parfois directement codé dans les handlers de l’IDT. Ensuite un ’8086 operating system service’, un programme permettant de représentant la tâche en virtual-mode sous forme de programme ‘normal’ par rapport au système, par exemple en real mode seul le premier méga de mémoire est accessible donc en utilisant la pagination on peut avoir plusieurs process émulant le virtual-mode en leur sein avant dans leur mémoire basse notre programme DOS. Sous Windows c’est le programme processes ntvdm.exe (NT Virtual DOS Machine) qui joue ce rôle. Enfin, lorsqu’on se trouve en virtual mode, notre code s’éxecute à un CPL ring3, l’IOPL vaut normalement 0, même si il est possible de modifier sa valeur avec la clé ‘VdmIOPL’ situé dans \HKLM\SYSTEM\CurrentControlSet\Control\Wow. En fonction de l’IOPL les instructions suivantes sont dites ‘sensibles’ c’est à dire quelles doivent être émulées par le monitor : CLI, STI, PUSHF, POPF, INT x et IRET.
Continuons de parcourir le man Intel, intéressons nous à la gestion des interruptions et exceptions, le gros morceau en fait, car si exécuter du code en virtual mode n’est pas trop difficile par contre définir le comportement du CPU dans le cas d’une interruption ou exception est autre chose.
Intel définit 3 types de classes pour les interruptions et exceptions :
- La première classe, regroupe à la fois les interruptions hardware (provenant de l’APIC), les NMI et les exceptions. Elles sont gérées en protected mode ring0 par les handlers de l’OS, ce que signifie que chaque gestionnaire d’interruptions ou d’exceptions doit vérifier le mode du CPU pour savoir s’il était en virtual mode avant de brancher sur le handler. Cette façon de fonctionner est donc assez lourde à mettre en place car chaque handler de l’IDT devra être capable de gérer des interruptions provenant aussi bien du virtual mode ou du protected mode. Dans le cas du noyau de Windows, on retrouve cette implémentation chaque handle fait appel à Ki386VdmReflectException ou Ki386VdmReflectException_A (Même chose que Ki386VdmReflectException sauf que l’IRQL est levé à APC_LEVEL) pour dispatcher l’exception au monitor V8086 en fonction du numéro de trap, pour info sous Windows KiTrap00, 01, 03, 04, 05, 07, 0C utilisent Ki386VdmReflectException_A et KiTrap06 et 0D utilisent Ki386VdmReflectException, notez bien la différentiation des handlers 06 (#UD Invalid Opcode) et (#GP General Protection) c’est important pour la suite. Reste le cas des NMIs, d’après ce que j’ai vu Windows s’en fou et gère les NMIs avec KiTrap02 sans prendre en compte le mode dans lequel se situait le core au moment de l’arrivée de l’interruption non masquable.
- La seconde classe est constituée des interruptions hardware masquables, quand je dis masquable c’est relatif à la position de l’IF flag de l’EFLags, qui, si il est à 1, permet d’inhiber l’interruption du core par ces interruptions. A partir de là imaginez que vous être en virtual mode, votre programme tourne tranquillement, il décide d’exécuter l’instruction CLI pour désactiver les interruptions, comme cette instruction est ‘sensible’ vous vous retrouver dans la partie ring0 de votre monitor à devoir émuler cette instruction. Normalement CLI met à 0 le IF flag de l’EFlags, le souci c’est qu’en virtual mode, autoriser une application qui se trouve être en réalité un programme user-land (en ring3 donc) à modifier ce bit peut poser de gros problèmes, simplement en empêchant les interruptions hardware d’arriver au core. Pour remédier à ce problème, Intel fournit une virtual mode extension, représenté par le VME flag (bit 0) du CR4, cette extension offre une gestion ‘virtuelle’ des interruptions hardware à travers 2 nouveaux bits de l’EFlags, le VIF (Virtual Intterrupt Flag) et le VIP (Virtual Interrupt Pending flag). Le VIF est tout simplement une virtualisation de l’IF, lorsqu’en en virtual mode avec l’extension active, les instructions CLI et STI sont exécutées, celles-ci iront modifier le VIF flag. Au moment de l’arrivée d’une interruption hardware sur le core délivrée par la local APIC, le handler ring0 du protected mode vérifie l’état du VIF flag, si il est à 1 alors il peut renvoyer l’interruption pour quelle soit gérée par le monitor V8086, sinon le monitor place le VIP à 1, signifiant qu’une ou des interruptions sont en attente. Dans ce cas, lorsque le programme 8086 exécute l’instruction STI, le core contrôle l’état du VIP, si il est à 0, il met juste le VIF à 1, par contre si le VIP est à 1 alors génère une #GP pour que le monitor ring0 s’occupe d’émuler le comportement de l’interruption en attente. Sous Windows, le noyau se charge d’activer l’extension VME du CR4 avec KeI386VdmInitialize qui envoie un IPI (Interprocessor Interrupt) sur tous les processeurs à l’aide de KiIpiGenericCall pour exécuter Ki386VdmEnablePentiumExtentions. Il est possible de désactiver l’extension VME avec la clé ‘DisableVme’ dans \HKLM\SYSTEM\CurrentControlSet\Control\Wow.
- Enfin, le dernière type d’interruptions correspond aux interruptions software, c’est à dire celles générées par l’instruction INT xx. Leur gestion dépend de plusieurs paramètres qui sont : l’état du bit VME, la valeur de l’IOPL et l’état du bit dans Interruption Redirection Bit Map du TSS courant. Le IntDirectionMap est un bitmap de 32 bytes situé dans le TSS, si le bit correspondant à une interruption vaut 0 alors son activation sera redirigé vers le handler pointé par l’IVT du programme 8086, si il est 1, l’interruption est renvoyé vers le handler de l’IDT #GP. Intel propose 6 méthodes pour traiter ces interruptions software :

Pour le moment, on sait que le VME est à 1, que l’IOPL est inférieur à 3, reste à trouver l’état des bits dans le IntDirectionMap, pour cela on examine le contenu du TSS :
kd> r tr
Last set context:
tr=00000028
kd> dg 28
P Si Gr Pr Lo
Sel Base Limit Type l ze an es ng Flags
---- -------- -------- ---------- - -- -- -- -- --------
0028 80042000 000020ab TSS32 Busy 0 Nb By P Nl 0000008b
kd> .tss 28
Unable to get program counter
eax=eb0bb70f ebx=50535051 ecx=8bc93302 edx=50561445 esi=ff500875 edi=5750f875
eip=3774187d esp=fff475ff ebp=ff50f075 iopl=1 vip ov up ei ng nz na pe cy
cs=5000 ss=0010 ds=33b5 es=7068 fs=c483 gs=5fc7 efl=0574db85
5000:187d 0401 add al,1
kd> dt nt!_KTSS 80042000 -a IntDirectionMap
+0x208c IntDirectionMap : "???"
[00] 0x4 '' (int 0 à 7)
[01] 0 '' (int 8 à 15)
[02] 0 '' (int 16 à 23)
[03] 0x18 '' (int 24 à 31)
[04] 0x18 '' (int 32 à 39)
[05] 0 ''
[06] 0 ''
[07] 0 ''
[08] 0 ''
[09] 0 ''
[10] 0 ''
[11] 0 ''
[12] 0 ''
[13] 0 ''
[14] 0 ''
[15] 0 ''
[16] 0 ''
[17] 0 ''
[18] 0 ''
[19] 0 ''
[20] 0 ''
[21] 0 ''
[22] 0 ''
[23] 0 ''
[24] 0 ''
[25] 0 ''
[26] 0 ''
[27] 0 ''
[28] 0 ''
[29] 0 ''
[30] 0 ''
[31] 0 ''
A partir de ce bitmap on peut dire que les interruptions softwares 2, 27 (0x1B), 28 (0x01C), 35(0×23) et 36(0×24) sont gérées en protected mode. Pour savoir à quoi elles correspondent on peut se référer à la fameuse Ralf Brown’s Interrupt List. Sous Windows, on se retrouve donc à utiliser les méthodes 3 et 6 pour les interruptions software. Pour le moment je n’ai pas encore étudié l’IVT du virtual mode pour retrouver les handlers, ceux qui m’intéressent sont les vieux gestionnaires DOS comme par exemple l’INT 21h et plus généralement tous ceux de cette liste, retrouver comment l’OS gère des fonctions DOS comme OPEN ou READ doit être assez intéressant, mais cela je vous dévoilerais, si je le trouve, plus tard.
Voilà, c’est finit pour la gestion des interruptions hardware, software et des exceptions. Il s’agit clairement d’une partie lourde à mettre en place, faisant appel à de nombreuses features complexes du CPU et demandant une implémentation très low-level dans le noyau.
Au passage un mot sur les I/Os, en fonction de l’IOPL les instructions IN, INS, OUT et OUTS sont ‘IOPL sensitives’, c’est à dire que pour faire un I/O il faut que le CPL soit inférieur ou égal à l’IOPL. Dans notre case nous avons un IOPL à 0 avec un CPL de 3, donc pas d’I/Os possibles.
Voilà, je m’arrête là pour le moment, je pense avoir couvert une bonne partie de la gestion du virtual mode sous Windows même si il me reste encore de nombreux composants à reverser. Pour cette partie j’ai juste exposé l’interfaçage entre les specs Intel sur l’émulation 8086 et le noyau Windows. Evidemment, cela ne suffit pas à faire tourner des applications DOS ou WOW, cela passe notamment par le fonctionnement des processes ntvdm.exe est wowexec.exe qui sont en fait des wrappers par rapport aux différentes librairies 16 bits nécessaire au fonctionnement de ces binaires. Mais cela je l’expliquerais dans un prochain post. Il me reste aussi à décrire la manière dont un thread gère une tâche 16 bits, comment l’espace mémoire DOS est agencé et de vous parler plus en détails de l’API native NtVdmControl et de l’instruction magique décrite par Rater dans 29A. Pour l’instant je laisse décanter cela, surtout pour moi en fait et je tente d’expliquer tout cela la prochaine fois.
En attendant, si vous avez des questions, des suggestion ou des remarques n’hésitez pas à les laisser en commentaires !
septembre 20th, 2008
De retour après une longue absence durant laquelle je faisais semblant de taff avec mon maître. Pour ceux qui croyaient que j’avais une vie sociale c’est raté, j’étais plutôt enfermé dans une salle sombre, humide et froide ; travaillant sur des sujets douteux avec pour unique lumière un laptop qui éclairait mon visage pâle et amaigrit, nourrit au café, écoutant du black métal : un rêve pour certains en quelque sorte. Cette période étant finie, je peux reprendre une activité normale. Cette fois on va descendre toujours plus loin dans notre OS préféré pour s’intéresser à un composant méconnu mais extrêmement important. Je veux parler du subsystem constitué par le process csrss.exe.
Le processes csrss (Client Server Runtime Process), crée par le système lors du boot, est responsable de la gestion des threads et processes en maintenant une liste interne de ceux-ci comme on peut le lire ici pour pouvoir effectuer diverses opérations sur ses objets en cas de besoin, même si j’ai du mal à comprendre exactement pourquoi. Csrss est aussi responsable de la Console Win32 et c’est sur ce sujet que je vais m’attarder ici. Csrss agit simplement comme un serveur fournissant aux processes console un ensemble de features non accessibles via l’API standard de Windows.
Quand on regarde le binaire csrss.exe sous IDA on s’aperçoit qu’il est très petit, 6ko seulement, par contre il charge une DLL csrss.dll qui export un ensemble de fonctions assez sympas :
->Export Table
Characteristics: 0x00000000
TimeDateStamp: 0x48023843 (GMT: Sun Apr 13 16:43:47 2008)
MajorVersion: 0x0000
MinorVersion: 0x0000 -> 0.00
Name: 0x000078D6 ("CSRSRV.dll")
Base: 0x00000001
NumberOfFunctions: 0x00000023
NumberOfNames: 0x00000023
AddressOfFunctions: 0x00007778
AddressOfNames: 0x00007804
AddressOfNameOrdinals: 0x00007890
Ordinal RVA Symbol Name
------- ---------- ----------------------------------
0x0001 0x000053C5 "CsrAddStaticServerThread"
0x0002 0x00004160 "CsrCallServerFromServer"
0x0003 0x00003FCE "CsrConnectToUser"
0x0004 0x00005C9C "CsrCreateProcess"
0x0005 0x00006056 "CsrCreateRemoteThread"
0x0006 0x00005F86 "CsrCreateThread"
0x0007 0x00006375 "CsrCreateWait"
0x0008 0x000062D8 "CsrDebugProcess"
0x0009 0x000062E5 "CsrDebugProcessStop"
0x000A 0x00004F8E "CsrDereferenceProcess"
0x000B 0x00005520 "CsrDereferenceThread"
0x000C 0x00006502 "CsrDereferenceWait"
0x000D 0x00005ECE "CsrDestroyProcess"
0x000E 0x00006110 "CsrDestroyThread"
0x000F 0x00005438 "CsrExecServerThread"
0x0010 0x00005010 "CsrGetProcessLuid"
0x0011 0x00004E6C "CsrImpersonateClient"
0x0012 0x000052D3 "CsrLockProcessByClientId"
0x0013 0x00005353 "CsrLockThreadByClientId"
0x0014 0x0000658C "CsrMoveSatisfiedWait"
0x0015 0x000064A4 "CsrNotifyWait"
0x0016 0x00002A17 "CsrPopulateDosDevices"
0x0017 0x00003FC3 "CsrQueryApiPort"
0x0018 0x00004F20 "CsrReferenceThread"
0x0019 0x00004EB3 "CsrRevertToSelf"
0x001A 0x0000305E "CsrServerInitialization"
0x001B 0x00004CA2 "CsrSetBackgroundPriority"
0x001C 0x000050F2 "CsrSetCallingSpooler"
0x001D 0x00004C7B "CsrSetForegroundPriority"
0x001E 0x000061C5 "CsrShutdownProcesses"
0x001F 0x00003204 "CsrUnhandledExceptionFilter"
0x0020 0x00005330 "CsrUnlockProcess"
0x0021 0x000061A2 "CsrUnlockThread"
0x0022 0x00004421 "CsrValidateMessageBuffer"
0x0023 0x0000449D "CsrValidateMessageString"
La fonction importante dans cette liste est CsrServerInitialization, c’est celle-ci qui va vraiment initialiser le subsystem, notamment en chargeant les autres fournisseurs. La ligne de commande de csrss.exe est assez marrante :
C:\WINDOWS\system32\csrss.exe ObjectDirectory=\Windows SharedSection=1024,3072,512 Windows=On SubSystemType=Windows ServerDll=basesrv,1 ServerDll=winsrv:UserServerDllInitialization,3 ServerDll=winsrv:ConServerDllInitialization,2 ProfileControl=Off MaxRequestThreads=16
On retrouve 2 noms de DLL, baserv.dll et winsrv.dll suivis de 2 noms faisant partit de leurs APIs exportées, UserServerDllInitialization et ConServerDllInitialization. Ces DLLS sont en fait des providers que csrss wrappe pour l’interfacer avec le reste du système. La communication avec ses providers est basée sur les LPC à travers 2 ports \Windows\ApiPort et \Windows\SbApiPort crées par cssrv.dll et gérés par les threads CsrApiRequestThread. On peut le voir facilement en regardant les stacks des threads de csrss avec Process Explorer et puis il y existe une ré-implémentation dans ReactOS ici. Pour info le LPC ApiPort gère toutes les fonctions du subsytem alors que le SbApiPort est utilisé par csrsrv.dll dans le contexte des sessions. J’ai repéré aussi d’autres providers possible qui sont consrv.dll et usersrv.dll mais ceux ci ne semblent pas chargés sur mon système.
Maintenant voyons la liste des fonctions qui sont fournies par basesrv.dll et winsrv.dll :
bassrv.dll
kd> dds basesrv!BaseServerApiDispatchTable l 20
75aed080 75ae4dab basesrv!BaseSrvCreateProcess
75aed084 75ae4b61 basesrv!BaseSrvCreateThread
75aed088 75ae4d10 basesrv!BaseSrvGetTempFile
75aed08c 75ae4cad basesrv!BaseSrvExitProcess
75aed090 75ae4d37 basesrv!BaseSrvDebugProcess
75aed094 75ae8410 basesrv!BaseSrvCheckVDM
75aed098 75ae84fe basesrv!BaseSrvUpdateVDMEntry
75aed09c 75ae702e basesrv!BaseSrvGetNextVDMCommand
75aed0a0 75ae857d basesrv!BaseSrvExitVDM
75aed0a4 75ae523b basesrv!BaseSrvIsFirstVDM
75aed0a8 75ae76ee basesrv!BaseSrvGetVDMExitCode
75aed0ac 75ae5a7d basesrv!BaseSrvSetReenterCount
75aed0b0 75ae4d44 basesrv!BaseSrvSetProcessShutdownParam
75aed0b4 75ae4d7d basesrv!BaseSrvGetProcessShutdownParam
75aed0b8 75ae8718 basesrv!BaseSrvNlsSetUserInfo
75aed0bc 75ae87a5 basesrv!BaseSrvNlsSetMultipleUserInfo
75aed0c0 75ae8bf3 basesrv!BaseSrvNlsCreateSection
75aed0c4 75ae64c9 basesrv!BaseSrvSetVDMCurDirs
75aed0c8 75ae65fb basesrv!BaseSrvGetVDMCurDirs
75aed0cc 75ae65aa basesrv!BaseSrvBatNotification
75aed0d0 75ae78b5 basesrv!BaseSrvRegisterWowExec
75aed0d4 75ae9f92 basesrv!BaseSrvSoundSentryNotification
75aed0d8 75ae9a30 basesrv!BaseSrvRefreshIniFileMapping
75aed0dc 75ae40e9 basesrv!BaseSrvDefineDosDevice
75aed0e0 75ae966e basesrv!BaseSrvSetTermsrvAppInstallMode
75aed0e4 75ae8a9a basesrv!BaseSrvNlsUpdateCacheCount
75aed0e8 75ae2db6 basesrv!BaseSrvSetTermsrvClientTimeZone
75aed0ec 75aea764 basesrv!BaseSrvSxsCreateActivationContext
75aed0f0 75ae4d37 basesrv!BaseSrvDebugProcess
75aed0f4 75ae4c01 basesrv!BaseSrvRegisterThread
75aed0f8 75ae86a7 basesrv!BaseSrvNlsGetUserInfo
75aed0fc 75ae283a basesrv!BaseSrvAppHelpQueryModuleData
winsrv.dll
kd> dds winsrv!UserServerApiDispatchTable l B
75b2d560 75b08910 winsrv!SrvExitWindowsEx
75b2d564 75b08f18 winsrv!SrvEndTask
75b2d568 75af7977 winsrv!SrvLogon
75b2d56c 75af7b6c winsrv!SrvRegisterServicesProcess
75b2d570 75b0796f winsrv!SrvActivateDebugger
75b2d574 75af16a9 winsrv!SrvGetThreadConsoleDesktop
75b2d578 75b078cc winsrv!SrvDeviceEvent
75b2d57c 75af7d09 winsrv!SrvRegisterLogonProcess
75b2d580 75b0787c winsrv!SrvWin32HeapFail
75b2d584 75b0787c winsrv!SrvWin32HeapFail
75b2d588 75afbd30 winsrv!SrvCreateSystemThreads
kd> dds winsrv!ConsoleServerApiDispatchTable l 55
75af89f0 75afbae8 winsrv!SrvOpenConsole
75af89f4 75b05a50 winsrv!SrvGetConsoleInput
75af89f8 75b16627 winsrv!SrvWriteConsoleInput
75af89fc 75b0694d winsrv!SrvReadConsoleOutput
75af8a00 75b169ab winsrv!SrvWriteConsoleOutput
75af8a04 75b16c0b winsrv!SrvReadConsoleOutputString
75af8a08 75b16cc1 winsrv!SrvWriteConsoleOutputString
75af8a0c 75b058c6 winsrv!SrvFillConsoleOutput
75af8a10 75af24e5 winsrv!SrvGetConsoleMode
75af8a14 75b10a82 winsrv!SrvGetConsoleNumberOfFonts
75af8a18 75b115b0 winsrv!SrvGetConsoleNumberOfInputEvents
75af8a1c 75af411a winsrv!SrvGetConsoleScreenBufferInfo
75af8a20 75b06a60 winsrv!SrvGetConsoleCursorInfo
75af8a24 75b10b3f winsrv!SrvGetConsoleMouseInfo
75af8a28 75b10b77 winsrv!SrvGetConsoleFontInfo
75af8a2c 75b10c15 winsrv!SrvGetConsoleFontSize
75af8a30 75b10c93 winsrv!SrvGetConsoleCurrentFont
75af8a34 75af2814 winsrv!SrvSetConsoleMode
75af8a38 75b10dc9 winsrv!SrvSetConsoleActiveScreenBuffer
75af8a3c 75b10e2c winsrv!SrvFlushConsoleInputBuffer
75af8a40 75b06495 winsrv!SrvGetLargestConsoleWindowSize
75af8a44 75b061cd winsrv!SrvSetConsoleScreenBufferSize
75af8a48 75b05c72 winsrv!SrvSetConsoleCursorPosition
75af8a4c 75b10e8f winsrv!SrvSetConsoleCursorInfo
75af8a50 75b06293 winsrv!SrvSetConsoleWindowInfo
75af8a54 75b10f0e winsrv!SrvScrollConsoleScreenBuffer
75af8a58 75b05ec4 winsrv!SrvSetConsoleTextAttribute
75af8a5c 75b110d2 winsrv!SrvSetConsoleFont
75af8a60 75b11189 winsrv!SrvSetConsoleIcon
75af8a64 75b05da3 winsrv!SrvReadConsole
75af8a68 75af358a winsrv!SrvWriteConsole
75af8a6c 75af41ff winsrv!SrvDuplicateHandle
75af8a70 75b17428 winsrv!SrvGetHandleInformation
75af8a74 75b17490 winsrv!SrvSetHandleInformation
75af8a78 75af4186 winsrv!SrvCloseHandle
75af8a7c 75af267c winsrv!SrvVerifyConsoleIoHandle
75af8a80 75b0d1a7 winsrv!SrvAllocConsole
75af8a84 75b0d35d winsrv!SrvFreeConsole
75af8a88 75af28c6 winsrv!SrvGetConsoleTitle
75af8a8c 75af8f17 winsrv!SrvSetConsoleTitle
75af8a90 75b16d8d winsrv!SrvCreateConsoleScreenBuffer
75af8a94 75b1611c winsrv!SrvInvalidateBitMapRect
75af8a98 75b15e6e winsrv!SrvVDMConsoleOperation
75af8a9c 75b0ee09 winsrv!SrvSetConsoleCursor
75af8aa0 75b0eeda winsrv!SrvShowConsoleCursor
75af8aa4 75b07402 winsrv!SrvConsoleMenuControl
75af8aa8 75b0d742 winsrv!SrvSetConsolePalette
75af8aac 75b0d9b6 winsrv!SrvSetConsoleDisplayMode
75af8ab0 75b06e32 winsrv!SrvRegisterConsoleVDM
75af8ab4 75b0dcf3 winsrv!SrvGetConsoleHardwareState
75af8ab8 75b0f725 winsrv!SrvSetConsoleHardwareState
75af8abc 75afc0f4 winsrv!SrvGetConsoleDisplayMode
75af8ac0 75b13b46 winsrv!SrvAddConsoleAlias
75af8ac4 75b13d48 winsrv!SrvGetConsoleAlias
75af8ac8 75b13f2c winsrv!SrvGetConsoleAliasesLength
75af8acc 75b11ef6 winsrv!SrvGetConsoleAliasExesLength
75af8ad0 75b1400d winsrv!SrvGetConsoleAliases
75af8ad4 75b11f4e winsrv!SrvGetConsoleAliasExes
75af8ad8 75b14570 winsrv!SrvExpungeConsoleCommandHistory
75af8adc 75b145d9 winsrv!SrvSetConsoleNumberOfCommands
75af8ae0 75b14648 winsrv!SrvGetConsoleCommandHistoryLength
75af8ae4 75b146da winsrv!SrvGetConsoleCommandHistory
75af8ae8 75b12081 winsrv!SrvSetConsoleCommandHistoryMode
75af8aec 75af278f winsrv!SrvGetConsoleCP
75af8af0 75b1129b winsrv!SrvSetConsoleCP
75af8af4 75b0de0e winsrv!SrvSetConsoleKeyShortcuts
75af8af8 75b0dd83 winsrv!SrvSetConsoleMenuClose
75af8afc 75b0dae0 winsrv!SrvConsoleNotifyLastClose
75af8b00 75b10d19 winsrv!SrvGenerateConsoleCtrlEvent
75af8b04 75b06bad winsrv!SrvGetConsoleKeyboardLayoutName
75af8b08 75b114e6 winsrv!SrvGetConsoleWindow
75af8b0c 75b11a6a winsrv!SrvGetConsoleCharType
75af8b10 75b11b3b winsrv!SrvSetConsoleLocalEUDC
75af8b14 75b11619 winsrv!SrvSetConsoleCursorMode
75af8b18 75b1168f winsrv!SrvGetConsoleCursorMode
75af8b1c 75b11704 winsrv!SrvRegisterConsoleOS2
75af8b20 75b11783 winsrv!SrvSetConsoleOS2OemFormat
75af8b24 75b117cd winsrv!SrvGetConsoleNlsMode
75af8b28 75b118c7 winsrv!SrvSetConsoleNlsMode
75af8b2c 75b11984 winsrv!SrvRegisterConsoleIME
75af8b30 75b11a35 winsrv!SrvUnregisterConsoleIME
75af8b34 75af2730 winsrv!SrvGetConsoleLangId
75af8b38 75b0d3cb winsrv!SrvAttachConsole
75af8b3c 75b10acd winsrv!SrvGetConsoleSelectionInfo
75af8b40 75b1151d winsrv!SrvGetConsoleProcessList
La console n’étant pas gérée par les APIS Win32 GDI classiques, il suffit de regarder un peu comment sont implémentées ces fonctions pour comprendre. En effet, toutes les requêtes sont en fait passées au subsystem et plus précisément aux fonctions de winsrv.dll. Maintenant, il reste à savoir comment s’interfacer avec le subsystem pour profiter des ces APIs. Tout ceci est bien évidemment non documenté donc si on veut plus d’infos il faut reverser, googler et lire les sources de ReactOS. En regardant le code des fonctions de kernel32.dll comme GetConsoleTitle, on peut voir quelles utilisent une partie des APIS exportées de ntdll qui sont :
ntdll export table :
CsrAllocateCaptureBuffer
CsrAllocateMessagePointer
CsrCaptureMessageBuffer
CsrCaptureMessageMultiUnicodeStringsInPlace
CsrCaptureMessageString
CsrCaptureTimeout
CsrClientCallServer
CsrClientConnectToServer
CsrFreeCaptureBuffer
CsrGetProcessId
CsrIdentifyAlertableThread
CsrNewThread
CsrProbeForRead
CsrProbeForWrite
CsrSetPriorityClass
Ce sont ces fonctions qui permettent de communiquer avec winsrv.dll, voici les prototypes des plus importantes.
from umfuncs.h@ndk
//
// CSR Functions
//
PVOID
NTAPI
CsrAllocateCaptureBuffer(
ULONG ArgumentCount,
ULONG BufferSize
);
ULONG
NTAPI
CsrAllocateMessagePointer(
struct _CSR_CAPTURE_BUFFER *CaptureBuffer,
ULONG MessageLength,
PVOID *CaptureData
);
VOID
NTAPI
CsrCaptureMessageBuffer(
struct _CSR_CAPTURE_BUFFER *CaptureBuffer,
PVOID MessageString,
ULONG StringLength,
PVOID *CapturedData
);
NTSTATUS
NTAPI
CsrClientCallServer(
struct _CSR_API_MESSAGE *Request,
struct _CSR_CAPTURE_BUFFER *CaptureBuffer OPTIONAL,
ULONG ApiNumber,
ULONG RequestLength
);
VOID
NTAPI
CsrFreeCaptureBuffer(
struct _CSR_CAPTURE_BUFFER *CaptureBuffer
);
Après, en cherchant un peu dans sur le net et surtout dans les headers du NDK on retrouve les définitions des structures qui doivent être envoyées au LPC. Le listing suivant donne juste les structures de bases. Il existe en effet une structure spécifique en fonction de l’API qu’on veut appeler, qui se trouve dans l’union de la structure CSR_API_MSG.
/************************************************************************************/
//
// Csrss headers
//
#define WINSS_OBJECT_DIRECTORY_NAME L"\\Windows"
#define CSRSRV_SERVERDLL_INDEX 0
#define CSRSRV_FIRST_API_NUMBER 0
#define BASESRV_SERVERDLL_INDEX 1
#define BASESRV_FIRST_API_NUMBER 0
#define CONSRV_SERVERDLL_INDEX 2
#define CONSRV_FIRST_API_NUMBER 512
#define USERSRV_SERVERDLL_INDEX 3
#define USERSRV_FIRST_API_NUMBER 1024
#define CSR_MAKE_API_NUMBER( DllIndex, ApiIndex ) \
(CSR_API_NUMBER)(((DllIndex) << 16) | (ApiIndex))
#define CSR_APINUMBER_TO_SERVERDLLINDEX( ApiNumber ) \
((ULONG)((ULONG)(ApiNumber) >> 16))
#define CSR_APINUMBER_TO_APITABLEINDEX( ApiNumber ) \
((ULONG)((USHORT)(ApiNumber)))
//
// This structure is filled in by the client prior to connecting to the CSR
// server. The CSR server will fill in the OUT fields if prior to accepting
// the connection.
//
typedef struct _CSR_API_CONNECTINFO {
OUT HANDLE ObjectDirectory;
OUT PVOID SharedSectionBase;
OUT PVOID SharedStaticServerData;
OUT PVOID SharedSectionHeap;
OUT ULONG DebugFlags;
OUT ULONG SizeOfPebData;
OUT ULONG SizeOfTebData;
OUT ULONG NumberOfServerDllNames;
OUT HANDLE ServerProcessId;
} CSR_API_CONNECTINFO, *PCSR_API_CONNECTINFO;
typedef struct _CSR_CLIENTCONNECT_MSG {
IN ULONG ServerDllIndex;
IN OUT PVOID ConnectionInformation;
IN OUT ULONG ConnectionInformationLength;
} CSR_CLIENTCONNECT_MSG, *PCSR_CLIENTCONNECT_MSG;
#define CSR_NORMAL_PRIORITY_CLASS 0x00000010
#define CSR_IDLE_PRIORITY_CLASS 0x00000020
#define CSR_HIGH_PRIORITY_CLASS 0x00000040
#define CSR_REALTIME_PRIORITY_CLASS 0x00000080
//
// This helps out the Wow64 thunk generater, so we can change
// RelatedCaptureBuffer from struct _CSR_CAPTURE_HEADER* to PCSR_CAPTURE_HEADER.
// Redundant typedefs are legal, so we leave the usual form in as well.
//
struct _CSR_CAPTURE_HEADER;
typedef struct _CSR_CAPTURE_HEADER CSR_CAPTURE_HEADER, *PCSR_CAPTURE_HEADER;
typedef struct _CSR_CAPTURE_HEADER {
ULONG Length;
PCSR_CAPTURE_HEADER RelatedCaptureBuffer;
ULONG CountMessagePointers;
PCHAR FreeSpace;
ULONG_PTR MessagePointerOffsets[1]; // Offsets within CSR_API_MSG of pointers
} CSR_CAPTURE_HEADER, *PCSR_CAPTURE_HEADER;
typedef ULONG CSR_API_NUMBER;
typedef struct _CSR_API_MSG {
PORT_MESSAGE h;
union {
CSR_API_CONNECTINFO ConnectionRequest;
struct {
PCSR_CAPTURE_HEADER CaptureBuffer;
CSR_API_NUMBER ApiNumber;
ULONG ReturnValue;
ULONG Reserved;
union {
//
// Place data for srv here
//
} u;
};
};
} CSR_API_MSG, *PCSR_API_MSG;
/************************************************************************************/
Une fois qu’on connaît ces structures on peut s’interfacer avec le subsystem en utilisant la fonction CsrClientCallServer. Le problème c’est qu’il faut connaître les structures spécifiques aux APIs qu’on veut utiliser et là, ce n’est pas gagner. Celles de winsrv.dll qui gèrent la console se reversent assez facilement et sont en partie disponibles ici, et celles de basesrv.dll le sont là.
Remarquez dans la ligne de commande de csrss les indices après les noms des fonctions d’init :
ServerDll=basesrv,1
ServerDll=winsrv:ConServerDllInitialization,2
ServerDll=winsrv:UserServerDllInitialization,3
Ces indices permettent de savoir quel provider utiliser puis quelle fonction appeler (les fonctions ont aussi un indice). Attention comme winsrv.dll fournit 2 interfaces il existe 2 indices de départ : BASESRV_FIRST_API_NUMBER qui vaut 0 et CONSRV_FIRST_API_NUMBER qui vaut 512.
En fait, le mécanisme intéressant est la manipulation des buffers entre le process client et le subsystem, car un LPC ne se casse pas la tête, il transmet juste un buffer entre 2 processes. Si le client à besoin de fournir une zone mémoire destinée à recevoir des données du serveur, il doit utiliser un CaptureBuffer avec CsrAllocateCaptureBuffer qui va allouer de l’espace dans un heap spécial du process, le CsrPortHeap. Ce heap vaut de l’or car il est situé dans une section qui est accessible par le subsystem ! C’est ce qu’on appel un Port Memory section … Pour s’en assurer vous pouvez regarder la fonction CsrpConnectToServer de ntdll et voir un appel à ZwCreateSection. Ensuite il faut appeler CsrCaptureMessageBuffer afin que le pointeur sur le buffer de destination des datas pointe sur notre CaptureBuffer. Par exemple :
typedef struct _CSR_CAPTURE_HEADER {
ULONG Length;
PCSR_CAPTURE_HEADER RelatedCaptureBuffer;
ULONG CountMessagePointers;
PCHAR FreeSpace;
ULONG_PTR MessagePointerOffsets[1]; // Offsets within CSR_API_MSG of pointers
} CSR_CAPTURE_HEADER, *PCSR_CAPTURE_HEADER;
CSR_CAPTURE_HEADER CaptureBuffer;
CaptureBuffer=CsrAllocateCaptureBuffer(1, b->TitleLen);
/* valeurs lues sous olly
CsrAllocateCaptureBuffer
CsrHeader.Length=0x11C
CsrHeader.RelatedCaptureBuffer=0x260178
CsrHeader.CountMessagePointers=0
CsrHeader.FreeSpace=0x26069C
CsrHeader.MessagePointerOffsets[0]=0;
*/
CsrCaptureMessageBuffer(CaptureBuffer,
NULL,
b->TitleLen,
(PVOID *)&b->Title);
/* valeurs lues sous olly
CsrAllocateCaptureBuffer
CsrHeader.Length=0x11C
CsrHeader.RelatedCaptureBuffer=0x260178
CsrHeader.CountMessagePointers=1
CsrHeader.FreeSpace=0x2607A0
CsrHeader.MessagePointerOffsets[0]=0x12FF40; //Pointeur sur l'adresse du buffer alloué par CsrAllocateCaptureBuffer, les datas suivent le CaptureBuffer
*/
En gros toutes ces opérations servent à translater les pointeurs des buffers du client vers le serveur. Pour finir, la fonction CsrClientCallServer effectuera le traitement final en remplissant le champ CaptureBuffer de la structure CSR_API_MSG puis appellera ZwRequestWaitReplyPort. Arrivé dans le serveur avec le LPC, un des threads CsrApiRequestThread qui attend avec ZwReplyWaitReceivePort dispatchera la requête. Bien évidemment le buffer est vérifié dans le serveur avec CsrValidateMessageBuffer
Le code suivant est simplement une ré-implemenation de GetConsoleTitle (ou plutôt de GetConsoleTitleInternal pour être précis) de kernel32.dll. On retrouve quasiment le même code dans ReactOS.
HANDLE GetConsoleHandle(VOID)
{
__asm
{
mov eax, fs:[18h]
mov eax, [eax+30h]
mov eax, [eax+10h]
mov eax, [eax+10h]
}
}
int main(int argc, char ** argv)
{
NTSTATUS Status;
CSR_API_MSG m;
PCONSOLE_TITLE_MSG b=&m.u.ConsoleTitle;
PCSR_CAPTURE_HEADER CaptureBuffer;
RtlZeroMemory(&m, sizeof(m));
b->ConsoleHandle=GetConsoleHandle();
b->TitleLen=260;
b->Unicode=0;
CaptureBuffer=CsrAllocateCaptureBuffer(1, b->TitleLen);
CsrCaptureMessageBuffer(CaptureBuffer,
NULL,
b->TitleLen,
(PVOID *)&b->Title);
Status=CsrClientCallServer((PCSR_API_MSG)&m,
CaptureBuffer,
CSR_MAKE_API_NUMBER(CONSRV_SERVERDLL_INDEX, CONSRV_FIRST_API_NUMBER+38), //38=SrvGetConsoleTitle index
sizeof(*b));
if(!NT_SUCCESS(Status))
{
printf("Error with CsrClientCallServer : 0x%X\n", Status);
CsrFreeCaptureBuffer(CaptureBuffer);
return 0;
}
printf("ConsoleTitle is : %s\n", m.u.ConsoleTitle.Title);
CsrFreeCaptureBuffer(CaptureBuffer);
return 0;
}
Pour les autres interfaces, je vous laisser creuser :], vous pouvez reverser les APIs de kernel32.dll pour retrouver les structures et faire mumuse avec le subsystem.
Voici le code+binaire de MyGetConsoleTitle :
http://ivanlef0u.fr/repo/MyGetConsoleTitle.rar
En espérant que vous continuerez à fouiner le sujet :]
septembre 3rd, 2008
Maître newsoft à un problème, parfois durant ses pentests de Windows (qu’il réussit toujours, bien évidemment) il voit que l’admin a programmé des taches planifiées, il sait grâce à son immense sagesse que le task manager de Windows a demandé à l’admin son login/pass pour pouvoir lancer la tâche. Donc en toute logique maître newsoft se demande ou sont stocké les credentials de l’admin par le système et surtout si il est possible de les récupérer, le pire c’est qu’il ne connaît aucun tool qui fait cela pour le moment. Il faut dire que dumper la base SAM avec pwdump puis péter les hash NTLM avec John c’est long et chiant, ça serait donc bien d’avoir une technique plus smooth pour obtenir les credentials. C’est à ce moment que moi, simple padawan, intervient, mon but ? Regarder comment fonctionne le Task Manager sous Windows et comprendre comment sont stockés les credentials des tâches planifiées.
C’est partit, première étage, jouer avec l’application et observer son comportement, direction le Panneau de configuration, icône « Tâches planifiées », une GUI se lance qui me demande le nom de la tâche, quelle appli je veux planifiée, à quelle date puis me demande mon login/pass. En parallèle je surveille avec Process Explorer le processus explorer.exe, la Dll mstask.dll (Fichier DLL d’interface du Planificateur de tâches) a été chargée dans explorer.dll. Un coup de IDA suffit à comprendre que c’est juste une Dll qui utilise des RPC pour communiquer avec le provider du Task Scheduler, reste plus qu’a trouver avec quels RPC elle communique.
Pour cela, on peut regarder le paramètre EndPoint passé à la fonction RpcStringBindingCompose car c’est lui qui contient le nom pipe ou du port RPC sur lequel vont se faire les requêtes. Dans mstask.dll on retrouve la string « »\PIPE\atsvc », un coup de Process Explorer nous indique que ce pique appartient à un processus svchost, il nous manque cependant le nom de la Dll qui crée le serveur RPC. En regardant les descriptions des Dll on peut voir pour schedsvc.dll « Moteur du Planificateur de tâches », un coup d’oeil dans les strings de la Dll (toujours avec Process Explorer) et on retrouve notre « \PIPE\atsvc », ok c’est celle la !
On va utiliser un plugin très pratique pour IDA de Tenable Network Security, Mida pour obtenir l’ensemble des interfaces RPC crée par schedsvc.dll à travers leurs structures MIDL :
---------------------------------------------------
mIDA Plugin v1.0.8
Copyright (C) 2006, Tenable Network Security
---------------------------------------------------
Scanning database for MIDL structures ...
Found MIDL structure at address 0x76B12D48 : 378e52b0-c0a9-11cf-822d-00aa0051e40f v1.0
Opcode : 0x00 , address : 0x76B24F60, name : _SASetAccountInformation
Opcode : 0x01 , address : 0x76B23905, name : _SASetNSAccountInformation
Opcode : 0x02 , address : 0x76B23C32, name : _SAGetNSAccountInformation
Opcode : 0x03 , address : 0x76B24DAF, name : _SAGetAccountInformation
Found MIDL structure at address 0x76B12F90 : 1ff70682-0a51-30e8-076d-740be8cee98b v1.0
Opcode : 0x00 , address : 0x76B262A0, name : _NetrJobAdd
Opcode : 0x01 , address : 0x76B269FD, name : _NetrJobDel
Opcode : 0x02 , address : 0x76B26502, name : _NetrJobEnum
Opcode : 0x03 , address : 0x76B26B0F, name : _NetrJobGetInfo
Found MIDL structure at address 0x76B136C0 : 0a74ef1c-41a4-4e06-83ae-dc74fb1cdd53 v1.0
Opcode : 0x00 , address : 0x76B33493, name : _ItSrvRegisterIdleTask
Opcode : 0x01 , address : 0x76B32D83, name : _ItSrvUnregisterIdleTask
Opcode : 0x02 , address : 0x76B3215C, name : _ItSrvProcessIdleTasks
Opcode : 0x03 , address : 0x76B32151, name : _ItSrvSetDetectionParameters
Number of MIDL structures found: 3
On peut voir que schedsvc fournit 3 types d’interfaces les Sa*, NetrJob* et ItSrv*. Un peu de google et on tombe sur ça mais aussi sur « [MS-TSCH]: AT Service Remote Protocol Specification« , w0ot ces protocoles sont documentés depuis que Microsoft a été obligé de les mettre en ligne. Dans notre cas on a les protocoles SASec et ATSvc pour le dernier on s’en fou. Petite description:
1.3 Protocol Overview (Synopsis)
The Task Scheduler Remoting Protocol is used to register and configure tasks or to query the statu
of running tasks on a remote server. The Task Scheduler Remoting Protocol primarily consists of
three separate remote procedure call (RPC) interfaces:
- Net Schedule (ATSvc)
- Task Scheduler Agent (SASec)
- Windows Vista Task Remote Protocol (ITaskSchedulerService)
All three interfaces use RPC as their transport to configure and manage tasks remotely, as shown
Figure 1. The three interfaces represent a continuum of increasing functionality, with ATSvc providing
rudimentary functionality and ITaskSchedulerService providing the most functionality.
Historically, the ATSvc interface is the oldest. The SASec interface was introduced in Windows 2000
and the ITaskSchedulerService interface made its debut in Windows Vista. The three interfaces
are not independent—they operate on the task store, shared persistent storage for tasks.
Pour le moment avec les interfaces disponibles on ne peut que ajouter/supprimer des jobs, énumérer/définir des infos dessus, il n’y a rien concernant les credentials. Pour info, la commande at.exe utilise la lib netapi32 (Schedule Functions) qui fonctionne aussi avec l’interface RPC ATSvc.
Arrivé à ce point on sait donc comment communiquer avec le Task Scheduler, c’est un service qui fournit des interfaces RPC avec une interface de base ATSvc et une plus aboutie SASec. Maintenant il faut creuser dans la Dll schedsvc pour trouver la fonction qui lancer les tâches puis voir comment elle récupère les credentials, IDA est notre ami en plus on accès aux symbols. Après quelques recherches on trouve une fonction intéressante nommée DecryptCredentials de proto :
__int32 __stdcall DecryptCredentials(const struct _RC2_KEY_INFO *, unsigned __int32, unsigned __int8 *, struct _JOB_CREDENTIALS *, int)
Avec au dessus des appels à ReadLsaData, GetCSPHandle (le provider crypto) et ComputeCredentialKey. A partir de là on peut donc penser que la Dll va récup les credentials cryptés dans le provider LSA, demander d’utiliser un custom cryptographic service provider avec les APIS CryptAcquireContext et CryptGenKey sur du RC2 http://en.wikipedia.org/wiki/RC2 puis calculer la clé de chiffrement qui à servit a crypté les credentials. D’après ce que j’ai pu voir cette clé dépend du nom de la machine de son SID et de d’autres valeurs, je ne me suis pas amusé à rentrer dans les détails, la flemme ! En regardant le graph de crossref à DecryptCredentials on peut voir :

Regardez la chaines de calls MainServiceLoop->RunLogonJobs->RunJobs->RunNtJobs->LogonAccount->GetAccountInformation->DecryptCredentials, cette série de calls montre que pour lancer une tâche, le Task Scheduler va récupérer les credentials du user qui la enregistré, normal :] Pour vérifier cela on se crée un tâche bidon on sort le kernel debugger puis mettre un BP sur DecryptCredentials dans schedsvc.dll. Svchost n’aimant pas du tout se faire debug par OllyDbg. Ensuite, on lance cette tâche à la main on espérant que ca break puis on examine la stack avant et après en espérant trouver des infos intéressantes, surtout on regarde le 4 ème paramètre de DecryptCredentials qui un pointeur sur un structure non documentée _JOB_CREDENTIALS.
1: kd> bp schedsvc!DecryptCredentials
breakpoint 0 redefined
1: kd> g
Breakpoint 0 hit
schedsvc!DecryptCredentials:
001b:77312787 8bff mov edi,edi
1: kd> kv
ChildEBP RetAddr Args to Child
0120ee30 77313c65 0120ee84 00000038 037724c4 schedsvc!DecryptCredentials (FPO: [Non-Fpo])
0120efd0 77317ce1 01ef35f0 0120f00c 0000001c schedsvc!GetAccountInformation+0x174 (FPO: [Non-Fpo])
0120f448 773182c9 01ef35f0 01ef3320 0120f974 schedsvc!LogonAccount+0x1ca (FPO: [Non-Fpo])
0120f938 7730ff5b 01ef3320 00359b80 0120f994 schedsvc!CSchedWorker::RunNTJob+0xfa (FPO: [Non-Fpo])
0120fbe8 773106c6 00359ee8 530995a0 00000102 schedsvc!CSchedWorker::RunJobs+0x304 (FPO: [Non-Fpo])
0120fe74 773109c5 7c80a6a9 00000000 00000000 schedsvc!CSchedWorker::CheckDir+0x36a (FPO: [Non-Fpo])
0120ff28 77310e9c 7730b532 00000000 000c979c schedsvc!CSchedWorker::MainServiceLoop+0x2b1 (FPO: [Non-Fpo])
0120ff2c 7730b532 00000000 000c979c 00099894 schedsvc!SchedMain+0xb (FPO: [1,0,0])
0120ff5c 7730b63f 00000001 000c9798 0120ffa0 schedsvc!SchedStart+0x266 (FPO: [Non-Fpo])
0120ff6c 010011cc 00000001 000c9798 00000000 schedsvc!SchedServiceMain+0x33 (FPO: [Non-Fpo])
0120ffa0 77deb48b 00000001 000c9798 0007e898 svchost!ServiceStarter+0x9e (FPO: [Non-Fpo])
0120ffb4 7c80b683 000c9790 00000000 0007e898 ADVAPI32!ScSvcctrlThreadA+0x12 (FPO: [Non-Fpo])
0120ffec 00000000 77deb479 000c9790 00000000 kernel32!BaseThreadStart+0x37 (FPO: [Non-Fpo])
1: kd> dd esp
0120ee34 77313c65 0120ee84 00000038 037724c4
0120ee44 0120f00c 00000001 0120f994 0120f974
0120ee54 00000000 00000000 00000078 00000084
0120ee64 03772484 0000004c 00000000 0120ef7c
0120ee74 037a6480 03772478 037c7580 00000000
0120ee84 00000000 00000000 29852de1 9a45f9f4
0120ee94 b347543e 4007221f 263c713a 7bafc308
0120eea4 d1d64357 90b82461 25100c36 eefab642
1: kd> gu
schedsvc!GetAccountInformation+0x174:
001b:77313c65 85c0 test eax,eax
1: kd> db 0120f00c
0120f00c 04 00 00 00 69 00 76 00-61 00 6e 00 00 00 1d 00 ....i.v.a.n.....
0120f01c a2 3f 83 7c 68 f5 20 01-02 00 80 00 04 f3 20 01 .?.|h. ....... .
0120f02c 00 00 00 00 d8 f0 20 01-a0 10 08 00 52 d6 6c 53 ...... .....R.lS
0120f03c 8c f0 20 01 c9 55 91 7c-04 00 80 00 04 f4 20 01 .. ..U.|...... .
0120f04c 02 00 80 00 45 09 91 7c-4e 09 91 7c c4 f3 20 01 ....E..|N..|.. .
0120f05c 24 00 02 00 b0 f1 20 01-02 00 00 00 90 41 91 7c $..... ......A.|
0120f06c 00 d0 fd 7f 05 10 90 7c-58 f0 20 01 00 00 00 00 .......|X. .....
0120f07c 28 f1 20 01 18 ee 90 7c-70 09 91 7c c0 e4 97 7c (. ....|p..|...|
1: kd> db 0120f00c+22C
0120f238 03 00 00 00 66 00 66 00-73 00 00 00 f8 00 00 00 ....f.f.s.......
0120f248 20 33 ef 01 00 00 00 00-00 00 00 00 00 00 00 00 3..............
0120f258 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
0120f268 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
0120f278 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
0120f288 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
0120f298 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
0120f2a8 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
Alors, « ivan » est mon login et « ffs » mon pass sur ma machine de test. Vous allez me demandez comment j’ai trouvé l’offset 0x22C ? Tout simplement en mattant le dissas de DecryptCredentials qui faisait des memcpy sur ces offsets. Dans tout les cas, on le login et le pass en clair qui apparaissent à la sortie de DecryptCredentials, cool ! On peut donc reconstituer la structure JOB_CREDENTIALS par :
typedef struct _JOB_CREDENTIALS
{
ULONG LoginLen; //+0
WCHAR Login[256]; //+4
UCHAR Padding[40];
ULONG PassLen; //+0x22C
WCHAR Pass[256];
}JOB_CRENDENTIALS, *PJOB_CREDENTIALS;
Si on regarde plus loin dans le binaire on voit que la tâche est lancée avec CreateProcessAsUser avec un token fournit par LogonUser.
Maintenant on va prendre un café et on pense. On sait que les credentials sont gérés par le provider LSA et qu’ils sont chiffrés par un algo RC2 avec une clé dépendant de la machine. Une première tentative d’attaque pourrait consister à reproduire le fonctionnement de schedsvc en allant obtenir les credentials dans LSA, recalculer la clé de chiffrement et recoder DecryptCredentials, bref un travail super long qui en plus requiert d’avoir le token « SYSTEM » pour aller taper les credentials de LSA, c’est mort ! Une approche plus smooth serait de tout simplement hooker la fonction DecryptCredentials dans schedsvc.dll en injectant un Dll dedans, même si cela requiert le SeDebugPrivilege on en revient à un pwdump qui fait lui aussi une injection de Dll sauf que c’est dans lsass.exe. Problème c’est que l’appel à la fonction DecryptCredentials se fera uniquement lorsque la tâche sera lancée c’est à dire aussi bien dans 1 minute que dans 1 mois, pas super pour un pentest surtout que le newsoft c’est un nerveux. Par contre si vous regardez attentivement le graph de crossrefs à DecryptCredentials vous pouvez voir quelle est appelée par GetNSAccountInformation et GetAccountInformation, intéressant car ces APIs font partie de celles fournies par l’interface RPC.
On pourrait donc monter une attaque qui consisterait à injecter une dll dans le svchost qui héberge schedsvc.dll, hooker la fonction DecryptCredentials, faire un appel RPC sur SaGetAccountInformation ou SaGetNSAccountInformation puis lire la sortie de DecryptCredentials, cela nous éviterait d’attendre que la tâche se lance pour obtenir les credentials, parfait !
Reste un gros souci que je n’ai toujours pas réussi à corriger, la fonction DecryptCredentials n’est pas exportée par schedsvc.dll et ne se trouve pas non plus près d’une fonction exportée. Le problème c’est que pour procéder à notre hook nous devons connaître son adresse précise dans le process svchost.exe. Pour le moment j’utilise donc un offset hardcodé relatif à l’ImageBase de schedsvc.dll pour calculer l’adresse de cette fonction, ouais je sais c’est moche, je vais voir si il est possible de faire une recherche par pattern matching dans le binaire pour avoir quelque chose de plus générique.
Arrivé à ce niveau on peut designer proprement l’attaque :
- Vérifier qu’on peut obtenir le SeDebugPrivilege sur la machine pour pouvoir jouer avec les processes des autres accounts.
- Vérifier que le service du Task Scheduler tourne sur la machine, pour cela on utilise les api du SCM (Service Control Manager) en regardant l’état du service nommé « Schedule », si il n’est pas actif on arrête.
- Vérifier qu’il y a des tâches dans le gestionnaire. On peut utiliser la méthode Enum de la classe IEnumWorkItems, si il n’y pas de tâche on stop.
- Retrouver le processus svchost.exe qui a chargé la Dll schedsvc.dll, pour cela on énumère les processus avec CreateToolHelp32Snapshot puis leurs modules avec EnumProcessModules.
- Pour pouvoir monitorer les messages provenant de la Dll qui va être injectée j’ai décidé de surveiller le buffer fournit sous forme de mapped section servant aux debug strings. La Dll va donc écrire dedans avec l’API OuputDebugString et notre process va crée un thread chargé de monitorer ce buffer, ce thread va filtrer la provenance des messages afin de ne conserver que ceux qui proviennent du process svchost contentant la Dll schedsvc.dll.
- On inject la Dll dans le bon svchost, elle va hooker la fonction DecryptCredentials avec un inline hook. Pour réaliser le hook j’ai utilisé la lib detours de Miscrosoft superbement bien foutue. La fonction qui remplace DecryptCredentials ne fait juste qu’appeler la vraie fonction DecryptCredentials puis dumper les champs Login et pass de la structure JOB_CRENDENTIALS avec des OutputDebugString qui seront filtré par la suite avec le thread du process injecteur.
- Pour trigger la fonction DecryptCredentials on demande d’obtenir les noms des users ayant crée les taches. J’ai choisit arbitrairement d’utiliser l’interface SAGetNSAccountInformation qui peut être appelé avec la méthode GetAccountInformation de la classe IScheduledWorkItem.
- Le hook nous renvoie les credentials, on les affiche, newsoft est content, il peut faire un beau rapport!
- On n’oublie pas bien sur de virer le hook et la Dll injectée afin de rendre quelque chose de propre après l’attaque.
Plusieurs utilisateurs peuvent en enregistrer des tâches planifiées, ce qui cool c’est que chaque user peut voir les tâches planifiées des autres. Cela veut dire que l’attaque va aussi dumper les credentials des tâches installés par les autres utilisateurs, et ça, c’est priceless ! Dans l’exemple suivant j’ai crée 2 tâches, chacune avec un user différent, le premier ivan:ffs, le second barp:rofl. Je crois que l’output de l’attaque parle de lui même :
Windows Task Scheduler credentials dumper
By Ivanlef0u, thanks to Nicolas Ruff
BE M4D!
Works on Windows XP SP3 and schedsvc.dll version 5.1.2600.5512
/|
\`O.o'
=(_|_)=
U
There is 2 tasks in queue
Current tasks are :
Command Prompt
barp
Error with EnumProcessModules : 299
schedsvc.dll found in process [svchost.exe:332]
Task Scheduler svchost pid is : 332
Dll to inject : C:\Test\tasksched\TaskPwdDmpDll.dll
Injecting DLL in process : 332
[Msg from PID 332]-> Dll injected in process 332
[Msg from PID 332]-> schedsvc dll is at : 0x77300000
[Msg from PID 332]-> Hooking DecryptCredentials at 0x77312787
[Msg from PID 332]-> Credentials barp:rofl
[Msg from PID 332]-> Credentials ivan:ffs
Dll successfully unloaded
Au final, on a une attaque qui permet de récup les credentials stockés en local par les users de la machine à travers les tâches planifiées. Je pense que cela est toujours pratique dans le cadre d’une intrusion même si 2 fortes contraintes interviennent, la première est le besoin du tool du SeDebugPrivilege, la seconde la difficulté de retrouver l’adresse de DecryptCredentials. Je vais essayer de travailler sur le second point.
En attendant je vous livre les sources de l’outil TaskPwdDump ainsi que les binaires. Pour l’utilisation il vous suffit de mettre le binaire TaskPwdDump.exe et la Dll TaskPwdDumpDll.dll dans le même dossier. Le tout ce DL ici :
http://ivanlef0u.fr/repo/TaskPwdDmp.rar
N’hésitez pas à me contacter pour tout problème rencontré.
Enjoy !
Au final je tient à remercier maître newsoft pour son aide sur le dev de cet outil !
Sinon Converge en concert, ca défonce !
juillet 25th, 2008
Prochainement à BlackHat aura lieu une conf abordant le thème du SMM (System Management Mode) ce fameux mode du CPU qui a fait tant parlé de lui depuis que Loic Duflot a présenté une conf à CanSecWest 2k6. Au point même qu’on en retrouve une colonne sur SecurityFocus et un article dans le dernier Phrack, bref tout cela a provoqué un bon petit buzz tout comme celui de Kris Kaspersky sur sa future conf de HITB sur l’exploitation de bugs dans les CPU en remote, miam ! Sur le papier le SMM à l’air vraiment cool, ca déchire sa race et ca poutre des loutres comme on dit nous les jeunes cependant je vais vous expliquer pourquoi je suis sceptique à propos de la conf de Sherri Sparks et Shawn Embleton à BlackHat.
Tout d’abord le SMM c’est quoi ? Le SMM est un mode existant sur les CPU x86 qui vient s’ajouter à ceux déjà existant, qui à la particularité d’être exécuté sans que l’OS en soit conscient, contrairement aux autres ou l’OS peut contrôler l’activation ou l’arrêt. C’est justement tout l’intérêt du SMM, pourvoir agir sur la mémoire et les périphériques sans que l’OS le sache, lorsque le CPU en SMM toutes les interruptions, exceptions et même les NMIs sont masquées à noter que le SMM ne peut pas être réentrant ce qui signifie donc que les autres SMIs sont masquées aussi. Le SMM est utilisé par des routines critiques comme celle qui est appelée lorsque le CPU chauffe trop pour l’éteindre ou bien pour gérer les erreurs sur le bus processeur, le fameux FSB.
Lorsque le CPU passe en SMM il se retrouve en real-mode, le bit 0 du cr0 (PE, Protection Enable) est à 0. Le real-mode exécute du code 16 bits en ayant une gestion de la segmentation et des exceptions réalisée de façon plus basique. Le calcul de l’adresse virtuelle (qui est en fait l’adresse physique car il n’y à pas de pagination) est effectué en décalant le segment selector courant (CS, DS, ES ou SS) de 4 bits puis en l’ajoutant à l’offset, ce qui au final donne une adresse sur 20 bits, soit un espace d’adressage allant de 0h à 10FFEFh (1Mo+64Ko). A noter que le support des segments FS et GS ainsi que la possibilité d’exécuter des instructions 32 bits est arrivé plus tard, pour plus d’info vous pouvez lire le « Intel 64 and IA-32 Architectures Software Developer’s Manual Volume 3A: System Programming Guide« .

Pour les exceptions on retrouve aussi une table des interruptions sauf que celle-ci porte le nom d’IVT (Interrupt Vector Table). C’est une table de 256 entrées dont les champs font 4 bytes, 2 pour l’offset du handler et 2 bytes pour le nouveau segment selector CS.

C’est donc dans ce contexte que s’exécute le SMM. Le passage du processeur en SMM se fait à travers une SMI (System Management Interrupt), ce n’est pas une interruption comme les autres dans le sens ou elle n’est pas gérée par un handler de l’IDT, c’est un signal envoyé au CPU sur le pin in SMI# depuis le bus APIC. Remarquez que le lorsque le CPU est en SMM il notifie le northbrige avec le pin SMIACT# ceci aura de l’importance pour la suite. Enfin, la sortie du SMM se fait en appelant l’instruction RSM, celle ci va juste restaurer le contexte du CPU qui a été sauvegardé en mémoire lors du passage en SMM.
Le code et des données SMM sont stocké dans une zone mémoire spéciale appelée la SMRAM, cette zone mémoire de 64 Ko commençant par défaut à l’adresse physique 0×30000 (SMBASE) est uniquement accessible lorsque le CPU est en SMM. Lors du passage en SMM le CPU sauvegarde son contexte dans la zone allant de [SMBASE+0xFE00] jusqu’à [SMBASE+0xFFFF] il va ensuite exécuter les instructions situées à partir de [SMBASE+0x8000] le reste de la SMRAM est libre et dépend de votre BIOS car c’est lui qui initialise cette zone mémoire lors du démarrage. En temps normal, la SMRAM pointe vers la mémoire vidéo.

Il est possible de modifier la SMBASE en changeant le registre SMBASE (offset SMBASE+0x7EF8) qui est sauvegardé lors du passage en SMM, le retour avec l’instruction RSM mettra à jour ce registre interne du CPU et les prochaines SMIs prendront en compte la nouvelle SMBASE. D’après ce que j’ai pu voir sur les systèmes actuels la SMRAM est relogée en 0xA0000. Il est même possible d’accéder à un espace mémoire de 4Go depuis le SMM avec l’aide d’un préfixe sur les instructions et adresses, le Operand-size override prefix (0×66) et le Address-size override prefix (0×67).
Finit l’introduction, passons aux choses sérieuses. Un attaquant aurait ainsi 2 raisons d’utiliser le SMM :
- Avoir son code placé dans une zone mémoire inaccessible en temps normal par les HIPS et qui récemment encore était inconnu du public.
- Travailler avec un niveau de privilège élevé sur la machine en ayant avec un accès total au CPU, à la mémoire et aux périphériques permettant de contourner toutes les protections mises en place par l’OS.
C’est donc sur ces points qu’insistent les conférences de Loic Dufloc et très certainement celle de BlackHat aussi. En fait le vrai problème si situe au niveau de l’accès à cette fameuse mémoire SMRAM, si un attaquant veut exécuter du code en il va devoir y écrire mais comme je l’ai dit précdemment la SMRAM est uniquement accessible lorsque le CPU est en SMM, donc pour le moment est bloqué. Serait-ce la fin de notre voyage ? Pas forcément, il est quand même possible de taper dans la SMRAM, cependant pour cela il faut descendre un peu plus bas sur la carte mère en allant jouer avec le nortbridge.
Le northbridge ce chipset faisant le lien entre votre CPU, votre bus PCI Express (ou AGP), votre SDRAM et le southbridge, contrôle en effet l’accès à la SMRAM à travers la notification par le pin out du CPU SMIACT#. En temps normal les adresses physiques de 0xA0000 à 0xBFFFF sont des memory mapped I/O gérées par le bus PCI. Sur l’image suivante on voit que ces adresses sont routées vers le chipset Intel Q965/Q963 qui est mon northbridge.

Hop on récupère datasheet du chipset sur le site de Intel et on passe en mode RTFM. A l’aide de la doc et des papiers sur le sujet on apprend qu’il existe un registre dans le northbridge qui sert à contrôler l’accès à la SMRAM, le SMRAMC (System Management RAM Control). Pour accéder au registre de quelconque périphérique sur un bus PCI on doit connaître l’indice du bus, l’indice du device, l’indice de la fonction du device et l’index du registre à lire/écrire, dans mon cas les devices accessibles depuis le northbridge peuvent être représenté par le schéma suivant :

Pour pouvoir accéder à la configuration de ces devices le mécanisme est assez simple : Il existe 2 registres d’I/O, le CONF_ADDR (4 bytes en 0xCF8)et le CONF_DATA (4 bytes en 0xCFC). Le premier sert à choisir le bus, le device, la fonction et le registre du device à accéder, le second sert à effectuer l’opération d’I/O. On définir ces registres à l’aide des instructions IN et OUT. Sachant que le SMRAMC est sur le bus 0, device 0, function 0, registre 0x9D, on définit le CONF_ADDR de la façon suivante :
#define MAKE_CONF_ADDR(Reg, Fct, Dev, Bus, Enable) (((Reg)&0xfc) | (((Fct)&7)<<8) | (((Dev)&31)<<11) | ((Bus&0xff)<<16) | ((Enable)<<31))
#define SMRAM 0x9D
ConfReg=MAKE_CONF_ADDR(SMRAM&0xFFFC, 0, 0, 0, 1); // On aligne notre I/O sur une adress multiple de 4
Problème, l’I/O ne fonctionne pas en user-land. Faire une I/O demande que le CPL (Current Privilege Level) soit inférieur ou egal à l’IOPL (I/O Privilege level, bits 12 et 13 de l’EFlags), comme par hasard on a par défautl un IOPL à 0 et un CPL de 3 en user-land. Une solution simple serait de coder un driver pour les I/O mais la flemme. Après quelques recherches sur le net j’ai trouvé une solution plus élégante : L’api native ZwSetInformationProcess permet avec l’InformationClass ProcessUserModeIOPL (16) de modifier l’IOPL du thread courant. On code un sample et là bim ! Echec ! La fonction nous renvoie le numéro d’erreur 0xC0000061 (STATUS_PRIVILEGE_NOT_HELD), en effet comme le spécifie l’article de Michael Wookey il faut que le token du process possède le privilège SeTcbPrivilege. Celui-ci peut être attribuer en allant dans Panneau de configuration -> Outils d’administration -> Stratégie de sécurité locale -> Stratégies locales -> Attribution des droits utilisateurs et en ajoutant votre utilisateur dans la stratégie « Agir en tant que partie du système d’exploitation » (après avoir ajouter votre user il faut vous relancer votre session). Voilà le bout de code :
BOOL EnablePrivilege(PTCHAR Privilege)
{
BOOL rc=FALSE;
HANDLE hToken;
LUID luid;
TOKEN_PRIVILEGES tokenPrivilege;
//
// Open the current process' token.
//
rc=OpenProcessToken(
GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
&hToken);
if(rc)
{
rc=LookupPrivilegeValue(NULL, Privilege, &luid);
if(rc)
{
tokenPrivilege.PrivilegeCount = 1;
tokenPrivilege.Privileges[0].Luid = luid;
tokenPrivilege.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
//
// Assign the given privilege.
//
rc=AdjustTokenPrivileges(
hToken,
FALSE,
&tokenPrivilege,
sizeof(tokenPrivilege),
NULL,
NULL);
}
}
if(hToken)
CloseHandle(hToken);
return rc;
}
BOOL EnableUserModeHardwareIO()
{
NTSTATUS Status;
ULONG IOPL=3;
//
// Enable SeTcbPrivilege
//
if(!EnablePrivilege(SE_TCB_NAME))
return FALSE;
//
// Grant user mode hardware IO access.
//
Status=ZwSetInformationProcess(
GetCurrentProcess(),
ProcessUserModeIOPL,
&IOPL,
sizeof(IOPL));
if(!NT_SUCCESS(Status))
{
printf("Error with ZwSetInformationProcess : 0x%x\n", Status);
return FALSE;
}
return TRUE;
}
Dorénavant on sait comment faire des I/O depuis le user-land, c’est beaucoup plus smooth que de coder un driver. Revenons donc à ce registre SMRAMC, ce qui nous intéresse c’est la valeur des bits D_OPEN et D_LCK, ceux qui contrôlent l’accès à la SMRAM. Par défaut le D_OPEN est à 0, signifiant que tout accès à une adresse comprise entre 0xA0000 et 0xBFFFF sera renvoyé vers la mémoire vidéo, par contre lorsque ce bit est à 1, le northbridge reroute les accès vers la SMRAM, c’est ce qui se passe lorsque le CPU est en SMM. Pour info voici à quoi ressemble le registre SMRAMC :

La question de-la-mort-fatale-atomique-qui-tue-avec-des-chocapicz est : Peut-on modifier ce bit librement pour accéder à la SMRAM sans être en SMM ?
La réponse est oui ! On peut avoir accès à la SMRAM ! Une fois le droit d’écriture obtenu il est possible d’ajouter notre propre handler en [SMBASE+0x8000] et hop le tour est joué ! Evidemment il faut prendre quelques précautions en écrivant un handler de SMI mais ca se fait. Je ne vais rentrer dans les détails de ce qu’il est possible de faire en SMM, vous avez les papers de phrack et de Duflot pour ça, nan je vais plutôt commencer à pleurer. En effet après avoir codé un tool qui dump le registre SMRAMC j’obtiens les résultats suivants :
Windows SMRAMC and ESMRAMC registers dumper
Only works on Intel northbridge chipsets
By Ivanlef0u
BE M4D!
Reading VID-Vendor Identification Register
Writing CONF_ADDR register with 0x80000000
CONF_DATA register 0x8086
Reading SMRAM and ESMRAMC register
Writing CONF_ADDR register with 0x8000009c
CONF_DATA register 0xb81a
Dumping SMRAM register ...
RESERVED0_BIT : 0
D_OPEN_BIT : 0
D_CLS_BIT : 0
D_LCK_BIT : 1
G_SMRAME_BIT : 1
C_BASE_SEG2_BIT : 0
C_BASE_SEG1_BIT : 1
C_BASE_SEG0_BIT : 0
Writing CONF_ADDR register with 0x8000009c
CONF_DATA register 0xb81a
Dumping ESMRAMC register ...
T_EN_BIT : 0
TSEG_SIZE_BIT0 : 0
TSEG_SIZE_BIT1 : 0
SM_L2_BIT : 1
SM_L1_BIT : 1
SM_CACHE_BIT : 1
E_SMERR_BIT : 0
H_SMRAME_BIT : 1
Regardez bien le bit D_LCK, il est à 1 ! Il verrouille donc le bit D_OPEN qui ne peut plus être modifié par l’utilisateur … YOU FAILED ! Ce qui est drôle c’est que tous les chipsets que j’ai pu tester avaient ce bit de lock à 1, m’empêchant ainsi de progresser dans mon ambition de conquête du monde. Juste un mot sur le registre ESMRAMC (Extended System Management RAM Control), pour faire court c’est une extension du registre SMRAMC, il permet de gérer le caching de la SMRAM ou bien un remapping de la SMRAM dans les adresses hautes.

Pour le moment on est au point mort, pas moyen de dumper cette foutue SMRAM. Loic Duflot propose dans sa thèse de contourner le bit D_OPEN en utilisant une feature du chipset AGP, l’ouverture graphique ou en anglais « aperture« , disponible sur les chipsets plus anciens. L’aperture permet au chipset AGP de faire croire à la carte graphique que le FrameBuffer est une zone contigue dans la mémoire physique alors qu’en réalité elle ne l’est pas, pour cela le chipset AGP agit comme un TLB (Translation Lookaside Buffer) mais au niveau des adresses physiques en les remappant vers d’autres adresses physiques. L’attaque pour contourner le bit D_LCK consiste en gros à crée une table de translation représentant la fonction identité, c’est à dire qui mappe chaque page sur elle-même, puis, comme si de rien n’était faire une écriture ou une lecture dans la SMRAM. Le chipset croyant qu’on tente un accès dans la mémoire vidéo va translater les adresses en fonction des TLB du chipset, en sachant que l’aperture est prioritaire sur la SMRAM la notre translation s’effectue sans problème nous laissant ainsi libre accès à la SMRAM. Dans la réalité cette technique est un peu plus compliqué que ça, d’ailleurs je pense que je n’ai pas tout parfaitement compris mais l’idée est là :p Au passage je fais de la pub pour les posts d’un pote qui à fait un travail intéressant sur le PCI et bootvid.dll.
Alors, de mon coté sur ma carte-mère je n’ai pas d’AGP, j’ai du PCI-Express, en lisant la doc on apprend que le mécanisme d’aperture n’existe plus avec le PCI-Express. Par contre le chipset Q963/Q965 possède un Integrated Graphics Device (IGD) une sorte de carte graphique de base qu’on peut configurer à travers le bus 0, device 2, function 0 (voir le schéma du bus PCI plus haut). Cet IGD offre la possibilité de mettre en place une Graphics Translation Table (GTT), un mécanisme d’aperture, W0ot ! Il ne reste plus qu’a faire le même type d’attaque qu’avec l’aperture AGP et c’est gagné. Sauf que la doc dit :
3.8.7 SMM Access Through GTT TLB (Intel 82Q965, 82Q963, 82G965 GMCH Only)
Accesses through GTT TLB address translation to enabled SMM DRAM space are no
allowed. Writes will be routed to memory address 000C_0000h with byte enables d
asserted and reads will be routed to memory address 000C_0000h. If a GTT TLB
translated address hits enabled SMM DRAM space, an error is recorded.
PCI Express and DMI Interface originated accesses are never allowed to access SM
space directly or through the GTT TLB address translation. If a GTT TLB translated
address hits enabled SMM DRAM space, an error is recorded.
PCI Express and DMI Interface write accesses through GMADR range will be snoop
Assesses to GMADR linear range are supported. PCI Express and DMI interface tile
and tileX writes to GMADR are not supported. If, when translated, the resulting
physical address is to enabled SMM DRAM space, the request will be remapped to
address 000C_0000h with de-asserted byte enables.
PCI Express and DMI Interface read accesses to the GMADR range are not support
therefore, will have no address translation concerns. PCI Express and DMI interfac
reads to GMADR will be remapped to address 000C_0000h. The read will complete
with UR (unsupported request) completion status.
Pan ! Dans les dents ! L’accès à la SMRAM avec la GTT est vérifié, YOU FAILED AGAIN ! Y’en a marre … j’abandonne.
Après toutes ces tentatives je me demande comment actuellement avec les derniers chipsets il est possible d’accéder à la SMRAM sans être en SMM. C’est pourquoi je suis si sceptique sur la prochaine conf de Sherri Sparks et Shawn Embleton parce que le bit D_LCK est à 1 sur tous les chipsets récents et que les accès à la SMRAM sont sévèrement contrôlés. On peut très bien supposer qu’ils aient trouvé une technique géniale qui permet d’accéder à la SMRAM même si ces protections sont activées et là c’est de la balle mais j’avoue en douter fortement. Je dirais plus qu’ils ont fait leurs expériences sur des b0x débridées leurs offrant un accès sans problèmes à la SMRAM. Au final on se retrouvait avec une technique de furtivité de code impressionnante mais qui marche uniquement sur des machines datant de la guerre des druides. Bref wait and see …
En attendant voici le code qui permet de dumper les registres SMRAC et ESMRAMC :
http://ivanlef0u.fr/repo/smram.rar
Si quelqu’un connaît une technique permettant d’accéder à la SMRAM, je suis très intéressé et prêt à payer en chocapicz !!!
Réf :
http://www.rcollins.org/ddj/Jan97/Jan97.html
juillet 21st, 2008
Toujours en quête de nouvelles techniques de rootkit pour Windows, je suis tombé en lisant mes RSS sur un POC chinois réalisé par Sudami qui permet de rendre un process immortel. Intéressé par la chose j’ai voulu prendre le code source mais manque de bol d’après le google translate il faut être inscrit et faire partie d’une communauté underground aux yeux bridés pour y avoir accès. Cependant on peut download le binaire librement. Après quelques heures de reverse je vous propose de plonger au coeur de ce nouveau bébé très innovant qui permet de bien faire chier n’importe quel utilisateur et AV. Le must pour un rootkit :]
Dans le .rar, on ne trouve qu’un simple binaire nommé « sudami.exe » qui fait 67,5 Ko, dès le départ on se doute qu’il va devoir charger un driver dans le noyau au vu du titre du post « DKOM to Protect EXE self,without any hook », DKOM signifiant Direct Kernel Object Manipulation on sait que le programme va opérer au niveau noyau. Le binaire est packé avec PECompact 2.x, pour l’unpack rien de très difficile, on trace le programme de SEH en SEH pour tomber sur celui qui saute sur l’OEP (Original Entry Point). Pour info ce saut est en 0x42BF8A (jmp eax). Une fois qu’on est sur l’OEP on peut tranquillement dumper le binaire avec OllyDump ou LordPE+Imprec pour retrouver le binaire d’origine. L’orignal pèse 180 Ko, un coup d’oeil aux ressources avec LordPe nous permet de voir une ressource nommée « SYS » qui, en regardant le dump hexa, commence avec « MZ » (0x4D 0x5A 0×90), ca nous suffit largement pour dire que cette ressource est un binaire, après extraction ce binaire fait 9 Ko. Le champ Subsystem du PE header du binaire extrait vaut 1, ce qui veut dire que celui-ci est un driver. En regardant le plus près le binaire celui-ci va chargé le driver en utilisant le Service Manager avec les APIs OpenSCManager, OpenService et ControlService puis va spawner une window avec plein de mots chinois qu’on comprend pas dedans. Il va ensuite envoyer une série d’IOCTL au driver (0×80002000, 0×80002008, 0×80002010 et 0×80002018) avec l’API DeviceIoControl pour rendre le process immortel. Le mieux est que vous regardiez par vous même le binaire sous IDA ou sous Olly pour voir son fonctionnement. Bref au final on se retrouve avec une popup en chinois qui reste ouverte sans qu’on puisse la killer. Tout cela n’est pas la partie visible de l’iceberg, le vrai stuff intéressant se trouve bien évidemment dans le driver.
Commence donc l’étude du driver, premièrement je le charge en VM histoire de regarder ce qu’il définit comme MajorFunctions et ses Devices :
0: kd> !drvobj \driver\sudami 3
Driver object (815c87e0) is for:
*** ERROR: Module load completed but symbols could not be loaded for sudami.sys
\Driver\sudami
Driver Extension List: (id , addr)
Device Object list:
813b1490
DriverEntry: f9e08005 sudami
DriverStartIo: 00000000
DriverUnload: f9e04250 sudami
AddDevice: 00000000
Dispatch routines:
[00] IRP_MJ_CREATE f9e042a0 sudami+0x12a0
[01] IRP_MJ_CREATE_NAMED_PIPE 804f9709 nt!IopInvalidDeviceRequest
[02] IRP_MJ_CLOSE f9e042a0 sudami+0x12a0
[03] IRP_MJ_READ 804f9709 nt!IopInvalidDeviceRequest
[04] IRP_MJ_WRITE 804f9709 nt!IopInvalidDeviceRequest
[05] IRP_MJ_QUERY_INFORMATION 804f9709 nt!IopInvalidDeviceRequest
[06] IRP_MJ_SET_INFORMATION 804f9709 nt!IopInvalidDeviceRequest
[07] IRP_MJ_QUERY_EA 804f9709 nt!IopInvalidDeviceRequest
[08] IRP_MJ_SET_EA 804f9709 nt!IopInvalidDeviceRequest
[09] IRP_MJ_FLUSH_BUFFERS 804f9709 nt!IopInvalidDeviceRequest
[0a] IRP_MJ_QUERY_VOLUME_INFORMATION 804f9709 nt!IopInvalidDeviceRequest
[0b] IRP_MJ_SET_VOLUME_INFORMATION 804f9709 nt!IopInvalidDeviceRequest
[0c] IRP_MJ_DIRECTORY_CONTROL 804f9709 nt!IopInvalidDeviceRequest
[0d] IRP_MJ_FILE_SYSTEM_CONTROL 804f9709 nt!IopInvalidDeviceRequest
[0e] IRP_MJ_DEVICE_CONTROL f9e043e0 sudami+0x13e0
[0f] IRP_MJ_INTERNAL_DEVICE_CONTROL 804f9709 nt!IopInvalidDeviceRequest
[10] IRP_MJ_SHUTDOWN 804f9709 nt!IopInvalidDeviceRequest
[11] IRP_MJ_LOCK_CONTROL 804f9709 nt!IopInvalidDeviceRequest
[12] IRP_MJ_CLEANUP 804f9709 nt!IopInvalidDeviceRequest
[13] IRP_MJ_CREATE_MAILSLOT 804f9709 nt!IopInvalidDeviceRequest
[14] IRP_MJ_QUERY_SECURITY 804f9709 nt!IopInvalidDeviceRequest
[15] IRP_MJ_SET_SECURITY 804f9709 nt!IopInvalidDeviceRequest
[16] IRP_MJ_POWER 804f9709 nt!IopInvalidDeviceRequest
[17] IRP_MJ_SYSTEM_CONTROL 804f9709 nt!IopInvalidDeviceRequest
[18] IRP_MJ_DEVICE_CHANGE 804f9709 nt!IopInvalidDeviceRequest
[19] IRP_MJ_QUERY_QUOTA 804f9709 nt!IopInvalidDeviceRequest
[1a] IRP_MJ_SET_QUOTA 804f9709 nt!IopInvalidDeviceRequest
[1b] IRP_MJ_PNP 804f9709 nt!IopInvalidDeviceRequest
0: kd> !devobj 813b1490
Device object (813b1490) is for:
devsudami \Driver\sudami DriverObject 815c87e0
Current Irp 00000000 RefCount 0 Type 00000022 Flags 00000040
Dacl e12a719c DevExt 00000000 DevObjExt 813b1548
ExtensionFlags (0000000000)
Device queue is not busy.
0: kd> !object \Global??
Object: e1002898 Type: (817ef418) Directory
ObjectHeader: e1002880 (old version)
HandleCount: 1 PointerCount: 126
Directory Object: e1001be0 Name: GLOBAL??
Hash Address Type Name
---- ------- ---- ----
00 e147efe0 SymbolicLink D:
e13173c8 SymbolicLink NDIS
e138fbd0 SymbolicLink DISPLAY1
[...................................]
31 e17b5fe0 SymbolicLink sudami
[...................................]
Le driver va donc créer un device nommé sudami avec un SymbolicLink (qui est donc mit dans l’ObjectDirectory \Global??) autorisant la communication depuis le user-land qui s’appelle aussi sudami. On retrouve seulement 2 MajorFunctions, la même pour les IRP_MJ_CREATE et IRP_MJ_CLOSE qui sert juste à complêter l’IRP avec un STATUS_SUCCESS.
0: kd> uf f9e042a0
sudami+0x12a0:
f9e042a0 8bff mov edi,edi
f9e042a2 55 push ebp
f9e042a3 8bec mov ebp,esp
f9e042a5 8b450c mov eax,dword ptr [ebp+0Ch] ; IRP
f9e042a8 c7401800000000 mov dword ptr [eax+18h],0 ; IRP.IoStatus.Status=0=STATUS_SUCCESS
f9e042af 8b4d0c mov ecx,dword ptr [ebp+0Ch] ; IRP
f9e042b2 c7411c00000000 mov dword ptr [ecx+1Ch],0 ; IRP.IoStatus.Information=0
f9e042b9 32d2 xor dl,dl
f9e042bb 8b4d0c mov ecx,dword ptr [ebp+0Ch]
f9e042be ff151860e0f9 call dword ptr [sudami+0x3018 (f9e06018)] (IofCompleteRequest)
f9e042c4 33c0 xor eax,eax
f9e042c6 5d pop ebp
f9e042c7 c20800 ret 8
Enfin on a la MajorFunction qui gère les IOCTL en 0xf9e043e0. Maintenant il est temps de sortir IDA pour analyser la routine DispatchDeviceControl.
Première chose qui saute aux yeux et nous fait pissé du sang, le code du driver est offusqué, il contient plein de junk code posé dans les fonctions et IDA n’apprécie pas du tout pour l’analyse. Juste pour déconner voici la gueule du DriverEntry du driver :
.text:000110B0
.text:000110B0 ; Attributes: bp-based frame
.text:000110B0
.text:000110B0 sub_110B0 proc near
.text:000110B0
.text:000110B0 var_18 = dword ptr -18h
.text:000110B0
.text:000110B0 8B FF mov edi, edi
.text:000110B2 55 push ebp
.text:000110B3 8B EC mov ebp, esp
.text:000110B5 83 EC 18 sub esp, 18h
.text:000110B8 C7 45 E8 00 00+ mov [ebp+var_18], 0
.text:000110BF
.text:000110BF loc_110BF:
.text:000110BF 8D 05 C5 10 01+ lea eax, loc_110C5
.text:000110C5
.text:000110C5 loc_110C5:
.text:000110C5 83 C0 0E add eax, 0Eh
.text:000110C8 EB 05 jmp short loc_110CF
.text:000110CA ; ---------------------------------------------------------------------------
.text:000110CA EB F3 jmp short loc_110BF
.text:000110CA ; ---------------------------------------------------------------------------
.text:000110CC DB 30 57 db 0DBh, 30h, 57h
.text:000110CF ; ---------------------------------------------------------------------------
.text:000110CF
.text:000110CF loc_110CF:
.text:000110CF FF E0 jmp eax
.text:000110CF sub_110B0 endp
.text:000110CF
.text:000110CF ; ---------------------------------------------------------------------------
.text:000110D1 E9 21 74 db 0E9h, 21h, 74h
.text:000110D4 03 75 01 E8 8B+ dd 0E8017503h, 0C708458Bh
.text:000110DC ; ---------------------------------------------------------------------------
.text:000110DC 40 inc eax
On retrouve ce schéma de junk dans quasiment toutes les fonctions du driver, l’auteur s’est dit qu’il y aurait bien des gars capables de reverser son driver pour piquer sa technique donc il ajouter du junk dans le code à la main en insérant des macros toutes les 2 lignes de code C pour rendre la tâche du reverser plus longue mais pas impossible ! En effet, en regardant de plus près on a un « lea eax, loc_110C5″ suivit d’un « add eax, 0EH », eax vaut donc loc_110C5+0EH. Ensuite nous avons un « jmp short loc_110CF » qui saute sur un « jmp eax », nous devons donc normalement arriver en loc_110C5+0EH et la IDA n’arrive pas analyser correctement, c’est l’échec :] En 0x110D3 (0x110C5+0xE) on peut voir l’opcode 0×74 qui correspond à un JZ et en 0x110D5 on peut voir l’opcode d’un JNZ (0×75), le plus drôle c’est que le JZ saute 3 bytes plus loin et le JNZ 1 byte plus loin, donc tous les deux sautent au même endroit, c’est à dire en 0x110D8. Hop on nettoie ca sous IDA pour obtenir le code clarifié :
.text:000110B8 C7 45 E8 00 00+ mov dword ptr [ebp-18h], 0
.text:000110BF 8D 05 C5 10 01+ lea eax, loc_110C5
.text:000110C5
.text:000110C5 loc_110C5:
.text:000110C5 83 C0 0E add eax, 0Eh
.text:000110C8 EB 05 jmp short loc_110CF
.text:000110C8 ; ---------------------------------------------------------------------------
.text:000110CA EB db 0EBh ; Ù
.text:000110CB F3 db 0F3h ; ¾
.text:000110CC DB db 0DBh ; ¦
.text:000110CD 30 db 30h ; 0
.text:000110CE 57 db 57h ; W
.text:000110CF ; ---------------------------------------------------------------------------
.text:000110CF
.text:000110CF loc_110CF:
.text:000110CF FF E0 jmp eax
.text:000110CF ; ---------------------------------------------------------------------------
.text:000110D1 E9 db 0E9h
.text:000110D2 21 db 21h ; !
.text:000110D3 ; ---------------------------------------------------------------------------
.text:000110D3 74 03 jz short loc_110D8
.text:000110D5 75 01 jnz short loc_110D8
.text:000110D5 sub_110B0 endp
.text:000110D5
.text:000110D5 ; ---------------------------------------------------------------------------
.text:000110D7 E8 db 0E8h ; Þ
.text:000110D8 ; ---------------------------------------------------------------------------
.text:000110D8
.text:000110D8 loc_110D8:
.text:000110D8
.text:000110D8 8B 45 08 mov eax, [ebp+arg_0]
.text:000110DB C7 40 38 A0 12+ mov dword ptr [eax+38h], offset sub_112A0
.text:000110E2 8D 05 E8 10 01+ lea eax, loc_110E8
Le junk consiste donc à un jmp sur 2 jumps conditionnels qui ensemble forment le même saut, jump if zero (JZ) qui saute si le ZeroFlag de l’EFlags est à 1 et jump if not zero (JNZ) qui fait l’inverse. On peut donc remplacer tout ce code par des NOP vu qu’il est inutile et regrouper les NOPs dans un tableau pour obtenir un disass clair. Après avoir fait le ménage on obtient :
.text:000110B8 C7 45 E8 00 00+ mov dword ptr [ebp-18h], 0
.text:000110B8 00 00 ; ---------------------------------------------------------------------------
.text:000110BF 90 90 90 90 90+ db 19h dup(90h)
.text:000110D8 ; ---------------------------------------------------------------------------
.text:000110D8 8B 45 08 mov eax, [ebp+8]
Ce qui est clairement plus lisible ! Il ne reste plus qu’a automatiser la tâche avec un script IDC. Pour le script j’ai fait simple, j’ai recherché tout les « jmp eax » dans le binaire avec la function FindBinary, j’ai noppé les instructions autour qui ne servaient à rien puis regrouper les NOPs dans un tableau. Voici le script très simple :
//
// Cleaner for driver http://hi.baidu.com/sudami/blog/item/1fe5b203005f45e909fa9368.html
// Ivanlef0u
//
#include <idc.idc>
static main()
{
auto i;
auto addr;
Message("Starting clean script ...\n");
addr=0;
do
{
//
// jmp eax= 0xFF 0xE0
//
addr=FindBinary(addr, SEARCH_DOWN, "FF E0");
Message("jmp eax at : 0x%x\n", addr);
//
// 16 bytes avant le "jmp eax" commence le junk et se termine 9 bytes plus loin
//
for (i=0; i<16+9; i++)
PatchByte(addr-16+i, 0x90);
//
// Instrution suivante
//
addr=addr+2;
}while(addr!=(-1+2)); //((addr=-1 lorsque plus rien)
addr=0;
//
// Met les nop dans des tableaux
//
do
{
addr=FindBinary(addr, SEARCH_DOWN, "90");
Message("nop at : 0x%x\n", addr);
for(i=0; i<25; i++)
{
MakeUnknown(addr+i, 1, DOUNK_SIMPLE);
}
MakeArray(addr, 25);
//
// Instrution suivante
//
addr=addr+26;
}while(addr!=(-1+26)); //((addr=-1 lorsque plus rien)
}
Une fois qu'on a nettoyé le driver on peut enfin commencer à analyser son fonctionnement, nous allons juste nous intéresser à la fonction DispatchDeviceControl. On sait que le driver sert à rendre le process immortel, il va donc devoir agir sur la structure EPROCESS et très certainement sur les structures ETHREADs correspondant aux threads du process. Pour analyser la routine de dispatch des IOCTLs nous allons juste suivre les fonctions appelées en fonction des IOCTLs passé au driver.
Commençons par le premier IOCTL (0x80002000) celui-ci nous va utliser IoGetCurrentProcess pour récupérer l'EPROCESS du process courant, donc celle du programme à protéger et à partir du pointeur sur la double liste chainée ThreadListHead de la KPROCESS (offset KPROCESS+0x50) va parcourir la liste de structures ETHREAD représentant les threads du process pour changer le champ KernelApcDisable (offset 0xD4 de la KTRHEAD), si ce champ est à 0 alors il le passe à la valeur 0xA98AC7 (grut?!). En regardant les symbols on peut voir que ce champ fait 2 bytes, alors d'après ce que j'ai compris ce champ agirait comme un compteur. Je m’explique lorsqu'on envoie un APC (Asynchronous Procedure Call) à un thread 3 routines sont mises en place, une KernelRoutine qui est appelée directement en préemptant le thread en kernel-land à un IRQL de APC_LEVEL, une NormalRoutine qui est généralement appelée en user-land lorsque le thread est en state Waiting puis une RundownRoutine qui est la routine qui sera appelée lorsque le thread sera killé et qui à pour rôle de faire le ménage sur les APC qui n'auront pas été exécutés. La fonction KiInsertQueueApc va vérifier la valeur du champ KernelApcDisable si celui-ci est à 0 alors la fonction queue un APC kernel pour le thread. Lorsque le thread appel une des APIs KeEnterCriticalRegion ou KeEnterGuardedRegion le champ KernelApcDisable est décrémenté inversement pour les APIS KeLeaveCriticalRegion et KeLeaveGuardedRegion, ce qui faut retenir c'est que la valeur de KernelApcDisable est différente de 0 dès que le thread rentre dans une région critique. Du fait qu'il est possible d'avoir des plusieurs appels à KeEnterCriticalRegione et KeEnterGuardedRegion un compteur est mis en place afin de connaître le nombre d'appels à ces APIs, après je n'arrive pas à comprendre pourquoi ce dernier compte à l'envers ... Je reviendrais sur rôle des APCs kernel plus loin dans ce post, d'abord voyons les actions des autres IOCTLs.
Continuons avec le second IOCTL envoyé au driver (0x80002008), la fonction associée va aussi parcourir la liste des threads du process mais cette fois ci pour modifier le champ State (offset 0x2D de la KTHREAD) à la valeur 4 qui correspond à l'état Terminated, celui la même qui est définit lorsqu'on appel TerminateThread. Juste pour rappel voici les différents états possibles pour un thread sous Windows :
//
// Thread scheduling states.
//
typedef enum _KTHREAD_STATE {
Initialized,
Ready,
Running,
Standby,
Terminated, //=4
Waiting,
Transition,
DeferredReady,
GateWait
} KTHREAD_STATE;
Le troisième IOCTL (0x80002010) est beaucoup plus freestyle, la fonction chargée de gérer cet IOCTL va commencer par récupérer l'adresse de la fonction PsTerminateSystemThread et scanner ses instructions une par une en utilisant un LDE (Length Disassembly Engine) qui permet d'obtenir la taille des instructions en fonction de leurs opcodes, ca se voit sous IDA vu la bouillie que crée la fonction LDE. La fonction de scan recherche l'instruction commençant par (0x80F6) puis lit le word situé 2 bytes plus loin par rapport au début de l'instruction. Pour savoir à quoi ça correspond il suffit de désassembler la fonction PsTerminateSystemThread :
lkd> uf PsTerminateSystemThread
nt!PsTerminateSystemThread:
805d2c2a 8bff mov edi,edi
805d2c2c 55 push ebp
805d2c2d 8bec mov ebp,esp
805d2c2f 64a124010000 mov eax,dword ptr fs:[00000124h]
805d2c35 f6804802000010 test byte ptr [eax+248h],10h
805d2c3c 7507 jne nt!PsTerminateSystemThread+0x1b (805d2c45)
nt!PsTerminateSystemThread+0x14:
805d2c3e b80d0000c0 mov eax,0C000000Dh
805d2c43 eb09 jmp nt!PsTerminateSystemThread+0x24 (805d2c4e)
nt!PsTerminateSystemThread+0x1b:
805d2c45 ff7508 push dword ptr [ebp+8]
805d2c48 50 push eax
805d2c49 e828fcffff call nt!PspTerminateThreadByPointer (805d2876)
nt!PsTerminateSystemThread+0x24:
805d2c4e 5d pop ebp
805d2c4f c20400 ret 4
L'instruction qui commence par 0x80F6 est "test byte ptr [eax+248h],10h", 2 bytes après le début des opcodes on retrouve la valeur 0x0248 qui tout l'offset d'un champ dans la structure ETHREAD. L'auteur à tout simplement implémenter une méthode générique pour retrouver l'offset de ce champ en se basant sur le code de la fonction noyau PsTerminateSystemThread, c'est pas mal, je dirais même bien mais je ne pense pas que ca soit nécessaire, un coup de PsGetVersion avec un switch pour définir les offsets suffit aussi et à l'avantage d'être beaucoup moins lourd. Quoiqu'il en soit à l'offset 0x248 de la structure on retrouve le champ CrossThreadFlags, un byte servant à définir différents flags pour le thread :
lkd> dt nt!_ETHREAD
[...]
+0x248 CrossThreadFlags : Uint4B
+0x248 Terminated : Pos 0, 1 Bit
+0x248 DeadThread : Pos 1, 1 Bit
+0x248 HideFromDebugger : Pos 2, 1 Bit
+0x248 ActiveImpersonationInfo : Pos 3, 1 Bit
+0x248 SystemThread : Pos 4, 1 Bit
+0x248 HardErrorsAreDisabled : Pos 5, 1 Bit
+0x248 BreakOnTermination : Pos 6, 1 Bit
+0x248 SkipCreationMsg : Pos 7, 1 Bit
+0x248 SkipTerminationMsg : Pos 8, 1 Bit
[...]
La fonction va parcourir la liste des threads du process et mettre le flag Terminated à 1 pour tous les threads.
Enfin le dernier IOCTL (0x80002018) va modifier le PID du process au niveau du champ UniqueProcessId à l'offset 0x84 de la structure EPROCESS à la valeur de 7 et le propager aux ETHREADS en modifiant le champ UniqueProcess de la structure CLIENT_ID située à l'offset 0x1Ec de la structure ETHREAD. Je suppose que cette modification est faite pour tromper la PspCidTable, cette table de handles qui correspond aux PID et qui permet de faire la translation PID->EPROCESS afin de tromper les APIs kernel qui normalement vérifient que les 2 valeurs de PID, celle envoyée et celle trouvée, correspondent, à vérifier quand même.
Ouf, finit le reverse d'IOCTLs, résumons à peu ce que nous avons 4 IOCTLS qui activent 4 protections différentes :
- Le premier sert à désactiver les APC kernel sur tout les threads du process.
- Le second va changer l'état du thread (State) à Terminated.
- Le troisième modifie les flags des différentes threads du process en activant le flag Terminated. La différence avec le point précédent c'est que le champ State sert uniquement au thread scheduler alors que le flag est utilisé par les APIs noyau pour vérifier l'état du thread.
- Enfin le dernier IOCTL change le PID du process et celui de ses threads à la valeur 7, une valeur qui n'existe pas dans la PspCidTable.
Je reviens sur la désactivation des APC noyau, le point qui me gênait le plus. En fait lorsqu'on appel l'API NtTerminateThread celle-ci appelle PspTerminateThreadByPointer après avoir référencé l'objet ETHREAD avec ObReferenceObjectByHandle et va envoyer un APC sur le thread avec la fonction KeInsertQueueApc. Remarquez que même la fonction PsTerminateSystemThread qui sert à killer un thread kernel en étant appelé par le thread lui même va appeler PspTerminateThreadByPointer. En fait en y pensant un peu cela est normal, un APC est une routine qui est exécutée dans le context d'un thread, il est normal qu'un thread ne puisse pas s'auto détruise, ca serait comme couper la branche sur laquelle il est, c'est donc une routine externe qui s'en charge. En l'occurrence cette routine s'appelle PsExitSpecialApc et lance PspExitThread. Donc comme le driver désactive les APC noyau en faisant croire que celui-ci est une région critique, l'APC chargé de killé le thread n'est pas lancé et ainsi le thread n'est pas détruit, tout s'explique ! (ou bien j’ai rien compris :p)
Pour conclure, je trouve le taff de Sudami vraiment très intéressant, ca innove et c'est basé sur des choses simples, on voit qu'il connaît très bien l'architecture du noyau Windows. En fait son code revient juste à faire croire au kernel que les threads du process sont déjà killés, le noyau ne va donc pas essayer de les détruire dans ce cas là et ils se verront toujours runner. La partie déobffuscation de code était aussi marrante, ce n’est pas très méchant, moi qui n'est pas vraiment l'habitude de voir des binaires comme ça j'ai apprit quelques trucs, cependant vivement IDA 5.3 qui permettra de faire du script avec des languages comme python ou ruby :]
Voilà, c'est fini, j'espère que vous avez appris pas mal de trucs, maintenant si quelqu'un est motivé pour recoder les fonctionnalités du driver, je suis preneur
Je vous file une archive avec le binaire original, le binaire unpacké, le driver extrait et l'ouput html du reverse sous IDA de ce dernier. Vous trouverez tout ça ici : http://ivanlef0u.fr/repo/sudami_KillMe.rar
Enjoy !
juillet 15th, 2008
Je viens de retrouver les pass de mon blog, je les avais laissés trainer sous forme alphabétique dans un .txt crypté avec un double rot13 sur une dedib0x … Je vais pouvoir enfin vous parler d’un projet que j’ai commencé récemment. Je mattais la doc de PAX quand je suis tombé sur la protection d’exécution utilisant la segmentation. L’idée consiste à utiliser une feature CPU datant des débuts de l’i386, la segmentation, pour séparer le code des datas dans des zones possédants des droits différents, une zone en lecture+exécution et l’autre en lecture+écriture. Ok l’idée est cool, c’est mignon, ca brille et dans le noir on peut le voir mais pour l’instant c’est sous l’OS des barbus et ce n’est pas vraiment mon kiff de faire mumuse avec. Je me suis donc lancé dans l’implémentation d’un équivalent pour Windows et pour être franc, j’en ai chié :]
Ok, je ne suis pas complètement partit de rien, l’implémentation de PAX consiste à utiliser un patch noyau, clairement je ne voulais pas de cela, trop compliqué à réaliser sous Windows. D’ailleurs je ne voulais même pas réaliser de driver pas comme dans l’exemple que j’ai fait avec la pagination dans ce post. Il me fallait donc une solution implémentable depuis le user-land qui ne va défoncer tout mon système en patchant/hookant/changeant la couleur des features de mon noyau.
Entrons un peu dans les détails de l’implémentation de PAX :
2. Implementation
The core of SEGMEXEC is vma mirroring which is discussed in a separate
document. The mirrors for executable file mappings are set up in do_mmap()
(an inline function defined in include/linux/mm.h) except for a special
case with RANDEXEC (see separate document). do_mmap() is the one common
function called by both userland and kernel originated mapping requests.
The special code and data segment descriptors are placed into a new GDT
called gdt_table2 in arch/i386/kernel/head.S. The separate GDT is needed
for two reasons: first it simplifies the implementation in that the CS/SS
selectors used for userland do not have to change, and second, this setup
prevents a simple attack that a single GDT setup would be subject to (the
retf and other instructions could be abused to break out of the restricted
code segment used for SEGMEXEC tasks). Since the GDT stores the userland
code/data descriptors which are different for SEGMEXEC tasks, we have
to modify the low-level context switching code called __switch_to() in
arch/i386/kernel/process.c and the last steps of load_elf_binary() in
fs/binfmt_elf.c (where the task is first prepared to execute in userland).
The GDT also has APM specific descriptors which are set up at runtime and
must be propagated to the second GDT as well (in arch/i386/kernel/apm.c).
Finally the GDT stores also the per CPU TSS and LDT descriptors whose
content must be synchronized between the two GDTs (in set_tss_desc() and
set_ldt_desc() in arch/i386/kernel/traps.c).
Since the kernel allows userland to define its own code segment descriptors
in the LDT, we have to disallow it since it could be used to break out of
the SEGMEXEC specific restricted code segment (the extra checks are in
write_ldt() in arch/i386/kernel/ldt.c).
Pour faire simple, PAX va créer une nouvelle GDT dans laquelle il va définir de nouveaux segment descriptors auxquels seront associés donc des segment selectors. En temps normal les segment selectors que sont CS, DS, ES, FS, GS et SS sont positionnés en flat model, c’est à dire que les segment descriptors associés représentent tout l’espace mémoire, 4 Gb sur un système 32 bits que ce soit sous Win ou nux (exception faite à FS et GS qui peuvent différentes valeurs en fonction de l’implémentation de l’OS). Ce qui signifie que l’EIP qui est utilisé dans l’espace représenté par le segment CS peut se promener ou il veut en mémoire, donc dans le cas d’une exploitation d’un buffer overflow dans la pile avec le shellcode placé dans le stack, EIP pouvant prendre n’importe quelle adresse, il pourra être redirigé sur le shellcode dans la stack sans problème puisque le segment CS l’autorise à aller partout. Attention cependant, même si le segment descriptor est en flat model, la notion de ring intervient toujours et vous empêche d’aller exécuter du code situer dans une page ring0 si votre CPL est à 3. Check this out !
Si vous voulez plus d’infos sur ce sujet allez lire le chapitre 3 « Protected-mode memory management » du man Intel « Intel 64 and IA-32 Architectures Software Developer’s Manual Volume 3A System Programming Guide, Part 1« .
Pour mémoire, un segment selector possède la forme :

L’index permet au core de retrouver le segment descriptor dans la GDT, sachant qu’un segment descriptor est définit par :

En flat model, tous les champs Base sont à 0 et le champ Limit vaut 0xFFFFFF, le champ Granularity étant à 1, il faut multiplier Limit par 4Kb et lui ajouter 0xFFF pour obtenir la taille du segment. Donc dans le cas flat on à une Base à 0 et une Limit à 0xFFFFFFFF, tout l’espace mémoire sur 32 bits. En fait le calcul de l’adresse virtuelle est réalisé en additionnant la base du segment avec l’adresse logique, comme en flat model la base vaut 0, on se retrouve avec adresse logique=adresse virtuelle. Dans la GDT on va donc retrouver 2 descriptors de code, un pour le code ring 3, l’autre pour le code ring 0, pareil pour les descripteurs de data. On se retrouve donc avec une GDT de la forme :
kd> !!display_all_gdt
#################################
# Global Descriptor Table (GDT) #
#################################
Processor 00
Base : 8003F000 Limit : 03FF
Off. Sel. Type Sel.:Base Limit Present DPL AVL Informations
---- ---- -------- ------------- -------- ------- --- --- ------------
0000 0000 NullDesc ....:........ ........ NO 0 0 Raw Descriptor : 00000000 00000000
0008 0008 Code32 00000000 FFFFFFFF YES 0 0 Execute/Read, accessed (Ring 0)CS=0008
0010 0010 Data32 00000000 FFFFFFFF YES 0 0 Read/Write, accessed (Ring 0)DS/SS/ES=0010
0018 001B Code32 00000000 FFFFFFFF YES 3 0 Execute/Read, accessed (Ring 3)CS=001B
0020 0023 Data32 00000000 FFFFFFFF YES 3 0 Read/Write, accessed (Ring 3)DS/SS/ES=0023
0028 0028 TSS32 80042000 000020AB YES 0 0 (Busy) Eip = 0c4d8b51
0030 0030 Data32 FFDFF000 00001FFF YES 0 0 Read/Write, accessed (Ring 0)FS=0030 FS:0->(KPCR*)FFDFF000
0038 003B Data32 00000000 00000FFF YES 3 0 Read/Write, accessed (Ring 3)FS=003B FS:0->(TEB*) 00000000
0040 0043 Data16 00000400 0000FFFF YES 3 0 Read/Write
0048 0048 Reserved ....:........ ........ NO 0 0 Raw Descriptor : 00000000 00000000
0050 0050 TSS32 80550880 00000068 YES 0 0 (Available) Eip = nt!KiTrap08 (804e069d)
0058 0058 TSS32 805508E8 00000068 YES 0 0 (Available) Eip = nt!KiTrap02 (804df5b6)
0060 0060 Data16 00022F30 0000FFFF YES 0 0 Read/Write, accessed
0068 0068 Data16 000B8000 00003FFF YES 0 0 Read/Write
0070 0070 Data16 FFFF7000 000003FF YES 0 0 Read/Write
0078 0078 Code16 80400000 0000FFFF YES 0 0 Execute/Read
0080 0080 Data16 80400000 0000FFFF YES 0 0 Read/Write
0088 0088 Data16 00000000 00000000 YES 0 0 Read/Write
0090 0090 Reserved ....:........ ........ NO 0 0 Raw Descriptor : 00000000 00000000
0098 0098 Reserved ....:........ ........ NO 0 0 Raw Descriptor : 00000000 00000000
00A0 00A0 Reserved ....:........ ........ NO 0 0 Raw Descriptor : 00000000 00000000
00A8 00A8 Reserved ....:........ ........ NO 0 0 Raw Descriptor : 00000000 00000000
00B0 00B0 Reserved ....:........ ........ NO 0 0 Raw Descriptor : 00000000 00000000
00B8 00B8 Reserved ....:........ ........ NO 0 0 Raw Descriptor : 00000000 00000000
00C0 00C0 Reserved ....:........ ........ NO 0 0 Raw Descriptor : 00000000 00000000
00C8 00C8 Reserved ....:........ ........ NO 0 0 Raw Descriptor : 00000000 00000000
00D0 00D0 Reserved ....:........ ........ NO 0 0 Raw Descriptor : 00000000 00000000
00D8 00D8 Reserved ....:........ ........ NO 0 0 Raw Descriptor : 00000000 00000000
00E0 00E0 Code16 FA7B5000 0000FFFF YES 0 0 Execute/Read, accessed
00E8 00E8 Data16 00000000 0000FFFF YES 0 0 Read/Write
00F0 00F0 Code16 804D8B28 0003B337 YES 0 0 Execute-Only
00F8 00F8 Data16 00000000 0000FFFF YES 0 0 Read/Write
0100 0100 Data32 FA7C5000 0000FFFF YES 0 0 Read/Write, accessed
0108 0108 Data32 FA7C5000 0000FFFF YES 0 0 Read/Write, accessed
0110 0110 Data32 FA7C5000 0000FFFF YES 0 0 Read/Write, accessed
0118 0118 Reserved ....:........ ........ NO 0 0 Raw Descriptor : 8003F120 00000000
0120 0120 Reserved ....:........ ........ NO 0 0 Raw Descriptor : 8003F128 00000000
[...]
Pour en revenir à PAX, ce dernier va définir une nouvelle GDT en modifiant les segment descriptors de code et data ring3 pour découper l’espace user-land en 2. Sous linux le user-space faisant 3 Go, on a 1,5 Go pour le code et le reste pour les datas. PAX va ainsi effectuer une séparation des segments de code et data, en laissant CS dans la partie mémoire basse l’empêchant d’aller trainer dans la partie haute de la mémoire user-land la ou seront contenu les data, la stack et le heap.
Un peu plus tard un pote m’a filé un lien provenant de felinemenace.org, un article d’Andrew Griffiths (il a un gros zizi le monsieur…) Pseudo-PaX-in-userland. Cette fois-ci à la place de créer une nouvelle GDT, Andrew va utiliser une LDT, l’équivalent d’une GDT sauf que celle-ci est spécifique à un thread ou process (en fonction de la manière dont l’OS définit la notion de tâche). Cela lui permet ainsi d’éviter de crée une nouvelle GDT et surtout d’appliquer le concept uniquement à une tâche précise. En effet, un segment selector permet de choisir dans quelle table récupérer les segments descriptors, si le champ Table est à 1 alors le CPU va prendre les segment descriptors de la GDT. L’adresse de base de la GDT est stockée dans le registre GDTR (32 bits), lorsqu’il y a une LDT, le LDTR (16 bits) correspond à un un segment selector sur un segment descriptor system de la GDT indiquant la base de la LDT pour le LDTR courant. On peut donc de cette manière avoir plusieurs LDT existant en parallèle sur le système. Pour info une GDT/LDT peut contenir 8192 entrées, ca laisse de la marge pour en ajouter des LDT :] Pour résumer voici un petit schéma tiré du man Intel.

Maintenant, je passe à l’étape Windows, je veux développer l’équivalent d’un PAX utilisant la segmentation depuis une LDT pour un process précis. Je veux pouvoir le faire tourner pour n’importe quel type de process tout en évitant d’être trop envahissant et sans avoir à recompiler celui-ci (gnii#@!). Concrètement si on prend n’importe quel binaire Windows, on se retrouve avec les sections de code suivies des sections de datas et ce pour chaque module chargé en mémoire, chaque module étant chargé en mémoire en fonction de son ImageBase (rellocation si nécessaire), en gros si on prend un dump de la mémoire d’un process on me retrouve avec (exemple avec un helloworld) :
Memory map
Address Size Owner Section Contains Type Access Initial
[...]
00400000 00001000 BofMe PE header Imag R RWE
00401000 00001000 BofMe .text code Imag R RWE
00402000 00001000 BofMe .rdata imports Imag R RWE
00403000 00001000 BofMe .data data Imag R RWE
77BE0000 00001000 MSVCRT PE header Imag R RWE
77BE1000 0004C000 MSVCRT .text code,imports Imag R RWE
77C2D000 00007000 MSVCRT .data data Imag R RWE
77C34000 00001000 MSVCRT .rsrc resources Imag R RWE
77C35000 00003000 MSVCRT .reloc relocations Imag R RWE
7C800000 00001000 kernel32 PE header Imag R RWE
7C801000 00084000 kernel32 .text code,imports Imag R RWE
7C885000 00005000 kernel32 .data data Imag R RWE
7C88A000 00076000 kernel32 .rsrc resources Imag R RWE
7C900000 00006000 kernel32 .reloc relocations Imag R RWE
7C910000 00001000 ntdll PE header Imag R RWE
7C911000 0007A000 ntdll .text code,exports Imag R RWE
7C98B000 00005000 ntdll .data data Imag R RWE
7C990000 00033000 ntdll .rsrc resources Imag R RWE
7C9C3000 00003000 ntdll .reloc relocations Imag R RWE
[...]
Les sections .text étant celles qui contiennent le code, si on veut reprendre le même principe que PAX il faudrait pouvoir dissocier les sections .text dans une espace mémoire réservé. Cela implique donc de modifier le loader de Windows et ca, il n’en est pas question, trop compliqué, pas forcément portable et trop long à faire. De plus, même si on arrivait à le faire se poserait le problème des références aux variables situées dans les sections de data. En effet, ces variables sont référencées à travers leurs adresses virtuelles, donc si on change l’adresse de chargement de ces sections, il faudra mettre à jour ces références. Dans le cas d’une DLL les relocations sont la pour ça, mais pour le binaire principal celui-ci ne possède pas de section .reloc, il faudrait donc mettre à jour ces adresses à la main … c’est mort je suis f0u mais pas à ce point !
Pour simplifier la chose, j’ai donc décidé de ne pas toucher au chargement des modules en mémoire et surtout de ne pas définir une seule région de code mais plusieurs régions de code, plus précisément une pour chaque section .text. Ainsi nous n’avons pas besoin de modifier l’agencement des modules en mémoire mais de bien définir notre LDT pour quelle contienne un segment descriptor de code pour chaque section .text. Concernant les segments de datas, ont les laisse en flat model avec les valeurs par défaut (donc celles de la GDT), on veut juste contrôler EIP pour qu’il reste dans nos segments de code.
Pour chaque .text on va donc avoir un segment descriptor dans la LDT, ce segment descritptor aura pour Base le début de la section, la Limit sera la taille de la section, on désactive aussi la Granularity dans le seg descriptor sachant qu’on peut représenter un espacé mémoire de 1Mo sans, cela devrait suffir pour la majorité des sections .text. On va donc avoir 0 <= Eip logique <= Limit qui donne lorsqu’on passe en virtuel, Base <= Eip virtuel <= Base+Limit.
Pour l’instant c’est l’idée, maintenant vient le passage à l’implémentation. J’ai décidé d’utiliser l’API de debug de Windows pour pouvoir surveiller le comportement du processus visé. Ainsi en debuggant le process nous somme capable de surveiller les chargements de modules, la création de thread et les exceptions. On va donc créer le process avec CreateProcess en activant les flags DEBUG_PROCESS et DEBUG_ONLY_THIS_PROCESS puis on rentre dans notre boucle de debug avec les APIs WaitForDebugEvent et ContinueDebugEvent comme dans la doc.
Il nous faut aussi être capable de définir une LDT sous Windows depuis le user-land. Pour cela nous allons faire appel aux APIs ZwSetInformationProcess et ZwQueryInformationProcess en les utilisant avec l’InformationClass ProcessLdtInformation (10). Il faut savoir que si ZwSetInformationProcess est appelé avec ProcessLdtInformation, celle-ci va automatiquement définir un segment descriptor de LDT dans le GDT avec les fonctions PspCreateLdt et Ke386SetLdtProcess. Dans les 2 cas la structure à passer à l’API est de type PROCESS_LDT_INFORMATION :
typedef struct _LDT_INFORMATION {
ULONG Start;
ULONG Length;
LDT_ENTRY LdtEntries[1];
} PROCESS_LDT_INFORMATION, *PPROCESS_LDT_INFORMATION;
Start étant l’endroit ou commence la copie/lecture des segment descriptors de la LDT, Length est la taille de la copie/lecture et LdtEntries est un tableau de structures LDT_ENTRY qui est tout simplement la définition d’un segment descriptor :
typedef struct _LDT_ENTRY
{
USHORT LimitLow;
USHORT BaseLow;
union
{
struct
{
UCHAR BaseMid;
UCHAR Flags1;
UCHAR Flags2;
UCHAR BaseHi;
} Bytes;
struct
{
ULONG BaseMid:8;
ULONG Type:5;
ULONG Dpl:2;
ULONG Pres:1;
ULONG LimitHi:4;
ULONG Sys:1;
ULONG Reserved_0:1;
ULONG Default_Big:1;
ULONG Granularity:1;
ULONG BaseHi:8;
} Bits;
} HighWord;
} LDT_ENTRY, *PLDT_ENTRY, *LPLDT_ENTRY;
Il existe aussi l’API native ZwSetLdtEntries qui permet de définir des segments descriptors par paires mais je n’avais pas envie de l’utiliser. Je vous donne quand même son prototype si vous avez envie de faire mumuse avec :
NTSTATUS
ZwtSetLdtEntries(
__in ULONG Selector0,
__in ULONG Entry0Low,
__in ULONG Entry0Hi,
__in ULONG Selector1,
__in ULONG Entry1Low,
__in ULONG Entry1Hi
)
/*++
Routine Description:
This routine sets up to two selectors in the current process's LDT.
The LDT will be grown as necessary. A selector value of 0 indicates
that the specified selector was not passed (allowing the setting of
a single selector).
Arguments:
Selector0 -- Supplies the number of the first descriptor to set
Entry0Low -- Supplies the low 32 bits of the descriptor
Entry0Hi -- Supplies the high 32 bits of the descriptor
Selector1 -- Supplies the number of the first descriptor to set
Entry1Low -- Supplies the low 32 bits of the descriptor
Entry1Hi -- Supplies the high 32 bits of the descriptor
Return Value:
NTSTATUS.
--*/
Cool, on est maintenant capable de définir une LDT, à noter que sous Windows, la notion de LDT est process specific, le LDTR est mit à jour lors de chaque changement de context à partir du champ LdtDescriptor (offset 0×20) de la structure KPROCESS.
Lorsque notre process va être lancé, le debugger va voir les tentatives de chargement de modules par le loader de Windows et ainsi à chaque nouveau module mettre à jour la LDT du debuggee pour créer un nouveau segment de code pour la section .text. Voici la routine qui va initialiser le nouveau segment descriptor :
VOID InitCodeDescriptor(PLDT_ENTRY LdtCodeSegDecriptor, ULONG Base, ULONG Limit)
{
ULONG GranLimit=0;
if((Base<0x1000) && (Base<=0x7ffeffff)) //MmHighestUserAddress
{
printf("Base is invalid\n");
return;
}
if((Base+Limit)>0x7ffeffff)
{
printf("Limit is invalid\n");
return;
}
RtlZeroMemory(LdtCodeSegDecriptor, sizeof(*LdtCodeSegDecriptor));
//
// Segment code descriptor
//
LdtCodeSegDecriptor->LimitLow=(USHORT)(Limit&0xFFFF);
LdtCodeSegDecriptor->BaseLow=(USHORT)(Base&0xFFFF);
LdtCodeSegDecriptor->HighWord.Bits.BaseMid=(UCHAR)((Base&0xFF0000)>>16);
LdtCodeSegDecriptor->HighWord.Bits.Type=0x1A; //type=1, code segment
LdtCodeSegDecriptor->HighWord.Bits.Dpl=3;
LdtCodeSegDecriptor->HighWord.Bits.Pres=1;
LdtCodeSegDecriptor->HighWord.Bits.LimitHi=((Limit&0xFF0000)>>16);
LdtCodeSegDecriptor->HighWord.Bits.Sys=0;
LdtCodeSegDecriptor->HighWord.Bits.Default_Big=1;
LdtCodeSegDecriptor->HighWord.Bits.Granularity=0;
LdtCodeSegDecriptor->HighWord.Bits.BaseHi=(UCHAR)((Base&0xFF000000)>>24);
return;
}
Reste le problème des changements d’EIP inter-segments, en effet notre binaire va forcément vouloir utiliser des APIs exportées par les autres modules et donc va vouloir sortir du segment de code courrant pour aller voir ailleur. Une tentative de définir un Eip à une valeur dépassant la Limit du segment génère une #GP ce qui correspont sous Win à STATUS_ACCESS_VIOLATION (0xC0000005L). Deux solutions à ce problème :
- L’émulation : On regarde ou veut sauter l’Eip puis on le met à jour à la main, sauf que dans le cas d’un call/jmp [reg32+XXh] c’est vraiment lourd à faire, il faut de plus penser à pusher l’adresse de retour sur la stack si c’est un call et à la fin mettre à jour le segment CS du fait que nous avons changé de module. Bref pas forcément simple à faire.
- La tracing : On repasse notre segment de code en flat model, on laisse faire le branchement en activant le TrapFlag dans l’Eflag, l’EIP arrive donc dans le module de destination, génère une exception (EXCEPTION_SINGLE_STEP) qui nous permet de remettre à jour le segment CS pour qu’il soit celui associé au nouveau module.
J’ai choisit la deuxième solution, j’ai donc décidé d’avoir dans ma LDT 1 segment de code flat dans ma LDT pour pouvoir géré les branchements inter-segments. Ce qui est cool avec cette méthode c’est que l’Eip qui est pushé sur la stack lors d’un call est un Eip virtuel ce qui signifie que lors du Ret inter-segment va être mit dans Eip une adresse virtuelle qui forcément sera plus grande que la Limit du segment et donc génèrera une exception. On gère donc cette exception de la même manière que le call en repassant sur un segment de code en flat puis en activant le TrapFlag et voilà nous sommes revenu dans le module de départ suivit d’une exception après le Ret, il ne reste plus qu’a redéfinir le segment de code sur la valeur du segment de code associé au segment descriptor du module de départ et ca roule ! w00t !
Reste le cas uber-particulier que je me suis prit dans la face. Par exemple avec une fonction de kernel32.dll on retrouve un schéma d’appel à import de la forme :
7C870DDD |. E8 41FB0000 CALL kernel32.NtTerminateProcess ; JMP to ntdll.ZwTerminateProcess
7C880923 > $- FF25 0814807C JMP NEAR DWORD PTR DS:[<&ntdll.NtTermina>; ntdll.ZwTerminateProcess
Un call sur un jmp [ptr de fct], c’est joli mais dans cas ca fout la merde. En effet l’Eip qui est pushé sur la stack lors du call est un Eip logique, valable dans le cadre du segment actuel, c’est le jmp qui va crée l’exception (#GP) parce qu’il va vouloir sortir du segment, or au moment du retour, l’Eip qui va être popé sera un Eip logique mais qui à de force d’être valable dans le contexte du segment du module qui contient la fonction appelée, donc il sera prit en compte mais partira n’importe tout. Ce qu’il faudrait pour corriger ce bug, c’est mettre à jour le saved Eip sur la stack lors d’un jmp inter-segment, pour cela je n’ai pas trouvé que d’utiliser une lib de disass pour vérifier le type d’instruction qui à généré l’exception au moment du branchement inter-segment. Ainsi si c’est un jmp et que le saved-Eip est inférieur à la taille de la plus grande section .text définie alors on met à jour le saved Eip en lui ajoutant la Base du segment decriptor utilisé lors du jmp. Oui je sais c’est un peu compliqué mais j’ai du le prendre le compte, j’en profite au passage pour faire de la pub à la lib de disass que j’ai utilisé celle de Beatrix, BeaEngine qui permet de connaitre de type de l’instruction désassemblée :
enum OPCODE_TYPE
{
JMP_TYPE = 1,
JCC_TYPE,
CALL_TYPE,
RET_TYPE,
ILLEGAL_TYPE,
SUPERFLUOUS_PREFIX,
INCOMPATIBLE_TYPE
};
Un autre problème aussi m’a été posé à cause des syscalls, en effet sysenter (KiFastSystemCall dans ntdll) et sysexit vont mettre à jour le l’Eip et CS avec des valeurs définies par le noyau avec les MSR SYSENTER_EIP_MSR et SYSENTER_CS_MSR, dans le cas du sysenter on s’en fout mais dans le cas du sysexit ca pose soucis. En effet sysexit renvoie l’Eip sur la routine KiFastSystemCallRet dans ntdll qui va directement effectuer un Ret, pas cool car le saved Eip pushé sur la stack est un Eip logique qui va être utilisé avec un CS en flat model, celui de la GDT mit à jour avec sysexit … Ouinz ! Pour contrer cela, j’ai décidé de mettre des breapoints sur KiFastSystemCall et KiFastSystemCallRet. Lorsque le thread de notre debugge effectue un syscall il passe par KiFastSystemCall, hop breakpoint (EXCEPTION_BREAKPOINT), on repasse en CS flat model en mettant l’Eip à jour (ajout de la Base du segment de code actuel) puis on laisse faire le syscall. Au retour on break sur KiFastSystemCallRet, exception (int 3), on remet à jour le segment de code sur celui associé à la section .text de ntdll dans notre LDT et on realigne eip en redonnant la main au programme.
Bon, évidemment ce ne sont pas les seuls soucis que j’ai eu et il en a bien d’autres, avec ca je suis capable de faire tourner des petits programmes.
Concernant l’implémentation actuelle, celle-ci ne gère que certains process console, des binaires relativement peu complexes c’est à dire avec peu de modules chargés en mémoire et ne générant pas des exceptions bizarres, il ne gère pas non plus les process multithreadés, je pourrais le faire mais pour le moment j’ai la flemme et autre chose à faire. Le code manque de commentaires aussi donc n’hésitez pas à poser des questions. Lorsque le programme détecte une Eip qui sort d’une section .text, il kill directement le debuggee, on fait dans la dentelle.
Pour le moment j’arrive à faire tourner des binaires comme nc.exe ou fport.exe. Ce n’est pas super puissant, ca plante souvent mais ca peut marcher pour des programmes simples
En fait le plus intéressant pour vous n’est pas savoir quel binaire ou fonctionne mais d’exploiter (avec une vraie exécution de code qui fait quelque chose) le binaire que j’ai mit dans l’archive, il s’agit d’un simple BofMe dont le code est :
#include <stdio.h>
int main(int argc, char *argv[])
{
char Buff[24];
if(argc>1)
{
strcpy(Buff, argv[1]);
printf(Buff);
}
return 0;
}
Je suis gentil, je vous ai même laissé une format string. Pour le lance faites par exemple : « C:\Code\ldt>UserlandPaxLdt.exe BofMe.exe aaaaaaaaaaaaaa > NUL ». Si vous voulez voir les logs du programme je vous conseil de faire « C:\Code\ldt>UserlandPaxLdt.exe BofMe.exe aaaaaaaaaaaaaa > barp.txt », vous verrez bien qu’il aime pas qu’on touche à son Eip :p Un indice, une soluce pour réussir une exploitation est indiqué dans l’article de Andrew Griffith.
Vous trouvez le binaire ici :
http://ivanlef0u.fr/repo/UserlandPaxLdtrar.rar
Allez, j’attends de vos retours sur ce petit projet !
Sinon quelques liens pour déconner (attention il y a un piège) :
http://en.wikipedia.org/wiki/List_of_problems_solved_by_MacGyver
http://seclists.org/dailydave/2008/q3/0000.html
http://pedobear.net
http://blogs.iss.net/archive/TheWebBrowserThreat.html
juillet 2nd, 2008
Troisième et dernière partie de notre périple à travers les hyperviseurs, la dernière fois j’ai montré comment créer la structure de contrôle de notre VM Monitor, la VMCS. Cette fois, on va en enfin finaliser notre petit joujou, il ne reste plus qu’à définir le handler des VM Exits, la routine qui lancera l’hyperviseur et celle qui le lancera. Ce n’est pas le plus difficile, cependant il faut être prudent car un bug dans la routine qui gère les VM Exits peut avoir des répercussions beaucoup plus loin dans le Guest et je peux vous dire que pour debugger ça, c’est la gastro totale :=) (même que le pouvoir de constipation des chocapicz n’est pas assez puissant pour le contrer …) Donc on reste attentif et on lache rien.
La routine de gestion des VM Exits, a un rôle très important, c’est le code du VM Monitor qui fait le lien entre le Guest et l’hyperviseur. Elle a pour but de traiter les demandes du Guest qui ne peuvent pas être directement envoyées au hardware sans vérification afin d’éviter les conflits et erreurs comme par exemple ceux sur l’allocation des pages de mémoire, vous imaginez bien qu’il est nécessaire de faire la différence entre les pages du Guest et celles du Host. Elle sert aussi à transmettre au Guest les différents événements extérieurs qui surviennent, typiquement les interruptions.
Dans notre cas, la plupart de ces problèmes sont simplifiés, on n’a pas à ce prendre la tête avec des problèmes comme la gestion (ou plutôt virtualisation) de la mémoire ou bien la virtualisation des interruptions avec des Local APIC et I/O APIC, bref de devoir lire des schémas de ce genre qui piquent les yeux :

Pour nous, quand on ne sait pas comment gérer un évènement on l’envoie au Guest, vu que c’est notre OS hôte il sera se débrouiller avec même si il existe des actions que nous devons émuler obligatoirement. En fait, il y en en gros 2 causes de VM Exit, les premières dues à des instructions qui sont forcément virtualisées, ces instructions sont : CPUID, GETSEC, INVD, MOV from/to CR3 et les instructions introduite par VMX : VMCALL, VMCLEAR, VMLAUNCH, VMPTRLD, VMPTRST, VMREAD, VMRESUME, VMWRITE, VMXOFF, VMXON, and XSETBV. Juste pour préciser, GETSEC est une instruction qui provient du Safer Mode Extensions (SMX), INVD sert à flusher les caches du CPU sans les écrire en mémoire, XSETBV permet de jouer avec le Extended Control Register XCR0, la future extension des registres de contrôles. Pour toutes ces instructions nous devons les exécuter nous même dans notre VM Exit handler. Le problème est maintenant de connaître la cause du VM Exit et les informations relatives au différentes causes comme par exemple quel registre général (GPR) a servit lors du MOV FROM CR3.
Pour cela, le CPU va mettre à jour plusieurs champs de la VMCS, ceux qui nous intéressent sont VM_EXIT_REASON et EXIT_QUALIFICATION. Le premier est de la forme suivante :

Le second dépend de la cause du VM Exit, par exemple lors d’un exit causé par un accès à un des control register on retrouve dans le EXIT_QUALIFICATION une structure du type :

Avec ça on a tous les éléments pour émuler un mov eax, cr3 sans problème. Voici la liste des différentes causes de Vm Exit :
//
// VMX Exit Reasons
//
#define VMX_EXIT_REASONS_FAILED_VMENTRY 0x80000000
#define EXIT_REASON_EXCEPTION_NMI 0
#define EXIT_REASON_EXTERNAL_INTERRUPT 1
#define EXIT_REASON_TRIPLE_FAULT 2
#define EXIT_REASON_INIT 3
#define EXIT_REASON_SIPI 4
#define EXIT_REASON_IO_SMI 5
#define EXIT_REASON_OTHER_SMI 6
#define EXIT_REASON_PENDING_INTERRUPT 7
#define EXIT_REASON_TASK_SWITCH 9
#define EXIT_REASON_CPUID 10
#define EXIT_REASON_HLT 12
#define EXIT_REASON_INVD 13
#define EXIT_REASON_INVLPG 14
#define EXIT_REASON_RDPMC 15
#define EXIT_REASON_RDTSC 16
#define EXIT_REASON_RSM 17
#define EXIT_REASON_VMCALL 18
#define EXIT_REASON_VMCLEAR 19
#define EXIT_REASON_VMLAUNCH 20
#define EXIT_REASON_VMPTRLD 21
#define EXIT_REASON_VMPTRST 22
#define EXIT_REASON_VMREAD 23
#define EXIT_REASON_VMRESUME 24
#define EXIT_REASON_VMWRITE 25
#define EXIT_REASON_VMXOFF 26
#define EXIT_REASON_VMXON 27
#define EXIT_REASON_CR_ACCESS 28
#define EXIT_REASON_DR_ACCESS 29
#define EXIT_REASON_IO_INSTRUCTION 30
#define EXIT_REASON_MSR_READ 31
#define EXIT_REASON_MSR_WRITE 32
#define EXIT_REASON_INVALID_GUEST_STATE 33
#define EXIT_REASON_MSR_LOADING 34
#define EXIT_REASON_MWAIT_INSTRUCTION 36
#define EXIT_REASON_MONITOR_INSTRUCTION 39
#define EXIT_REASON_PAUSE_INSTRUCTION 40
#define EXIT_REASON_MACHINE_CHECK 41
#define EXIT_REASON_TPR_BELOW_THRESHOLD 43
#define VMX_MAX_GUEST_VMEXIT EXIT_REASON_TPR_BELOW_THRESHOLD
Continuons, nous sommes capables de connaître la cause du Vm Exit et d’émuler l’instruction, pour relancer le Guest, il faut bien garder à l’esprit que nous devons lui redonner les mêmes GPR (sauf pour esp, eip et eflags) que ceux lors du Vm Exit, avec bien évidement la mise à jour nécessaire du à l’émulation d’une instruction les modifiants comme CPUI ou RDMSR. Pour faire simple, on sauvegarde un contexte avec pushad et on le modifie dans notre Vm Exit handler. Il faut aussi penser à mettre à jour l’eip, en effet, comme nous avons émulé le comportement de l’instruction nous devons faire pointer l’eip du Guest vers l’instruction suivante et pour cela la VMCS contient un champ nommé VM_EXIT_INSTRUCTION_LEN qui donne la taille de l’instruction qui a causé le VM Exit, même pas besoin d’un LDE (Length Disassembly Engine) \o/. Enfin on n’oublie pas d’exécuter l’instruction VMRESUME pour rendre la main au Guest. Au final, on a un gros switch sur les causes de VM Exit avec des « case » de la forme :
case EXIT_REASON_CPUID :
{
//
//
//
MyKdPrint("EXIT_REASON_CPUID occurs in process [PID : %lu TID : %lu]\n", PsGetCurrentProcessId(), PsGetCurrentThreadId());
__asm
{
MOV EAX, LocalExitContext.GuestEAX
CPUID
MOV LocalExitContext.GuestEAX, EAX
MOV LocalExitContext.GuestEBX, EBX
MOV LocalExitContext.GuestECX, ECX
MOV LocalExitContext.GuestEDX, EDX
}
WriteVMCS(GUEST_EIP, GuestEip+ExitInstructionLen);
break;
}
Ok, on avance traquillement, on code ce qu’il faut pour émuler le reste des instructions qui sont obligatoirement virtualisées et on a notre VM Exit handler minimaliste. Maintenant On va pouvoir coder la routine qui lance l’hyperviseur mais avant il reste à régler un petit problème.
Lors du lancement de la VM par l’instruction VMLAUNCH, qui sera le premier VM Entry, le CPU va placer l’eip et l’esp du Guest en fonction des valeurs de la VMCS, GUEST_EIP et GUEST_ESP. De la même façon, lors d’un VM Exit, le VMX va mettre à jour les valeurs d’esp et d’eip en fonction des champs HOST_EIP et HOST_ESP. Commençons par résoudre le cas des VM Exit, on sait que le HOST_EIP doit pointer sur la routine qui gère les VM Exit, par contre quelle valeur de esp prendre pour le retour dans l’Host ? Sachant qu’un VM Exit peut intervenir dans le contexte de n’importe quel thread on risque d’abimer la stack ou pire de tomber dans une zone mémoire non valide. Du fait que lors de l’initialisation de la VMCS on a définit les segments du Host comme des segments ring0, on va se retrouver dans un contexte ring0, heureusement d’ailleurs car des instructions comme rdmsr ou invd sont privilégiées (elles génèrent un #GP(0) en ring3). Le plus simple est donc de faire pointer l’esp du Host vers une zone mémoire alloué en NonPaged Pool qui servira de stack pour la routine qui gère les VM Exit.
Concernant le premier VM Entry, j’avoue que j’ai fait simple, j’ai prit la valeur de esp avant VMLAUNCH et je l’ai écrite dans la VMCS et pour eip j’ai prit l’adresse de l’instruction suivant le VMLAUNCH ce qui donne le code suivante (attention ça fait de la peur) :
VOID StartVMX()
{
EFLAGS EFlags;
ULONG GuestStack;
ULONG Error;
__asm MOV GuestStack, ESP
//FireBp();
//
// Set ESP for the Guest right before calling VMLAUNCH
//
WriteVMCS(GUEST_ESP, GuestStack);
//
// Execute VMLAUNCH to launch the guest VM. If VMLAUNCH fails due to any
// consistency checks before guest-state loading, RFLAGS.CF or RFLAsiGS.ZF will
// set and the VM-instruction error field will contain the error
// code.
//
__asm
{
PUSH barp
MOV EAX, GUEST_EIP
_emit 0x0F //
_emit 0x79 //
_emit 0x04 // VMWRITE EAX, [ESP]
_emit 0x24 //
ADD ESP, 4
_emit 0x0F //
_emit 0x01 // VMLAUNCH
_emit 0xC2 //
barp:
}
EFlags=GetEFlags();
if((EFlags.CF==1) || (EFlags.ZF==1))
{
//
// Get the ERROR number using VMCS field VM_INSTRUCTION_ERROR
//
ReadVMCS(VM_INSTRUCTION_ERROR, &Error);
KdPrint(("VMLAUNCH failed, VM Instruction Error : %lu\n", Error));
FireBp();
}
KdPrint(("VMLAUNCH OK!\n"));
}
Remarquez le bel asm inline avec notamment les instructions VMWRITE et VMLAUNCH hardcodées en _emit, le compilo C ne connaissant pas ces instructions il faut les écrire en hard … Par contre dans des fichiers .asm le compilo asm du WDK ne pose pas de soucis … va falloir que je recode tout cela de manière plus propre quand même :p
Whaou, on peut lancer notre hyperviseur, c’est le pied ! Maintenant si on veut arrêter la virtualisation il faut appeler l’instruction VMXOFF mais c’est à l’hyperviseur de la faire. Arrive donc le problème de la communication du guest avec le HVM et pour cela il existe l’instruction VMCALL, cette instruction qui ne peut être appelée qu’en ring0 oblige le Guest à faire un VM Exit, après à vous de traiter comme vous voulez cet exit dans votre VM Exit Handler. Perso j’effectue un VMCALL dans la routine DriverUnload de mon hyperviseur avec une valeur précise dans eax, une fois dans ma routine qui gère les VM Exit je lance VMXOFF, je restaure le contexte intervenu lors du VM Exit et je saute sur l’instruction située après VMLAUNCH. C’est ma façon de faire, à vous de voir si vous trouvez plus simple.
Alors, j’ai dit plus haut qu’il existait 2 types de VM Exit, les premiers, ceux dus à la virtualisation obligatoire de certaines instructions, maintenant nous allons voir ceux que l’utilisateur peut choisir. Le mieux étant un exemple concret nous allons effectuer un VM Exit sur les I/O, plus précisément sur l’ensemble des instructions IN, INS/INSB/INSW/INSD, OUT, OUTS/OUTSB/OUTSW/OUTSD. Le jeu d’instruction VMX nous offre 2 choix, le premier consiste à faire un VM Exit tout le temps dès qu’il y a un I/O, le second nous offre la possibilité de choisir les I/O sur lesquels le Guest doit effectuer un VM Exit.
J’ai choisit d’utiliser la second feature, pour cela, il faut mettre à 1 le bit « Use I/O bitmaps » (25) du PRIMARY_CPU_BASED_VM_EXEC_CONTROL. En faisait cela nous demandons au VMX de consulter lors d’un I/O les bitmaps dont les adresses physiques sont stockées dans les champs IO_BITMAP_A, IO_BITMAP_HIGH, IO_BITMAP_B et IO_BITMAP_B_HIGH de la VMCS. Ces bitmaps représentent tout simplement des bits qui indiquent s’ils sont à 1 de faire un VM Exit sur l’I/O indiqué par leur indice, par exemple si le 1337 ème bit est à 1 alors chaque I/O de 1 byte sur le port 1337 créera un VM Exit. Chaque IO_BITMAP est d’une taille de 4ko, c’est-à-dire que l’IO_BITMAP_A concerne les ports 0×0000 à 0x7FFF et que l’IO_BITMAP_B gère les ports 0×8000 jusqu’à 0xFFFF.
On sait que le champ EXIT_QUALIFICATION de la VMCS lors d’un I/O est de la forme :

A partir de là on peut savoir quel type d’I/O désire effectuer le Guest, reste le problème de l’émulation de l’instruction. Si on décide de l’émuler dans l’hyperviseur on va devoir prendre en compte chaque cas possible de forme d’instructions, c’est long, trop long … Il existe une façon plus élégante pour gérer cela, laisser le Guest faire l’I/O en mettant à 0 les bits du ou des ports concernés et de reprendre l’exécution là ou elle avait laissée au moment du VM Exit. Attention si vous faites par exemple un « in ax, 0×10 » comme l’I/O se fait sur 16 bits l’instruction in va lire les 8 bits de poids faible sur le port 0×10 et les 8 bits de poids forts sur le port 0×11. Il faut donc bien prendre en compte la taille de l’I/O pour savoir combien de bits on va mettre à 0.
Cette méthode est pratique mais ajoute 2 nouveaux problèmes, on ne sait pas quand on va remettre les bits du port à 1 dans le bitmap et on ne peut connaître le résultat d’un out sur un port. Solution, tracer l’instruction à l’aide du TrapFlag de l’eflags, en effet si on set ce bit dans l’eflags du Guest une exception sera levée après l’exécution d’une instruction et comme de par hasard notre hyperviseur nous permet d’effectuer des VM Exit sur certaines exceptions, pour monitorer les « int 1″ il suffit de mettre le bit 1 de l’EXCEPTION_BITMAP du VMCS à 1. Dans le cas ou le TrapFlag est à 1, on obtient une « int 1 » après l’exécution de chaque instruction donc notre VM Exit Handler n’aura plus qu’a gérer ça en contrôlant si l’eip d’où provient l’I/O est un eip tracé, si oui alors on restaure l’I/O bitmap et le TrapFlag sans réinjecter l’exception au Guest et ca repart :]
Reste le cas ou l’on tombe dans un VM Exit avec un « int 1 » provenant d’une instruction d’I/O non tracée par notre HVM, il faut la réinjecter au Guest. Lors d’une exception on retrouve dans le VM_EXIT_INTR_INFO de la VMCS la structure suivante :

Il est possible d’injecter un event dans l’IDT du Guest lors d’un VM Entry en utilisant le champ VM_ENTRY_INTR_INFO_FIELD qui est de la même forme que la structure VM_EXIT_INTR_INFO puis de définir les champs VM_ENTRY_INTR_INFO_FIELD et VM_ENTRY_INSTRUCTION_LEN comme dit dans la doc (oui j’ai la flemme de décrire) :

Avec cette application il nous est possible de contrôler tous les I/O sur les ports de notre machine, on pourrait très bien s’amuser à regarder les valeurs des in et out sur les ports clavier par exemple :p. Evidemement cela est une des possibilité offerte par la virtualisation mais rien qu’avec cette application on peut réaliser de nombreuses choses, c’est simple vous conaissez un tool qui vous permet de monitorer et de faire des stats sur les I/O de votre PC ?
Ici ce termine la série sur la création d’un hyperviseur de type bluepill, j’ai essayé d’être le plus clair possible même si je reconnais qu’il est loin d’être trivial d’implémenter une telle chose, c’est long, c’est difficile (surtout le debuggage), il faut lire et relire la doc dans tous les sens mais c’est très enrichissant. On apprend à connaître encore plus les fondements de notre OS et on se rapproche de plus en plus du hardware. Si vous avez des questions n’hésitez pas en tout cas. Concernant la realease du code de Abyss, je préfère attendre un peu et la remettre à plus tard, pourquoi ? Tout simplement parce que j’aimerais faire quelque chose de plus intéressant avec et que certaines parties du code font de la peur tellement qu’on dirait que c’est un québécois de l’underground qui les a faites (admirez la pub !).
En attendant vous pouvez regarder les codes mis en référence en bas du post que j’ai trouvé sur le net et qui m’ont servit dans mon approche de la virtualisation.
Voilà, je n’en ai pas finit avec la virtualisation, je vous promets encore de nombreuses applications à venir et j’espère sincèrement que vous aussi :]
https://www.rootkit.com/vault/mobydefrag/vmxcpu.rar
http://deroko.phearless.org/cpuid_break.rar
http://bluepillproject.org/stuff/nbp-0.32-public.zip
mai 26th, 2008
Suite de notre voyage dans le monde des hyperviseurs, dans la 1ère partie j’ai montré comment initialiser le support VMX sur le CPU, cette fois-ci, on passe aux choses sérieuses avec la création de la VMCS (Virtual Machine Control data Structure), cette zone mémoire va contenir toutes les infos pour contrôler le comportement du Guest en mode non-root. Lors de certains événements clés, le jeu d’instructions VMX va utiliser la VMCS pour sortir du Guest et restaurer le contexte de l’Host. Une fois que l’hyperviseur rend la main, la VMCS permet de rétablir le contexte du Guest sur le CPU. Dans notre cas, du fait que nous virtualisons notre OS à la volée la création de la VMCS est moins compliquée, elle n’en est pas moins fastidieuse …
La création de la VMCS demande les même propriétés que la VMXONRegion, une mémoire non cachée créée avec MmAllocateNonCachedMemory, la taille est la même que la VMXONRegion, c’est à dire celle qu’on lit dans le champ VMRegionSize du MSR IA32_VMX_BASIC (0×480), les 4 premiers bytes de la VMCS sont composés aussi de la valeur du champ RevId du IA32_VMX_BASIC, jusqu’ici c’est exactement pareil que pour la VMXONRegion.
Une fois que notre VMCS est alloué, nous devons la « clear » avec l’instruction VMCLEAR qui prend comme argument l’adresse physique de notre VMCS (obtenue avec MmGetPhysicalAddress), cette étape sert juste à mettre la VMCS dans un état neutre, je pense aussi que même si on pouvait le faire à la main, l’instruction VMCLEAR (comme les autres fournis par Intel) garantie le fait que notre mémoire n’aille pas dans le cache. On doit vérifier que VMCLEAR s’est bien déroulée en regardant si les flags CF et ZF de l’EFlags sont à 0. En application cela donne le code :
;VOID _VmxClear(PHYSICAL_ADDRESS Addr)
_VmClear PROC StdCall LowPart, HighPart
mov ebp, esp
sub esp, 8
push HighPart
push LowPart
vmclear qword ptr [esp]
leave
retn 8
_VmClear ENDP
Maintenant il faut dire au core actif que nous allons utiliser cette zone mémoire en tant que VMCS, pour cela l’instruction VMPTRLD prend l’adresse physique de notre VMCS et la définie comme « active » sur le core courant. On a donc le bout de code :
;VOID _VmPtrLd(PHYSICAL_ADDRESS Addr)
_VmPtrLd PROC LowPart, HighPart
mov ebp, esp
sub esp, 8
push HighPart
push LowPart
vmptrld qword ptr [esp]
leave
retn 8
_VmPtrLd ENDP
Les 32 bits suivant le RevId au début de la VMCS sont nomé par « VMX-abort indicator », un VM Abort apparait au moment d’un VM Exit qui pose problème, VMX va mettre à jour ce champ pour dire ce qu’il s’est passé, vous pouvez retrouver ces valeurs à la section 23.7 du « Intel 64 and IA-32 Architectures Software Developer’s Manual Volume 3B: System Programming Guide, Part 2« .
La partie suivante de la VMCS nous incombe, nous devons tout remplir nous même. La VMCS se compose de 6 zones :
- Guest-state area : L’état du core est chargé depuis cette zone lors d’un VM Entry et sauvegardé dedans lors d’un VM Exit.
- Host-state area : Le contexte du core est récupéré ici lors d’un VM Exit pour relancer l’Host.
- VM-execution control fields : Ces variables contrôlent le comportement du Guest en mode non-root et détermine les VM Exit.
- VM-exit control fields : Ces champs déterminent le comportement de l’hyperviseur lors de certains VM Exit.
- VM-entry control fields : Ces champs déterminent le comportement du CPU lors des VM Entry.
- VM-exit information fields : Donne des informations sur la cause et la nature du VM Exit.
On a de la chance, la zone « M-exit information fields » est en lecture seule, c’est le VMX qui la met à jour lors d’un VM Exit, c’est cool, ça nous fait moins de boulot pour init la VMCS :]
Alors, grosse particularité de la VMCS, on y écrit et lit pas comme on veut dedans. En effet pour éviter les problèmes de cache et les problèmes de padding du au futurs changement et extensions sur cette structure Intel demande d’accéder au VMCS à travers les instructions VMREAD et VMWRITE. Chacune d’elles prend 2 arguments, le premier est un registre ou un pointeur qui va recevoir la valeur lu ou donnera la valeur à écrire, le second argument est un registre contenant l’encoding du champ auquel on souhait accéder. A partir de là on peut concevoir les wrappers suivants :
; ULONG32 _VmRead(ULONG32 Encoding)
_VmRead PROC StdCall Encoding
push ebp
mov ebp, esp
sub esp, 4
vmread Encoding, esp
mov eax, dword ptr [esp]
leave
retn 8
_VmRead ENDP
; VOID _VmWrite(ULONG32 Encoding, ULONG32 Value)
_VmWrite PROC StdCall Encoding, Value
push eax
mov eax, Encoding
vmwrite eax, Value
pop eax
retn 8
_VmWrite ENDP
L’encoding des champs de la VMCS possède cette forme :

Heureusement Intel fournit dans son manuel, l’encoding de tout les champs :
/* VMCS Encodings */
enum
{
// 16 bits Guest State Fields
GUEST_ES_SELECTOR = 0x00000800,
GUEST_CS_SELECTOR = 0x00000802,
GUEST_SS_SELECTOR = 0x00000804,
GUEST_DS_SELECTOR = 0x00000806,
GUEST_FS_SELECTOR = 0x00000808,
GUEST_GS_SELECTOR = 0x0000080a,
GUEST_LDTR_SELECTOR = 0x0000080c,
GUEST_TR_SELECTOR = 0x0000080e,
// 16 bits Host State Fields
HOST_ES_SELECTOR = 0x00000c00,
HOST_CS_SELECTOR = 0x00000c02,
HOST_SS_SELECTOR = 0x00000c04,
HOST_DS_SELECTOR = 0x00000c06,
HOST_FS_SELECTOR = 0x00000c08,
HOST_GS_SELECTOR = 0x00000c0a,
HOST_TR_SELECTOR = 0x00000c0c,
// 64 bits Control Fields
IO_BITMAP_A = 0x00002000,
IO_BITMAP_A_HIGH = 0x00002001,
IO_BITMAP_B = 0x00002002,
IO_BITMAP_B_HIGH = 0x00002003,
MSR_BITMAP = 0x00002004,
MSR_BITMAP_HIGH = 0x00002005,
VM_EXIT_MSR_STORE_ADDR = 0x00002006,
VM_EXIT_MSR_STORE_ADDR_HIGH = 0x00002007,
VM_EXIT_MSR_LOAD_ADDR = 0x00002008,
VM_EXIT_MSR_LOAD_ADDR_HIGH = 0x00002009,
VM_ENTRY_MSR_LOAD_ADDR = 0x0000200a,
VM_ENTRY_MSR_LOAD_ADDR_HIGH = 0x0000200b,
TSC_OFFSET = 0x00002010,
TSC_OFFSET_HIGH = 0x00002011,
VIRTUAL_APIC_PAGE_ADDR = 0x00002012,
VIRTUAL_APIC_PAGE_ADDR_HIGH = 0x00002013,
// 64 bits Guest State Fields
VMCS_LINK_POINTER = 0x00002800,
VMCS_LINK_POINTER_HIGH = 0x00002801,
GUEST_IA32_DEBUGCTL = 0x00002802,
GUEST_IA32_DEBUGCTL_HIGH = 0x00002803,
// 64 bits Host-State Field
HOST_IA32_PERF_GLOBAL_CTRL = 0x00002C04,
HOST_IA32_PERF_GLOBAL_CTRL_HIG = 0x00002C05,
// 32 bits Control Fields
PIN_BASED_VM_EXEC_CONTROL = 0x00004000,
PRIMARY_CPU_BASED_VM_EXEC_CONTROL = 0x00004002,
EXCEPTION_BITMAP = 0x00004004,
PAGE_FAULT_ERROR_CODE_MASK = 0x00004006,
PAGE_FAULT_ERROR_CODE_MATCH = 0x00004008,
CR3_TARGET_COUNT = 0x0000400a,
VM_EXIT_CONTROLS = 0x0000400c,
VM_EXIT_MSR_STORE_COUNT = 0x0000400e,
VM_EXIT_MSR_LOAD_COUNT = 0x00004010,
VM_ENTRY_CONTROLS = 0x00004012,
VM_ENTRY_MSR_LOAD_COUNT = 0x00004014,
VM_ENTRY_INTR_INFO_FIELD = 0x00004016,
VM_ENTRY_EXCEPTION_ERROR_CODE = 0x00004018,
VM_ENTRY_INSTRUCTION_LEN = 0x0000401a,
TPR_THRESHOLD = 0x0000401c,
SECONDARY_CPU_BASED_VM_EXEC_CONTROL = 0x000401e,
// 32 bits Read Only Data Fields
VM_INSTRUCTION_ERROR = 0x00004400,
VM_EXIT_REASON = 0x00004402,
VM_EXIT_INTR_INFO = 0x00004404,
VM_EXIT_INTR_ERROR_CODE = 0x00004406,
IDT_VECTORING_INFO_FIELD = 0x00004408,
IDT_VECTORING_ERROR_CODE = 0x0000440a,
VM_EXIT_INSTRUCTION_LEN = 0x0000440c,
VMX_INSTRUCTION_INFO = 0x0000440e,
// 32 bits Guest State Fields
GUEST_ES_LIMIT = 0x00004800,
GUEST_CS_LIMIT = 0x00004802,
GUEST_SS_LIMIT = 0x00004804,
GUEST_DS_LIMIT = 0x00004806,
GUEST_FS_LIMIT = 0x00004808,
GUEST_GS_LIMIT = 0x0000480a,
GUEST_LDTR_LIMIT = 0x0000480c,
GUEST_TR_LIMIT = 0x0000480e,
GUEST_GDTR_LIMIT = 0x00004810,
GUEST_IDTR_LIMIT = 0x00004812,
GUEST_ES_AR_BYTES = 0x00004814,
GUEST_CS_AR_BYTES = 0x00004816,
GUEST_SS_AR_BYTES = 0x00004818,
GUEST_DS_AR_BYTES = 0x0000481a,
GUEST_FS_AR_BYTES = 0x0000481c,
GUEST_GS_AR_BYTES = 0x0000481e,
GUEST_LDTR_AR_BYTES = 0x00004820,
GUEST_TR_AR_BYTES = 0x00004822,
GUEST_INTERRUPTIBILITY_INFO = 0x00004824,
GUEST_ACTIVITY_STATE = 0x00004826,
GUEST_SM_BASE = 0x00004828,
GUEST_SYSENTER_CS = 0x0000482A,
// 32 bits Host State Field
HOST_IA32_SYSENTER_CS = 0x00004c00,
// Natural width Control Fields
CR0_GUEST_HOST_MASK = 0x00006000,
CR4_GUEST_HOST_MASK = 0x00006002,
CR0_READ_SHADOW = 0x00006004,
CR4_READ_SHADOW = 0x00006006,
CR3_TARGET_VALUE0 = 0x00006008,
CR3_TARGET_VALUE1 = 0x0000600a,
CR3_TARGET_VALUE2 = 0x0000600c,
CR3_TARGET_VALUE3 = 0x0000600e,
// Natural Width Read Only Data Fields
EXIT_QUALIFICATION = 0x00006400,
GUEST_LINEAR_ADDRESS = 0x0000640a,
// Natural Witdh Guest State Fields
GUEST_CR0 = 0x00006800,
GUEST_CR3 = 0x00006802,
GUEST_CR4 = 0x00006804,
GUEST_ES_BASE = 0x00006806,
GUEST_CS_BASE = 0x00006808,
GUEST_SS_BASE = 0x0000680a,
GUEST_DS_BASE = 0x0000680c,
GUEST_FS_BASE = 0x0000680e,
GUEST_GS_BASE = 0x00006810,
GUEST_LDTR_BASE = 0x00006812,
GUEST_TR_BASE = 0x00006814,
GUEST_GDTR_BASE = 0x00006816,
GUEST_IDTR_BASE = 0x00006818,
GUEST_DR7 = 0x0000681a,
GUEST_ESP = 0x0000681c,
GUEST_EIP = 0x0000681e,
GUEST_EFLAGS = 0x00006820,
GUEST_PENDING_DBG_EXCEPTIONS = 0x00006822,
GUEST_SYSENTER_ESP = 0x00006824,
GUEST_SYSENTER_EIP = 0x00006826,
// Natural Width Host State Fields
HOST_CR0 = 0x00006c00,
HOST_CR3 = 0x00006c02,
HOST_CR4 = 0x00006c04,
HOST_FS_BASE = 0x00006c06,
HOST_GS_BASE = 0x00006c08,
HOST_TR_BASE = 0x00006c0a,
HOST_GDTR_BASE = 0x00006c0c,
HOST_IDTR_BASE = 0x00006c0e,
HOST_IA32_SYSENTER_ESP = 0x00006c10,
HOST_IA32_SYSENTER_EIP = 0x00006c12,
HOST_ESP = 0x00006c14,
HOST_EIP = 0x00006c16,
};
Les champs 64 sont découpés en 2 parties, les 32 bits de poids fort sont dans le champ finissant par _HIGH.
A ce moment nous sommes prêts pour remplir la VMCS. On va commencer simplement avec les Guest-state et Host-state areas. Comme je l’ai dit au début, du fait que nous virtualisons notre OS « on the fly » (ho yeah) cette partie sera simple à compléter, il suffira de mettre les mêmes valeurs pour le Guest-state et Host-State. Concrètement nous avons en commun pour l’Host et le Guest les segments, la GDT, l’IDT, certaines valeurs de MSR, les registres de contrôles CR0 et CR4. Pour toutes ces valeurs il nous suffit de prendre les valeurs de l’Host et de le mettre dans les champs correspondants des Host-state et Guest-state, pratique le bluepill :=)
Pour être clair, prenons les encoding qui concernent le segment CS, nous avons donc GUEST_GS_SELECTOR, GUEST_CS_BASE, GUEST_CS_LIMIT, GUEST_CS_AR_BYTES et HOST_CS_SELECTOR. GUEST_GS_SELECTOR et HOST_CS_SELECTOR sont égaux, GUEST_CS_BASE et GUEST_CS_LIMIT s’obtiennent en lisant la GDT de l’Host avec les fonctions suivantes :
ULONG GetSegmentDescriptorBase(PVOID GdtBase, SEGMENT_SELECTOR SegSelector)
{
ULONG SegDescBase=0;
PSEGMENT_DESCRIPTOR SegDescriptor;
SegDescriptor=(PSEGMENT_DESCRIPTOR)((PUCHAR)GdtBase+SegSelector.Index*8);
SegDescBase=SegDescriptor->BaseHigh;
SegDescBase<<=8;
SegDescBase|=SegDescriptor->BaseMid;
SegDescBase<<=16;
SegDescBase|=SegDescriptor->BaseLow;
return SegDescBase;
}
ULONG GetSegmentDescriptorLimit(PVOID GdtBase, SEGMENT_SELECTOR SegSelector)
{
ULONG Limit=0;
PSEGMENT_DESCRIPTOR SegDescriptor;
SegDescriptor=(PSEGMENT_DESCRIPTOR)((PUCHAR)GdtBase+SegSelector.Index*8);
Limit=SegDescriptor->LimitHigh;
Limit<<=16;
Limit|=SegDescriptor->LimitLow;
//
// Check granularity
//
if(SegDescriptor->Gran)
{
Limit*=0x1000;
Limit+=0xFFF;
}
return Limit;
}
Le GUEST_CS_AR_BYTES est un peu particulier, il s’agit d’une version modifiée des segments descriptor de la GDT, normalement ceux-ci on la forme suivante :

La structure SEGMENT_ACCESS_RIGHTS reprend celle d’un SEGMENT_DESCRIPTOR en enlevant les champs LimitLow, BaseLow, BaseMid, LimitHigh et BaseHigh. Elle est donc définie par :
typedef struct _SEGMENT_ACCESS_RIGHTS
{
union
{
struct
{
unsigned Type :4;
unsigned System :1; // Segment type (0=system, 1=code or data)
unsigned DPL :2;
unsigned Present :1; // Segment Present
unsigned Reserved1 :4;
unsigned Avl :1;
unsigned Reserved2 :1;
unsigned DB :1;
unsigned Gran :1;
unsigned UnUsable :1;
unsigned Reserved3 :15;
};
ULONG Access;
};
}SEGMENT_ACCESS_RIGHTS;
On pourrait se dire qu’il est contraignant de remplir une structure SEGMENT_ACCESS_RIGHTS à partir d’un SEGMENT_DESCRIPTOR mais nan en fait, il suffit de faire l’opération suivante : SegRights=(*(PULONG)((PUCHAR)SegDescriptor+5)) & 0x0000F0FF. En SegDescriptor+5 nous sommes au niveau du champ Type du SEGMENT_DESCRIPTOR, on récupère 4 bytes qu’on masque avec la valeur 0xF0FF pour récupérer les 8 premiers bits et les 4 derniers.
On effectue les mêmes opérations sur les segments ES, DS, SS, GS, FS et les registres TR (Task Register) et LDTR (LDT Register). Ouf, nous avons finit d’initialiser les Guest et Host state …
Passons maintenant aux VM-execution control fields. Les champs PRIMARY_CPU_BASED_VM_EXEC_CONTROL et SECONDARY_CPU_BASED_VM_EXEC_CONTROL sont les plus importants, ce sont des ensembles de bits qui servent à activer des nouveaux evenement sur lesquels le Guest devra effectuer un VM Exit. Par exemple, le bit 12 du PRIMARY_CPU_BASED_VM_EXEC_CONTROL, « RDTSC exiting » définit si l’instruction RDTSC provoque un VM Exit ou non. L’utilisation du PRIMARY_CPU_BASED_VM_EXEC_CONTROL demande de prendre en compte les bits reservés de ce bitmap, c’est pourquoi Intel demande de lire le MSR IA32_VMX_PROCBASED_CTLS pour savoir comment définir ces bits. Comme dit dans la doc, les 32 premiers bits du MSR définissent les bits qui doivent être à 0 alors que les 32 bits suivant définissent les bits qui doivent être à 1. Il faut faire pareil avec le SECONDARY_CPU_BASED_VM_EXEC_CONTROL. J’avoue c’est un peu lourd à supporter, au final, l’opération minimaliste consiste à faire :
//
// The IA32_VMX_PROCBASED_CTLS MSR (index 482H) reports on the allowed
// settings of the primary processor-based VM-execution controls (see Section 20.6.2):
//
// - Bits 31:0 indicate the allowed 0-settings of these controls. VM entry fails if bit X
// in the primary processor-based VM-execution controls is 0 and bit X is 1 in this
// MSR.
// - Bits 63:32 indicate the allowed 1-settings of these controls. VM entry fails if bit X
// in the primary processor-based VM-execution controls is 1 and bit 32+X is 0 in
// this MSR.
//
ReadMsr(IA32_VMX_PROCBASED_CTLS, &Msr);
Tmp=0;
Tmp|=Msr.Low;
Tmp&=Msr.High;
WriteVMCS(PRIMARY_CPU_BASED_VM_EXEC_CONTROL, Tmp);
Dans l’exemple au dessus, je ne m’occupe pas des bits utiles, je les laisse tous à 0, après rien ne vous empêche de les seter à 1, faites attention de bien lire la doc, car certains comme le bit 28 (Use MSR bitmaps) font appel à des champs du VMCS comme MSR_BITMAP et MSR_BITMAP_HIGH.
Un mot sur l’EXCEPTION_BITMAP, chaque bit de ce champ contrôle un VM Exit sur un numéro d’exception, concernant les page faults (bit 14) comme nous voulons qu’il soit gérer par le Guest il faut faire attention, l’implémentation étant particulière. En fait, au moment d’un page fault, le CPU consulte le page-fault error-code [PFEG] puis 2 masques, le page-fault error-code mask [PFEC_MASK] et le page-fault error code match [PFEC_MATH]. Le CPU effectue l’opération suivante : PFEC & PFEC_MASK = PFEC_MATCH, si il y égalité alors le CPU regarde la valeur du bit 14 et effectue ou non un VM Exit. S’il n’y a pas égalité, alors le sens du bit 14 est inversé. DONC !@#! Si on veut qu’il n’y ait pas de VM Exit lors d’un page fault, il suffit de mettre le PFEC_MASK à 0, le PFEC_MATCH à 0xFFFFFFFF et le bit 14 à 1. Comme ça, il n’y aura jamais égalité et donc pas de VM Exit !
Chose marrante aussi, les CR0_GUEST_HOST_MASK et CR0_READ_SHADOW (la même chose existe pour le CR4), dans le cas ou un des bits du CR0_GUEST_HOST_MASK est un 1, une tentative de modifier le bit correspondant par le Guest produira un VM Exit. Truc cool, une tentative de lecture d’un des bits à 1 du MASK se verra retourner celui du CR0_READ_SHADOW. Cela veut dire qu’il est possible de faire croire que le bit 13 du CR4, celui qui indique si le jeu d’instruction VMX est dispo sur le CPU, est à 0 alors qu’il est à 1 pour l’Host sans provoquer de VM Exit.
Bon je ne vais pas m’amuser à décrire toutes les fonctionnalités des VM-execution control fields, déjà que je suis loin de les maîtriser, si vous voulez les connaitre mieux allez lire la doc ca sera plus simple :p
Il nous reste à régler les VM-exit control fields et VM-entry control fields. Pour les VM Exit, ces champs permettent de contrôler les MSR à charger pour l’Host à travers un tableau de Msr Entry, il faut utiliser les champs VM_EXIT_MSR_LOAD_ADDR et VM_EXIT_MSR_LOAD_ADDR_HIGH pour spécifier l’adresse physique du tableau, le champ VM_EXIT_MSR_STORE_COUNT contenant le nombre d’entrés. Il existe la même chose pour les MSR ayant besoin d’être stockés lors du VM Exit avec un tableau de VM_EXIT_MSR_STORE_COUNT situé en VM_EXIT_MSR_STORE_ADDR et VM_EXIT_MSR_STORE_ADDR_HIGH.
En parallèle on retrouve la même chose pour le VM Entry avec les champs VM_ENTRY_MSR_LOAD_ADDR et VM_ENTRY_MSR_LOAD_ADDR_HIGH qui pointent sur un tableau de VM_ENTRY_MSR_LOAD_COUNT MSR Entry qui seront chargés lors du Vm Entry.
Ouf ! Nous avons finit avec l’initialisation de la VMCS, c’était long (et bon !) mais c’est nécessaire pour lancer notre hyperviseur. Je n’ai pas voulu tout détaillé dans ce post ca m’aurait prit trop de places et puis il faut bien que vous bossiez un peu vous aussi, j’ai essayé de dégrossir et de montrer les fonctionnalités de base, à vous d’adapter en fonction de vos besoins et pour cela votre meilleur atout sera de lire la doc, je sais c’est long et chiant mais c’est comme ça et au final les docs Intel ne sont pas si mal faites, il suffit de prendre son temps :]
La prochaine fois nous verrons enfin comment implémenter la routine qui devra gérer les VM Exit obligatoire, la manière de lancer notre HVM et comment l’arrêter avec en cadeau une petite feature pour Abyss
En attendant je retourne bosser dessus.
Et n’oubliez pas, il s’appel Robert Paulson …
mai 11th, 2008
Next Posts
Previous Posts