6/12/2010

CTF: Подробное решение квеста crackme.com

/** Positive Technologes for Positive People! CTF preview. **/

Собственно, рассмотрим крякмис от Positive Technologies, суть которого заключается в поиске логина и пароля, на основе которого расшифровывается строка "Positive Technologes for Positive People! CTF preview". Тест представлен ввиде MSDOS- приложения crackme.com (см. архив).

При запуске программа просит ввести логин и пароль, после чего проводит некоторую обработку и выводит расшифрованную строку на экран. Банальный брут здесь не эффективен, поэтому следует погрузиться в недры теста и провести некоторую рекогносцировку, чем собственно и займемся. И так:


start_0 proc near ; CODE XREF: start
seg000:06AD
seg000:06AD loginlen = word ptr -8
seg000:06AD iter = word ptr -6
seg000:06AD xored_login_add_1= word ptr -4
seg000:06AD var_2 = byte ptr -2
seg000:06AD
seg000:06AD enter 8, 0
seg000:06B1 push 76Eh ; offset of "login:"
seg000:06B4 call printf
seg000:06B7 add sp, 2
seg000:06BA mov [bp+loginlen], 0
seg000:06BF loc_106BF: ; CODE XREF: start_0+2B
seg000:06BF call read_key
seg000:06C2 mov si, [bp+loginlen]
seg000:06C5 mov [si+79Ah], al
seg000:06C9 call echo_key
seg000:06CC inc [bp+loginlen]
seg000:06CF cmp [bp+loginlen], 0FFh
seg000:06D4 jge short loc_106DA
seg000:06D6 cmp al, 0Dh
seg000:06D8 jnz short loc_106BF
seg000:06DA loc_106DA: ; CODE XREF: start_0+27
seg000:06DA mov al, 0Ah
seg000:06DC call echo_key
seg000:06DF dec [bp+loginlen]
seg000:06E2 mov si, [bp+loginlen]
seg000:06E5 mov byte ptr [si+79Ah], 0
seg000:06EA push large 0
seg000:06ED pop large [dword_10420]
seg000:06F2 mov [bp+iter], 0
seg000:06F7 loc_106F7: ; CODE XREF: start_0+65
seg000:06F7 mov ax, [bp+loginlen]
seg000:06FA cmp [bp+iter], ax
seg000:06FD jge loc_10714
seg000:0701 mov si, [bp+iter]
seg000:0704 movsx eax, byte ptr [si+79Ah]
seg000:070A xor dword_10420, eax
seg000:070F inc [bp+iter]
seg000:0712 jmp short loc_106F7
seg000:0714 loc_10714: ; CODE XREF: start_0+50
seg000:0714 and dword_10420, 10h
seg000:071A push 775h ; offset "password:"
seg000:071D call printf
seg000:0720 add sp, 2
seg000:0723 mov [bp+iter], 0
seg000:0728 mov ax, word ptr dword_10420 ; xored login
seg000:072B inc ax
seg000:072C mov [bp+xored_login_add_1], ax
seg000:072F loc_1072F: ; CODE XREF: start_0+A3
seg000:072F call read_key
seg000:0732 mov [bp+var_2], al
seg000:0735 mov al, [bp+var_2]
seg000:0738 mov si, [bp+iter]
seg000:073B mov [si+424h], al
seg000:073F mov ax, [bp+xored_login_add_1]
seg000:0742 cmp [bp+iter], ax
seg000:0745 jge loc_1074C
seg000:0749 inc [bp+iter]
seg000:074C loc_1074C: ; CODE XREF: start_0+98
seg000:074C cmp [bp+var_2], 0Dh
seg000:0750 jnz short loc_1072F
seg000:0752 push 442h
seg000:0755 push 89Ah
seg000:0758 push 37h
seg000:075A push 424h
seg000:075D call stream_decode@8
seg000:0760 push 89Ah
seg000:0763 push 77Fh
seg000:0766 call printf
seg000:0769 add sp, 4
seg000:076C leave
seg000:076D retn
seg000:076D start_0 endp


В начале программа читает логин до тех пор, пока не будет введен 0x0D (по нашему клавиша Enter) или не будет достигнута максимальная длина логина. После ввода логина программа последовательно XOR-ит каждый последущий символ логина:


seg000:06F7 mov ax, [bp+loginlen]
seg000:06FA cmp [bp+iter], ax
seg000:06FD jge loc_10714
seg000:0701 mov si, [bp+iter]
seg000:0704 movsx eax, byte ptr [si+79Ah]
seg000:070A xor dword_10420, eax
seg000:070F inc [bp+iter]
seg000:0712 jmp short loc_106F7


