Copy Link
Add to Bookmark
Report

2x06 Phearless Challenge #2 Reversme

eZine's profile picture
Published in 
phearless
 · 14 Jan 2024

                                 
...................
...::: phearless zine #2 :::...

................>---[ Phearless Challenge #2: Reversme ]---<..............

...........................>---[ by sunnis ]---<..........................
sunnis[at]gmail[dot]com

Sadrzaj:

<1> Uvod u sekciju - Phearless Challenge

<2> Uvod u reversme

<3> Runtime nativna dekompresija i nativni API

<4> Phun with CLR

<5> Tko kaze da nema asemblera u .NET-u?

<6> asm i Nt*

<7> Brainfuck

<8> Running-line

<9> One ring to bring 'em all...

<10> Greets

<11> Reference


///////////////////////////////////////////////////////////////////////////
--[ Uvod u sekciju - Phearless Challenge
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\

Ovaj papir jest prvi u sekciji Phearless Challenge. Nosi broj 2 iako je de facto
prvi papir u toj kategoriji, samo zato da se poklopi sa numerickom verzijom
samog zine-a, kako ne bi doslo do zabuna.

Phearless Challenge jest nova kategorija koja je namijenjena objavljivanju
izazova samim citateljima (i piscima :) zine-a. U njoj ce biti objavljeni
prije svega *originalni* izazovi koji bi citatelji trebali rijesiti, te rjesenje
poslati autoru izazova, kako bi stekli besmrtnu slavu u sluzbenom rjesenju koje
ce sam autor objaviti u zine-u ;)

A izazov moze biti manje-vise sve. Najscesce ce to biti neki crackme, reversme,
exploitme, neko elegantno rjesenje nekog hacking problema...uglavnom bilo sto
sto autor odgovarajuceg izazova smisli. Naglasak je, ponavljam, na
*originalnosti*. c/p-eri i oni koji su prvi put u zivotu otkrili i napisali neki
komadic koda, i sad zele ostati "zapamceni" u povijesti kao phearless autori
mogu komotno odjebati. Ova sekcija nije namijenjena njima niti ce ikad biti.

A autor izazova moze biti bilo tko, tj. ne ogranicavamo se samo na standardne
autore clanaka :) Dakle ako ste bas *VI* dovoljno l33t, i imate neki jako kul
problemcic i odgovarajuce rjesenje na pameti, bilo sto sto mislite da je
dovoljno zanimljivo da udje u ovu sekciju, pozivam Vas, nemojte se libiti
poslati problem meni, ili uredniku zine-a, pa ako je dovoljno dobar i zanimljiv,
slobodno cemo ga objaviti!

Ali ponavljam, *samo* problem, bez rjesenja (kojeg cete bas Vi prezentirati u
broju zine-a nakon objavljivanja problema skupa sa pristiglim rjesenjima). U
stvari najbolje je da dodjete na #ugs @ irc.krstarica.com pa da o tome popricamo
:)

Konkretno sam ja imao na umu da se izazov objavljuje ili na sluzbenoj stranici
od zine-a, ili na prikladnom forumu na [es]. Recimo ovaj moj je objavljen ovdje:

http://www.elitesecurity.org/poruka/692956

Buduci da se konkretno radi o reverseme-u, ide u Cracking forum. Recimo bas ova
sekcija zine-a cilja tematski konkretno na skupinu foruma u kategoriji
Sigurnost:

http://www.elitesecurity.org/kategorija/9

Ali naravno, prihvacamo sve sto je dovoljno originalno i zanimljivo :)


///////////////////////////////////////////////////////////////////////////
--[ Uvod u reversme
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\


Ovaj reversme sam napisao cisto iz zabave, da dokazem prije svega sebi da su
neke stvari moguce, i da uvjerim neke craxore koji vele "Tko kaze da je VB
lame"
, te se ponasaju bahato i umisljeno i misle da su popili svu pamet svijeta.

E pa niste gospodo. Konkretno za ovaj izazov u 10 dana koliko tamo stoji nisam
dobio nijedan mejl u kojem netko kaze da ga je i pokusao rijesiti, a kamoli da
me ispita o nekim nacinima zastite :)

E pa da sad nesto malo vise kazem o samom reversmeju.

Ovo je prije svega zamisljen kao fascinantan komadic koda koji ce kombinirati
neko slojeva (razina) zastite u razlicitim programskim jezicima i u razlicitim
okolinima. Konkretno su demonstrirane uporabe sljedecih tehnologija:

- neki popularni i manje popularni anti-* trikovi
- nativna runtime dekompresija
- inline koristenje nativnih API-ja u asmu i u C/C++
- zastita MSIL koda
- embeddanje asm koda u .NET jezike
- running line enkripcija
- skakanje u ring0 i pozivanje kernel-mode servisa bez pisanja drivera
- brainfuck ;)

Uglavnom, prije nego sto sam ga i poceo pisat, manje-vise sam cijeli crackme kao
takav imao vec "u glavi" kako bi trebao izgledat, te sam ga pisao (kako to i
prilici :) od tezih prema laksim dijelovima. Dosta stvari ovdje prikazani su
jako kul, i premda recimo neki anti-* trikovi nisu novi, sa vecinom ostalih
tehnologija ovdje prezenziranih jest 90% crackera jako slabo upoznato. Tj.
nemaju pojma.

Cilj je bio prije svega da svi nesto nauce. Onaj koji cita papir, onaj koji
rijesava izazov i onaj koji ga pise. Ja sam svoj dio posla napravio, a nadam
se da cete citajuci ovaj tekst uzivati barem onoliko koliko sam ja BSOD-ova
dobio dok sam radio ring0 komponentu ;)


///////////////////////////////////////////////////////////////////////////
--[ Runtime nativna dekompresija i nativni API
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\

Prvi sloj zastite jest mala aplikacija napisana u manje-vise cistom C-u, koja
koristi 3 nedokumentirane nativne fje: RtlCompressBuffer(),
RtlDecompressBuffer() i RtlGetCompressionWorkSpaceSize() (vise informacija o
ovim fjama mozete pronaci na [1] [2] i [3]). Analizirajmo kako one izgledaju
redom, koristeci njihove deklaracije iz \private\ntos\rtl\ntrtlp.h:

NTSTATUS
RtlCompressBuffer (
IN USHORT CompressionEngine,
IN PUCHAR UncompressedBuffer,
IN ULONG UncompressedBufferSize,
OUT PUCHAR CompressedBuffer,
IN ULONG CompressedBufferSize,
IN ULONG UncompressedChunkSize,
OUT PULONG FinalCompressedSize,
IN PVOID WorkSpace
);

Sa NtAllocateVirtualMemory() alociramo buffer koji ce koristiti
RtlCompressBuffer(). Zasad NT podrzava samo jedan kompresijski format koji
odgovara CompressionEngine parametru, a koji se zove LZNT1 (vjerojatno bazirano
na Lempel-Ziv enkodiranju) te je definiran izmedju ostaloga sljedecom konstantom
u winnt.h:

#define COMPRESSION_FORMAT_NONE (0x0000)
#define COMPRESSION_FORMAT_DEFAULT (0x0001)
#define COMPRESSION_FORMAT_LZNT1 (0x0002)

#define COMPRESSION_ENGINE_STANDARD (0x0000)
#define COMPRESSION_ENGINE_MAXIMUM (0x0100)

Sto se tice ostalih kompresijskih formata, disasemblirajmo
RtlCompressBuffer!ntdll.dll sa ucitanim debug simbolima te sredimo malo imena
parametara i labela, te pogledajmo sto se sve tamo krije:

.text:7C9612AD ; Exported entry 440. RtlCompressBuffer
.text:7C9612AD mov edi, edi
.text:7C9612AF
.text:7C9612AF ; --------------- S U B R O U T I N E ---------------------------
.text:7C9612AF
.text:7C9612AF ; Attributes: bp-based frame
.text:7C9612AF
.text:7C9612AF sub_7C9612AF proc near
.text:7C9612AF
.text:7C9612AF CompressionEngine= dword ptr 8
.text:7C9612AF UncompressedBuffer= dword ptr 0Ch
.text:7C9612AF UncompressedBufferSize= dword ptr 10h
.text:7C9612AF CompressedBuffer= dword ptr 14h
.text:7C9612AF CompressedBufferSize= dword ptr 18h
.text:7C9612AF UncompressedChunkSize= dword ptr 1Ch
.text:7C9612AF FinalCompressedSize= dword ptr 20h
.text:7C9612AF WorkSpace = dword ptr 24h
.text:7C9612AF
.text:7C9612AF push ebp
.text:7C9612B0 mov ebp, esp
.text:7C9612B2 mov ecx, [ebp+CompressionEngine]
.text:7C9612B5 xor eax, eax
.text:7C9612B7 mov al, cl

; maskiraj LSB za COMPRESSION_FORMAT_* konstantu


.text:7C9612B9 and eax, 0FFh
.text:7C9612BE jz short __invalid_param
.text:7C9612C0 cmp ax, 1

; nevrijedeci parametri su 0 i 1

.text:7C9612C4 jz short __invalid_param
.text:7C9612C6 test al, 0F0h
.text:7C9612C8 jz short __ok_compression
.text:7C9612CA
.text:7C9612CA loc_7C9612CA:

; nepodrzane kompresije su 3h - 0Fh

.text:7C9612CA mov eax, STATUS_UNSUPPORTED_COMPRESSION
.text:7C9612CF jmp short __unsupported_compression
.text:7C9612D1
.text:7C9612D1 __ok_compression:
.text:7C9612D1 push [ebp+WorkSpace]

; outmaskaj LSB i ostale parametre proslijedi standardno

.text:7C9612D4 and ecx, not 0FFh
.text:7C9612DA push [ebp+FinalCompressedSize]
.text:7C9612DD movzx eax, ax
.text:7C9612E0 push [ebp+UncompressedChunkSize]
.text:7C9612E3 push [ebp+CompressedBufferSize]
.text:7C9612E6 push [ebp+CompressedBuffer]
.text:7C9612E9 push [ebp+UncompressedBufferSize]
.text:7C9612EC push [ebp+UncompressedBuffer]
.text:7C9612EF push ecx
.text:7C9612F0 call ds:_RtlCompressBufferProcs[eax*4]
.text:7C9612F7 jmp short __unsupported_compression
.text:7C9612F9
.text:7C9612F9 __invalid_param:
.text:7C9612F9 mov eax, STATUS_INVALID_PARAMETER
.text:7C9612FE
.text:7C9612FE __unsupported_compression:
.text:7C9612FE pop ebp ; vrati frame pointer i izadji
.text:7C9612FF retn 20h
.text:7C9612FF sub_7C9612AF endp
.text:7C9612FF
.text:7C961302 align 4
.text:7C961304 _RtlCompressBufferProcs dd 0
.text:7C961308 dd 0
.text:7C96130C dd offset _RtlCompressBufferLZNT1@32 ; RtlCompressBufferLZNT1(x,x,x,x,x,x,x,x)
.text:7C961310 dd offset _RtlCompressBufferNS@32 ; RtlCompressBufferNS(x,x,x,x,x,x,x,x)
.text:7C961314 dd offset _RtlCompressBufferNS@32 ; RtlCompressBufferNS(x,x,x,x,x,x,x,x)
.text:7C961318 dd offset _RtlCompressBufferNS@32 ; RtlCompressBufferNS(x,x,x,x,x,x,x,x)
.text:7C96131C dd offset _RtlCompressBufferNS@32 ; RtlCompressBufferNS(x,x,x,x,x,x,x,x)
.text:7C961320 dd offset _RtlCompressBufferNS@32 ; RtlCompressBufferNS(x,x,x,x,x,x,x,x)
.text:7C961324 dd 90909090h
.text:7C961328 db 90h


RtlCompressBufferProcs niz koji se nalazi na adresi 7C961304h predstavlja
tablicu adresa fja koje sluze za kompresiju odgovarajucim engine-ima. Na adresi
7C9612F0h se nalazi call na odgovarajuci indeks u tu tablicu, u ovisnosti o
vrijednosti u eax, u kojem ce se u konacnici nalaziti LSB CompressionEngine
parametra.

E sad, u toj se tablici nalazi 6 vrijedecih adresa, od cega su prakticno 2
razlicite fje. Na ideksima 0 i 1 se nalaze NULL ptri, ali oni se filtriraju na
adresama 7C9612BEh i 7C9612C4h. Indeks 2 koji dogovara COMPRESSION_FORMAT_LZNT1
parametru pokazuje na internu RtlCompressBufferLZNT1() fju koja nije exportana,
dok ostali pokazuju na fju RtlCompressBufferNS() na kojoj se nalazi pogodite
sto?

.text:7C961416 ; --------------- S U B R O U T I N E ---------------------------
.text:7C961416
.text:7C961416
.text:7C961416 ; __stdcall RtlCompressBufferNS(x,x,x,x,x,x,x,x)
.text:7C961416 _RtlCompressBufferNS@32 proc near
.text:7C961416 mov eax, STATUS_UNSUPPORTED_COMPRESSION
.text:7C96141B retn 20h
.text:7C96141B _RtlCompressBufferNS@32 endp

Dakle onaj NS postfix jest vjerojatno kratica od Not Supported :>

Kao sto rekoh, RtlCompressBufferLZNT1() nije nazalost exportana pa cemo trebati
joj pristupati preko RtlCompressBuffer(). Sto se tice ostalih parametara, oni su
manje vise sami po sebi razumljivi po svome nazivu, jedino se mozda isplati
objasniti jos ove:

IN ULONG UncompressedChunkSize

- obicno stavi 1000h, tj. jedan page, a mozes i 0 komotno...

IN PVOID WorkSpace

- ptr na privremeni buffer nad kojim kompresijski engine obavlja svoj posao.
Njega sami alociramo (recimo koristeci NtAllocateVirtualMemory()), a njegovu
velicinu saznajemo dinamicki koristeci RtlGetCompressionWorkSpaceSize().

NTSTATUS
RtlGetCompressionWorkSpaceSize (
IN USHORT CompressionEngine,
OUT PULONG CompressBufferWorkSpaceSize,
OUT PULONG CompressFragmentWorkSpaceSize
);

pri cemu:

OUT PULONG CompressBufferWorkSpaceSize

- vraca potrebnu velicinu buffera kojeg trebamo alocirati

OUT PULONG CompressFragmentWorkSpaceSize

- obicno vraca 4000h

Ovaj API vjerojatno postoji u slucaju da u buducnosti budu implementirani neki
drugi algoritmi i kodiranja osim LZNT1. Osim tog jednog algoritma (algoritam
cine bitovi 0-15 u CompressionEngine parametru), postoje i 2 engine-a koje ovaj
algoritam podrzava, jedan za standardnu i jedan za visoku kompresiju
(COMPRESSION_ENGINE_STANDARD i COMPRESSION_ENGINE_MAXIMUM). Ovaj za visoku
kompresiju je i do 5 puta sporiji, ali zato kompresira dodatno 5-15% bolje. U
konacnici je CompressionEngine bitwise OR odabranog algoritma i razine
kompresije.

#define COMPRESSION_ENGINE_STANDARD (0x0000)
#define COMPRESSION_ENGINE_MAXIMUM (0x0100)

Specificno da demonstriram uporabu ovih fja, napisao sam jedan mali C++ (dobro,
manje-vise je to cisti C :) proggy koji kompresira/dekompresira proizvoljnu
datoteku. Mozete ga kompajlirati koristeci Visual C++ 2005 Express.

------------------------------ C O D E ------------------------------------

#include "stdafx.h" // precompiled headeri koje generira AppWizard
#include <iostream>
#include <windows.h>
using namespace std;

//
// Neke korisne strukture i tipovi koji ce nam trebati.
//

#define COMPRESSION_FORMAT_NONE (0x0000)
#define COMPRESSION_FORMAT_DEFAULT (0x0001)
#define COMPRESSION_FORMAT_LZNT1 (0x0002) // samo ovaj koristimo!!

#define COMPRESSION_ENGINE_STANDARD (0x0000)
#define COMPRESSION_ENGINE_MAXIMUM (0x0100)

typedef UINT NTSTATUS; // nativni API-ji vracaju NTSTATUS kod u eax

#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)

typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING;
typedef UNICODE_STRING *PUNICODE_STRING;

typedef struct _OBJECT_ATTRIBUTES {
ULONG Length;
HANDLE RootDirectory;
PUNICODE_STRING ObjectName;
ULONG Attributes;
PVOID SecurityDescriptor;
PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES;
typedef OBJECT_ATTRIBUTES *POBJECT_ATTRIBUTES;

typedef enum _SECTION_INHERIT {
ViewShare = 1,
ViewUnmap = 2
} SECTION_INHERIT;

//
// Deklaracije nativnih fja koje importamo iz ntdll.dll
//

NTSTATUS ( __stdcall *NtCreateSection ) (
OUT PHANDLE SectionHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN PLARGE_INTEGER MaximumSize OPTIONAL,
IN ULONG SectionPageProtection,
IN ULONG AllocationAttributes,
IN HANDLE FileHandle OPTIONAL
);

NTSTATUS ( __stdcall *NtMapViewOfSection ) (
IN HANDLE SectionHandle,
IN HANDLE ProcessHandle,
IN OUT PVOID *BaseAddress,
IN ULONG_PTR ZeroBits,
IN SIZE_T CommitSize,
IN OUT PLARGE_INTEGER SectionOffset OPTIONAL,
IN OUT PSIZE_T ViewSize,
IN SECTION_INHERIT InheritDisposition,
IN ULONG AllocationType,
IN ULONG Protect
);

NTSTATUS ( __stdcall *NtUnmapViewOfSection ) (
IN HANDLE ProcessHandle,
IN PVOID BaseAddress
);

NTSTATUS ( __stdcall *NtClose ) (
IN HANDLE Handle
);

NTSTATUS ( __stdcall *RtlGetCompressionWorkSpaceSize ) (
IN USHORT CompressionEngine,
OUT PULONG CompressBufferWorkSpaceSize,
OUT PULONG CompressFragmentWorkSpaceSize
);

NTSTATUS ( __stdcall *RtlCompressBuffer ) (
IN USHORT CompressionEngine,
IN PUCHAR UncompressedBuffer,
IN ULONG UncompressedBufferSize,
OUT PUCHAR CompressedBuffer,
IN ULONG CompressedBufferSize,
IN ULONG UncompressedChunkSize,
OUT PULONG FinalCompressedSize,
IN PVOID WorkSpace
);

NTSTATUS ( __stdcall *RtlDecompressBuffer ) (
IN USHORT CompressionEngine,
OUT PUCHAR UncompressedBuffer,
IN ULONG UncompressedBufferSize,
IN PUCHAR CompressedBuffer,
IN ULONG CompressedBufferSize,
OUT PULONG FinalUncompressedSize
);

NTSTATUS ( __stdcall *NtAllocateVirtualMemory ) (
IN HANDLE ProcessHandle,
IN OUT PVOID *BaseAddress,
IN ULONG_PTR ZeroBits,
IN OUT PSIZE_T RegionSize,
IN ULONG AllocationType,
IN ULONG Protect
);


NTSTATUS ( _stdcall *NtFreeVirtualMemory ) (
IN HANDLE ProcessHandle,
IN OUT PVOID *BaseAddress,
IN OUT PSIZE_T RegionSize,
IN ULONG FreeType
);

//
// Ispisuje banner o pravilnoj uporabi toola u slucaju pogresnih parametara.
//

void Uporaba( LPSTR ime )
{
cout << "Tool za (de)kompresiju datoteka koristeci nativni API"
"(x) 2005 sunnis" << endl;
cout << ime << " [opcije] <ime datoteke>" << endl;
cout << "Opcije:" << endl;
cout << " -x:\t Kompresiraj" << endl;
cout << " -d:\t Dekompresiraj" << endl;
}

//
// Trazi adrese fja u ntdll.dll, te vraca FALSE ukoliko se bar jedna od njih
// nije mogla naci.
//

BOOLEAN LocirajNTDLLFje()
{
if ( !(NtCreateSection = (NTSTATUS (__stdcall *)
( PHANDLE,
ACCESS_MASK,POBJECT_ATTRIBUTES,
PLARGE_INTEGER,
ULONG,
ULONG,
HANDLE ))
GetProcAddress( GetModuleHandle("ntdll.dll"),
"NtCreateSection" )) ) {
return ( FALSE );
}

else if ( !(NtMapViewOfSection = (NTSTATUS (__stdcall *)
( HANDLE,
HANDLE,
PVOID *,
ULONG_PTR,
SIZE_T,
PLARGE_INTEGER,
PSIZE_T,
SECTION_INHERIT,
ULONG,
ULONG ))
GetProcAddress( GetModuleHandle("ntdll.dll"),
"NtMapViewOfSection" )) ) {
return ( FALSE );
}

else if ( !(NtUnmapViewOfSection = (NTSTATUS (__stdcall *)
( HANDLE,
PVOID ))
GetProcAddress( GetModuleHandle("ntdll.dll"),
"NtUnmapViewOfSection" )) ) {
return ( FALSE );
}

else if ( !(NtClose = (NTSTATUS (__stdcall *)(HANDLE))
GetProcAddress( GetModuleHandle("ntdll.dll"),
"NtClose" )) ) {
return ( FALSE );
}

else if ( !(RtlGetCompressionWorkSpaceSize = (NTSTATUS (__stdcall *)
( USHORT,
PULONG,
PULONG ))
GetProcAddress( GetModuleHandle("ntdll.dll"),
"RtlGetCompressionWorkSpaceSize" )) ) {
return ( FALSE );
}

else if ( !(RtlCompressBuffer = (NTSTATUS (__stdcall *)
( USHORT,
PUCHAR,
ULONG,
PUCHAR,
ULONG,
ULONG,
PULONG,
PVOID ))
GetProcAddress( GetModuleHandle("ntdll.dll"),
"RtlCompressBuffer" )) ) {
return ( FALSE );
}

else if ( !(RtlDecompressBuffer = (NTSTATUS (__stdcall *)
( USHORT,
UCHAR,
ULONG,
PUCHAR,
ULONG,
PULONG ))
GetProcAddress( GetModuleHandle("ntdll.dll"),
"RtlDecompressBuffer" )) ) {
return ( FALSE );
}

else if ( !(NtAllocateVirtualMemory = (NTSTATUS (__stdcall *)
( HANDLE,
PVOID *,
ULONG_PTR,
PSIZE_T,
ULONG,
ULONG ))
GetProcAddress( GetModuleHandle("ntdll.dll"),
"NtAllocateVirtualMemory" )) ) {
return ( FALSE );
}

else if ( !(NtFreeVirtualMemory = (NTSTATUS (__stdcall *)
( HANDLE,
PVOID *,
PSIZE_T,
ULONG ))
GetProcAddress( GetModuleHandle("ntdll.dll"),
"NtFreeVirtualMemory" )) ) {
return ( FALSE );
}

else return ( TRUE );
}

//
// Otvara i mapira specificiranu datoteku u memoriju.
//

BOOLEAN MapirajDatoteku( LPSTR ime,
DWORD velicina,
PHANDLE hFile,
PHANDLE hMem,
PVOID *pBaznaAdresa )
{
NTSTATUS status;
LARGE_INTEGER secSize;

*hFile = CreateFile( ime,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_ALWAYS,
0,
NULL );

if ( *hFile == INVALID_HANDLE_VALUE ) {
return ( FALSE );
}

if ( velicina == 0 ) {
velicina = GetFileSize( *hFile, NULL );
}

secSize.QuadPart = velicina;

status = NtCreateSection( hMem,
SECTION_ALL_ACCESS,
NULL,
&secSize,
PAGE_READWRITE,
SEC_COMMIT,
*hFile );

if ( status != STATUS_SUCCESS ) {
CloseHandle( hFile );
return ( FALSE );
}

// da ne bi Mm pomislio da treba mapirati naku nasu adresu...

*(DWORD*)pBaznaAdresa = NULL;

status = NtMapViewOfSection ( *hMem,
HANDLE (-1), // trenutni proces
pBaznaAdresa,
0,
velicina,
NULL,
&velicina,
ViewShare,
0,
PAGE_READWRITE );

if ( status != STATUS_SUCCESS ) {
NtClose( hMem );
CloseHandle( hFile );
return ( FALSE );
}

return ( TRUE );
}

//
// Alocira workspace, tj. buffer koji interno koristi kompresijski engine,
// a ciju velicinu dobivamo sa RtlGetCompressionWorkSpaceSize().
//

BOOLEAN StvoriWorkspace( USHORT CompressionFormat,
USHORT CompressionEngine,
PVOID *hMem)
{
DWORD CompressBufferWorkSpaceSize;
DWORD CompressFragmentWorkSpaceSize;
NTSTATUS status;

// ne forsiramo alociranje memorije na neku predefiniranu adresu

*(DWORD*) hMem = NULL;

RtlGetCompressionWorkSpaceSize( CompressionFormat | CompressionEngine,
&CompressBufferWorkSpaceSize,
&CompressFragmentWorkSpaceSize );

status = NtAllocateVirtualMemory( (HANDLE)-1, // trenutni proces
hMem,
0,
&CompressBufferWorkSpaceSize,
MEM_COMMIT,
PAGE_EXECUTE_READWRITE );

if ( status != STATUS_SUCCESS ) {
return ( FALSE );
}

return ( TRUE );
}

//
// Kompresira specificirani buffer te ga sprema u specificiranu datoteku.
//

BOOLEAN Kompresiraj( SHORT CompressionFormat,
USHORT CompressionEngine,
PUCHAR NekompresiraniBuf,
DWORD NekompresiranaVelicina,
DWORD* KonacnaVelicina,
LPSTR ime_kompresirane_datoteke,
PVOID Workspace )
{
DWORD KompresiranaVelicina = DWORD(NekompresiranaVelicina*1.13 + 4);
HANDLE hFile;
HANDLE hMem;
PVOID BaznaAdresa = NULL;
NTSTATUS status;

MapirajDatoteku( ime_kompresirane_datoteke,
KompresiranaVelicina,
&hFile,
&hMem,
&BaznaAdresa );

status = RtlCompressBuffer( CompressionFormat | CompressionEngine,
NekompresiraniBuf,
NekompresiranaVelicina,
(PUCHAR)BaznaAdresa,
KompresiranaVelicina,
0,
KonacnaVelicina,
Workspace );

// zatvori MMF, postavi file pointer na kompresiranu velicinu te zatvori
// otvorene handleove.

status = NtUnmapViewOfSection( (HANDLE)-1, BaznaAdresa );
NtClose( hMem );
SetFilePointer( hFile, *KonacnaVelicina, 0, 0 );
SetEndOfFile( hFile );
NtClose( hFile );

return ( TRUE );
}

//
// Dekompresira specificirani buffer te ga sprema u specificiranu datoteku.
//
BOOLEAN Dekompresiraj( USHORT CompressionEngine,
PVOID KompresiraniBuffer,
DWORD KompresiranaVelicina,
DWORD* KonacnaVelicina,
LPSTR ime )
{
DWORD NekompresiranaVelicina
LPVOID NekompresiranBuffer = NULL;
NTSTATUS status;
HANDLE hFile;
HANDLE hMem;
LPVOID BaznaAdresa = NULL;

// max. moguca kompresija (92%)

NekompresiranaVelicina = DWORD(KompresiranaVelicina * 12.5);

status = MapirajDatoteku( ime,
NekompresiranaVelicina,
&hFile,
&hMem,
&BaznaAdresa );

_ASSERTE ( status == STATUS_SUCCESS );

status = RtlDecompressBuffer( CompressionEngine,
(PUCHAR)BaznaAdresa,
NekompresiranaVelicina,
(PUCHAR)KompresiraniBuffer,
KompresiranaVelicina,
KonacnaVelicina );

_ASSERTE ( status == STATUS_SUCCESS );

status = NtUnmapViewOfSection( (HANDLE)-1, BaznaAdresa );

_ASSERTE ( status == STATUS_SUCCESS );

NtClose( hMem );
SetFilePointer( hFile, *KonacnaVelicina, 0, 0 );
SetEndOfFile( hFile );
NtClose( hFile );

return ( TRUE );
}

int _tmain( int argc, _TCHAR* argv[] )
{
HANDLE hMem;
HANDLE hFile;
LPSTR datoteka;
LPVOID BaznaAdresa = NULL;
LPVOID CompressionWorkspace;
DWORD konacna_velicina;
CHAR tempf[MAX_PATH];

// provjeri da li je dobar broj argumenata

if ( argc != 3 ) {
Uporaba( argv[0] );
exit(1);
}

char *opcija = argv[1];

// provjeri da li je opcija dobra

if ( strcmp(opcija, "-x") && strcmp(opcija, "-d") ) {
Uporaba( argv[0] );
exit(1);
}

if ( !LocirajNTDLLFje() ) {
cerr << "Greska: Ne mogu locirati fje unutar ntdll.dll!" << endl;
exit(1);
}

datoteka = argv[2];

// Uzmi potrebnu velicinu workspace-a i alociraj memoriju za njega.

if ( !(StvoriWorkspace( COMPRESSION_FORMAT_LZNT1,
COMPRESSION_ENGINE_MAXIMUM,
&CompressionWorkspace )) ) {
cerr << "Greska: Ne mogu odrediti defaultni workspace za "
"kompresiju!" << endl;
exit(1);
}

if ( !(MapirajDatoteku( datoteka, 0, &hFile, &hMem, &BaznaAdresa )) ) {
cerr << "Greska: Ne mogu mapirati datoteku!" << endl;
exit(1);
}

// Alociraj privremeno ime datoteke u trenutnom direktoriju.

GetTempFileName( ".", "ble", 0, tempf );

// Sad trebamo provjeriti da li radimo kompresiju ili dekompresiju.

if ( strcmp(opcija, "-x") == 0 ) {
Kompresiraj( COMPRESSION_FORMAT_LZNT1,
COMPRESSION_ENGINE_MAXIMUM,
(PUCHAR)BaznaAdresa,
GetFileSize(hFile, 0),
&konacna_velicina,
tempf,
CompressionWorkspace );
}

else {
Dekompresiraj( COMPRESSION_FORMAT_LZNT1,
(PUCHAR)BaznaAdresa,
GetFileSize(hFile, 0),
&konacna_velicina,
tempf );
}

NtUnmapViewOfSection( (HANDLE)-1, BaznaAdresa );
NtClose( hMem );
CloseHandle( hFile );

DeleteFile( datoteka );
MoveFile( tempf, datoteka );

return (0);
}

------------------------------ C O D E E N D -----------------------------

Ovim jednostavan tool demonstrira gore spomenute fje, te omogucava kompresiranje
i dekompresiranje proizvoljnih datoteka. Nativna kompresija je prije svega
zanimljiva vx-erima iz 2 razloga: jako je brza i zahtijeva poziv samo 3 vanjske
fje! Koliko mi je poznato trenutno ne postoje virusi koji koriste nativnu
kompresiju iz jednostavnog razloga sto ona nije dosad bila previse
dokumentirana, i vecina ljudi ni ne zna da ona postoji :>

Tool koristi samo maximalnu kompresiju, pa ce kompresiranje velikih datoteka
nesto potrajati. Medjutim, nista vas ne sprecava da dodate u program i switch za
normalnu kompresiju, ukoliko vam zatreba.

Sam crackme koristi samo fju Dekompresiraj(), te na disk dumpa crackme u obliku
drugog sloja zastite, pod (pseudo)slucajnim imenom, te ga pokrece. Interno je on
zapisan ne u klasican resurs, vec kao jedan ogromni niz koji ce se na kraju
spremiti u .data sekciju. Zapis u tom obliku je dobiven koristeci z0mbiev HAXOR
tool, koji je i napravljen za tu svrhu. Takav se zapis dekompresira u memoriji
te sprema na disk u temp direktorij, te odatle pokrece sa WinExec(). Da stvar
bude malchice teze za skuziti, korisim vlastitu implementaciju GetProcAddress()
i to bez ASCIIZ vec preko hasheva, i to samo za win32API (koji je i u tom obliku
mogao biti izbjegnut da sam htio, ali i ovako je dovoljno tesko :), dok su
nativni API-ji inlineirani preko asm-a. Takodjer demonstrira uporabu nativnih
fja u C/C++ bez potrebe sa parsanjem ntdll.dll:

------------------------------ C O D E ------------------------------------


#include "stdafx.h" // precompiled headeri koje generira AppWizard
#include <iostream>
#include <windows.h>

// ovdje je spremljen niz sa stage2 BYTE dumpom

#include "stage2.h"

// ovdje su neke klasicne strukutre koje sam vec spomenuo, da ustedimo
// malo prostora.

#include "ntutils.h"

// ovdje su deklaracije fja te inline asm implementacije nativnih fja.

#include "importi.h"

using namespace std;

/ *
Kad se koristi inline (m)asm preko __asm {..} izgleda da se 1) ne mogu
koristiti labele 2) ne radi (ili ja ne znam) implementacija nekih
karakteristicnih snippeta koda preko C-style makroa. Ovo je klasicni @sysenter
preko makroa, ali ne radi... mozda nekome od vas proradi... Nadalje, inline
asm nazalost ni ne prepoznaje sysenter opkod, a da stvar bude jos zabavnija,
ne zeli asemblirati raw konstante kao kod, pa sam bio prisiljen napraviti dirty
hack i morfirati sljedecu instrukciju u letu. Zbog toga atzribut .text sekcije
mora biti +w, kako bi Loader podesio permisije pageva koda da budu writable.
Znam, mogao sam to napraviti rucno sa VirutalProtectEx(), ali mi se ne da...
Za tu svrhu se koristi old skul alatich by Jacky Qwerty zvan pewrsec! Dodajte
ga u post-build event u Visual Studio pod opcijama projekta i ne zaboravite da
prepoznaje samo 8.3 zapis path-a :)
*/

