Map and projects (the most frequently updated page of this blog)

2010/01/18

Sail to the edge and I'd be there

Messing with the TLS
TLS, aka Thread Local Storage, is a way to execute some code before the EntryPoint or after ExitThread/ExitProcess.
the 10th Data Directory points to a structure, and one of the elements (VA, not RVA) points to null-terminated list of callbacks, which will be called one after the other.
This list is stored as VAs (it includes the ImageBase then), which makes it quite uncommon among the PE structures.
IMAGE_TLS_DIRECTORY32:
...
AddressOfCallBacks dd Callbacks ; VA
...
Callbacks:
dd TLS
dd 0 ; null-terminated list

The size of the Data Directory is not taken into account. Some tool may ignore wrongly the TLS if it's not defined, though.

Callbacks are executed on (before) thread start and on (after) thread exit. However, (credits goes to Peter Ferrie and Kris Kaspersky here), TLS callbacks execution won't happen if no dll importing kernel32 is imported itself. So, if kernel32.dll is the only 'official' import (it doesn't mean it's the only dll in the program space), the callbacks are not executed.

Nothing happens if there is no (valid) callbacks. It will trigger an exception but it's handled by the system so it doesn't trigger an visible error. After an exception or a null dword is found, the list is not parsed anymore. Tools building the list until a null dword is found can be fooled, which would create bogus entries.

The list is reloaded between each callback. Callback N might modify the list at position N+1 and that will be taken into account.
Similarly, if the EntryPoint code updates the callbacks' list, it will be taken into account during thread exit.
the 'no callback execution if only Kernel32 is imported' status is updated during the EntryPoint code too: if the EntryPoint code loads, say, user32, the callbacks list will be executed on thread exit. If, at this point, one of the callbacks frees user32, the next callback will still be executed.

Like EntryPoint values, the Callback address can point outside the PE, similar to my previous post (it's just a VA though, not an RVA), which can make the callbacks list difficult to analyze, but it's not a standard case - even among packers.

Consequences:
Because some debuggers automatically put a software breakpoint on the EntryPoint, and code at TLS is executed first, the TLS code can detect it (check if it's CC) and act accordingly.

the 'only kernel32' rule can be used as an anti-emulator.

Sometimes this rule seems wrong because a debugger plugin loaded another dll itself.

If a callback on loading calls ExitProcess, the other callbacks (and EP) are never executed. Which makes TLS callbacks another way to ignore the EP value altogether, like mentioned in my previous post.

Setting a TLS callback from the EntryPoint and exiting can be yet another way to 'Jump'. It's just so unnatural to expect code after the 'final' ExitProcess call. (there IS a life after death!). Same for setting a callback from a previous one, but the 'beyond final' effect is lost.

To break on TLS callbacks in OllyDbg, using OllyAdvanced is an easy solution. Otherwise, set the 'first pause' on 'System breakpoint'.

Files:
tls_standard just uses standard tls callbacks.
tls_fake fills the callbacks list with bogus entries:
Callbacks:
db 'lets fill the Callbacks...'

no_relocs_tls_loader uses a callback outside the pe
;%IMPORT no_relocs.dll!Export
...
Callbacks:
dd 330200h

tls_on_the_fly shows that modifying the callback list is taken into account
TLS0:
mov dword [Callbacks + 4], TLS
retn
TLS:
push MB_ICONINFORMATION ; UINT uType
push aTls1 ; LPCTSTR lpCaption
...

tls_no_user32 shows that just loading kernel32 doesn't enable the usual callbacks behavior, then loading user32 in the EP code re-enables it.
EntryPoint:
push aUser32 ; LPCTSTR lpFileName
call LoadLibraryA
...

Sources and binaries

[...]

Faire des misères au TLS
Le TLS, autrement dit le Thread Local Storage, permet d'exécuter du code avant l'EntryPoint ou après un appel à ExitThread/ExitProcess.
Le 10ème Data Directory pointe vers une structure, et un des éléments (VA, pas RVA) pointe vers une liste de callbacks terminée par zéro, qui seront
appelés l'un après l'autre.
Cette liste est stockée sous forme de VAs (ImageBase inclue, donc), ce qui est inhabituel dans les strucures du PE.
IMAGE_TLS_DIRECTORY32:
...
AddressOfCallBacks dd Callbacks ; VA
...
Callbacks:
dd TLS
dd 0 ; null-terminated list

La taille du Data Directory n'est pas prise en compte. Certains outils pourraient ignorer - à tort - le TLS si elle n'est pas définie.

Les callbacks sont exécutés à l'initialistation (avant) du thread start et après sa fin. Cependant, (l'honneur revient à Peter Ferrie et Kris Kaspersky, ici), l'exécution n'aura pas lieu si aucune DLL n'important kernel32 n'est elle-même importée. Donc, si kernel32.dll est le seul import 'officiel' (cela ne veut pas dire que ca soit la seule DLL dans l'espace mémoire), les callbacks ne sont pas exécutés.

Rien ne se passe si il n'y a pas de callbacks (valide). Ça va déclencher une exception, mais elle sera gérée par le système, donc pas d'erreur visible. Après une exception ou un zéro terminal, la liste n'est plus parcourue. Les outils construisant la liste jusqu'à un zéro terminal peuvent être induit en erreur, ce qui créeraient des entrées factices.

La liste est rechargée à chaque callback. le N-ème Callback N peut changer la liste à la position N+1, et cela sera pris en compte.
De manière similaire, si l'EntryPoint modifie la liste, cela sera pris en compte lors de la sortie du Thread.

Le statu de la règle 'pas d'exécution si seule Kernel32 est importée' est mis à jour lors du code de l'EntryPoint également : si l'EntryPoint charge - par exemple - user32, les callbacks seront exécutés lors de la sortie. Si, à ce moment-là, un des callbacks décharge User32, le callback suivant sera toujours exécuté.

A l'instart des EntryPoint, les callbacks peuvent être en dehors du PE, comme dans mon billet précédent (c'est une juste une VA, pas une RVA), ce qui peut rendre la liste des callbacks difficile à analyser, mais ce n'est pas un cas standard, même dans les packeurs.

Conséquences:
Puisque certains débogueurs (désinsectiseurs ? :) ) mettent un point d'arrêt logiciel sur l'EntryPoint, et que le code des callbacks est exécuté d'abord, le code du callback peut le détecter (vérifier si c'est CC), et agir en conséquence.