Говоря на языке C конструкция выглядит так:


extern unsigned char login[0xFF];
extern DWORD login_xored_0x0420;
extern unsigned char password_0x0424[0x10+1];

extern DWORD namelen;
for ( login_xored_0x0420 =0 , i=0; i < loginlen; i++ )
{
login_xored_0x0420 = login_xored_0x0420 ^ (DWORD)(login[i]);
}


После чего на XOR-еный логин накладывает маску ввиде AND login_xored_0x0420, 0x10. По сути, dw_0x0420 определеяет длину вводимого пароля, что мы и увидим ниже, а на данный момент достаточно выполнения условия, что (XOR(login)) AND 0x10 == 0x10.

Далее читается пароль, длиной которого является значение dw_0x0420. Сам пароль размещается сразу за login_xored_0x0420 по смещению 0x0424. После того, как пароль считан, вызывается нечто:


seg000:0752 push 442h ; arg_3 пока неизвестно
seg000:0755 push 89Ah ; arg_2 пока неизвестно
seg000:0758 push 37h ; arg_1 пока неизвестно
seg000:075A push 424h ; arg_0 пока неизвестно
seg000:075D call stream_decode@8


Названа stream_decode@8 таковой потому, что больше собственно расшифровывать текст в квесте некому. Для начала хотелось бы понять, что за параметры она принимает. При анализе кода start, stream_decode@8 выясняется, что:

arg_0==0х0424 - адрес пароля
arg_1==0x0037 - длина результирующей строки
arg_2==0х089A - адрес строки, выводимой на экран как результат.
arg_3==0х0442 - адрес вот такого массива:
unsigned char Mask[] = {
0x34, 0x69, 0xDB, 0xB8, 0x87, 0xB1, 0x7C, 0x3A, 0xE1, 0x4C,
0x4F, 0x46, 0x37, 0x20, 0x44, 0x75, 0x29, 0x1B, 0x9F, 0x86,
0x74, 0x16, 0xCD, 0x40, 0x38, 0x0F, 0x03, 0x22, 0x4E, 0x52,
0x4F, 0x65, 0xF2, 0x44, 0xF2, 0x76, 0xA6, 0xE3, 0x5E, 0x3A,
0x70, 0x16, 0x81, 0x73, 0x8A, 0x4C, 0x51, 0x12, 0xDB, 0xBF,
0x72, 0xE4, 0x52, 0x81, 0x06
};


Отбросим мусор в stream_decode@8 и выделим код, который несет нагрузку:
; arg_pass_value адрес пароля (==0х0424)
; arg_output_length в качестве (==0x0037)
; arg_out_string адрес результирующей строки (==0х089A)
; arg_pMask адрес массива Mask (==0х0442)
; var_Iter в качестве итератора (принимает значения от 0 до arg_output_length)
; offset_of_pointer как некоторое значение, которое косвенно определяет позиции символов, расшифровываемых
; на каждом этапе итерации (рассмотрим ниже, как к этому пришли)
; var_decoded_char промежуточное значение, которое было получено в результате вызова
; decode_current_char@8
;
seg000:056B mov ax, [bp+arg_output_length] ;
seg000:056E mov [bp+offset_of_pointer], ax
...
seg000:061D loc_1061D:
seg000:061D mov [bp+var_Iter], 0
seg000:0623 loc_10623: ; CODE XREF: stream_decode@8+1BC
seg000:0623 mov ax, [bp+arg_output_length]
seg000:0626 cmp [bp+var_Iter], ax
seg000:062A jge locret_106A9
... ;
... ; МУСОР
... ;
seg000:0640 push [bp+offset_of_pointer]
seg000:0644 push [bp+arg_pass_value] ; 0x0424
seg000:0647 call decode_current_char@8
seg000:064A mov [bp+var_decoded_char], al
... ;
... ; МУСОР
... ;
seg000:0660 mov si, [bp+arg_pMask]
seg000:0663 add si, [bp+var_Iter]
seg000:0667 mov al, [si]
seg000:0669 xor al, [bp+var_decoded_char]
seg000:066D mov si, [bp+arg_out_string] ; 0x089A
seg000:0670 add si, [bp+var_Iter] ; current iter
seg000:0674 mov [si], al
... ;
... ; МУСОР
... ;
seg000:0683 movzx ax, [bp+var_decoded_char]
seg000:0688 add ax, [bp+var_Iter]
seg000:068C mov [bp+offset_of_pointer], ax
... ;
... ; МУСОР
... ;
seg000:06A2 inc [bp+var_Iter]
seg000:06A6 jmp loc_10623
seg000:06A9 locret_106A9: ; CODE XREF: stream_decode@8+140
seg000:06A9 leave
seg000:06AA retn 8