#define SYSENTER ( a, b ) { \
__asm { \
call $+26 \
mov eax, (a) \
mov edx, esp \
call $+5 \
pop ecx \
mov word ptr [ecx+7], 340Fh \
push 0 \
call $-21 \
add esp, (b*4) + 4 \
} \
}

/*
Uzima bazu ntdll.dll iz PEB bloka. Klasika -> metoda dokumentirana by
Ratter/29a, vjerojatno otkriveno od strane ElicZ, ali on nije bas vx koder pa
nije znao zasto to iskoristiti pa je sva slava pripala Ratteru :) Nadalje,
forsiramo __forceinline kako bi iznervirali lamere koji ne znaju sto je to PEB,
:)))
*/


__forceinline HMODULE UzmiBazuNtdll()
{
HMODULE baza;

__asm {
mov eax, dword ptr fs:[30h] ; PEB baza
mov eax, [eax+0ch] ; PEB_LDR_DATA

; prvi zapis u InitOrderModuleList LIST_ENTRY nizu.

mov eax, [eax+1ch]
mov eax, [eax+8] ; jest ntdll.dll
mov baza, eax
}
return ( baza );
}

//
// Uzima bazu kernel32.dll iz PEB bloka. Ista stvar. Opet forsiramo
// __forceinline.
//

__forceinline HMODULE UzmiBazuKernel32()
{
HMODULE baza;

__asm {
mov eax, dword ptr fs:[30h]
mov eax, dword ptr [eax+0ch]
mov eax, dword ptr [eax+1ch]

; drugi zapis jest kernel32.dll

mov eax, [eax]
mov eax, dword ptr [eax+08h]
mov baza, eax
}

return ( baza );
}


/*
Klasicno hashiranje z0mbievim algoritmom. LSD-ovci su u svom debilnom papiru o
win32 shellcode-u, vele, "testirali ga na nekoliko desetaka tisuca primjeraka
imena uobicajenih APU-ja i istvrdili da uopce ne daje kolizije"
. Mislim,
debilno. To je ipak z0mbie. Kralj. Za razliku od njih debila koji samo kradu
tudji rad i prezentiraju ga u fancy papirima da bi i ostali debili mogli skuzit
sto majstori rade. Inace asm implementaciju ovog algoritma koristi moja
load_dll_address fja koju cemo tek susresti u nastavku :)
*/


DWORD GetHash(LPSTR str)
{
DWORD h = 0; // hash algo (x) by z0mbie

while ( *str ) {
h = (h << 7) | (h >> 25); // rol h, 7 u asmu, C sux.
h ^= (BYTE) *str++;
}
return (h);
}

//
// Klasika, parsanje export tablice i racunajne hasha imena API-ja exportanih po
// imenu i usporedjivanje sa hashom. Tako to virusi rade ;)
//

PVOID MojGetProc(PVOID Baza, DWORD hash)
{
// uzmi ptr na DOS MZ zaglavlje

PIMAGE_DOS_HEADER mz_zaglavlje = PIMAGE_DOS_HEADER(Baza);

// uzmi ptr na NT zaglavlje

PIMAGE_NT_HEADERS nt_zaglavlje = PIMAGE_NT_HEADERS( LPSTR(Baza) +
mz_zaglavlje->e_lfanew );

// dodji do clana niza DataDirectory koji odgovara exportima
// (nulti clan inace)

PIMAGE_DATA_DIRECTORY export_direktorij =
nt_zaglavlje->OptionalHeader.DataDirectory +
IMAGE_DIRECTORY_ENTRY_EXPORT;

// uzmi informacije o tom direktoriju, velicinu i RVA na koju je mapiran

ULONG velicina = export_direktorij->Size;
ULONG adresa = export_direktorij->VirtualAddress;

// normaliziraj adresu

PIMAGE_EXPORT_DIRECTORY exporti = PIMAGE_EXPORT_DIRECTORY( PCHAR(Baza)
+ adresa );

PULONG funkcije = PULONG(PCHAR(Baza) + exporti->AddressOfFunctions);

PSHORT redni_brojevi = PSHORT( PCHAR(Baza) +
exporti->AddressOfNameOrdinals );

PULONG imena_fja = PULONG(PCHAR(Baza) + exporti->AddressOfNames);

PVOID adresa_apija = 0;

for (ULONG i = 0; i < exporti->NumberOfNames; i++) {

// iteriraj za svako ime exportanog API-ja

ULONG redni_broj = redni_brojevi[i];

if ( funkcije[redni_broj] < adresa ||
funkcije[redni_broj] >= adresa + velicina) {

// ako smo pronasli API koji trazimo po imenu,
// uzmi njegov RVA i normaliziraj ga.

if (GetHash(LPSTR(PCHAR(Baza) + imena_fja[i])) == hash)
adresa_apija = PCHAR(Baza) + funkcije[redni_broj];
}
}
return adresa_apija;
}

/*
Fja trazi adrese fja koje nam trebaju, te vraca FALSE ukoliko se bar jedna od
njih nije mogla naci. Hashe-ve nalazimo tako sto unutar koda kad debuggiramo
vidimo sto ce nam GetHashCode() vratiti za ime API-ja. Mogao sam staviti
direktno, ali bi se onda direktno unutar koda mogli vidjeti kojeg se API-ja
adresa trazi preko raw ACIIZ stringa koji je parametar za GetHashCode(), ovako
je bar malchice tezhe :) I ne trazi samo za NTDLL vec i za kernel32, naziv je
ostao zbog "povijesnih razloga" ;)
*/


