Posts filed under 'RE'

VDM!

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 :]

6 comments novembre 25th, 2008

TaskPwdDmp update

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.

  1. 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.
  2. 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

9 comments octobre 27th, 2008

Virtual Mode on Windows

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 :

TSS

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 :

  1. 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.
  2. 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.
  3. 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 !

4 comments septembre 20th, 2008

Windows Subsytem Csrss

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 .

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 :P

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 :]

13 comments septembre 3rd, 2008

Task Scheduler credentials dumper

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 :

  1. Vérifier qu’on peut obtenir le SeDebugPrivilege sur la machine pour pouvoir jouer avec les processes des autres accounts.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. Le hook nous renvoie les credentials, on les affiche, newsoft est content, il peut faire un beau rapport!
  9. 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 !

22 comments juillet 25th, 2008

SMM

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 :

  1. 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.
  2. 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.
pci

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 :
bus

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

6 comments juillet 21st, 2008

Sudami KillMe

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 !

18 comments juillet 15th, 2008

Win userland Pax-like with segmentation

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 :
Segment selector

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

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.
Gdt Ldt

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 :

  1. 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.
  2. 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

8 comments juillet 2nd, 2008

Hypervisor Abyss, part 3

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 :
Virtual IDT

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 :
Exit Reason

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 :
Exit qualification structure

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 :
Exit qualification I/O

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 :
VM Exit interruption info

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) :
VM Entry interruption info

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

6 comments mai 26th, 2008

Hypervisor Abyss, part 2

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 :

  1. 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.
  2. Host-state area : Le contexte du core est récupéré ici lors d’un VM Exit pour relancer l’Host.
  3. VM-execution control fields : Ces variables contrôlent le comportement du Guest en mode non-root et détermine les VM Exit.
  4. VM-exit control fields : Ces champs déterminent le comportement de l’hyperviseur lors de certains VM Exit.
  5. VM-entry control fields : Ces champs déterminent le comportement du CPU lors des VM Entry.
  6. 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 :

Vmcs encoding
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 …

6 comments mai 11th, 2008

Next Posts Previous Posts


Calendar

décembre 2018
L Ma Me J V S D
« fév    
 12
3456789
10111213141516
17181920212223
24252627282930
31  

Posts by Month

Posts by Category