La règle 'seule Kernel32' peut-être utilisée comme un anti-émulateur.

Parfois cette règle semble fausse car un plugin du débogueur a lui-même chargé une DLL supplémentaire.

Si un callback appelle ExitProcess lors de l'initialisation, les callbacks suivent et l'EP ne seront jamais exécutés. Ce qui fait que le TLS est un moyen de plus d'ignorer l'EP, comme dans mon billet précédent.

Définir un callback de l'EntryPoint et terminer est un moyen de plus de 'sauter'. Mais il est tellement inhabituel d'attendre du code apres l'appel 'final' à Exitprocess. (Il y A donc une vie après la mort !). De même de définir un callback à la suite d'un autre, mais l'effet 'après la mort' est perdu.

Pour s'arrêter sur les callbacks dans OllyDbg, utiliser OllyAdvanced est une solution simple. Sinon, mettre 'first pause' sur 'System breakpoint'.

Fichiers:
tls_standard utilise le TLS standard.
tls_fake remplit la liste d'entrées factices :
Callbacks:
db 'lets fill the Callbacks...'

no_relocs_tls_loader utilise un callback hors du PE :
;%IMPORT no_relocs.dll!Export
...
Callbacks:
dd 330200h

tls_on_the_fly montre les que les changements apportés à la liste sont pris en compte à la volée :
TLS0:
mov dword [Callbacks + 4], TLS
retn
TLS:
push MB_ICONINFORMATION ; UINT uType
push aTls1 ; LPCTSTR lpCaption
...

tls_no_user32 montre qu'importer uniquement kernel32 n'active pas le comportement habituel, et que charger user32 dans l'EP le rétablit.
EntryPoint:
push aUser32 ; LPCTSTR lpFileName
call LoadLibraryA
...

Sources et binaires

2 comments:

  1. There's another trick (in my Anti-Unpacking paper) where the TLS contains the import table. Then you have a table of RVAs which Windows converts to VAs at runtime, but which a scanner probably won't resolve correctly. If you import something like WinExec, you also get a "free" parameter on the stack, which points to the image header. Place a filename in there, and Windows will run the file for you.

    ReplyDelete