BOOLEAN LocirajNTDLLFje()
{
if ( !(RtlDecompressBuffer = (NTSTATUS (__stdcall *)
( USHORT,
PUCHAR,
ULONG,
PUCHAR,
ULONG,
PULONG ))
MojGetProc( UzmiBazuNtdll(), 0x9daf495f )) ) {
return ( FALSE );
}

if ( !(fCreateFileA = (HANDLE (__stdcall *)
( LPCSTR,
DWORD,
DWORD,
LPSECURITY_ATTRIBUTES,
DWORD,
DWORD,
HANDLE ))
MojGetProc( UzmiBazuKernel32(), 0x08f8f114 )) ) {
return ( FALSE );
}

if ( !(fSetFilePointer = (DWORD (__stdcall *)
( HANDLE,
LONG,
PLONG,
DWORD ))
MojGetProc( UzmiBazuKernel32(), 0xef48e03a )) ) {
return ( FALSE );
}

if ( !(fSetEndOfFile = (BOOL (__stdcall *)(HANDLE))
MojGetProc( UzmiBazuKernel32(), 0x2d0d9d61 )) ) {
return (FALSE);
}

if ( !(fWinExec = (UINT (__stdcall *)
( LPCSTR,
UINT ))
MojGetProc( UzmiBazuKernel32(), 0xe8bf6dad )) ) {
return ( FALSE );
}

if ( !(fGetTempFileNameA = (UINT (__stdcall *)
( LPCSTR,
LPCSTR,
UINT,
LPSTR ))
MojGetProc( UzmiBazuKernel32(), 0x0fa4f502 )) ) {
return ( FALSE );
}

if ( !(fGetTempPathA = (DWORD (__stdcall *)
( DWORD,
LPSTR ))
MojGetProc( UzmiBazuKernel32(), 0x58fe7abe )) ) {
return ( FALSE );
}

if ( !(fGetFileSize = (DWORD (__stdcall *)
( HANDLE,
LPDWORD))
MojGetProc( UzmiBazuKernel32(), 0xaef7cbf1 )) ) {
return ( FALSE );
}

if ( !(fDeleteFileA = (BOOL (__stdcall *)(LPCSTR))
MojGetProc( UzmiBazuKernel32(), 0x81f0f0df )) ) {
return (FALSE);

else return ( TRUE );
}

//
// Otvara i mapira specificiranu datoteku u memoriju. Gotovo identicno prosloj
// implementaciji samo sto sad koristim f* fje te NtClose() umjesto
// CloseHandle() :)
//

BOOLEAN MapirajDatoteku( LPSTR ime,
DWORD velicina,
PHANDLE hFile,
PHANDLE hMem,
PVOID *pBaznaAdresa )
{
NTSTATUS status;
LARGE_INTEGER secSize;
HANDLE Handle = NULL;

*hFile = fCreateFileA( ime,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_ALWAYS,
0,
NULL );

if ( *hFile == INVALID_HANDLE_VALUE ) {
return (FALSE);
}

if ( velicina == 0 ) {
velicina = fGetFileSize( *hFile, NULL );
}

secSize.QuadPart = velicina;

status = NtCreateSection( hMem,
SECTION_ALL_ACCESS,
NULL,
&secSize,
PAGE_READWRITE,
SEC_COMMIT,
*hFile );

if ( status != STATUS_SUCCESS ) {
NtClose( hFile );
return ( FALSE );
}

// da ne bi Mm pomislio da treba mapirati neku nasu adresu..

*(DWORD*)pBaznaAdresa = NULL;

status = NtMapViewOfSection ( *hMem,
HANDLE (-1), // trenutni proces
pBaznaAdresa,
0,
velicina,
NULL,
&velicina,
ViewShare,
0,
PAGE_READWRITE );

if ( status != STATUS_SUCCESS ) {
NtClose( hMem );
NtClose( hFile );
return ( FALSE );
}

return ( TRUE );
}

//
// Dekompresira specificirani buffer te ga sprema u specificiranu datoteku. ovi
// _ASSERTE makroi ce nestati u Debug buildu, samo da napomenem ukoliko jos ne
// znate :>
//

BOOLEAN Dekompresiraj( USHORT CompressionEngine,
PVOID KompresiraniBuffer,
DWORD KompresiranaVelicina,
DWORD* KonacnaVelicina,
LPSTR ime )
{
DWORD NekompresiranaVelicina;
LPVOID NekompresiranBuffer = NULL;
NTSTATUS status;
HANDLE hFile;
HANDLE hMem;
LPVOID BaznaAdresa = NULL;

// max. moguca kompresija (92%)

NekompresiranaVelicina = DWORD( KompresiranaVelicina * 12.5 );

status = MapirajDatoteku( ime,
NekompresiranaVelicina,
&hFile,
&hMem,
&BaznaAdresa );

_ASSERTE ( status == TRUE );

status = RtlDecompressBuffer( CompressionEngine,
(PUCHAR)BaznaAdresa,
NekompresiranaVelicina,
(PUCHAR)KompresiraniBuffer,
KompresiranaVelicina,
KonacnaVelicina );

_ASSERTE ( status == STATUS_SUCCESS );

status = NtUnmapViewOfSection( (HANDLE)-1, BaznaAdresa );

_ASSERTE ( status == STATUS_SUCCESS );

NtClose( hMem );
fSetFilePointer( hFile, *KonacnaVelicina, 0, 0 );
fSetEndOfFile( hFile );
NtClose( hFile );

return (TRUE);
}

int _tmain( int argc, _TCHAR* argv[] )
{
DWORD konacna_velicina;
CHAR tempf[MAX_PATH];
CHAR tempp[MAX_PATH];

if ( !LocirajNTDLLFje() ) {
exit(1);
}

// Alociraj privremeno ime datoteke u temp direktoriju, sa prefiksom
// " #_", ala ce bit veselo ako netko ovo vidi u Task Manageru ;)

fGetTempPathA(MAX_PATH, tempp);
fGetTempFileNameA( tempp, " #_", 0, tempf );

// Dekompresiraj u nju i pokreni!

Dekompresiraj( COMPRESSION_FORMAT_LZNT1,
(PUCHAR)pakirana_datoteka,
pakirana_datoteka_size,
&konacna_velicina,
tempf );

fWinExec(tempf, SW_SHOWDEFAULT);

// brisi fajl da ga netko ne bi analizirao :)

fDeleteFileA( tempf );

return (0);
}

------------------------------ C O D E E N D -----------------------------

Jos je samo ostalo da objasnim na primjeru kako su direktno u asmu
implementirani nativni API-ji, kao sto je vec spomenuto, i kao sto mozete
vidjeti uostalom u import.h u prilozenom izvornom kodu, NtCreateSection() je
npr. implementiran ovako:

__forceinline NTSTATUS NTAPI NtCreateSection (
OUT PHANDLE SectionHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN PLARGE_INTEGER MaximumSize OPTIONAL,
IN ULONG SectionPageProtection,
IN ULONG AllocationAttributes,
IN HANDLE FileHandle OPTIONAL
)
{
__asm {
push FileHandle
push AllocationAttributes
push SectionPageProtection
push MaximumSize
push ObjectAttributes
push DesiredAccess
push SectionHandle

call $+26
mov eax, 32h
mov edx, esp
call $+5
pop ecx
mov word ptr [ecx+7], 340F0
push 0
call $-21

add esp, 7*4 + 4
}
}

Prije svega je vazno uociti __forceinline kojim kompajler *forsiramo* da
inline-ira ovu fju, tj. da *ne* generira stanardan kod za prolog i epilog, te da
ne stavlja parametre na stog umjesto nas. To nam je potrebno pije svega jer
prije poziva sysenter edx mora biti ptr na parametre push-ane na stog, pa ne
zelimo nista ostaviti slucaju sta kompajler misli dodavati izmedju dijela koda
kojim poziva neku nasu fju koja radi inline asm sysenter i dijela kad se to
stvarno dogadja. Dakle uglavnom za eliminiranje potencijalnih bugova, a bogami i
za ustedu prostora te da onome tko reversa temeljito oteza posao :)

Kao sto sam vec rekao, glupi inline asm ne prepoznaje sysenter instrukciju, pa u
letu patchamo onaj dummy "push 0", sa 340F0h sto predstavlja opkod za sysenter, te
osim toga call-ove pravimo relativno na location counter posto ne smijemo ni
labele koristiti. Ako nista drugo kod je jebeno l33t :)


///////////////////////////////////////////////////////////////////////////
--[ Phun with CLR
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\

Drugi je sloj zastite program napisan u C# i obsfukiran sa Xenocode 2005. Taj
program u C# je zanimljiv iz nekoliko razloga, jer demonstrira uporabu anti-*
trikova u managed kodu te kako ebmeddati asm kod u bilo koji managed jezik.

Krenimo od AntiDebug.cs datoteke te AntiDebug klase koja se u njoj nalazi.
Ta je klasa sealed i ima privatni konstruktor, zato jer su sve metode u njoj
staticke. Sadrzi sljedece metode: ChechBadDriverCreateFile()
CheckSysDebuggerCreateFile() te ProvjeriBpx(). Te fje koriste P/Invoke za
sljedeci win32 API: LoadLibrary(), FreeLibrary(), GetProcAddress(),
CloseHandle() te CreateFile(), te su jos u istoj toj regiji koda deklarirane
neke win32 konstante koji ti API-ji koriste. Inace, gomilu signatura za P/Invoke
mozete naci na [4].

CheckSysDebuggerCreateFile() metoda radi jaaako star i poznat trik za detekciju
sistemskih (kernel-mode debuggera). Trenutno postoje 3 (koliko ja znam):
SotfIce, TRW2000 (ovaj je samo za win9x) te Syser. Ovaj zadnji sam otkrio tek
prije 3 dana i izgleda da malo tko zna trenutno za njega. Inace, koliko sam ja
shvatio rade ga 2 kineza i cilj im je napraviti neku verziju super-softice.
Zasad im to i vise nego uspjeva jerbo syser ima GUI koji je za klasu ispred sice
-ovog, ima tabove, fine boje, i on prima pluginove i za razliku od softice, na
XP SP2 se, barem kod mene, pokazao savrseno stabilan pri trace-anju ring0 koda.
Mozete vise informacija vidjeti na ES forumu na temi [5], te tamo se nalazi
linkovi na direktan download.

Syser nisam dodao iz jednostavnog razloga jer ne znam kako bi ste drugacije
trace-ali ring0 kod, premda ima jos jedna forica ali nije sad vazno, i sto
sam ga otkrio tek nakon sto je ovaj sloj u cijelosti napisan. Ali dobro sad,
provjera za aktivnim SyserApp.exe bogznakakva kvantna fizika, a cini se da i
driver komponenta sjebe dosta stvari (noskrnl.exe se nekad zamijeni sa
kernel1.exe, nesto prcka i sa bazom IDT tablice i jos neke sitnice...).

/// <summary>
/// Provjerava da li je SoftIce/TRW2000 driver loadan preko CreateFileA() trika.
/// </summary>
public static bool CheckSysDebuggerCreateFile()
{
string[] bad_drivers = { @"\\.\SICE", @"\.\SICE", @"\\.\NTICE", @"\.\NTICE", @"\\.\TRW", @"\\.\TRWDEBUG" };

foreach ( string device in bad_drivers ) {
int hmod = CreateFile(device, // File name
GENERIC_READ | GENERIC_WRITE, // Desired access
FILE_SHARE_READ | FILE_SHARE_WRITE, // Share mode
( IntPtr )0, // Attributes
OPEN_EXISTING, // Creation disposition
FILE_ATTRIBUTE_NORMAL, // Flags and attributes
( IntPtr )0); // Template file

if ( hmod != INVALID_HANDLE_VALUE ) {
CloseHandle(hmod);
return true;
}
}

return false;
}

Dakle, u bad_drivers spremamo stringove imena drivera sistemskih debuggera, te
ih odreda pokusavamo otvoriti sa CreateFileA(), te ako uspijemo, tada je jedan
od njih aktivan, tj. ne dopusta "exclusive" mod pristupa. "\\.\SICE" je verzija
za win9x, "\\.\NTICE" za NT-bazirane win. TRW2000 radi samo na win9x pa ima samo
jednu verziju :>

ChechBadDriverCreateFile() fja radi slicnu stvar, ali za drivere koji koriste
popularni alaitchi regmon, filemon te icedump tool koji sluzi za dump memorije
u datoteku (ne zelimo spijune :)

public static bool ChechBadDriverCreateFile()
{
string[] bad_drivers = { @"\\.\FILEMON", @"\\.\REGMON", @"\\.\ICEDUMP" };

foreach ( string device in bad_drivers ) {
int hmod = CreateFile(device, // File name
GENERIC_READ | GENERIC_WRITE, // Desired access
FILE_SHARE_READ | FILE_SHARE_WRITE, // Share mode
( IntPtr )0, // Attributes
OPEN_EXISTING, // Creation disposition
FILE_ATTRIBUTE_NORMAL, // Flags and attributes
( IntPtr )0); // Template file

if ( hmod != INVALID_HANDLE_VALUE ) {
CloseHandle(hmod);
return true;
}
}
return false;
}

Fja ProvjeriBpx() radi sto joj i ime kaze, provjerava breakpoint na zadanom
API-ju koji se nalazi u zadanom DLL-u. Prvo se zadani DLL mapira u nas adresni
prostor, te nakon toga u temp_buf kopiramo prvih 13 bajtova sa adrese API-ja te
provjeravamo da li se na nekom od njih nalazi 0xCC, tj. opkod za "int 3" koji
debuggeri umecu na dio koda na kojem postavljas breakpoint.

public static bool ProvjeriBpx(string ImeDLL, string API)
{
bool ima_bpx = false;
byte[] temp_buf = new byte[13];

IntPtr hLib = LoadLibrary(ImeDLL);
IntPtr adresa_API = GetProcAddress(hLib, API);

if ( hLib.ToInt32() == 0 || adresa_API.ToInt32() == 0 ) {
System.Diagnostics.Debugger.Log(0, "1",
String.Format(CultureInfo.InvariantCulture, "badAPICall: ", API, ImeDLL));
return false;
}

Marshal.Copy(adresa_API, temp_buf, 0, temp_buf.Length);

for ( int i = 0; i < temp_buf.Length - 1; i++ )
if ( temp_buf[i] == 0xCC ) ima_bpx = true;

FreeLibrary(hLib);
return ima_bpx;
}

Krenimo sad na Crackme.cs gdje je glavna akcija. Sad ide prvo malo teorije o
jednoj jako kul stvarchici - inline asm u C#. Nazalost nije to pravi inline
asm, niti C# podrzava inline MSIL, vec jedan dirty hack :>

///////////////////////////////////////////////////////////////////////////
--[ Tko kaze da nema asemblera u .NET-u?
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\

Delegati su u C# tipovi izvedeni iz System.MulticastDelegate i predstavljaju
type-safe verziju C-style pokazivaca na funkcije.Pri deklaraciji delegata
nemoguce je specificirati njihovu implementaciju (moguce je u MSIL, premda ne u
C# direktno), drugim rijecima pruza je sam CLR. U biti oni za sam koncept CLR-a
predstavljaju neku apstrakciju standardnih fja koje pozivaju neke druge fje.

Pri deklaraciji recimo sljedeceg delegata:

private delegate uint DummyDelegat(uint a);

C# kompajler generira klasu koja je izvedena iz System.MulticastDelagate sa
metodama .ctor (defaultni konstruktor), BeginInvoke, Invoke i EndInvoke, MSIL
disasm preko .NET Reflectora izgleda ovako:

.class nested private auto ansi sealed DummyDelegat
extends [mscorlib]System.MulticastDelegate
{
.method public hidebysig specialname rtspecialname instance void .ctor(object 'object', native int 'method') runtime managed
{
}

.method public hidebysig newslot virtual instance [mscorlib]System.IAsyncResult BeginInvoke(unsigned int32 a,
[mscorlib]System.AsyncCallback callback, object 'object') runtime managed
{
}

.method public hidebysig newslot virtual instance unsigned int32 EndInvoke([mscorlib]System.IAsyncResult result) runtime managed
{
}

.method public hidebysig newslot virtual instance unsigned int32 Invoke(unsigned int32 a) runtime managed
{
}

}

Ove su metode "runtime managed", sto znaci da je CLR odgovoran za njihovu
implementaciju. Mada neki misle da je nemoguce, MOGUCE je pruziti njihovu
vlastitu implementaciju u MSIL tako sto cemo "runtime managed" dijelove
promijeniti u "cil managed" (CIL = Common Intermediate Language, po ECMA
standardu, MSIL je MS-ova implementacija istog). Recimo ovako:

.method public hidebysig newslot virtual instance unsigned int32 Invoke(unsigned int32 a) cil managed
{
ldstr "Pozdrav iz delegata!"
call void [mscorlib]System.Console::WriteLine(string)
ret
}

Stoga, ako ne volite ove "runtime managed" crne kutije, uvijek mozete pruziti
vlastitu implementaciju. Doduse, nece proci PEVerfiy test, a ovaj hack ne radi
ni recimo pod mono-om. Ko ga jebe, glavno da radi na .NET ;)

E sad se vi pitate sto je tako zanimljivo u vezi delegata i koji ih ja jebeni
kurac spominjem. E pa spominjem ih jebeno zato, sto cemo iskoristiti jebeni
nacin na koji su jebeni delegati implementirani, da izvedemo asm kod iz C#
direktno, bez pozivanja jebenih vanjskih unmanaged win32 API-ja.

Pokrenimo .NET reflector i napravimo RCE od System.Delegate. Uocimo pri dnu
polja:

...

// Privatna polja
private RuntimeMethodInfo _method;
private IntPtr _methodPtr;
private IntPtr _methodPtrAux;
private object _target;
}

Logicno bi bilo zakljuciti da _methodPtr i _methodPtrAux pokazuju na metodu koju
instancirani delegat enkapsulira, sto ce se na kraju i pokazati i kao istina :>

Nakon malo prckanja po Rotoru [6] i malo disasma dosao sam do sljedeceg
zakljucka: U slucaju da je staticka metoda proslijedjena delegatu kao parametar,
methodPtrAux bi sadrzavalo ono sto bi inace sadrzavalo _methodPtr polje, a sam
_methodPtr pokazuje na mali stub koji mice "this" pokazivac prije nego skace na
_methodPtrAux. Dakle, kontroliranjem ta 2 polja (u slucaju da se radi o
statickoj

metodi, posve je svejedno kojeg), mozemo napraviti redirekciju  
_methodPtr na neku proizvoljnu lokaciju, recimo hrpu bajtova koji predstavljaju
neki nas nasty asm kod.

Izgleda da CLR interno koristi __fastcall konvenciju (koju recimo po defaultu
koristi bcc32), sto ce reci da su parametri proslijedjeni kroz registre (redom
edx, ecx..), zato napravimo ovakav kod:

static byte[] asm_kod = {
0xCC, // int 3
0x89,0xD0, // mov eax, edx
0xC3 // retn
};

Dakle nekakva fja koja prima jedan parametar (u edx), vraca ga u eax, a prije
toga sam stavio int3 kako bismo mogli sve to pratiti u olly-u :>

Nadalje, buduci da cemo raditi sa unsafe kodom, bilo bi korisno (a i moramo jer
ce inace C# kompajler prijaviti gresku!) dodijeljivanje adrese ovog koda nekom
proizvoljnom pokazivacu staviti pod "fixed" komandom, koja sprecava GC da
napravi relokaciju buffera ciju adresu uzimamo, kao rezultat neke interne
"optimizacije" izgleda memorije (.NET GC je generacijski, pa je cak ova
situacija vrlo izgledna!) da bi sprijecio njezinu fragmentaciju.


Dakle, ovako nekako:

fixed ( byte* adresa = asm_kod )
{
...
}

Unutar tijela fixed naredbe GC *sigurno* nece relocirati asm_kod[] buffer!

Da bismo napravili redirekciju _methodPtrAux/_methodPtr polja morat cemo
koristiti mehanizme refleksije:

// Uzmi enkapsulaciju tipa u varijablu

Type delegat = typeof(System.Delegate);

// Uzmi FieldInfo strukturu koja sluzi za opis nekog polja (field) tipa,
// umjesto _methodPtrAux moze biti i _methodPtr posto je DummyMetoda
// staticka! NonPublic zastavica posto su ta polja privratna i Instance
// posto nisu staticka!

FieldInfo MethodPtr = delegat.GetField("_methodPtrAux", BindingFlags.NonPublic | BindingFlags.Instance);

// stvorimo delegat oko neke dummy metode koja nista ne radi

DummyDelegat d = new DummyDelegat(DummyMetoda);

// bejbe!

MethodPtr.SetValue(d, ( IntPtr )adresa);


Ova dummy metoda moze raditi bilo sta, vazno je samo da prima 1 argument koji
u asm kodu citamo iz edx:

static uint DummyMetoda(uint a)
{
return 0;
}

E sad, cijeli program bi izgledao ovako nekako:

namespace asm_in_cis
{
class Glavna
{
delegate uint DummyDelegat(uint a);
static uint DummyMetoda(uint a)
{
return 0;
}

static byte[] asm_kod = {
0xCC, // int 3
0x89,0xD0, // mov eax, edx
0xC3 // retn
};

unsafe static void Main(string[] args)
{
fixed ( byte* adresa = asm_kod ) {
Type delegat = typeof(System.Delegate);
FieldInfo MethodPtr = delegat.GetField("_methodPtrAux", BindingFlags.NonPublic | BindingFlags.Instance);
DummyDelegat d = new DummyDelegat(DummyMetoda);
MethodPtr.SetValue(d, ( IntPtr )adresa);
Console.WriteLine(d(5));
}
}
}
}

Pri cemu dodajemo poziv Console::WriteLine() jer ce inace cijeli Main() biti
dead code koji ce kompajler optimizirati tako sto ce ga maknuti :)

Sad mozemo ovako kompajliran program otvoriti pod olly-em, i vidjeti gdje cemo
"sletjeti":

00B12BFF 00CC ADD AH,CL
00B12C01 89D0 MOV EAX,EDX
00B12C03 C3 RETN

Dakle, JIT-er je kod kompajlirao negdje na hrpi i direktno ga pozvao. Na ovaj
nacin mozemo cisto iz nekog managed jezika pozivati asm kod, bez potrebe
pozivanja nekih unmanaged API-ja tipa CreateThread().

Sta sve mozemo na ovaj nacin? Pa prije svega, mozemo u kod embeddati sto god
hocemo, a sto je prije napisano u asm-u na offset-independant nacin, konkretno
mislim na virus :) Recimo, virus bi mogao ildasm-ati neki .NET asemblij, ubaciti
ovih 10 linija koda na proizvoljnu lokaciju za koju zna da ce se pozvati (recimo
prije Main), pri cemu ce izvedeni kod biti tijelo virusa napisanog u win32asm.
Nadalje, moguca je i direktna infekcija bez ildasm-anja tako sto ce se patchati
u letu neki MSIL opkodi te napraviti redirekcija na dodano tijelo virusa...ili
ako je tijelo virusa preveliko, napravimo u asm-u stub koji ce potraziti tijelo
virusa append-ano na asemblij, mapirati ga na proizvoljnu lokaciju u memoriji i
opaliti CreateThread()....mogucnosti su u glavnom nebrojene.