В языке С эта функция выглядит примерно так:


extern unsigned char decode_current_char ( char * sz_password_in , unsigned short offset_of_pointer_in ) ;

void decode_stream (
__in char * sz_password_in ,
__in unsigned short length_in ,
__out char *sz_result_string_in ,
__in unsigned char * mask_in
)
{
unsigned short offset_of_pointer = length_in ;
unsigned char var_decoded_char ;

for ( unsigned short Iter = 0 ; Iter < length_in ; Iter++ )
{
var_decoded_char = decode_current_char ( sz_password_in , offset_of_pointer ) ;
sz_result_string_in[Iter] = var_decoded_char ^ mask_in[Iter] ;
offset_of_pointer = Iter + (unsigned short)var_decoded_char ;
}
return;
}


Из нее следует подчерпнуть следующую информацию, а именно: во время XOR-а var_decoded_char и элемента массива Mask на каждом шаге итерации мы получаем конечное значение символа разшифровываемой строки. Исходя из этого, мы можем построить массив "правильных" значений (назовем его result_before_xor) var_decoded_char для каждого символа конечной строки "Positive Technologes for Positive People! CTF preview":


unsigned char result_before_xor[0x37];
char * result = "Positive Technologes for Positive People! CTF preview.";
for (int i=0; i < 0x37 ; i++ )
{
result_before_xor[i] = result[i] ^ Mask[i]; // Mask определен выше
}


Получается нечто, похожее на:

unsigned char result_before_xor[0x37] =
{
0x64, 0x06, 0xa8, 0xd1, 0xf3, 0xd8, 0x0a, 0x5f,
0xc1, 0x18, 0x2a, 0x25, 0x5f, 0x4e, 0x2b, 0x19,
0x46, 0x7c, 0xfa, 0xf5, 0x54, 0x70, 0xa2, 0x32,
0x18, 0x5f, 0x6c, 0x51, 0x27, 0x26, 0x26, 0x13,
0x97, 0x64, 0xa2, 0x13, 0xc9, 0x93, 0x32, 0x5f,
0x51, 0x36, 0xc2, 0x27, 0xcc, 0x6c, 0x21, 0x60,
0xbe, 0xc9, 0x1b, 0x81, 0x25, 0xaf, 0x26
};


И так, мы знаем каждое значение для верного пароля, которое будет на выходе функции decode_current_char@8. Нам остается создать функцию, гипотетически обратную функции decode_current_char@8. Поясню: есть функция Yi = F(X1,Xi2), которой по сути является decode_current_char, тогда нужно создать такую функцию Z, что a = Z(F(X1,Xi2)), где а - символ (один или более) пароля. Следовательно, рассмотрим алгоритм decode_current_char@8. При создании функции Z следует обратить внимание на то, возможно ли сделать инверсию алгоритма или нет. Смотрим:


seg000:0479 decode_current_char proc near ; CODE XREF: stream_decode@16+15D
seg000:0479
seg000:0479 poisition_in_pass = dword ptr -6
seg000:0479 pair_chars_of_pass = word ptr -2
seg000:0479 arg_sz_pass_value = word ptr 4
seg000:0479 offset_of_pointer = word ptr 6
seg000:0479
seg000:0479 enter 6, 0
seg000:047D movsx eax, [bp+arg_offset_of_pointer]
seg000:0482 shr eax, 3
seg000:0486 mov [bp+poisition_in_pass], eax
seg000:048A and [bp+offset_of_pointer], 7
seg000:048E loc_1048E: ; CODE XREF: decode_current_char+29
seg000:048E mov eax, dword_10420
seg000:0492 cmp [bp+poisition_in_pass], eax
seg000:0496 jb loc_104A4
seg000:049A mov eax, dword_10420
seg000:049E sub [bp+poisition_in_pass], eax
seg000:04A2 jmp short loc_1048E
seg000:04A4 loc_104A4: ; CODE XREF: decode_current_char+1D
seg000:04A4 mov si, [bp+arg_sz_pass_value]
seg000:04A7 add si, word ptr [bp+poisition_in_pass]
seg000:04AA movzx ax, byte ptr [si]
seg000:04AD mov [bp+pair_chars_of_pass], ax
seg000:04B0 inc [bp+poisition_in_pass]
seg000:04B4 mov eax, dword_10420
seg000:04B8 cmp [bp+poisition_in_pass], eax
seg000:04BC jb loc_104C8
seg000:04C0 mov eax, dword_10420
seg000:04C4 sub [bp+poisition_in_pass], eax
seg000:04C8 loc_104C8: ; CODE XREF: decode_current_char+43
seg000:04C8 mov si, [bp+arg_sz_pass_value]
seg000:04CB add si, word ptr [bp+poisition_in_pass]
seg000:04CE movzx ax, byte ptr [si]
seg000:04D1 shl ax, 8
seg000:04D4 or [bp+pass_char], ax
seg000:04D7 inc [bp+poisition_in_pass]
seg000:04DB mov cl, byte ptr [bp+offset_of_pointer]
seg000:04DE shr [bp+pair_chars_of_pass], cl
seg000:04E1 mov al, byte ptr [bp+pair_chars_of_pass]
seg000:04E4 and al, 0FFh
seg000:04E6 leave
seg000:04E7 retn 4
seg000:04E7 decode_current_char endp


