2009-12-29:

BSWAP + 66h prefix

x86:assembler:bochs:qemu:medium
Ostatnimi czasy przypominam sobie osdev (ostatni raz coś więcej niż boot menu napisałem jakieś 6 lat temu), więc prawdopodobnie parę następnych postów będzie związane z assemblerem, emulatorami x86 oraz podobnymi instytucjami. Dzisiejszy post poświęcę instrukcji bswap reg16 w trybie chronionym, która jak się okazuje może posłużyć np. do detekcji emulatora BOCHS lub QEMU.

Instrukcja bswap reg16 to w zasadzie bswap reg32 z prefiksem 66h, czyli "operand-size override". Jak można wyczytać w manualu Intela (tom 2a), użcie instrukcji bswap w połączeniu z tymże prefiksem daje nieokreślony wynik:

When the BSWAP instruction references a 16-bit register, the result is undefined.

Jak można przeczytać w 86BUGS.LST By Harald Feldmann Revision 04 (w inter61d.zip), jest to raczej nienowe zachowanie tejże instrukcji - podobne (identyczne?) było w procesorach 80486:

Mnemonic: BSWAP reg32
Opcode  : 0F C8+reg# (00001111 11001rrr)
Bug in  : 486
[...]
Do not use this instruction with 16 bit registers as operand.
Results are undefined in that case.


Oczywiście, jak tylko researcher przeczyta "undefined", to od razu chce sprawdzić jak wygląda to "undefined". Tak więc powstał niniejszy kawałek kodu (jak widać skorzystałem z nagich funkcji gcc):

// gcc bswap.c -masm=intel
#include <stdio.h>

extern unsigned int test(unsigned int a);
__asm("\n\
  .global _test\n\
   _test:\n\
     mov eax, [esp+4]\n\
     .byte 0x66\n\
     bswap eax\n\
     ret");

int
main(void)
{
unsigned int i;
for(i = 0; i < 0xf0000000; i += 1237)
  printf("%.8x -> %.8x\n", i, test(i));


return 0;
}