Jedini je zajeb sto moramo prvo testirati kod u odvojenom asembleru pa ga onda
dodati u C# kod koristeci recimo z0mbijev HAXOR tool.

Nadalje, ovime su otvorene nebrojene mogucnosti u internom manipuliranju CLR-om.
Ideje poput ".NET globalne rezidentnosti", "infekcije GAC-a u letu" i sl. bi
ubrzo mogle postati stvarnost.

A sad se vratimo na Crackme.cs. Krenimo od Main() metode. TESTING define (a la
C-style pretprocesor, mada C# nema takvu vrstu pretprocesora) koristi za debug
svrhu te onemogucava anti-debug dijelove crackme-ja.

unsafe static void Main(string[] args)
{

#if !TESTING

Sad ide dio koda kojim provjeravamo da li se izvodimo pod VM ili je VM jedan od
aktivnih "pravih"/virtualnih mreznih adaptera. Naime, ima jedna kul forica, a to
je da su i VMWare i VirtualPC (MS) morali registirati prva 24 bita
(vendor-defined) MAC adrese mreznih adaptera koje "stvaraju"/emuliraju, te tu
cinjenicu koristimo protiv njih :) U [7] mozete vidjeti po kompanijama koje su
MAC adrese pod kojom kompanijom registirane. Nas konkretno zanimaju 2
najpopularnije virtualne masine na win, a to su VMWare i VirtualPC:

IEEE registrirane MAC adrese za VMware su:

00:0C:29:XX:XX:XX
00:05:69:XX:XX:XX
00:50:56:XX:XX:XX

a za Microsoft (valjda za VirtualPC, nisam isprobao doduse..jerbo VMware rox :)

00:0D:3A:XX:XX:XX
00:12:5A:XX:XX:XX
00:50:F2:XX:XX:XX
00:03:FF:XX:XX:XX

Ta se inace prva 3 bajta zovu OUI (Organizationally Unique Identifier) te ih
dodijeljuje IANA (Internet Assigned Numbers Authority) i publiciraju se u RFC
dokumentima (RFC 1700).

E sad, maske za ove adrese sve strpavamo u polje stringova bad_mac[] te redom
enumeriramo sve mrezne adapter koji imaju postavljenu IP adresu (sl. kao output
komande ipconfig), te u tu svrhu koristimo WMI koji je pod .NET-om izlozen preko
System.Management namespece-a.

Za enumeriranje se koriste SQL-like upiti, a za tocan raspored u WMI
hijerarhiji mozete vidjeti u WMI Object Browseru [8]. Nas konkrentno zanima
Win32_NetworkAdapterConfiguration klasa koja ima svojstvo IPEnabled postavljeno
na TRUE, tj. mrezni adapter sa dodijeljenom IP adresom, pa koristimo sljedeci
upit:

"SELECT * FROM Win32_NetworkAdapterConfiguration WHERE IPEnabled = 'TRUE'"

Nadalje, "IPAddress" svojstvo sadrzi listu IP adresa dodijeljenih adapteru, te
njih sve skupa, za sve adaptere ubacujemo u kolekciju tipa ArrayList, te nakon
toga obradjujemo:

foreach ( string s in adrese_svih_adaptera ) {
IPHostEntry hostInfo = Dns.GetHostByAddress(s);
foreach ( IPAddress ip in hostInfo.AddressList ) {
byte[] ab = new byte[6];
int len = ab.Length;

// izracunaj fizicku adresu

int r = SendARP(( int )ip.Address, 0, ab, ref len);
string mac = BitConverter.ToString(ab, 0, 6);
foreach (string bad_mac_maska in bad_mac) {
if ( mac.StartsWith(bad_mac_maska) )
Process.GetCurrentProcess().Kill();
}
}
}

Fizicku smo adresu takodjer mogli uzeti iz MAC objekta, ali nju je vrlo lako
spoofati na razini samog WMI, pa zato koristi P/Invoke-ani iphlpapi SendARP()
koji na zeljenu adresu salje ARP zahtijeve te vraca MAC adresu primatelja u
pMacAddr:

DWORD SendARP(
IPAddr DestIP,
IPAddr SrcIP,
PULONG pMacAddr,
PULONG PhyAddrLen
);

Nakon toga nas zanimaju vendor-specific prvih 24 bita te rezultat pretvaramo u
string te u petlji kompariramo sa "zlocestim" MAC adresama u bad_mac_maska nizu.
Ukoliko je u njoj, koljemo aktivni proces od crackme-a.

Nakon toga koristimo provjeru u registry-u za VMWare/VirtualPC-specific
kljucevima:

// Provjera za VirtualPC
RegistryKey rk = Registry.ClassesRoot.OpenSubKey("Virtual.Machine.VMC");
if ( rk != null ) {
rk.Close();
}

// Provjera za VMware
rk = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\VMware, Inc.");
if ( rk != null ) {
rk.Close();
Process.GetCurrentProcess().Kill();
}

if (System.Diagnostics.Debugger.IsAttached == true )
Process.GetCurrentProcess().Kill();

Ovi su kljucevi pristuni i na stvarnom i na emuliranom OS-u.

Nakon toga radimo pozivamo anti-dump kod:

fixed ( byte* adresa = AntiProcDump ) {
Type delegat = typeof(System.Delegate);
FieldInfo MethodPtr = delegat.GetField("_methodPtrAux", BindingFlags.NonPublic | BindingFlags.Instance);
DummyDelegat AntiDump = new DummyDelegat(DummyMetoda);
MethodPtr.SetValue(AntiDump, ( IntPtr )adresa);
AntiDump(0);
}

Pri cemu se u AntiProcDump polju bajtova koje izvodimo nalazi sljedeci asm kod:

mov eax, fs:[TEB_PEB]
mov eax, [eax.PEB_PebLdrData]
mov eax, [eax+0Ch]
add dword ptr [eax+20h], 3137h
xor eax, eax
retn

tj. povecavamo svoju virtualnu velicinu u LDR_MODULE strukturi polje SizeOfImage
za 3137h pri cemu ce dumpanje programa sa nekim externim alaticem a la procdump
uzrokovati page fault, jer ce program dumpati dijelove memorije koje ne postoje (a koje misli da su alocirane od strane programa). LDR_MODULE je interna
struktura koju inace koristi NT Loader i nije direktno dostupa preko win32API,
pa ce svi dumper koji ga koriste pogresno iscitati stvarnu velicinu image-a
datoteke.

Nakon toga se poziva ClearDebuggerFlag blok koda koji resetira u PEB bloku
DebuggerPresent polje, a sto opet ni nema nekog smisla :), samo sluzi za
zbunjivanje crackera... :> Taj se dio koda naime poziva ovako:

fixed ( byte* adresa = ClearDebuggerFlag ) {
Type delegat = typeof(System.Delegate);
FieldInfo MethodPtr = delegat.GetField("_methodPtrAux", BindingFlags.NonPublic | BindingFlags.Instance);
DummyDelegat ClearDebugFlag = new DummyDelegat(DummyMetoda);
MethodPtr.SetValue(ClearDebugFlag, ( IntPtr )adresa);

for (int i = 0; i < 256; i++) {
uint k = (uint)(i * 256 / Math.PI);
k ^= (uint)AppDomain.CurrentDomain.GetHashCode();
k += (uint)Thread.CurrentThread.GetHashCode();
ClearDebugFlag((uint)(k-i));
ClearDebugFlag((uint)(k + i));
ClearDebugFlag((uint)(k * i));

byte[] heh = new byte[ClearDebuggerFlag.Length];
for ( int j = 0; j < heh.Length; j++ )
heh[j] = ClearDebuggerFlag[j];

DummyDelegat ClearDebugFlag2 = new DummyDelegat(DummyMetoda);
fixed ( byte* adresa2 = heh ) {
MethodPtr.SetValue(ClearDebugFlag2, ( IntPtr )adresa2);
ClearDebugFlag2(( uint )( k * Math.Pow(Math.E, Math.PI) ));
}
}


uint k je cisto dummy i sluzi za tipicnu obsfukaciju, bas kao i operacije u
petlji sa i. byte[] polje h se alocira dinamicki te se u njega kopira
ClearDebuggerFlag bajt po bajt, opet se alocira delegat i poziva...sve ovo sluzi
za zbunjivanje crackera te da ga odvrati od primarne svrhe programa.

Nakon toga kreiramo nit u kojoj ce se pozivati anti-trace kod te izlazimo iz
fixed bloka:


Thread at = new Thread(new ThreadStart(AntiTrace));
at.Priority = ThreadPriority.Normal;
at.Start();
}

Pogledajmo sto se u toj statickoj metodi krije:

static void AntiTrace()
{

I ova metoda sadrzi gomilu koda kojem je svrha cista obsfukacija :) Prvo
inicijaliziramo CSP koji koristimo za generiranje kriptografski-sigurnih
pseudoslucajnih brojeva:

RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
byte[] b = new byte[1];

Sad dolazi dio koda u kojoj testiramo da li je attachan CLR debugger, te je,
normalno, taj dio koda *izvan* TESTING define-a, kako bismo mogli debuggirati
nas vlastiti kod :) Ovo je managed verzija IsDebuggerPresent() API-ja!

Inace, standardno se kod moze debuggirati ili pod Visual Studiom, ili pod
CLR debuggerom kojeg mozete naci u:

X:\Program Files\Microsoft.NET\SDK\v1.1\GuiDebug\DbgCLR.exe

Ukoliko imate .NET SDK [9] instaliran u taj direktorij, te ce naveden
primjerak koda uspjesno detektirati debugging pod njim:


#if !TESTING
if (System.Diagnostics.Debugger.IsAttached == true )
Process.GetCurrentProcess().Kill();
#endif

E sad u beskonacnoj petlji radimo sljedece: ucitavamo pseudoslucajan broj te iz
njega izvlacimo broj novih niti koje stvaramo, od 0 do 16. Nove niti imaju
entrypint u metodi AntiTrace::ProvjeriTrace i AntiTrace::ProvjeriTrace2 metodama
koje cemo objasniti u nastavku. Njihove ThreadStart delegate spremamo u polje,
te pri startu njihov prioritet postavljamo na ThreadPriority.BelowNormal kako ne
bi opteretili previse izvodjenje primarnih niti. Nakon toga odspavamo trenutnu
nit na slucajan broj milisekundi, te cekamo u petlji da sve od novostvorenih
niti zavrse (napravimo Join()), te ukoliko su nekim cudom jos zive (IsAlive),
abortiramo ih.

Nakon toga sve to stvaranje i gasenje niti ponavljamo dokle god je program
aktivan. L00dilo za craxore jerbo je multithreaded programe (pogotovo ove sa
dvoznamenkastim brojem niti :) uzhasno tesko trace-ati normalno, i raumjeti
kako tocno funkcioniraju..

while ( true ) {
rng.GetBytes(b);
int niti_broj = b[0] % 16;
Thread[] niti = new Thread[niti_broj];
ThreadStart[] ts = new ThreadStart[niti_broj];
AntiTrace at = new AntiTrace();

for ( int p = 0; p < niti_broj; p++ ) {
ts[p] = new ThreadStart(at.ProvjeriTrace);
ts[p] += new ThreadStart(at.ProvjeriTrace2);
niti[p] = new Thread(ts[p]);
niti[p].Priority = ThreadPriority.BelowNormal;
niti[p].Start();
}
Thread.Sleep(b[0] * 31337 % 10000);
for ( int p = 0; p < niti_broj; p++ ) {
niti[p].Join();
if ( niti[p].IsAlive == true ) {
niti[p].Abort();
}
}
}
}

Sto se AntiTrace klase i dviju metoda unutar njih (ProvjeriTrace() i
ProvjeriTrace2()), one koriste 2 asm snippeta: jedan koji radi AntiProcDump (ovo
u slucaju da craxor rucno podesi ovo polje :) i jedan koji provjerava za trace
koda (GetTracedStatus). Ovaj prvi je vec objasnjen, pogledajmo sad kako radi
GetTracedStatus (antitrace.asm u izvornom kodu):

"$TRAP_TEST_START$" je marker za HAXOR tool kojim dobivam dump dijela koda
oznacenog pocetnim i zavrsnim markerom kao CPP-style BYTE niz (ima opcije i za
pascal, asm.. imam i ja svoj alatich za C#, ali otom potom :)

start: jmp __1
db '$TRAP_TEST_START$'

Nakon toga izracunavamo delta-offset - sjetite se, ovo ce se izvoditi JIT-ano
negdje na hrpi, vjerojatno na drugacijoj lokaciji u memoriji svaki put kad je
pokrenuto.. zato trebamo koristiti delta-offset za svako apsolutno
referenciranje!

__1: pusha
call $+5
trap_delta: pop ebx

_tp equ <-trap_delta[ebx]>


Sad izracunavamo offset varijable _traced koja je dio koda, te je @ runtime
patchamo u kodu gdje se ona koristi koristeci trik sa modifikacijom LC-a. Nakon
toga pozivamo dio koda __trap koji postavlja trap flag unutar SEH-a, te kao
prvi SEH handler postavlja dio koda ispod poziva __trap labele. Nakon toga se
postavlja TF (bit 8) u EFLAGS registru, sto ce uzrokovati iznimku
EXCEPTION_SINGLE_STEP koju ce postavljen SEH handler uhvatiti te inkrementirati
varijablu _traced. E sad, postavljanje TF ce sjebati manje-vise sve userland
debuggere (probajte u ollyu trace-ati ovaj snippet koda :), dok ce ga kernel-
mode (sice, syser) jednostavno ignorirati, tj. EH se uopce nece pozivati vec ce
kod jednostavno nastaviti se izvoditi dalje, kao da se nije nista dogodilo. Nakon
toga jednostavno vracamo stari EH, pop-amo nazad sve registre da ne bismo nesto
sjebali te vracamo sadrzaj varijable _traced kao rezultat fje u eax. Tj. ovaj
snippet koda vraca TRUE ukoliko je trace-an, FALSE ukoliko nije.

pusha
lea eax, _traced _tp
mov _traced_off _tp, eax
xor eax, eax
call __trap

mov eax, 12345678h
_traced_off equ $-4
inc byte ptr [eax]
xor eax, eax
retn

__trap: push dword ptr fs:[eax]
mov fs:[eax], esp

pushf
or byte ptr [esp+1], 1
popf
nop

xor eax, eax
pop dword ptr fs:[eax]
pop ebx

; poravnaj stog ukoliko je doslo do iznimke, inace makni povratnu adresu

popa
and [esp.Pushad_eax], 0
dec (_traced _tp).byte ptr 0
js _traceani_smo

popa
retn

_traceani_smo: inc [esp.Pushad_eax]
popa
retn

_traced db 0

Sad kad ovo znamo, pogledajmo sto se krije u ProvjeriTrace() metodi:

unsafe public void ProvjeriTrace()
{
fixed ( byte* adresa = AntiProcDump ) {
Type delegat = typeof(System.Delegate);
FieldInfo MethodPtr = delegat.GetField("_methodPtrAux", BindingFlags.NonPublic | BindingFlags.Instance);
DummyDelegat AntiDump = new DummyDelegat(DummyMetoda);
MethodPtr.SetValue(AntiDump, ( IntPtr )adresa);
AntiDump(0);
}

U pocetku prvo pozivamo samo AntiProcDump kod, nista posebno.

lock ( this ) {
fixed ( byte* adresa = GetTracedStatus ) {
Type delegat = typeof(System.Delegate);
FieldInfo MethodPtr = delegat.GetField("_methodPtrAux", BindingFlags.NonPublic | BindingFlags.Instance);
DummyDelegat GetTraced = new DummyDelegat(DummyMetoda);
MethodPtr.SetValue(GetTraced, ( IntPtr )adresa);

for ( uint k = 0; k < 0x1337; k++ ) {
if ( GetTraced(k) == 1 )
AppDomain.Unload(AppDomain.CurrentDomain);
}
}
}
}

Nakon toga radimo lock nad cijelom metodom. E sad, buduci da koristimo gomilu
niti koje dijele dosta koda (prije svega mislim na onaj embedded asm kod, tj.
delegati koji pokazuju na isti kod u memoriji!), bilo bi lijepo da ih lockamo.
Mada ja to bas i ne radim :) Ali lijepo je recimo za crackera da vidi kako ce
JIT-er stvoriti "lock cmpxchg" instrukcije na pocetku i kraju bloka koji je
lockan, a koje izmjenjuju/stavljaju ID trenutne niti u SyncBlock indeks za lock-
ani blok koda. U slucaju detekcije trace-anja unloadamo svoj AppDomain (tj.
gasimo svoj proces u win32 nomenklaturi :)

ProvjeriTrace2() fja takodjer ne sadrzi nista novo, vec samo sluzi kao ispomoc
jer konstantno provjerava attachanje managed debuggera te u petlji poziva kod za
detekciju trace-anja. Na taj nacin ce cracker, ukoliko zeli sprijeciti samo-
gasenje programa, trebati bpx-ati i GetCurrentProcess().Kill() koji poziva
TerminateProcess() te dio za unloadanje trenutnog AppDomain koji je totalno
managed, te se ne moze na jednostavan nacin bpx-ati. Uglavnom, puno zabave za
craxore :)

unsafe public void ProvjeriTrace2()
{

#if !TESTING
if (System.Diagnostics.Debugger.IsAttached == true )
Process.GetCurrentProcess().Kill();
#endif
byte[] heh = new byte[GetTracedStatus.Length];
for ( int j = 0; j < heh.Length; j++ )
heh[j] = GetTracedStatus[j];

fixed ( byte* adresa = heh ) {
Type delegat = typeof(System.Delegate);
FieldInfo MethodPtr = delegat.GetField("_methodPtrAux", BindingFlags.NonPublic | BindingFlags.Instance);
DummyDelegat GetTraced = new DummyDelegat(DummyMetoda);
MethodPtr.SetValue(GetTraced, ( IntPtr )adresa);

for ( uint i = 0; i < 0x1337; i++ ) {
if ( GetTraced(i) == 1 )
Process.GetCurrentProcess().Kill();
}
}
}

Vratimo se sad na glavnu nit programa. Nakon toga se poziva ogromni blok koda u
BrainfuckNaClipboardu polju kojeg cemo nasnije detaljno obradit, te se glavna
nit uspava na 2s koliko je (u slucaju da imate Pentium 233) potrebno u nekom
najgorem ludom slucaju asm kodu u njemu da se izvede, te se trenutni proces
terminira:

fixed ( byte* adresa = BrainfuckNaClipboardu ) {
Type delegat = typeof(System.Delegate);
FieldInfo MethodPtr = delegat.GetField("_methodPtrAux", BindingFlags.NonPublic | BindingFlags.Instance);
DummyDelegat BrainfuckPhun = new DummyDelegat(DummyMetoda);
MethodPtr.SetValue(BrainfuckPhun, ( IntPtr )adresa);
BrainfuckPhun(0);
}

Thread.Sleep(2000); // 2s za obradu clipboarda, nakon toga se gasimo...
Process.GetCurrentProcess().Kill();
while ( true ) { } ;
}

U slucaju da je cracker lukav pa stavi da TerminateProcess() se jednostavno vraca
(tj. ne radi nista), dodao sam jednu zabavnu beskonacnu petlju koja ce ga jako
obradovati :) Iskreno ce mu biti JEBENO razluciti je li se proces killa sam zbog
detekcije nekog anti-alata, ili zbog loseg inputa..

///////////////////////////////////////////////////////////////////////////
--[ asm i Nt*
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\


Treci sloj zastite cini sam asm kod spremljen u BrainfuckNaClipboardu polju, te
rl.asm datoteka u prilozenom izvornom kodu. U biti se u toj jednoj
datoteci nalaze i stage 3 i stage 4 slojevi zastite.

Krenimo redom analizirati sadrzaj te datoteke.

Prije pocetka je vazno napomenuti 2 cinjenice

1) Ne ocekuje se izlazak nakon pozivanja ovog koda. Ovaj fja poziva gomilu
API-ja te nije bez razloga stavljena na sam kraj Program.cs. Rezultat ove fje
moze biti zastopanje programa (prije nego se sam ugasi nakon defaultnog timeouta
od 2 s), moze biti BSOD sa statusnim kodom 0xbadc0de koji inicira pogresan
input, te moze biti dobar BSOD cija ce lista statusnog koda i parametara biti
kombinacija ASCII kodova stringa: "challenge completed!".