Сперва определяется стартовая позиция символов пароля, на основе которых будет расшифровываться очередной символ зашифрованной строки.Сделует отметить, что каждый символ зашифрованной строки расшифровывается на основе двух соседних символов пароля, позиция первого символа задается вычисляемым значением poisition_in_pass. В случае если poisition_in_pass является последним символом в пароле, то в качестве второго символа задействуется нулевой символ пароля. Последовательность действий следующая:
-1- получить смещение пары символов пароля, на основе которых строится символ расшифровываемый строки (точнее символ из масиива result_before_xor[]- его мы определили ранее). Обозначим смещение как i.
-2- определить число битов для сдвига для "спаренного" значения двух соседних символов пароля. Обозначим как shifter.
-3- извлечь i-ый символ пароля как S1.
-4- извлечь ((i+1)&0x10)-ый символ пароля и сдвинуть его значение нв 8 бит влево. Обозначим рез-т как S2.
-5- произвести операцию OR на элементами из п.4 и п.5 (RES1 = OR(S2,S1))
-6- сдвинуть вправо результат из п.5 на количество битов, определенное в п.2 (RES2 = RES1 >> shifter)
-7- взять младший байт из результата п.6 и вернуть. (return LOWBYTE(RES2))

Стоит отметить один важную особенность алгоритма: ЕСЛИ ЗНАЧЕНИЕ shifter НУЛЕВОЕ, ТО РЕЗУЛЬТИРУЮЩИЙ БАЙТ ЯВЛЯЕТСЯ ПЕРВЫМ СИМВОЛОМ ИЗ ПАРЫ СИМВОЛОВ ПАРОЛЯ. Эта особенность позволяет сразу выявить без предварительного анализа некоторые символы пароля.
Теперь построим обратный алгоритм. Вспомним массив Mask[]. Там каждый элемент является усечением результата pair_chars_of_pass, то есть выполнимо тождество:


mask[i] == (unsigned char)pair_chars_of_pass. Здесь pair_chars_of_pass имеет тип unsigned short.



Теперь ВАЖНЫЙ МОМЕНТ: мы помним как вычисляется значение offset_of_pointer: как сумма результирующего байта var_decoded_char и значения Iter (если придерживаться 'C'-версии функции decode_stream), причем для Iter==0 offset_of_pointer==0x0037. Таким образом мы знаем значения и arg_offset_of_pointer, и poisition_in_pass (из участка кода, представляющего ф-цию decode_current_char). Будем использовать три входных параметра:
I-значение итерации,
Res - I-ый байт из массива mask[],
offset_of_pointer- расчитываемое значение смещения первого используемого символа пароля.

При этом для offset_of_pointer действует следующая зависимость:
offset_of_pointer(i) = Res(i-1) + i;
Теперь осталось построить функционал преобразования этих трех параметров, который выплюнет нам на выходе конечный символ пароля или некотороую битовую составляющую, которая обязательно должна быть представлена в символе пароля.
В этом алгоритме самым важных пожалуй будет получить значение pair_chars_of_pass (назовем его как Ored) до опереции:


...
seg000:04DE shr [bp+pair_chars_of_pass], cl
...


где cl == (offset_of_pointer(i) & 7).
А значение Ored определим как результат операции, обратной к операции по адресу seg000:04DE, то есть
Ored(i) = Res(i) << (offset_of_pointer(i) & 7).
Согласен с тем, что Ored(i) может не содержать полного представления двух символов пароля, поскольку мы обладаем только усеченным значением pair_chars_of_pass:


seg000:04E1 mov al, byte ptr [bp+pair_chars_of_pass]
seg000:04E4 and al, 0FFh