Jak się okazało (w tym miejscu podziękowania dla MeMeK'a i oshogbo za odpalenie powyższego kodu na procesorach firmy AMD) za równo na nowych Intel'ach (testowane na Core 2 Duo i Core 2 Quad), jak i na AMD (Duron 800 MhZ i Athlon XP 2000+) instrukcja bswap reg16 powoduje wyzerowanie rejestru reg16 (czyli dolnych 16 bitów rejestru EAX w wypadku powyższego kodu).

Powstaje pytanie - czy tak samo dzieje się na emulatorach x86, typu bochs czy QEMU?

Rzućmy okiem w kod. Na pierwszy ogień idzie bochs (plik cpu/bit.cc):

void BX_CPP_AttrRegparmN(1) BX_CPU_C::BSWAP_ERX(bxInstruction_c *i)
{
#if BX_CPU_LEVEL >= 4
 Bit32u val32, b0, b1, b2, b3;

 if (i->os32L() == 0) {
   BX_ERROR(("BSWAP with 16-bit opsize: undefined behavior !"));
 }


 val32 = BX_READ_32BIT_REG(i->opcodeReg());
 b0 = val32 & 0xff; val32 >>= 8;
 b1 = val32 & 0xff; val32 >>= 8;
 b2 = val32 & 0xff; val32 >>= 8;
 b3 = val32;
 val32 = (b0<<24) | (b1<<16) | (b2<<8) | b3;

 BX_WRITE_32BIT_REGZ(i->opcodeReg(), val32);
#else
 BX_INFO(("BSWAP_ERX: required CPU >= 4, use --enable-cpu-level=4 option"));
 exception(BX_UD_EXCEPTION, 0, 0);
#endif
}


Jak widać na powyższym listingu, wykrywane jest czy używany jest 32-bitowy rejestr (co nie ma miejsca w przypadku użycia prefiksu 66h w trybie chronionym), i jeżeli nie jest, to do logów leci stosowany error, ale wykonywanie instrukcji jest kontynuowane. Tak więc bswap reg16 działa w przypadku bochs identycznie jak bswap reg32!

Czas na QEMU (plik target-i386/translate.c):
   case 0x1c8 ... 0x1cf: /* bswap reg */
       reg = (b & 7) | REX_B(s);
#ifdef TARGET_X86_64
       [...]
#else
       {
           gen_op_mov_TN_reg(OT_LONG, 0, reg);
           tcg_gen_bswap_i32(cpu_T[0], cpu_T[0]);
           gen_op_mov_reg_T0(OT_LONG, reg);
       }
#endif
       break;


Wygląda na to że QEMU zakłada, że bswap zawsze dostanie rejestr 32-bitowy, i ignoruje wszelkie użycia prefiksu 66h. Czyli w zasadzie działa identycznie jak bochs.

Podsumowując, przy użyciu instrukcji bswap reg16 można wykryć bochs i QEMU.

Po co jednak wykrywać ów emulatory? Cóż, np. bochs jest dość wygodnym narzędziem do analizy wszelkiego malware'u atakującego MBR (np. Sinowal MBR aka Win32/Mebroot). Reverserzy powinni więc mieć świadomość, że wystąpienie niewinnej instrukcji bswap w analizowanym malware może wpłynąć negatywnie na / utrudnić analizę - np. autor malware wykonałby bswap a następnie porównanie czy w reg16 są zera, i jeśli ich tam nie ma, to skok do oryginalnego kodu z pominięciem infekcji.

No i tyle...

UPDATE: Rolf Rolles w komentarzach po angielskojęzycznej stronie lustra napisał, że VMProtect korzysta z tego hacka. Zachęcam również do rzucenia okiem na komentarze poniżej ;>
UPDATE 2: Peter Ferrie w komentarzach do niniejszego postu na openrce wspomniał o niedawno zgłoszonym (ale chyba jeszcze nie poprawionym, a przynajmniej nie mogę się doszukać poprawki w wersji release) przez niego bugu w DOSBox dotyczącym własnie bswap 16-bit. Pisze również, że działanie bswap nie zmieniło się od czasów 486.
Warto wrzucić również okiem na blog "Insanely Low-Level" i dyskusje o tym jak bswap 16-bit powinien być disasemblowany (mowa tam o silniku diStorm).

Comments:

2009-12-29 16:45:29 = ...
{
Generalnie zachowanie bierze się z tego, że wewnętrznie wszystkie wartości są rozciągane do pełnego rozmiaru rejestrów wewnętrznych, po wykonaniu operacji wynik jest przycinany i wstawiany do rejestru docelowego. Efekt jest taki: górne dwa bajty są wyzerowane przy `rozciągniu`, po obrocie lądują w dolnej części i `zerują` rejestr docelowy.
}
2009-12-29 18:12:12 = Gynvael Coldwind
{
@...
Brzmi sensownie! :)
Możesz zdradzić gdzie to info znalazłeś? Tom 1 Intela or sth ?
}
2009-12-29 18:36:31 = ...
{
Szerze powiedziawszy - nie pamiętam. Wieki temu robiłem eksperymenty i posiłkowałem się manualami Intela, chyba w trzecim jest opisany model wykonywania instrukcji, z którego takie wnioski można wyciągnąć.

Ostatni raz bawiłem się tym za czasów świetności ctrl-d, bodaj tam zresztą dyskusja na ten temat się wywiązała po moim poście.

BTW, jestem pewny, że jeszcze jakaś instrukcja podatna na podobne anomalie była, ale za cholerę przypomnieć sobie nie mogę... starość nie radość.
}
2009-12-30 06:38:46 = Gynvael Coldwind
{
@...
Mhm, poszperam, thx ;>
Co do innych instrukcje, to co jakiś czas słówko 'undefined' się przewija w manualach ;>
}
2010-01-07 16:28:28 = c
{
tak btw: masz literówkę przy "bugu" w update2. (a hreg zamiast href, przez co link nie dziala)
}
2010-01-07 16:34:10 = Gynvael Coldwind
{
@c
Thx ;> Fixed ;>
}

Add a comment:

Nick:
URL (optional):
Math captcha: 1 ∗ 7 + 1 =