2) Ovaj kod mora biti offset-independent, te sam minimizirao koristenje globanih
varijabli te pokusao lokalno natrpati koliko sam god mogao (zato je main
definiran kao proc btw, te minimizirati koristenje nenativnih API-ja tj.
pozivanje load_dll_address procedure te minimizirati uporabu iz istih razloga
makroa @get_delta.

Krenimo ispocetka od samog koda unutar proca main:

@build_UNICODE_STRING <\Device\PhysicalMemory>

@build_UNICODE_STRING je makro koji sam napisao jer me je jako izivciralo
rucno inicijaliziranje UNICODE_STRING strukture te pozivanje
RtlInitUnicodeString() API-ja (btw evo moje verzije tog API-ja koji se jako
cesto koristi za inicijaliziranje te strukture:

; rippano iz ntdll.dll i malchice optimizirano :>
; Argumenti:
; DestinationString - ptr na UNICODE_STRING strukturu koju inicijaliziramo
; SourceString - ptr na UNICODE string kojim inicijaliziramo

RtlInitUnicodeString proc c uses edi

arg DestinationString
arg SourceString

mov edi, SourceString
mov edx, DestinationString
and dword ptr [edx], 0
mov [edx.UNICODE_STR_Buffer], edi
test edi, edi
jz __exit

or ecx, 0FFFFFFFFh
xor eax, eax
repnz scasw
not ecx
shl ecx, 1
cmp ecx, 0FFFEh
jbe __1
mov ecx, 0FFFEh

__1: mov [edx+2], cx
dec ecx
dec ecx
mov [edx], cx

__exit: ret ; ne zaboravi generirati jebeni epilog
RtlInitUnicodeString endp

Svi nativni API-ji koriste UNICODE verzije stringova, tj. cijeli NT kernel je
interno unicode-baziran kad se dodje do imena resursa i stringova. Prakticno
cijeli win32 API jest transformacija Ansi verzija u Wide + malo provjera
validnosti parametara & housekeeping nekih cacheiranih win32 struktura + poziv
nativnih API-ja. Zato se naviknite na jebene UNICODE_STRING ASAP.

UNICODE_STRING struc
UNICODE_STR_Length dw ?
UNICODE_STR_MaximumLength dw ?
UNICODE_STR_Buffer dd ?
UNICODE_STRING ends

A sam moj makro radi sljedece:

; o: eax = ptr na UNICODE_STRING
; eax+4 = ptr na raw unicode string

@build_UNICODE_STRING macro p1, p2
local @l, @m, @n, @p

errifnb <p2> "Too much arguments in macro @build_unicode_str."

push edi

call @l
@m: irpc c, <p1>
db "&c", 0h
endm
@n equ $ - @m
@l: pop edi
call @p

dw @n
dw @n + 2
dd 0

@p: pop eax
mov [eax+4], edi ; patchaj offset na raw string

pop edi
endm

Dakle sto sve radi: prvo provjeravamo da li je kreten pozvao makro sa viskom
parametara (*UVIJEK* radite kad pisete makroe provjere viska parametara jer
tasm32 puno stvari nekad zna pretpostaviti, sto zna dovesti do jako ruznih
bugova), nakon toga spremamo edi te iteriramo po karakterima prvog parametra te
ih dumpamo kao WORD, s tim da prvo ide byte dump karaktera pa onda binarna nula.

E sad imamo raw UNICODE string, na kojeg ptr se nalazi u adi na lokalnoj labeli
@l, te sadpozivamo @p labelu gdje cemo patchovati u ona 2 WORDA i jedan DWORD
koji predstavljaju UNICODE_STRING strukturu prvo UNICODE_STR_Buffer polje, dok
se UNICODE_STR_Length i UNICODE_STR_MaximumLength automatski generiraju od
strane asemblera.

Ovaj makro ustedi puno zivaca i rucnog brojanja karaketera. Vjerujte mi ili se
uvjerite na tezi nacin :)

Idemo dalje:

push eax

lea edi, arg_start
lea ecx, arg_end
sub ecx, edi
xor eax, eax
cld
rep stosb

Spremamo eax koji je ptr na alociranu strukturu i pri tome obracam paznju na
jednu jebeno vaznu cinjenicu, a ta je da ta UNICODE_STRING struktura *mora* biti
jebeno DWORD-aligned inace ce jebeni nativni API-ji koji je koriste vracati (iz
ntstatus.h gdje mozete vidjeti definicije i ostalih NTSTATUS i inih statusnih
kodova):

//
// MessageId: STATUS_DATATYPE_MISALIGNMENT
//
// MessageText:
//
// {EXCEPTION}
// Alignment Fault
// A datatype misalignment was detected in a load or store instruction.
//
#define STATUS_DATATYPE_MISALIGNMENT ((NTSTATUS)0x80000002L) // winnt

Kita. Zato pazite gdje cuvate ptre :) Recimo ako stringove ovako pravite u .data
sekciji onda mozete koristiti "align 4" direktivu direktno u kodu, koja ce to
uraditi umjesto vas, a sto je i pozeljan nacin. Jedino ako ovako u letu to
zelite napraviti kao ja :)

Zato trebamo spremiti jebeni eax registar jer da smo taj unicode string
alocirali nakon gore navedenog snippeta koda, morali bi raditi nop-alignment,
sto sucka.

Nadalje ako embeddate ovakav kod u C#, morat cete dovadati nop-alignment na 4
na pocetku programa, kako bi on radio kako treba.

Gornji snippet koda (od cijeg opisa ja izgleda bjezim cijelo vrijeme :)
jednostavno inicijalizira sve lokalne varijable alocirane na stogu na nulu, pri
cemu se koriste dummy lokalne varijable (koje se ne koriste u biti) arg_start i
arg_end. To radimo zato jer su nam vecina polja nekih lokalnih struktura (recimo
OBJECT_ATTRIBUTES) treba biti inicjaliziran na 0 pri koristenju, pa ovako sve
radimo u letu.
pop edi

mov objattr.OBJETCT_ATTR_Length, OBJECT_ATTRIBUTES_SIZE
mov objattr.OBJETCT_ATTR_Attributes, OBJ_CASE_INSENSITIVE + OBJ_KERNEL_HANDLE
mov objattr.OBJETCT_ATTR_ObjectName, edi

Uglavnom popamo ptr na strukturu kasnije u edi te inicjaliziramo jedno callgate
polje koje cu kasnije objasnit te polja OBJECT_ATTRIBUTES strukture (objattr)
koju inace radi InitializeObjectAttributes makro u DDK.

Nakon toga gradimo unicode string od imena DLL-a user32.dll:

@build_UNICODE_STRING <user32.dll>
xchg esi, eax

Ovaj nam je DLL vazan jerbo se u njemu nalaze API-ji za manipulaciju
clipboardom, i premda koristimo njihovne nativne verzije iz ntdll.dll,
moramo laodati i ovaj dll jer on uzrokuje lodanje cijelog GDI subsystema bez
kojeg ni nativni API-ji nece raditi kako treba? Trebalo mi je sat vremena da
skuzim da je u tome jebeni problem :)

E sad se bavimo malo nativnim apija i njihovim koristenjem u asm-u. Od winXP se
syscalli koji su sadrzani u KeServiceDescriptorTable polju (koje sadrzi u biti
4 tavlice, te jos jedna shadow tablica koja nije exportana u ntoskrnl.exe) vise
ne pozivaju preko klasicnog trapgatea (int 2eh/2bh/2ch do winXP), vec preko
sysenter (sysexit za izlaz) instrukcije koju je Intel prakticki dizajnirao za
NT: naime sysenter zahtijeva da u ring0 SS = ring0 CS + 8, sysexit zahtijeva da
je ring3 CS = ring0 CS + 10h. Za NT je ring0 CS = 08h, ring0 SS = 10h,
ring3 CS = 1bh, ring3 SS = 23h, tj. bas kao sto i treba biti :>

Iz Intelove dokumentacije [10]:

The SYSENTER and SYSEXIT instructions do not constitute a call/return pair;
therefore, the system call "stub" routines executed by user code (typically in
shared libraries or DLLs) must perform the required register state save to
create a system call/return pair.

To je bas ovdje i slucaj, jerbo su vecina win32API-ja bas to - stubovi za
nativni API koji obavljaju odgovarajuce spremanje registara i housekeeping
stoga.

Pogledajmo sljedece eksporte u ntdll.dll:

.text:7C90EB8B ; Exported entry 41. KiFastSystemCall
.text:7C90EB8B
.text:7C90EB8B ; --------------- S U B R O U T I N E ---------------------------
.text:7C90EB8B
.text:7C90EB8B
.text:7C90EB8B ; __stdcall KiFastSystemCall()
.text:7C90EB8B public _KiFastSystemCall@0
.text:7C90EB8B _KiFastSystemCall@0 proc near
.text:7C90EB8B mov edx, esp
.text:7C90EB8D sysenter
.text:7C90EB8F nop
.text:7C90EB90 nop
.text:7C90EB91 nop
.text:7C90EB92 nop
.text:7C90EB93 nop
.text:7C90EB93 _KiFastSystemCall@0 endp
.text:7C90EB93
.text:7C90EB94 ; Exported entry 42. KiFastSystemCallRet
.text:7C90EB94
.text:7C90EB94 ; --------------- S U B R O U T I N E ---------------------------
.text:7C90EB94
.text:7C90EB94
.text:7C90EB94 ; __stdcall KiFastSystemCallRet()
.text:7C90EB94 public _KiFastSystemCallRet@0
.text:7C90EB94 _KiFastSystemCallRet@0 proc near
.text:7C90EB94 retn
.text:7C90EB94 _KiFastSystemCallRet@0 endp
.text:7C90EB94
.text:7C90EB95 ; ¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦
.text:7C90EB95 lea esp, [esp+0]
.text:7C90EB9C lea esp, [esp+0]
.text:7C90EBA0 nop
.text:7C90EBA1 nop
.text:7C90EBA2 nop
.text:7C90EBA3 nop
.text:7C90EBA4 nop
.text:7C90EBA5 ; Exported entry 43. KiIntSystemCall
.text:7C90EBA5
.text:7C90EBA5 ; --------------- S U B R O U T I N E ---------------------------
.text:7C90EBA5
.text:7C90EBA5
.text:7C90EBA5 ; __stdcall KiIntSystemCall()
.text:7C90EBA5 public _KiIntSystemCall@0
.text:7C90EBA5 _KiIntSystemCall@0 proc near
.text:7C90EBA5
.text:7C90EBA5 arg_4 = dword ptr 8
.text:7C90EBA5
.text:7C90EBA5 lea edx, [esp+arg_4]
.text:7C90EBA9 int 2Eh ; DOS 2+ internal - EXECUTE COMMAND
.text:7C90EBA9 ; DS:SI -> counted CR-terminated command string
.text:7C90EBAB retn
.text:7C90EBAB _KiIntSystemCall@0 endp
.text:7C90EBAB
.text:7C90EBAC ; ¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦




_KiFastSystemCall sadrzi sysenter instrukciju, kao sto i sami vidite,
_KiFastSystemCallRet je obican near return, dok je _KiIntSystemCall stari nacin
skakanja trapgatea preko sw interrupta. On se koristi u slucaju da korisnicki
CPU na koji se win XP instalira ne podrzava SYSENTER instrukciju (ima CPUID flag
za testiranje toga), te se u tom slucaju koristi int 2eh. sysenter je znatno
brzi od interrupta, a buduci da se jaaako puno UM->KM prijelaza dogadjaja
stalno, kako program poziva nove API-je, razumljiva je MS-ova odluka da koristi
specijalizirane instrukcije bas za tu svrhu. Na linuxu jos uvijek imate int 80h
LOLZ ;)

Ovo testiranje preko CPUID se dogadja, naravno, za vrijeme instlacije sistema.
Tada se odabire pravilan syscall dispatcher i patcha se par DWORD-a tako da svi
API-ji pokazuju na onaj pravi.

U izvornom kodu win [11] proucite \private\ntos\ke\i386\trap.asm ako vas ovo vise
zanima. Pogotovo obratite paznju na Kss_ExceptionHandler koji se obavlja kad se
kontrola dobije preko vektora 2EH, te _KiFastCallEntry ciji pocetak ide ovako:

align 16
PUBLIC _KiFastCallEntry
_KiFastCallEntry proc
; At entry:
; EAX = service number
; EDX = Pointer to caller's arguments
; ECX = User return address
;

; Create a stack frame like a call to inner privilege
; Load ESP from Tss.Esp0. ints are disabled and esp is not loaded.
ifndef NT_UP
mov esp, KGDT_R0_PCR
mov fs, sp
mov esp, fs:PCR[PcTss]
else
mov esp, ss:PCR[PcTss]
endif ;; NT_UP
mov esp, ss:[esp].TssEsp0

push KGDT_R3_DATA OR RPL_MASK ; Push user SS
push edx ; Push ESP+4
sub dword ptr [esp], 4
pushfd ; Push EFlags
or dword ptr [esp], EFLAGS_INTERRUPT_MASK
push KGDT_R3_CODE OR RPL_MASK ; Push user CS
push ecx ; Push Return EIP

ifndef NT_UP
; For the MP case, FS is already loaded above
ENTER_SYSCALL kfce_a, kfce_t, NoFSLoad
else
; set up trap frame and save state
ENTER_SYSCALL kfce_a, kfce_t
endif ;; NT_UP

jmp _KiSystemServiceRepeat

_KiFastCallEntry endp

Pogotovo obratite paznju na onaj jmo na _KiSystemServiceRepeat jerbo i
_KiFastCallEntry i Kss_ExceptionHandler zavrsavaju na isti nacin, poanta je bila
iskoristiti kod koji oba dva koriste, te iskoristiti odgovarajuci (brzhi!)
mehanizam za ulazak u _KiSystemServiceRepeat.

Da ne ulazim sad u teoriju previse, pogledajmo sto se dogadja kad pozovemo, npr.
NtClose( HANDLE handle):

.text:7C90D586 ; __stdcall NtClose(x)
.text:7C90D586 public _NtClose@4
.text:7C90D586 _NtClose@4 proc near
.text:7C90D586
.text:7C90D586 mov eax, 19h
.text:7C90D58B mov edx, 7FFE0300h
.text:7C90D590 call dword ptr [edx]
.text:7C90D592 retn 4
.text:7C90D592 _NtClose@4 endp

e sad se vi pitate, sto je to tako magicno na adresi 7FFE0300h. To je stub koji
poziva sysenter/int 2eh, u slucaju sysenter zahtijeva kao sto je komentirano gore
u uvodu _KiFastCallEntry:

; EAX = service number
; EDX = Pointer to caller's arguments
; ECX = User return address

a odgovarajuci @sysenter makro koji ga emulira izgleda ovako:

sysenter macro syscall, params
local @l, @m

errifb <syscall> "Invalid argument in macro @sysetner: Missing syscall number"
errifb <params> "Invalid argument in macro @sysenter: Missing parameters"

call @m

; ntdll!KiFastSystemCall

@l: mov eax, syscall
mov edx, esp
dw 340Fh ; sysenter

@m: call @l
add esp, (params*4) + 4 ; +4 za dummy EIP

endm

Vazno je uociti 2 stavari: 1) mi smo ti koji cistimo svoje parametre sa stoga
2) syscall broj za svaki Nt* jest razlicit te ga trebamo rucno vaditi iz disasma
ntdll.dll. On se geneira od strane skripte pri svakom buildu NT kernela, te je
vjerojatno za isti API razlicit na razlicitim buildovima. Postoji metoda kako @
runtime koristiti odgovarajuci syscall broj tako da radi na svim, ali ona
nazalost nadilazi opseg ovog clanka i ne da mi se o njoj sad pisati :)

dakle, taj makro koristimo na sljedeci nacin:

@sysenter ImeNt*Apija, broj_parametara

Vratimo se sad na rl.asm. @Ntmalloc makro radi alociranje memorije preko
NtAllocateVirtualMemory() te sprema odredisnu adresu u registar dan kao
parametar, 0 u slucaju greske. Inace, nativni API-ji vracaju STATUS_SUCCESS u
slucaju uspjesnog izvodjenja:

#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)

Dakle, ako je u eax bilo sta != 0 doslo je do greske te opis statusnog koda
gledamo ili u ntstatus.h iz DDK, ili ntstatus <statusni kod> komandom u SoftIce.

U kodu se uglavnom koristi @Ntmalloc, ali na jednom primjeru cak se koristi i
NtAllocateVirtualMemory() da vidite kako se sa njim radi.

@get_delta makro u letu izracunava delta offset te se registar dan kao parametar
moze koristiti kao delta registar. Nisam uspio sve strpati kao lokalne
varijable, te je u nekim dijelovima koda cak i nemoguce koristiti lokalne
varijable koje su definirane u nekoj drugoj proceduri (main), recimo u SEH
handleru, pri skoku u ring0, pa se dosta puta delta offset mora ispocetka
racunati pa sam iskodirao ovaj makro da mi bude lakse.

@Ntmalloc 2048, ecx
jecxz kraj

@get_delta TMP_DLTA3, edx
mov (lde_ptr TMP_DLTA3).dword ptr 0, ecx

; inicijaliziraj interni 2K buffer od LDE32 (raspakiraj interne zastavice)

push ecx
call disasm_init
add esp, 4 ; trebas poravnati stog!

LDE32 he z0mbiev LDE (Length Disassemble Engine) - engine koji vraca duzinu
instrukcije na adresi proslijedjenoj kao parametar, te se taj engine koristi u
running line engine-u kojeg cu kasnije objasniti. Poanta je da on zahtijeva 2K
interni buffer kojeg alociramo i proslijedjujemo sa disasm_init.

mov eax, dword ptr fs:[30h]
mov eax, [eax+0ch]
mov eax, [eax+1ch]
mov ebx, [eax+8] ; uzmi bazu od ntdll.dll

gethash <LdrLoadDll>
push hash
call load_dll_address

xor ebx, ebx
push esp
push esi
push ebx
push ebx
call eax
pop eax ; baza od user32.dll

Nakon toga loadamo user32.dll koristeci ntdll!LdrLoadDll, koja je interna
Loaderova fja za loadanje DLL-ova. Njenu dokumentaciju mozete naci u
\private\ntos\dll\ldrsnap.c, a ovdje je koristim samo da jos vise razveselim
potencijalnog reversera :)

Nakon toga slijedi dio koji je manje vise sam po sebi razumljiv:


xor edx, edx
push edx
push esp
push ebx
@sysenter NtUserOpenClipboard, 2

pop edx
test eax, eax
jz greska
NtUserOpenClipboard() poziva inace OpenClipboard() API, ovo je varijacija sa
hWndNewOwner parametrom postavljenim na NULL.

push MAXIMALNA_VELICINA_BF_POLJA+1
mov edx, esp
xor eax, eax ; neka nam sistem dodijeli baznu adresu
push eax
mov ecx, esp

push PAGE_READWRITE ;
push MEM_COMMIT
push edx
push ebx ; ZeroBits, 0 uvijek.
push ecx ; bazna adresa kako nam se dodijeli..
push -1 ; trenutni proces
@sysenter NtAllocateVirtualMemory, 6
pop esi ; bazna adresa koju smo dobili
pop ecx ; poravnaj stog

MAXIMALNA_VELICINA_BF_POLJA jest maximalna velicina jednog polja ciju cu namjenu
kasnije objasniti :) Bas kao i sljedeci dio koda za MAXIMALNA_VELICINA_STDOUT.

test eax, eax
jnz greska
push esi ; za kasnije

push MAXIMALNA_VELICINA_STDOUT+1
mov edx, esp
xor eax, eax ; neka nam sistem dodijeli baznu adresu
push eax
mov ecx, esp

push PAGE_READWRITE
push MEM_COMMIT
push edx
push ebx ; ZeroBits, 0 uvijek.
push ecx ; bazna adresa kako nam se dodijeli..
push -1 ; trenutni proces
@sysenter NtAllocateVirtualMemory, 6
pop esi ; bazna adresa koju smo dobili
pop ecx ; poravnaj stog

test eax, eax
jnz greska
push esi ; za kasnije