И следовательно первый символ символьной пары из пароля есть как LOWBYTE(Ored(i)), а второй HIGHBYTE(Ored(i)).
Ранее было выявлено очень важное свойство алгоритма: если значение (offset_of_pointer(i) & 7) равно нулю, то получаем 100% правильное значение 1-го символа из парольной пары. По остальным символам можно поступить следующим образом: если "светится" очердная позиция символа пароля, то значение этого символа можно определить как
password[i] = password[i] | LOWBYTE(Ored(i))
password[i+1] = password[i+1] | HIGHBYTE(Ored(i)).
Позиции символов мы расчитваем так же как они представлены в функции decode_current_char.

После переноса рассуждений и умозаключений в программную реализацию получаем следующий пароль (звездочкой помечены те символы пароля, которые были вычислены на основе равенства нулю результата выражения offset_of_pointer(i) & 7):
pass[0] = '@';
pass[1] = 'T'; // *
pass[2] = '_'; // *
pass[3] = 'B';
pass[4] = 'T';
pass[5] = 'F'; // *
pass[6] = '_'; // *
pass[7] = '2'; // *
pass[8] = '0';
pass[9] = '1';
pass[0xA] = '0';
pass[0xB] = '_'; // *
pass[0xC] = 'a';
pass[0xD] = 'p'; // *
pass[0xE] = '2';
pass[0xF] = 'l'; // *
Как вы могли заметить, логин определяет только размер пароля: либо 0х10 символов, либо 0 (во втором случае программа не вернет вам управление). Считайте, что логин явялется флагом, определяющим возможность дальнейшей обработки ввода.

А вот код программы на закуску:


#include "windows.h"
#include

#define STREAM_LENGTH 55

// расшифрованная строка
unsigned char *stream_decoded = "Positive Technologes for Positive People! CTF preview.";

unsigned char stream_mask[STREAM_LENGTH] = {
0x34, 0x69, 0xDB, 0xB8, 0x87, 0xB1, 0x7C, 0x3A, 0xE1, 0x4C, 0x4F, 0x46, 0x37, 0x20, 0x44, 0x75,
0x29, 0x1B, 0x9F, 0x86, 0x74, 0x16, 0xCD, 0x40, 0x38, 0x0F, 0x03, 0x22, 0x4E, 0x52, 0x4F, 0x65,
0xF2, 0x44, 0xF2, 0x76, 0xA6, 0xE3, 0x5E, 0x3A, 0x70, 0x16, 0x81, 0x73, 0x8A, 0x4C, 0x51, 0x12,
0xDB, 0xBF, 0x72, 0xE4, 0x52, 0x81, 0x06
};

unsigned char stream_unXORed[STREAM_LENGTH];

#define PASSWORD_LEN 0x10
unsigned char password[PASSWORD_LEN];


int main(int argc, char **argv)
{
for (int i=0;i {
stream_unXORed[i] = stream_decoded[i] ^ stream_mask[i];
}

memset(password,0, PASSWORD_LEN);

for (int t = 0x02 ;t {
unsigned char res_prev = stream_unXORed[t];
unsigned char res_t = stream_unXORed[t+1];
unsigned char It = res_prev + t;
unsigned short unkn=0, ored=0;
unsigned char ored_high=0, ored_low=0;
unsigned int shifter=0; // вычисляемая позиция первого символа из пары символов пароля

__asm {
pushad
xor eax,eax
mov al, It
shr eax, 3
mov shifter, eax
popad
}
unkn = (unsigned short)(It)&0x7;
while(shifter>=0x10)
{
shifter -= 0x10;
}
__asm
{
pushad
xor eax, eax
xor ecx, ecx
mov al, res_t
mov cx, unkn
shl eax, cl
mov ored, ax
mov ored_low, al
mov ored_high, ah
popad
}
password[shifter] = password[shifter] | ored_low;
((shifter+1)==0x10)
? (password[0] = password[0] | ored_high )
: (password[shifter+1] = password[shifter+1] | ored_high );
printf( "----------------- T= 0x%02x RES_t= 0x%02x -----------------\n"
" password[0x%01X;0x%01X]\n"
" unkn={0x%02x}\n"
" ored={0x%04X} %s\n\n",
t,res_t,
shifter, shifter+1,
unkn,
ored,
((unkn) ? " " : "100% MATCHED")
);
}

printf("-------------PASSWORD------------\n");
for (int m=0;m {
printf("pass[0x%02x] = 0x%02X\n", m,password[m]);
}

return 0;
}


3 комментария: