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

2010/02/03

Et puis celles qu'on doit pas...

Undocumented opcodes and behaviors

Ever seen this before?
00400181 0F1F ??? ; Unknown command

As my opcode file is now close to completion, I made a working test executable with undocumented or uncommon opcodes, that you could use to test your own emulator or disassembler.

Note that if you use an older tool, opcodes might not be disassembled at all. If you're using Ollydbg (1.1), get a copy of BeatriX' FullDisasm to add support for the latest opcodes.
Let's start:

Setalc (or salc), a.k.a Set al on carry, is an originally undocumented opcode that sets AL to 00 or FF depending on CF state:
clc
salc
cmp al, 0
jnz bad

stc
salc
cmp al, 0ffh
jnz bad



While BSWAP has an expected behavior on 32bits (turns a value of 12345678 into 78563412), it's undefined on 16bits and yet commonly used: It swaps the register contents with a null word, so it just resets it.
mov eax, 12345678
bswap ax
cmp eax, 12340000
jnz bad



AAD and AAM don't have officially an operand, but it's just that they had a default operand of Ah, as they were used for BCD arithmetic operations.
Their behavior with a different base is unofficially defined, so they can be used for breaking 'standard' emulators:
AAD X sets AH to 0, and AL to AH * X + AL
AAM X sets AH to AL / X, and AL to AL % X
mov ax, 0325h
aad 7
cmp ax, 003Ah
jnz bad

aam 3
cmp ax, 1301h
jnz bad


This makes AAD the first add and multiply opcode on Intel CPU, on 8 bits only.


I explored SMSW behavior in detail already: on 32 bits, it's supposedly not defined, but it actually just copies CR0 value, which is constant in the usual conditions:
smsw eax
cmp eax, 08001003bh
jnz bad



the FPU is full of undocumented aliases (check Sandpile for a complete list), I just inserted them as junk, not for their actual operations:
ffreep st0
fstp1 st0
fcom2 st0
fcomp3 st0
fxch4 st0
fcomp5 st0
fxch7 st0
fstp8 st0
fstp9 st0
fneni
fndisi



Group 3 (f6-f7) Opcode 001 is actually an alias of 000 (Test).
This gives you a standard TEST opcode on memory reference that might not be disassembled, thus emulated correctly.
Ex:
F70878563412 test dword ptr [eax], 12345678h
OllyDbg 1.1 says
00400155 F7 ??? ; Unknown command



Similarly, SAL (Shift Arithmetic Left) is functionally identical to SHL. Thus it's commonly assembled as SHL directly, and some disassemblers/emulators might not support it, even though it's a standard SHL.


NOP is not just 090h anymore. It has taken different forms in modern CPUs, for example, to give hints about the execution. It even has an operand, but you can't trigger an exception with it, hopefully.
So, it still basically does nothing, provided you disassemble it correctly. See for yourself:
0F1F00 nop dword ptr [eax]
0F1984C000000080 hint_nop dword ptr [eax+eax*8-80000000h]



IceBP, aka Int1, is a very famous undocumented opcode that triggers a Single Step exception. Int 1 (CD01) itself would trigger an access violation exception.

F1 IceBP
...
cmp dword ptr [edx] , 80000004h



On branching instructions (Call/Retn/Jmp/Jxx/Loop), the 66 Operand size prefix makes the opcode work only on the lower word of EIP.
Thus a familiar-looking

50 push eax
66c3 retn
...
cmp dword ptr [edx] , C0000005h

will actually jump to AX, which will trigger an Access violation exception - it's not your standard RET here!


CMPS* and MOVS* are the only 2 opcodes that use 2 memory reference operands. However, they're often shown without in their compact form, without their operands at all. If you use them with a segment prefix such as 64 FS, the source is taking the prefix.
Such a hidden FS:[ESI] => [EDI] could be used to set a SEH.
(For extra SEH tricks, check roy g biv's Subtle SEH)


LOOP is influenced by the Operand size prefix, but also the Address size prefix, in which case only CX is checked (same for Jecxz, but in this case it's written differently, jcxz). Thus with the right value ECX, the 'same' loop might end up in different places:

mov ecx, 0ffff0001h
67
loop bad
loop good


Source and binary

[...]

Opcodes et comportements non documentés

Ça vous dit quelque chose ?
00400181 0F1F ??? ; Unknown command

Alors que mon fichier d'opcodes est bientôt complet, j'ai écrit un exécutable de test qui marche correctement et qui contient des opcodes rares ou non documentés, que vous pourrez utiliser pour tester votre désassembleur ou émulateur perso.

Il faut noter que si vous utilisez un outil un peu ancien, les opcodes ne seront peut-être pas gérés du tout. Si vous utilisez Ollydbg (1.1), installez FullDisasm de BeatriX pour le mettre à jour.

Commençons:
Setalc (or salc), autrement dit Set al on carry, est un opcode non documenté au début, qui met AL à 00 ou FF en fonction de l'état de CF :
clc
salc
cmp al, 0
jnz bad

stc
salc
cmp al, 0ffh
jnz bad



Alors que BSWAP a un comportement prévisible sur 32 bits (change 12345678 en 78563412), il est non défini sur 16 bits, et pourtant couramment utilisé : il échange le contenu du registre avec un mot nul, donc il le remet à zéro.
mov eax, 12345678
bswap ax
cmp eax, 12340000
jnz bad



AAD et AAM n'ont officellement pas d'opérande, mais c'est en fait une opérande par défaut à Ah, puisqu'ils sont utilisés pour les opérations arithmétiques avec des nombres BCD (1020 est stocké sous la forme 1020h, pas 0x3fc).
Leur comportement avec une base autre que décimale est défini officieusement, donc ils peuvent être utilisés pour casser des émulateurs trop standard:
AAD X met AH à 0, et AL à AH * X + AL
AAM X met AH à AL / X, et AL à AL % X
mov ax, 0325h
aad 7
cmp ax, 003Ah
jnz bad

aam 3
cmp ax, 1301h
jnz bad


Ce qui fait de AAD le premier opcode d'addition et multiplication combinées sur processeur Intel, mais en 8 bits uniquement.


J'ai déjà exploré en détail le comportement de SMSW : sur 32 bits, il n'est soit-disant pas défini, mais en fait il copie juste la valeur de CR0, qui est constante dans les conditions normales :
smsw eax
cmp eax, 08001003bh
jnz bad



La FPU est remplie d'alias non documentés (allez voir Sandpile pour une liste complète), j'en ai juste inséré pour remplir, pas pour leurs opérations effectives:
ffreep st0
fstp1 st0
fcom2 st0
fcomp3 st0
fxch4 st0
fcomp5 st0
fxch7 st0
fstp8 st0
fstp9 st0
fneni
fndisi



dans le groupe 3 (f6-f7), l'opcode 001 est en fait un synonyme du 000 (Test).
Cela vous donne un opcode TEST standard qui pourrait ne pas être désassemblé correctement, donc émulé correctement.
Ex:
F70878563412 test dword ptr [eax], 12345678h
OllyDbg 1.1 donne
00400155 F7 ??? ; Unknown command



De manière similaire, SAL (Shift Arithmetic Left) est fonctionnellement identique à SHL. Il est en général assemblé en SHL, et certains désassembleurs ou émulateurs peuvent ne pas le gérer, alors qu'il n'est qu'un SHL standard.


NOP n'est plus uniquement 90h. Il a plusieurs formes sur les processeurs modernes, par exemple, pour donner des indications sur l'exécution. Il a même une opérande, mais heureusement on ne peut pas déclencher d'exception avec.
Donc, il ne fait toujours rien, mais encore faut-il le désassembler correctement. Voyez vous-même:
0F1F00 nop dword ptr [eax]
0F1984C000000080 hint_nop dword ptr [eax+eax*8-80000000h]



IceBP, parfois appelé Int1, est un opcode non documenté très connu qui déclenche une exception Single Step. Le vrai Int 1 (CD01) déclenche une exception de type access violation.

F1 IceBP
...
cmp dword ptr [edx] , 80000004h



Devant les instructions de saut (Call/Retn/Jmp/Jxx/Loop), le préfixe de taille d'opérande 66 fait sauter sur le mot faible de EIP.
Donc un innocent

50 push eax
66c3 retn
...
cmp dword ptr [edx] , C0000005h

va en fait sauter vers AX, ce qui déclenchera une exception Access violation - ce n'est pas votre RET habituel!


CMPS* et MOVS* sont les 2 opcodes qui utilisent 2 opérandes de références mémoires. Ils sont souvent affichés sous leur forme compacte, sans leurs opérandes. Si on les utilise avec un préfixe de segment tel que 64 FS, la source est influencée par ce préfixe.
Ce qui nous donne un FS:[ESI] => [EDI] caché, qui peut être utiliser pour définir un SEH.
(Pour plus d'astuces de SEH, voir Subtle SEH de roy g biv).


LOOP est donc influencée par le préfixe de taille d'opérande, mais aussi par celui de taille d'adresse, auquel cas seul CX est contrôlé (de même pour Jecxz, mais dans ce cas, il est écrit différemment, jcxz). Donc, avec une bonne valeur dans ECX, la 'même' LOOP peut donner des résultats différents :

mov ecx, 0ffff0001h
67
loop bad
loop good


Source et Binaires

No comments:

Post a Comment