Sad provjeravamo da li se na clipboardu nalaze podaci koji su tekstualnog tipa
(CF_TEXT). Ovo odgovara IsClipboardFormatAvailable() API-ju. Ukoliko je tekst,
tada inas program locka handle na clipboard sa NtUserGetClipboardData() te 2
puta pozivamo NtUserCreateLocalMemHandle(). Prvi put prosljedjujemo 0 kao
velicinu buffera te nam vraca STATUS_BUFFER_TO_SMALL statusni kod, te na
prosljedjeni ptr na handle sadrzi potrebnu velicinu buffera koji moramo
opet rucno alocirat, te ponovno pozvat. Ne pitajte me zasto ovo ovako
funkcionira, ovako je to implementirano u ntdll.dll :)

push CF_TEXT ; je li na clipboardu tekst?
@sysenter NtUserIsClipboardFormatAvailable, 1
xchg eax, ecx
jecxz zatvori_clip

push ebx
push ebx
push ebx
push esp ; ptr na neku gubavu strukturu od 3 DWORD-a
push CF_TEXT
@sysenter NtUserGetClipboardData, 2 ; uzmi handle na clipboard
test eax, eax
jz zatvori_clip ; vraca handle, NULL u slucaju greske
add esp, 4*3 ; pocisti stog, vraca tip koji *jest* dostupan

mov edi, eax

; push eax
; call user32!CreateLocalMemHandle
push ebx
push esp ; 0 ili ako je buffer premal potrebna velicina buffera
push ebx ; velicina alocirane memorije
push ebx ; ptr na alociranu memoriju
push eax ; handle koji je vratio NtUserGetClipboardData
@sysenter NtUserCreateLocalMemHandle, 4 ; prvi nam put vraca STATUS_BUFFER_TO_SMALL

;pop edx ; potrebna velicina buffera, mislim da vraca alociranu velicinu
mov edx, esp ; ptr na potrebnu velicinu buffera
xor ecx, ecx
push ecx ; neka nam sistem dodijeli baznu adresu
mov ecx, esp

push PAGE_READWRITE
push MEM_COMMIT
push edx
push ebx ; ZeroBits, 0 uvijek.
push ecx ; bazna adresa kako nam se dodijeli..
push -1 ; trenutni proces
@sysenter NtAllocateVirtualMemory, 6

pop esi ; bazna adresa koju smo dobili
pop ecx ; poravnaj stog! neka na stogu ostane velicina alocirane memorije

test eax, eax
jnz greska

push ebx
push ecx
push esi ; esi = ptr na clipboard buffer
push edi
@sysenter NtUserCreateLocalMemHandle, 4

E sad izracunavamo delta handle u ecx te spremamo dobivene alocirane adrese
na fixne memorijske lokacije.

@get_delta TMP_DLTA1, ecx

pop (bf_sim_stdout_polje TMP_DLTA1).dword ptr 0
pop (bf_sim_polje TMP_DLTA1).dword ptr 0
mov (bf_sim_clip TMP_DLTA1).dword ptr 0, esi

Sad bih mogao objasniti kako je crackme zamisljen i koja je svrha ovih
varijabli. Naime, zadatak je reversera skuziti sta program radi i kako prima
input, te mu proslijediti odgovarajuci input. Metoda proslijedjivanja je,
ocito, clipboard, a tip podataka jest neki tekst. Ja sam zamislio da taj tekst
bude program u brainfucku [12] koji radi nesto specificno.

E sad se vi pitate koji je kurac brainfuck i zasto sam njega odabrao :)


///////////////////////////////////////////////////////////////////////////
--[ Brainfuck
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\

Brainfuck je prije svega jedan bolestan jezik kojeg je izmislio Urban Müller,
kojemu je cilj bio napraviti jezik za Amiga OS 2.0 za kojeg ce napisati
kompajler ispod 260 bajtova. Na kraju je i uspio :)

Brainfuck program ima jedan glavni "pokazivac", koji se slobodno krece u polju
velicine 30000B, ciji su elementi inicijalno postavljeni na 0. Na pocetku je taj
pokazivac inicijaliziran da pokazuje na pocetak tog niza.

Sam jezik se sastoji od 8 komandi koje su predstavljene jednim karakterom:

> Inkrementiraj pokazivac.
< Dekrementiraj pokazivac.
+ Inkrementiraj bajt nad pokazivacem.
- Dekrementiraj bajt nad pokazivacem.
. Ispisi bajt nad pokazivacem (ASCII reprezenzaciju).
, Ucitaj jedan bajt i spremi na ga lokaciju na koju pokazicac pokazuje.
[ Idi naprijed ispred odgovarajuceg ] ukoliko je bajt na koji pokazivac
pokazuje nula.
] Idi nazad do odgovarajuceg [ osim ako je bajt na koji pokazivac pokazuje
nula.

Semantika brainfuck komandi se moze vrlo ekspresivno iskazati u C-like sintaksi,
pod pretpostavkom da je pokazivac p deklariran kao char* :

> postaje ++p;
< postaje --p;
+ postaje ++*p;
- postaje --*p;
. postaje putchar(*p);
, postaje *p = getchar();
[ postaje while (*p) {
] postaje }


Na netu imate gomilu primjera kompajlera i kodova u brainfucku..pokusajte malo
kodirati u njemu, jako je cudan osjecaj nakon nekog vremena :)

Napisao sam u asmu dvije procedure: brainfuck_provjeri_sintaksu i
brainfuck_simuliraj. brainfuck_provjeri_sintaksu provjerava sintaksu danog bloka
memorije, tj. da li taj blok sadrzi validne brainfuck komande (NULL-terminiran).

U ovom su mom specijalnom slucaju dozvoljene sve komande osim ",", tj.
ucitavanja znaka, jerbo nema smisla da crackme sad ucitava znak. Poanta ja inace
crackmeja napisati brainfuck program cijim ce se izvodjenjem "ispisati"
specifican string. Koji? vidjet cete kasnije..

Evo kako sam ja implementirao tu proceduru:

Argumenti:
p -> ptr na polje u memoriji koje sadrzi potencijalni brainfuck program,
vraca 1 u slucaju da je validan, 0 inace.

brainfuck_provjeri_sintaksu proc pascal uses ebx

arg p

mov ecx, p
mov eax, ecx
jmp __petljaj

; validni su znakovi sve standardne brainfuck komande osim ","

__loop: movsx edx, byte ptr [eax]
cmp edx, '+'
jz __ok_znak
cmp edx, '-'
jz __ok_znak
cmp edx, '<'
jz __ok_znak
cmp edx, '>'
jz __ok_znak
cmp edx, '['
jz __ok_znak
cmp edx, ']'
jz __ok_znak
cmp edx, '.'
jz __ok_znak
xor eax, eax
jmp __izlaz

__ok_znak: inc eax

; polje je NULL-terminirano

__petljaj: cmp byte ptr [eax], 0
jnz __loop

; sad provjeravamo da li u polju ima neuparenih '[' i ']'

xor ebx, ebx
mov eax, ecx
jmp __1

__zagrade: movsx ecx, dl
cmp ecx, '['
jnz __3
inc ebx

__3: cmp ecx, ']'
jnz __2
dec ebx

__2: inc eax

__1: mov dl, [eax]
test dl, dl
jnz __zagrade
test ebx, ebx
jz __ok_izlaz
xor eax, eax
jmp __izlaz

__ok_izlaz: mov eax, 1

__izlaz: ret

Dok sam brainfuck simulator ovako implementirao:

Argumenti:
clip -> ptr na validan brainfuck program
polje -> brainfuck polje velicine 30k bajtova.
stdout_polje -> polje koje simulira stdout

brainfuck_simuliraj proc pascal uses ebx esi edi

arg clip
arg polje
arg stdout_polje

local len
local bf_stdout

xor eax, eax

mov edx, clip
mov len, eax
xor eax, eax
jmp __tmp

__1: inc len
inc eax

__tmp: cmp eax, MAXIMALNA_VELICINA_PROGRAMA
jge __chk_len
cmp byte ptr [edx+eax], 0
jnz __1

__chk_len: cmp len, MAXIMALNA_VELICINA_PROGRAMA-1
jnz __len_ok
xor eax, eax
jmp __izlaz

__len_ok: mov ecx, stdout_polje
xor esi, esi
mov bf_stdout, ecx
xor ecx, ecx
xor eax, eax
cmp eax, len
jge __izlaz_OK

__loop: movsx ebx, byte ptr [edx+eax]
cmp ebx, '+'
jnz __2
mov ebx, polje
inc byte ptr [ebx+esi]
jmp __provjera

__2: movsx ebx, byte ptr [edx+eax]
cmp ebx, '-'
jnz __3
mov ebx, polje
dec byte ptr [ebx+esi]
jmp __provjera

__3: movsx ebx, byte ptr [edx+eax]
cmp ebx, '>'
jnz __4
inc esi
jmp __provjera

__4: cmp ebx, '<'
jnz __5
dec esi
jmp __provjera

__5: cmp ebx, '['
jnz __6
mov ebx, polje
cmp byte ptr [ebx+esi], 0
jnz __provjera
inc eax
jmp __loopie

__pici_naprid:
movsx ebx, byte ptr [edx+eax]
cmp ebx, '['
jnz __shittie
inc ecx

__shittie: cmp ebx, ']'
jnz __y
dec ecx

__y: inc eax

__loopie: test ecx, ecx
jg __pici_naprid
movsx ebx, byte ptr [edx+eax]
cmp ebx, ']'
jnz __pici_naprid
jmp __provjera

__6: movsx ebx, byte ptr [edx+eax]
cmp ebx, ']'
jnz __7
dec eax
jmp __loopie2


__naprid_jos: movsx ebx, byte ptr [edx+eax]
cmp ebx, ']'
jnz __z
inc ecx

__z: cmp ebx, '['
jnz __temp
dec ecx

__temp: dec eax

__loopie2: test ecx, ecx
jg __naprid_jos
movsx ebx, byte ptr [edx+eax]
cmp ebx, '['
jnz __naprid_jos
dec eax
jmp __provjera

__7: movsx ebx, byte ptr [edx+eax]
cmp ebx, '.'
jnz __provjera
mov ebx, polje
mov edi, bf_stdout
mov bl, [ebx+esi]
mov [edi], bl
inc bf_stdout

__provjera: inc eax
cmp eax, len
jl __loop

__izlaz_OK: mov eax, 1

__izlaz: ret

brainfuck_simuliraj endp

Da bude lakse za skuziti sta ovi potprogrami rade, napisao sam (cisto za
vjezbu :) i njihovu verziju u C-u. Dakle evo brainfuck simulator cijeli:

------------------------------ C O D E ------------------------------------

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>

#define BF_DEBUG

#define MAXIMALNA_VELICINA_PROGRAMA 1024
#define MAXIMALNA_VELICINA_BF_POLJA 30000-1
#define MAXIMALNA_VELICINA_STDOUT 500-1

int brainfuck_provjeri_sintaksu(char* p)
{
for( char* clip = p; *clip; clip++) {
if ( !(*clip == '+' || *clip == '-' || *clip == '<' || *clip == '>' || *clip == '[' || *clip == ']' || *clip == '.') )
return FALSE;
}

// sad provjeravamo da li u polju ima neuparenih '[' i ']'

unsigned int br = 0;

for (char* clip = p; *clip; clip++) {
if( *clip == '[' ) br++;
if( *clip == ']' ) br--;
}

if (br != 0) {
fprintf(stderr, "Neuparene zagrade u kodu!\n");
return FALSE;
}
return TRUE;
}

int brainfuck_simuliraj(char* clip)
{
char* polje = (char*) malloc (sizeof(char) * MAXIMALNA_VELICINA_BF_POLJA + 1);
memset(polje, 0, MAXIMALNA_VELICINA_BF_POLJA+1);

char* stdout_polje = (char*) malloc(sizeof(char) * MAXIMALNA_VELICINA_STDOUT +1);
memset(stdout_polje, 0, MAXIMALNA_VELICINA_STDOUT +1);

int len = 0;

for ( int i = 0; i < MAXIMALNA_VELICINA_PROGRAMA && clip[i] != 0; i++ )
len++;

if ( len == MAXIMALNA_VELICINA_PROGRAMA-1 ) {
fprintf(stderr, "Prevelik ulazni program!");
return FALSE;
}

char* bf_stdout = stdout_polje;

int br = 0;
int l = 0;

for ( int i = 0; i < len; i++) {
if ( clip[i] == '+' ) polje[br]++;

else if ( clip[i] == '-' ) polje[br]--;

else if ( clip[i] == '>' ) br++;

else if ( clip[i] == '<' ) br--;

else if (

clip[i] == '[' ) { 
if ( polje[br] == 0 ) {
i++;
while ( l > 0 || clip[i] != ']' ) {
if ( clip[i] == '[' ) l++;
if ( clip[i] == ']' ) l--;
i++;
}
}
}

else if ( clip[i] == ']' ) {
i--;
while ( l > 0 || clip[i] != '[' ) {
if ( clip[i] == ']' ) l++;
if ( clip[i] == '[' ) l--;
i--;
}
i--;
}
else if ( clip[i] == '.' ) *bf_stdout++ = polje[br];
}

fprintf(stdout, "Simulacija zavrsena, stanje output buffera: \"%s\"\n", stdout_polje);
free (stdout_polje);
free (polje);
return TRUE;
}

int main(int argc, char *argv[])
{
OpenClipboard(NULL);

if ( IsClipboardFormatAvailable(CF_TEXT) == TRUE )
{
HANDLE hPodaci = GetClipboardData(CF_TEXT);
if ( hPodaci == NULL ) return 1;

LPVOID podaci = GlobalLock(hPodaci);
char* clip = (char*) podaci;

if (brainfuck_provjeri_sintaksu(clip) == FALSE )
fprintf(stderr, "Leksicka analiza je pala!\n");
else {
fprintf(stdout, "Validna sintaksa, zapocinjem simuliranje!\n");

if (brainfuck_simuliraj(clip) == TRUE )
fprintf(stdout, "Simulacija uspjseno zavrsena!\n");
else
fprintf(stdout, "Simulacija nesupjela :(\n");

GlobalUnlock(hPodaci);
return 0;
}
}
else fprintf(stderr, "Greska: nemam nista na clipboardu!");

CloseClipboard();
return 0;
}

------------------------------ C O D E E N D -----------------------------

Ne da mi se sad komentirat sta kod radi, pokusajte sami skuzit, nije to kvantna
fizika..

E sad, u vezi ove 2 procedure. Svaka je zasticena za posebnom zastitom i to je u
biti car slojevitosti ovog crackmeja. Zapocnimo sa brainfuck_provjeri_sintaksu.
Naime, ona je zasticena sa jednim old skul cudom sto se zove running line
enkripcija. Ako vidite od cega se sastoji procedura unutar koda, vidjet cete da
je to uglavnom hex dump + 2 markera i mini stub:


brainfuck_provjeri_sintaksu proc pascal uses ebx

arg p

call udji_ss

dd RL_START_MARKER

db 0a8h, 06eh, 02bh, 0a8h, 0e2h, 0c8h, 008h, 02ch
db 09dh, 033h, 0a0h, 0d9h, 008h, 057h, 001h, 0a0h
db 0d9h, 00eh, 057h, 03eh, 0a0h, 0d9h, 01fh, 057h
db 03bh, 0a0h, 0d9h, 01dh, 057h, 030h, 0a0h, 0d9h
db 078h, 057h, 02dh, 0a0h, 0d9h, 07eh, 057h, 02ah
db 0a0h, 0d9h, 00dh, 057h, 027h, 010h, 0e3h, 0c8h
db 010h, 063h, 0a3h, 01bh, 023h, 056h, 0f3h, 010h
db 0f8h, 0a8h, 0e2h, 0c8h, 033h, 02ch, 09dh, 0e9h
db 0a0h, 0dah, 078h, 056h, 022h, 060h, 0a0h, 0dah
db 07eh, 056h, 022h, 068h, 063h, 0a9h, 033h, 0a7h
db 0f1h, 056h, 0c9h, 0a6h, 0f8h, 057h, 027h, 010h
db 0e3h, 0c8h, 02ah, 09bh, 022h, 023h, 023h, 023h

dd RL_MARKER

ret

brainfuck_provjeri_sintaksu endp

E sad se vi s pravam pitate koji je to jebeni kurac :)


///////////////////////////////////////////////////////////////////////////
--[ Running-line
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\

Running line je tehnika koju je izmislio Serge Pachkovsky [13] te je
provotno prakticirana na MS-DOS-u. Ta se metoda sastoji od hookiranja vektora za
int 1, postavljanja TF nakon svake instrukcije, te dekriptiranja svake
instrukcije *neposredno prije* se ona izvodi, de kriptiranje nazad *nakon* sto
se ona izvede.

Ocito, na win iz ring3 ne mozemo bas prckati po IDT te postavljati svoj int 1
handler (mogli bismo iz kernel-mode, ali o tom drugom prilikom..), ali zato u tu
svrhu mozemo iskoristiti SEH.

Prvo proucimo udji_ss proceduru:

udji_ss proc near
pop eax
pushf
or byte ptr [esp+1], 1
popf
jmp eax
udji_ss endp

TF je bit 8 koji postavljamo i pop-amo u EFLAGS. Prva instrukcija (onaj jmp eax)
se ne trace-a), te se iznimka tipa EXCEPTION_SINGLE_STEP obradjuje u defaultnom
exception handleru.

@SEH_SetupRL je varijacija poznatog @SEH_SetupFrame makroa koja bas sluzi za
svrhu running line i ovako sam je napisao:

@SEH_SetupRL macro p1, p2, p3, p4, p5, p6, p7, p8, p9
local set_new_eh

call set_new_eh
irp param, <<&p1>, <&p2>, <&p3>, <&p4>, <&p5>, <&p6>, <&p7>, <&p8>, <&p9>>
ifb <param>
exitm
endif
param ; za running line prvo trebamo raditi dekodiranje instrukcija!
endm
xor eax, eax ; EXCEPTION_CONTINUE_SEARCH = 0
retn

set_new_eh: xor edx, edx
push dword ptr fs:[edx]
mov dword ptr fs:[edx],esp
endm

Jedina je razlika u tome sto se na radi

mov esp,[esp.EH_EstablisherFrame]

vec direktno izlista parametre (tj. instrukcije koje se izvode, obicno samo
jedna..) te onaj

xor eax, eax ; EXCEPTION_CONTINUE_SEARCH = 0
retn

koji nam je potreban da bi se iznimke mogle dalje konzekutivno procesirati. Za
vise info-a o SEH-u, ukoliko ne znate sto je to, preporucam fenomelan post na
newsima Anatolya Vorobeya [18].

Zato prije poziva brainfuck_provjeri_sintaksu postavljamo SEH na sljedeci nacin:

@SEH_SetupRL <call dekodiraj_seh>

dekodiraj_seh je, pogadjate, running-line engine. Pogledajmo kako on
funkcionira.

dekodiraj_seh proc c

arg eh:Except_Handler

Svaki EH kao arguement prima Except_Handler strukturu koja je ovako definirana
(myapis.inc):

Except_Handler STRUC
EH_Dummy DD ? ; Ret address
EH_ExceptionRecord DD EXCEPTION_RECORD PTR ?
EH_EstablisherFrame DD BYTE PTR ?
EH_ContextRecord DD CONTEXT PTR ?
EH_DispatcherContext DD BYTE PTR ?
Except_Handler ENDS

EH_ExceptionRecord polje sadrzi EXCEPTION_RECORD strukturu u cijem polju
ER_ExceptionCode mozemo vidjeti razlog zbog kojeg se iznimka dogodila (lose
dereferenciranje ptra, dijeljenje s nulom, postavljen trap flag :))

E sad nas bas zanima ta iznimka kad je trap flag postavljen. Ona inace ima
statusni kod EXCEPTION_SINGLE_STEP. Dakle, mi mozemo, nakon sto se instrukcija
izvede, uciniti da tok izvodjenja programa skoci na nas EH, te imati pristup
registririma u kontekstu toka programa u kojem se iznimka dogodila, preko
EH_ContextRecord polja.

CONTEXT struktura sadrzi "kontekst" niti, trenutno stanje registara u njoj. Pri
suspendiranju niti to se stanje registara sprema, a pri nastavku izvrsavanja,
kopira se nazad u sve registre. Pogledajte npr. uporabu Set/GetThreadContext()
API-ja za detalje.

Mi unutar EH-a mozemo dakle kontrolirati *sve* registre running line koda koji
se izvrsava. Ukljucujuci i EIP :) Dakle, me mozemo efektivno natjerati kod da
skoci na proizvoljnu lokaciju, mozemo raditi sto god hocemo sa memorijom koju
program koristi i koja se nalazi u njegovom adresnom prostoru.

Na slican nacin mi bas to i radimo. Vec sam spomenuo da dekriptiramo i
enkriptiramo po jednu instrukciju kako se izvodi. E sad, problem je kako
odrediti duzinu instrukcije. Recimo deroko je u jednom papiru koji se takodjer
nalazi u ovom zine-u to rijesio dodavanjem int 3 nakon svake instrukcije (0CCh
opkod). Ja koristim malo elegantniji nacin ipak, dekriptiram prvih X bajtova
gdje je X duljina najvece instrukcije u bloku koda (ovdje sam uzeo 10 tek tako)
te na taj blok koda pozivam LDE32. Nakon toga kriptiram nazad ostatak.

TF se u EFLAGS resetira svaki put nakon iznimke, te ga je potrebno postaviti
rucno unutar EH-a. Nadalje, potrebno je takodjer odrediti nacin kako znati kad
treba zavrsiti dekriptiranje, tj. potreban je specijalan marker za kraj (i
eventualno za pocetak ukoliko zelimo neki inicijalizacijski kod raditi). U mom
engine-u koristim RL_START_MARKER i RL_END_MARKER za pocetak odnosno kraj bloka
koda koji se dekriptira.

Nadalje, takodjer su potrebne 3 externe varijable:

lde_ptr dd 0
preth_instr_len dd 0
offset_prosla dd 0

Sjetite se da je cijeli engine smjesten u EH-u, dakle morat ce ponovo sam
racunat delta offset za pristup registrima jer ce oni svaki put opet biti
sjebani. lde_ptr je adresa onog privremenog 2K buffera za LDE32 kojeg moramo
proslijedjivati kao parametar, preth_instr_len sadrzi adresu a offset_prosla
offset prosle instrukcije.

Adresa prosle instrukcije nam je potrebna jer LDE32 ne moze disasmati "unazad",
u biti nam je dosta adresa i onda bismo teoretski mogli pozvati LDE32 opet da
dobijemo duzinu, ali eto, ja sam to ovako napravio i sta sad :)

Dakle, nakon sto dodjemo do trenutne instrukcije i dekriptiramo tocno njezinu
duljinu, reenkriptiramo i ostatak od onog 10-bajtnog buffera i jednu prethodnu
instrukciju koja se izvodila.

Dakle sve u svemu, relativno jednostavan koncept, a tako jebeno mocan :)
Pogledajmo sad implementaciju:

dekodiraj_seh proc c

arg eh:Except_Handler

pusha

Provjerimo razlog zbog kojeg je EH pozvan. Ako kod iznimke nije
EXCEPTION_SINGLE_STEP dogodio se bug u samom programu koji se
emulira, sto nije dobro :/

mov eax, eh.EH_ExceptionRecord
cmp dword ptr [eax.ER_ExceptionCode], EXCEPTION_SINGLE_STEP
jnz __kraj

Sad uporedjujemo da li je *eip == RL_END_MARKER, tj. da li smo
dosli do kraja enkriptiranog koda. Ako da, eip += 4 i makni TF.

mov ebx, eh.EH_ContextRecord
mov edi, [ebx.CONTEXT_Eip]
cmp dword ptr [edi], RL_END_MARKER
jz __RL_END_MARKER

Inace i dalje ostavi TF.

or [ebx.CONTEXT_EFlags], 100h

Provjeri da li je *eip == RL_START_MARKER, tj. da li smo dosli
do pocetka enkriptiranog koda. Ako da, eip +=4 i ostavljamo TF.

cmp dword ptr [edi], RL_START_MARKER
jnz __nije_pocetak

E sad smo prije pocetka koda kojeg trebamo en/dekriptirati

add [ebx.CONTEXT_Eip], 4 ; preskoci RL_START_MARKER
add edi, 4

Dekriptiramo 10 instrukcija, izracunavamo duljinu prve te je spremamo
u varijablu i reenkriptiramo. U biti mozemo proizvoljan broj, sad
je RL_START_MARKER postavljen na 10. Enkripcija jest naaaajobicnije
XOR-anje u ovom slucaju, dakle nista komplicirano te reverser moze
skodirati dekripciju za 10s....pod uvjetom da skuzi sta ovaj engine
radi ;) Naravno, nista vas ne sprijevava da ovdje dodate bilo koji,
proizvoljni enkripcijski algoritam, kao ida pozivate neku externu
fju koja ce raditi *samo* enkripciju/dekripciju proslijedjenog
komadica koda... Mogucnosti je bezbroj, ovo je samo kostur :)

__nije_pocetak: push MAX_DEKRIP_LEN
pop ecx

xor edx, edx
__dekrip_lde: xor byte ptr [edx+edi], XOR_BAJT
inc edx
loop __dekrip_lde

@get_delta TMP_DLTA4, edx

push edi
push (lde_ptr TMP_DLTA4).dword ptr 0
call disasm_main
add esp, 8

inc eax
jz __error
dec eax ; duljina prve instrukcije

mov esi, (preth_instr_len TMP_DLTA4).dword ptr 0

UVIJEK spremamo offset prosle instrukcije u varijablu zbog mogucih grananja i
inih picki materina... kao i zbog cinjenice da LDE32 ne moze ici unazad :/

push (offset_prosla TMP_DLTA4).dword ptr 0
mov (offset_prosla TMP_DLTA4).dword ptr 0, edi

mov (preth_instr_len TMP_DLTA4).dword ptr 0, eax
push MAX_DEKRIP_LEN
pop ecx
sub ecx, eax ; MAX_DEKRIP_LEN - (duljina sljedece instrukcije)
add edi, eax ; ostatak nazad kriptiramo
xor edx, edx

__krip_lde: xor byte ptr [edx+edi], XOR_BAJT
inc edx
loop __krip_lde

pop edx ; edx = vrijednost starog offset_prosla

jos moramo kriptirati i prethodnu instrukciju. u slucaju da je to
RL_START_MARKER to ne radimo:

sub edi, eax
cmp [edi-4], RL_START_MARKER
jz __kraj

ecx = 0 samo u slucaju prve instr.

mov ecx, edx
jecxz __nije_prosla_skok
mov edi, ecx
jmp __1

__nije_prosla_skok: sub edi, esi
__1: mov ecx, esi ; duljina prethodne instrukcije
xor edx, edx
__krip_ldey: xor byte ptr [edx+edi], XOR_BAJT
inc edx
loop __krip_ldey

jmp __kraj

__RL_END_MARKER: add [ebx.CONTEXT_Eip], 4 ; preskoci RL_END_MARKER
and [ebx.CONTEXT_EFlags], 0FFFFFEFFh ; makni trap flag
jmp __kraj

__error: jmp $ ; greska, zastopaj se :>
__kraj: popa
xor eax, eax
ret

dekodiraj_seh endp

Moguce je naravno postaviti int3 unutar SEH-a i gledati kako ovo funkcionira
u stvarnosti..Naravno, moguce je i trace-ati izvorni program, ali samo prvu
instrukciju u syser/sice, jerbo ce oni sjebati TF tako da se nas EH nece
pozvati, pa trebamo TF rucno postaviti nakon svakog poziva udji_ss.

Sto se tice stvaranja dump-a XOR-iranog nekim kljucem, napravio sam mali C#
programcic koji radi istu stvar sto i HAXOR tool, samo sto jos i XOR-ira sa
danim kljucem svaki bajt:

------------------------------ C O D E ------------------------------------
using System;
using System.IO;
using System.Text;

namespace dumpXOR
{
class Program
{

static void IspisiUporabu()
{
Console.WriteLine("dumpXOR <ulazna datoteka> <izlazna datoteka> /S=<start labela> /E=<end labela> /XOR=<XY>");
}

static void Main(string[] args)
{
if ( args.Length != 5 ) {
IspisiUporabu();
return;
}

if ( File.Exists(args[0]) == false ) {
Console.WriteLine("Greska: datoteka {0} ne postoji!", args[1]);
return;
}

if ( args[2].StartsWith("/S=") == false || args[3].StartsWith("/E=") == false || args[4].StartsWith("/XOR=") == false
|| args[4].Length != 7 ) {
Console.WriteLine("Greska: pogresna sintaksa!");
return;
}

string xor_kljucs = args[4].Substring(5);

char prvi_znak = Char.ToUpper(xor_kljucs[0]);
char drugi_znak = Char.ToUpper(xor_kljucs[1]);

if ( !( Char.IsNumber(prvi_znak) || prvi_znak == 'A' || prvi_znak == 'B' || prvi_znak == 'C' ||
prvi_znak == 'D' || prvi_znak == 'E' || prvi_znak == 'F' ) ||
!( Char.IsNumber(drugi_znak) || drugi_znak == 'A' || drugi_znak == 'B' || drugi_znak == 'C' ||
drugi_znak == 'D' || drugi_znak == 'E' || drugi_znak == 'F' ) ) {
Console.WriteLine("Greska: XOR kljuc {0} nije validan hex bajt!", xor_kljucs);
return;
}

byte xor_kljuc = Convert.ToByte(xor_kljucs, 16);

Console.WriteLine("+Koristim kljuc {0}", xor_kljuc);
if ( File.Exists(args[1]) == true ) {
Console.WriteLine("->Upozorenje: brisem vec postojecu odlaznu datoteku {0}", args[1]);
File.Delete(args[1]);
}

using ( BinaryReader ulazni = new BinaryReader (new FileStream(args[0], FileMode.Open, FileAccess.Read) ) ) {
using ( StreamWriter odlazni = new StreamWriter(new FileStream(args[1], FileMode.CreateNew, FileAccess.Write)) ) {
string start_labela = args[2].Substring(3);
Console.WriteLine("+Koristim startnu labelu {0}", start_labela);
string end_labela = args[3].Substring(3);
Console.WriteLine("+Koristim end labelu {0}", end_labela);

byte[] buf = new byte[start_labela.Length];
Console.WriteLine("+Velicina datoteke {0} bajtova", ulazni.BaseStream.Length);

bool nadjena_signatura = false;
int start_offset = 0;
int end_offset = 0;

byte[] buf2 = new byte[end_labela.Length];
bool nadjena_signatura2 = false;

for ( int i = 0; i < ulazni.BaseStream.Length; i++ ) {
ulazni.Read(buf, 0, buf.Length);
ulazni.BaseStream.Position = i + 1;
if ( Encoding.ASCII.GetString(buf) == start_labela ) {
Console.WriteLine("+Pronadjena start signatura na offsetu {0}", i);
nadjena_signatura = true;
start_offset = i + start_labela.Length;
}
}

if ( nadjena_signatura == false ) {
Console.WriteLine("Greska: ne mogu pronaci u datoteci signaturu {0}!", start_labela);
return;
}

else {
for ( int i = start_offset; i < ulazni.BaseStream.Length; i++ ) {
ulazni.Read(buf2, 0, buf2.Length);
ulazni.BaseStream.Position = i + 1;
if ( Encoding.ASCII.GetString(buf2) == end_labela ) {
Console.WriteLine("+Pronadjena end signatura na offsetu {0}", i);
nadjena_signatura2 = true;
end_offset = i;
}
}
}

if ( nadjena_signatura2 == false ) {
Console.WriteLine("Greska: ne mogu pronaci u datoteci signaturu {0}!", end_labela);
}

odlazni.WriteLine("; generirana datoteka. *ne* editirati\n");
odlazni.WriteLine("XOR_BAJT\t\tequ\t\t0{0}h{1}", xor_kljucs, Environment.NewLine);
odlazni.WriteLine("dumpXOR:");
int j = 0;
byte b;
ulazni.BaseStream.Position = start_offset;
Console.Write("+Zapisujem datoteku....");
for ( int i = start_offset; i < end_offset; i++ ) {
if ( j++ % 8 == 0 ) odlazni.Write("db ");
b = ulazni.ReadByte();
byte x = (byte)(b ^ xor_kljuc);
odlazni.Write(String.Format("0{0:x2}h", x));
if ( j % 8 == 0 ) odlazni.Write(Environment.NewLine);
else odlazni.Write(", ");
}
Console.WriteLine("gotovo.");
}
}
}
}
}
------------------------------ C O D E E N D -----------------------------

Program se poziva ovako:

dumpXOR <ulazna datoteka> <izlazna datoteka> /S=<start labela> /E=<end labela> /XOR=<XY>

gdje je XY je hex bajt kojim XOR-iramo kod izmedju start i end labele.

Naravno, u slucaju da promijenite vrstu enkripcije, morat cete ili napraviti
svoj tool ili uraditi nesto trece..

Nakon sto ste tako dump-ali svoj kod, samo dodajte RL_START_MARKER i
RL_END_MARKER na pocetak odnosno kraj te postavite TF, engine ce sam obaviti
svoje..

I na taj nacin stitimo provjeru sintakse programa :)

Running line je nesto jaaaako jaaaaako jaaaako kul, vjerujte mi. Mogucnosti
manipulacije su zaista beskrajne, pogtovo u VX tehnologiji. Recimo primarno
tijelo virusa mozemo rastaviti pojedincno na instrukcije, te onda svaku zasebno
alocirati u zasbenoj adresi na hrpi te izvoditi koristeci running line engine.
Tako bismo napravili *dinamcku* obsfukaciju koda, za razliku od klasicnih
polimorfnih i metamorfnih engine-a, koji su staticke obsfukacije. Na taj bi
nacin efektivno unistili prepoznavanje tijela virusa preko signature.

Kao sto sam vec spomenuo, mjesta za napredak ima dosta, pogotovo u dijelu gdje
se kod dekriptira/enkriptira. Sigurno je potrebna neka naprednija zastita sa
klizecim kljucem.

Nadalje, potrebno je zastiti i sam running line engine od reversanja, te od
njegove detekcije od strane AV. Ako smo uspjeli zastiti running line engine,
nema nikakve jebene sanse de AV detektira tako "rasprseno" tijelo virusa...
pa cak i da nije rasprseno, kroz neki x-raying posrani, ukoliko je enkripcija
dovoljno kompleksna.

Sam engine bi se mogao zastititi, pogodite kako...jos jednim running line
slojem! Teoretski bi ih mogli ugnijezdjivati koliko god zelimo :> Buduci da je
sam engine relativno mal, moze se strpati u 100-ak instrukcija, mozemo ga jako
opako morfirati sa bilo kojim externim morferom (tipa KME), te ga mozemo
zastititi dodatnim running line slojevima.

Engine je u cijelosti napisan iz glave, a debuggiranje ovakvog jebenog govneta
koje je tako jebeno otporno na jebene debuggere je jebena nocna mora. Jedino
info sto sam nasao na netu jest u jednom codebreakers magazinu [14],
ali autor (The +Q) nazalost prezentira kod koji bi trebao demonstrirati
izvodjenje koda unazad preko running line, ali njegov kod je bugovit i ne radi
:>

Navodno postoji i jedan DOS-ovski virus koji se u cijelosti izvodi UNAZAD
koristeci running line!


///////////////////////////////////////////////////////////////////////////
--[ One ring to bring 'em all...
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\

ring0. Savrsentvo. Nema ogranicenja. Nema kontrole. Nema zastite. Sve je
apsolutno moguce, i nitko nas jebeno ne moze zaustaviti da ne uradimo bilo sta..
Sjebemo EEPROM BIOS-a, sjebemo particijsku tablicu, sakrijemo virus u NTFS bad
sectorima...sve.

Konacno je postala jebena stvarnost i to bez koristenja jebenih exernih drivera
:)

Ustvari sad bih vas najradije uputio na odlican clanak crazylorda [15] koji je i
otkrio ovu metodu. Radi se o sljedecem: Pathcamo \Device\PhysicalMemory tj.
device koji predstavlja fizicku memoriju (/dev/mem na linuxu, ne /dev/kmem koji
je virtualna memorija) i to na nacin da odredimo bazu preko sgdt, pretvorimo je
u fizicku adresu te instaliramo callgate koji pokazuje na nas kod i koji
pozivamo sa jednim far call-om.

Prije toga trebamo podesiti sigurnosni deksriptor (tj. ACL) od objekta kako
bismo imali write pristup na \Device\PhysicalMemory objekt, tj. samu fizicku
memoriju koju preko njega mapiramo.

Zaista mi se ne da sad objasnjavat u detalje teoriju, pogledajte crazylordov
papir, tamo vam je sve to lijepo objasnjeno. U prilozenom papiru kod je u C-u,
te je, kako to i prilici nama l33t haxorima :), preveden u asm. Stavio sam
nativne API-je gdje je to god bilo moguce.

Prvo alociramo 2 buffera velicine SYSTEM_MODULE_INFORMATION_SIZE i
SYSTEM_BASIC_INFORMATION_SIZE koji su strukture koje popunjava
NtQuerySystemInformation() API.

NtQuerySystemInformation vraca gomilu sistemskih postavki..sa njim je moguce
saznati neke detalje koje win32 API uopce ni ne pruza..nama su u ovom trenutku
vazne 2 kategorije SystemBasicInformation i koji popunjava lokalnu varijablu
sys_info i SystemModuleInformation koji koristimo za trazenje baze ntoskrnl.exe.

SYSTEM_BASIC_INFORMATION_SIZE nam je potreban zbog polja
SYS_BASIC_INFO_AllocationGranularity (obicno bude 10000h) koji se koristi pri
izracunavanju offseta pri translaciji u fizicku adresu (algo by ElicZ ;).

@Ntmalloc SYSTEM_MODULE_INFORMATION_SIZE, ecx
jecxz kraj
mov modinfo, ecx

@Ntmalloc SYSTEM_BASIC_INFORMATION_SIZE, ecx
jecxz kraj
mov sys_info, ecx

push 0
push SYSTEM_BASIC_INFORMATION_SIZE
push ecx
push SystemBasicInformation
@sysenter NtQuerySystemInformation, 4

Enumerirajmo sistemske module i potrazimo onaj koji je "ntoskrnl.exe".
MojPathFindFileName je moja, rucna verzija PathFindFileName API-ja, koji vraca
samo filename danog full path-a. syser debugger dosta sjebe stvari u kernelu,
te nekad zna promijeniti ntoskrnl.exe u kernel1.exe, pa ovaj blok koda nece
raditi kako treba. Zato sam u slucaju debuggiranja sa sysermo stavio da vraca
fiksnu adresu kernela.

GetNtoskrnl: push 0 ; enumemeriraj module
push SYSTEM_MODULE_INFORMATION_SIZE
push modinfo
push SystemModuleInformation
@sysenter NtQuerySystemInformation, 4

mov esi, modinfo ; eax = broj modula
lodsd ; esi = ptr na prvi modul
xchg eax, ebx

__loop: mov edi, esi
add edi, SYSMOD_abName

push edi
call MojPathFindFileName

IFDEF SYSER_DEBUGGING
; kad je syser instaliran imamo kernel1.exe!

mov eax, 0804D7000h

mov ntoskrnl, eax
jmp __nasao_ntoskrnl
ENDIF
or [eax], 20202020h ; pretvori u mala slova, 5. bit postavljen -> malo slovo
cmp [eax], 'sotn'
jnz __2
or [eax+4], 20202020h
cmp [eax+4], 'lnrk'
jnz __2
or [eax+8], 20202020h
cmp [eax+8], 'exe.'
jnz __2

mov eax, [esi.SYSMOD_pAddress] ; imamo kernel!
mov ntoskrnl, eax
jmp __nasao_ntoskrnl

__2: add esi, SYSTEM_MODULE_SIZE
dec ebx
jns __loop

jmp $ ; nismo nasli kernel...major fuckup :/

U slucaju da smo nasli bazu od ntoskrnl.exe, otvaramo handle na inicijalizirani
objattr:

__nasao_ntoskrnl: lea eax, objattr
push eax
push WRITE_DAC + READ_CONTROL
lea eax, mem_section

push eax
@sysenter NtOpenSection, 3

mov ebx, mem_section

Sad uzimamo sigurnosni deskriptor handle-a i modificiramo ga, jerbo po defaultu
samo Admininistrator/SYSTEM mogu pisati po \Device\PhysicalMemory, pri cemu
stvaramo novi ACL sa zeljenim pravima, te apdejtamo sigurnosni deskriptor sa tim
ACL:

xor ebx, ebx

@get_dll_api advapi32.dll, GetSecurityInfo

lea ecx, p_sec_descr
push ecx
push ebx
lea ecx, p_old_dacl
push ecx
push ebx
push ebx
push DACL_SECURITY_INFORMATION
push SE_KERNEL_OBJECT
push mem_section
call eax
mov access.EXPACCESS_grfAccessPermissions, SECTION_ALL_ACCESS
inc ebx
mov access.EXPACCESS_grfAccessMode, ebx ; GRANT_ACCESS
mov access.EXPACCESS_Trustee.TRUSTEE_TrusteeForm, ebx ; TRUSTEE_IS_NAME
mov access.EXPACCESS_Trustee.TRUSTEE_TrusteeType, ebx ; TRUSTEE_IS_USER
dec ebx

@pushsz "CURRENT_USER"
pop access.EXPACCESS_Trustee.TRUSTEE_ptstrName

@get_dll_api advapi32.dll, SetEntriesInAclA
xchg eax, edx

lea ecx, p_new_dacl
push ecx
push p_old_dacl
lodsd
push esi ; offset access
push 1
call edx ; stvori novi ACL

@get_dll_api advapi32.dll, SetSecurityInfo

push ebx
push p_new_dacl
push ebx
push ebx
push DACL_SECURITY_INFORMATION
push SE_KERNEL_OBJECT
push mem_section
call eax ; apdejtaj sigurnosni deskriptor sa novim ACL

push mem_section
@sysenter NtClose, 1


Sad kad imamo r/w pristup, ponovno otvaramo handle:

lea eax, objattr
push eax
push SECTION_MAP_READ + SECTION_MAP_WRITE
lea eax, mem_section
push eax
@sysenter NtOpenSection, 3

E sad trebamo stvoriti callgate kako spada. U biti mi nas callgate instaliramo
na zapis broj GDT_SELEKTOR, a koji je postavljen na 99 u ovom kodu (trebao bi
biti slobodan na svim verzijama win). Pravilna bi metoda bila skeniranje svih
zapisa u gdt, te trazenje slobodnog, tj. onog koji nije prisutan (nema
postavljen P bit (od Present)).

Bazu gdt dobivamo sgdt instrukcijom, a izgled strukture koju ona vraca je u DDK
definirana ovako:

typedef struct _KGDTENTRY {
WORD LimitLow; // velicina GDT u bajtovima
WORD BaseLow; // adresa GDT (donji dio)
WORD BaseHigh; // adresa GDT (gornji dio)
} KGDTENTRY, *PKGDTENTRY;

Pri cemu je dobivena adresa virtualna, te je trebamo prevesti u fizicku, tj.
pozivamo GetPhysicalAddress proceduru sa parametrom BaseHigh:BaseLow.

Sam callgate deskriptor zapis jest definiran sljedecom strukturom:

typedef struct _CALLGATE_DESCRIPTOR {
USHORT offset_0_15; // donji dio adrese r0 koda
USHORT selector;
UCHAR param_count :4;
UCHAR some_bits :4;
UCHAR type :4; // tip segmenta ili gate-a
UCHAR app_system :1; // segment deskriptor (0) ili sistemski segment (1)
UCHAR dpl :2; // iz kojeg prstena ga mozemo zvati
UCHAR present :1;
USHORT offset_16_31; // gornji dio adrese r0 koda
} CALLGATE_DESCRIPTOR, *PCALLGATE_DESCRIPTOR;

te je u nasem asm kodu ovako preddefiniran varijablom callgatex:

callgatex dw 0 ; donji dio adrese
dw KGDT_R0_CODE ; NT ring0 selektor
dw 1110110000000000b ; misc bitovi (5 najnizih = # parametara)
dw 0
KGDT_R0_CODE je u DDK definiran kao ring0 selektor na NT-u (08h)

Takodjer mozemo specificirati broj parametara ukoliko ih ring0 procedura uopce
prima. Vazno je naglasiti da u tom slucaju ona sama mora pocistiti stog prije
izlaska.

@get_delta TMP_DLTA5, edx
lea eax, ring0_proc TMP_DLTA5

; donji dio adrese

mov (callgatex TMP_DLTA5).word ptr 0, ax

; gornji dio adrese

shr eax, 16
mov (callgatex+6 TMP_DLTA5).word ptr 0, ax

sgdt (l_gdt_limit).fword ptr 0

movzx ebx, (l_gdt_limit).word ptr 0 ; 3FFh
mov eax, l_gdt_base-2 ; 8003F000h

call GetPhysicalAddress
mov fizicka_adresa, eax

; izracunaj fizicku adresu GDT-a, vidi crazylordov papir za vise detalja..

mov eax, l_gdt_base-2
xor edx, edx
push eax
mov ecx, sys_info
add ecx, SYS_BASIC_INFO_AllocationGranularity ; 10000
mov ecx, [ecx]
div ecx
mov edi, edx
pop eax
inc ebx
mov esi, ebx ; mapirana velicina
add esi, edi
sub eax, edi
call GetPhysicalAddress

mov fizicka_adresa, eax
mov mapped_size, esi
mov ecx, mem_section

push PAGE_READWRITE
push 0
push 1
lea eax, mapped_size
push eax
lea eax, fizicka_adresa
push eax
push esi
push 0
lea eax, map_base
push eax
push -1
push ecx
@sysenter NtMapViewOfSection, 10

@get_delta TMP_DLTA6, ecx

; sad patchajmo fizicku adresu GDT sa nasim zapisom, broj GDT_SELEKTOR

add map_base, edi
mov eax, map_base
push (callgatex TMP_DLTA6).dword ptr 0
pop dword ptr [eax+GDT_SELEKTOR*8]
push (callgatex+4 TMP_DLTA6).dword ptr 0
pop dword ptr [eax+GDT_SELEKTOR*8+4]

; za farcall nam treba 6 bajtova, selektor:adresa. Sad patchajmo u varijabli
; farcall nas selektor.

mov (farcall+4).word ptr 0, GDT_SELEKTOR*8+3

; E sad moramo uraditi jednu sitnicu koja je jako vazna, a to je pri pozivu
; ring0 koda page u kojem se on nalazi *mora* biti prisutan u trenutnom
; kontekstu, inace cemo dobiti jedan fini BSOD :) To radimo tako sto lockamo
; virtualnu memoriju koju zauzima kod koji se izvrsava u ring0 koristeci
; nativni API NtLockVirtualMemory().

lea eax, ring0_proc TMP_DLTA6
push eax
mov eax, esp

push ring0_end - ring0_proc
mov edx, esp

push MAP_PROCESS
push edx ; lockamo kod i tako smanjujemo vjerojatnost da ce biti outpage-an
push eax
push -1 ; trenutni proces
@sysenter NtLockVirtualMemory, 4
add esp, 8

; opali farcall i skoci na 08h:ring0_proc

call fword ptr farcall

Pogledajmo sto se sve dogadja u ring0.

Prva stvar sto trebamo raditi jest spremiti *sve registre*

ring0_proc: pushf
pusha

; Moramo fs postaviti na 30h jer sistem ocekuje ds fs registar pokazuje na
; Processor Control Region kad se nit izvodi u ring0, a selektor 30h pokazuje na
; deskriptor cija je bazna adresa jednaka adresi PCR-a. Ovaj pocetni
; housekeeping jest nuzan uvijet ukoliko zelimo da nasa ring0 nit moze zvati
; kernel-mode servise koje exporta hal i ntoskrnl.

push fs
mov ebx, 30h
mov fs, bx

; Stvori nesto malo prostora na stogu, prolog standardni. Ovaj korak *moramo*
; napraviti jer je vezan za logiku kojom radi page faulthandler. Inace cemo
; dobiti BSOD.

sub esp, 50h
mov ebp, esp

; postavi exception frame na NULL, spremi stari na stog

mov edx, cs:[0FFDFF000h]
mov ds:[0FFDFF000h], 0FFFFFFFFh
mov [ebp], edx

; Spremi neka polja u TEB koja SoftIce zove "KSS EBP" (Kernel Stack Segment?);
; ovo moramo uraditi te ovo polje postaviti na trap frame stvoren u prethodnom
; koraku.

mov esi, cs:[0FFDFF124h] ; FFDFF124h = threnutni ETHREAD blok!
mov edx, [esi+128h] ; KTRAP_FRAME *TrapFrame
mov [ebp+4], edx ; spremi stari trap frame na stog
mov [esi+128h], ebp ; i postavi novi

; spremi vrijeme kernela i thread mode (kernel/user)

mov edi, [esi+137h] ; uint32 KernelTime (138h???)
mov [ebp+8], edi

; postavi thread mode (kernel/user) baziran na selektoru koda

and dword ptr [esi+137h], 1

; sad izracunavamo delta handle te u ring0 pozivamo rutinu za emuliranje
; brainfuck progrma. Ukoliko se u njoj dogodi neka greska..hm..BSOD :)

@get_delta TMP_DLTA2, ecx

mov edi, (bf_sim_stdout_polje TMP_DLTA2).dword ptr 0

push edi
push (bf_sim_polje TMP_DLTA2).dword ptr 0
push (bf_sim_clip TMP_DLTA2).dword ptr 0
call brainfuck_simuliraj2

; sad je u edi = rezultat izvodjenja programa, tj. onaj "stdout". Izracunavamo
; hash tog stringa:

xor eax, eax ; inicijalizacija hasha

__1: rol eax, 7
xor al, [edi]
inc edi ; slovo po slovo
cmp byte ptr [edi], 0
jnz __1

; sa PRODUKCIJA radimo provjeru da li je ovo testni ili stvarni kod za
; deployment kod reversera. U slucaju da je PRODUKCIJA definirana,
; radimo BSOD u slucaju da stdout ne sadrzi ono sto zelimo, inace se
; samo vracamo u ring3 kod.

IFDEF PRODUKCIJA

; pogodite koji je pass za crackme? :)

gethash <ph34rl355> ; y0 dudez! =)
mov ebx, hash
xor ebx, eax
jz ok_pass

;oh joy, we're being cracked! :o)

call get_ntoskrnl
mov ebx, eax

gethash <KeBugCheckEx>
push hash
call load_dll_address
call eax, 0bAdc0Deh, 0Badh, 0c0deh, 0Badh, 0c0deh, 0Badh ; bad code ;)

; nisam bas bio puno mashtovit ;)

ok_pass: call get_ntoskrnl
mov ebx, eax

gethash <KeBugCheckEx>
push hash
call load_dll_address

; "challenge completed!" = 63 68 61 6C 6C 65 6E 67 65 20 63 6F 6D 70 6C 65 74 65 64 21

call eax, 06368616Ch, 06C656E67h, 06520636Fh, 06D706C65h, 074656421h
ENDIF

; sad radimo standardni cleanup, vracamo sve kako je bilo i elegantno se vracamo..

; vrati KSS EBP
mov esi, cs:[0FFDFF124h]
mov ebx, [ebp+4]
mov [esi+128h], ebx

; vrati exception frame

mov ebx, [ebp]
mov fs:[0], ebx

; vrati thread mode

mov ebx, [ebp+8]
mov esi, fs:[124h]
mov byte ptr [esi+137h], bl
add esp, 50h
pop fs

popa
popf
retf

Onaj dio na pocetku i na kraju je jaaaako vazan ukoliko zelimo pozivate Ke* i
ostale kerne-mode rutine, jerbo inace ce njihovi pozivi vracati samo BSOD.
Koliko sam ja shvatio, mi jednostavno stvaramo ring0 nit "ni iz cega", te moramo
emulirati neka sranja na stogu koja radi PsCreateSystemThread(), prije nego bi
bili u stanju pozivati ntoskrnl exporte.

Nadalje, ja izgleda nikako nisam uspio natjerati da mi proradi jebeni kernel-
mode SEH: svaki put mi je top handler = 0ffffffffh :/ Mislim da se problem moze
popraviti stvaranjem nove sistemske niti sa PsCreateSystemThread(), ili nekako
drugacije, jer na netu postoji nekoliko kodova koji koriste SEH u ovakvom kodu
bez problema, ali pitam se da li su ih autori uopce testirali...

Nadalje, skok u ring0 ce sjebati apsolutno svaki application-level debugger i
tracer, dumper i hooker, tako da je ovo savrsena zastita protiv njih. I syser i
sice se na XP SP2 takodjer sjebu (daju BSOD) neposredno prije nekih dijelova
koda, jerbo valjda nit radi nesto sto oni misle da ne bi smjela radit :)

Primjenjivost ovakve tehnike je univerzalna, kao zastita. Recimo unutar ring0
mislim da je takodjer moguce napraviti running line (u slucaju da proradi SEH),
ili jos bolje, kao u dobrom starom MS-DOS-u, moguce je hookirat int 1 vektor i
preko njega raditi running line!!!

Mada bi mi trebalo valjda 5 dana i 5000 BSOD-ova samo da takvo nesto bolesno
proradi, pa implementaciju prepustam citatelju.

Recimo kul forica jest pozovete KeDelayExecutionThread() u beskonacnoj petlji
sa nekim timoutom bezveznim, i time ste efektivno vas proces ucinili besmrtnim,
jerbo win ne mogu terminirati proces sve dok on ima ring0 komponentu aktivnu.
Vrlo kul :)

U ring0 kodu trazenje kernel-mode exporta hal-a i ntoskrnl radimo na isti nacin
kao sto u ring3 radimo trazenje adresa API-ja: parsanjem export tablice. Samo
sto za naci bazu ntoskrnl mozemo koristiti jedan trik:

---------------------------------------------------------------------------
; Trazi bazu od ntoskrnl.exe koristeci mid-delta metodu. Pocetna adresa jest
; adresa prvog zapisa u IDT koji se uvijek nalazi negdje unutar ntoskrnl.exe
; Mozemo koristiti sidt, ali posto se IDT referencira uvijek sa ptr-om na
; adresi FFDFF038, mozemo ga koristiti direktno.
; i: r0
;
; o: eax = baza od ntoskrnl.exe.
; NB: na XP SP2 ntoskrnl.exe se nalazi na adresi 804D7000
; ---------------------------------------------------------------------------
get_ntoskrnl:

IFDEF SYSER_DEBUGGING
mov eax, 0804D7000h
retn
ENDIF

pusha

mov esi, ds:[0FFDFF038h]
lodsd ; esi = ptr na IDT
cdq
lodsd ; eax = ptr na neku adresu unutar ntoskrnl.exe
and eax, -1 shl 12 ; poravnaj na 1 page
add eax, 1000h

__loop: sub eax, 1000h
cmp [eax], 00905a4dh ; provjeri 'MZ' signaturu
jnz __loop
mov [esp.Pushad_eax], eax
popa

retn

Koji, kako sam ja sa veseljem punim desetina BSOD-ova otkrio, nece raditi
ukoliko je syser aktivan jerbo on relocira IDT negdje u svoj vlastiti driver,
tako da na kraju dobijemo njegovu adresu :) Zato sam i stavimo hard-coded adresu
da fja vraca ukoliko sa njim debuggiramo (0804D7000h na XP SP2).

Ali postoji jos jedna puno bolja metoda koju ja ovdje ne koristim, pogledajte
[16] i [17] za vise informacija. PCR rox :>

Rekao sam takodjer da nas ring0 kod mora sam pocistiti stog. U slucaju da mu iz
ring3 proslijedjujemo parametre, moramo u opisu callgate dekskriptora naznaciti
njihov broj, te u onaj zadnji retf u ring0 kodu dodati 4*broj_parametara. Cesto
se poneka informacija koju je lako prikupiti u ring3 moze (recimo baza od
ntoskrnl) moze tako, na lak nacin, prenijeti ring0 kodu, bez da ovaj radi nesto
potencijalno bugovito.

Vratimo se sada na ring3 kod koji je opalio farcall. Radimo standardni cleanup:

; prvo unlockamo komadic virtualne memorije koju smo lockali

@get_delta TMP_DLTA7, ecx
lea eax, ring0_proc TMP_DLTA7
push eax
mov eax, esp

push ring0_end - ring0_proc
mov edx, esp

push MAP_PROCESS
push edx
push eax
push -1 ; trenutni proces
@sysenter NtUnlockVirtualMemory, 4
add esp, 8

; nakon toga zatvaramo \Device\PhysicalMemory objekt te sve alocirane handlove
; te clipboard i alociranu memoriju, te gasimo nas vlastiti proces. Sjetite se
; da se u onom C# kodu ova rutina pozva *zadnja*, tako da i u slucaju da u ovom
; kodu nesto se srusi, C# kod ce sam nakon 2s se ugasiti.

push map_base
push -1
@sysenter NtUnmapViewOfSection, 2

push mem_section
@sysenter NtClose, 1

@Ntfree SYSTEM_MODULE_INFORMATION_SIZE, modinfo

@Ntfree SYSTEM_INFO_SIZE, sys_info

zatvori_clip: @sysenter NtUserCloseClipboard, 0

jmp kraj

greska: jmp $

kraj: push ebx
push -1
@sysenter NtTerminateProcess, 2

I to bi bilo to :)

Ako ste pazljivo citali, a ne samo skocili na kraj da vidite koje je rjesenje
crackmeja (lameri, sod off!), vec sad znate da je rjesnje crackmeja program
napisan u brainfucku koji na stdout ispisuje "ph34rl355" pejstan na clipboard!

String "ph34rl355" se ne pojavljuje direktno u kodu, vec se u onoj ring0
proceduri usporedjuju hashevi. Algoritam za hashiranje ima 7 instrukcija i moze
se valjda brutforsati u nekom realnom vremenu :> Takodjer vjerojatno ima vec i
gomila stringova (i ne samo stringova, nego opcenito nizova bajtova) koji imaju
isti hash, ali ja valjda pretpostavljam da ce brutforser biti dovoljno razuman
samo da ukljuci one iz ASCII charseta i da pretpostavi da naziv ima nekakve veze
sa phearless :)

I to bi bilo to!

///////////////////////////////////////////////////////////////////////////
--[ Greets
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\

deroko - kad cemo izmlatiti dum-duma? =)

Shatterhand - pazi da ti oci ne ispadnu dok ovo budes editirao :P

DownBload - uuu, sad smo profi :)) l33t!

_bl00dz3r0_ - nmap -sS -sV -sU -vv -D picka,sisa,usta www.kurac.com :>

De1Rekt0n - nemoj vise DoS-ati forum :)

reiser - ide li taj crv vise, vec godinu dana ga pises? :D

argv - kad cemo vise pokoriti du? :-) Ja dam botove, ti wlan
hackove :>

ventura - stvarno nista ne mozes nauciti kroz ovaj virmaking, ha?

te jos root9, VRKY, ap0x, Exoduks, BaCkSpAcE, esc, ACidCooKie, m4rk0, EArthquake
i svima na #ugs, [es] (Asembler/Virii..) & blackhat forums.

vx 4ever.

///////////////////////////////////////////////////////////////////////////
--[ Reference
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\


[1] http://undocumented.ntinternals.net/UserMode/Undocumented%20Functions/Compression/RtlCompressBuffer.html

[2] http://undocumented.ntinternals.net/UserMode/Undocumented%20Functions/Compression/RtlDecompressBuffer.html

[3] http://undocumented.ntinternals.net/UserMode/Undocumented%20Functions/Compression/RtlGetCompressionWorkSpaceSize.html

[4] www.pinvoke.net

[5] http://www.elitesecurity.org/tema/106239-syser-vas-novi-omiljeni-kernel-mode-debugger

[6] http://msdn.microsoft.com/net/sscli/

[7] http://standards.ieee.org/regauth/oui/oui.txt

[8] http://www.microsoft.com/downloads/details.aspx?displaylang=en&FamilyID=6430F853-1120-48DB-8CC5-F2ABDC3ED314

[9] http://www.microsoft.com/downloads/details.aspx?FamilyId=9B3A2CA6-3647-4070-9F41-A333C6B9181D&displaylang=en

[10] www.fh-zwickau.de/doc/prmo/docs/pdf/24368901.pdf

[11] ed2k://|file|windows_2000_source_code.zip|213748207|34BB9F3A3E8D3E0C4490A96EC30B9F3C|/

[12] http://www.muppetlabs.com/~breadbox/bf/

[13] http://www.madchat.org/vxdevl/vxmags/riot7/REALITY.004

[14] http://www.codebreakers-journal.com/include/getdoc.php?id=28&article=21&mode=pdf

[15] http://www.phrack.org/phrack/59/p59-0x10.txt

[16] http://www.bitrake.com/phpBB2/viewtopic.php?t=121

[17] http://www.rootkit.com/newsread.php?newsid=153

[18] http://groups.google.com/groups?q=SEH+__try&hl=hr&lr=&group=comp.os.ms-windows.programmer.nt.kernel-mode&selm=5bunp0%24ok%40dim.intersurf.net&rnum=3


Korisna literatura:

* Microsoft Press: Inside Microsoft Windows 2000 3d Edition by by David A. Solomon and Mark E. Russinovich

* Microsoft Press: Programming the Microsoft Windows Driver Model 2nd Edition by Walter Oney

← previous
next →
loading
sending ...
New to Neperos ? Sign Up for free
download Neperos App from Google Play
install Neperos as PWA

Let's discover also

Recent Articles

Recent Comments

Neperos cookies
This website uses cookies to store your preferences and improve the service. Cookies authorization will allow me and / or my partners to process personal data such as browsing behaviour.

By pressing OK you agree to the Terms of Service and acknowledge the Privacy Policy

By pressing REJECT you will be able to continue to use Neperos (like read articles or write comments) but some important cookies will not be set. This may affect certain features and functions of the platform.
OK
REJECT