Wellcome, уважаемая аудитория.
Не так давно у нас опять появились задания с очередного ещё одного цтф. Осё здесь ссылочка
Для примера, возьмём заданьице полегче. Pirate. Из инструментов IDA Pro 7.0 + Visual Studio 2017 + WinHex (да-да, можно без него, но я вот так привык). Тут нам обещают антиотладку. Плагинов для сокрытия нет никаких, работаем.
Загрузив файл, видим приглашение ввести флаг:
Ниже чтение флага и сравнение его длины с 35:Code:push offset Str ; "Enter the flag please: " mov eax, ds:std::basic_ostream<char,std::char_traits<char>> std::cout push eax ; int call sub_4014F0
АнтиотладкаCode:push 0Ah push 0 push 24h lea ecx, [ebp+Str] push ecx mov ecx, ds:std::basic_istream<char,std::char_traits<char>> std::cin call ds:std::basic_istream<char,std::char_traits<char>>::getline(char *,__int64,char) lea edx, [ebp+Str] push edx ; Str call ds:strlen add esp, 4 cmp eax, 23h jbe short loc_401140
- GetTickCount
- CheckRemoteDebuggerPresent
- int 3
- int 1
- segment register mod + pushfd
- ud2
Кратко по приколам - ничего нового. GetTickCount - делает замеры тиков для выявления трейса/остановки на брейкпоинте. CheckRemoteDebuggerPresent - вернёт по адресу второго параметра инфу, есть ли дебаггер или нет. int 1/int 3/ud2 сгенерят исключение без дебаггера. segment register mod + pushfd достанет установленный в 1 флаг трассировки, если была трассировка.
Понатыкать-то этого всего понатыкали, а что оно защищает и как? Сердце этого всего - функция Encode:
Первый вызов вообще наглядно показывает суть всей антиотладки:Code:push ecx push offset Transform lea edx, [ebp+Str] push edx call Encode
Видите, как IDA дала имя переменной - pbDebuggerPresent. Если дебаггера нет - третий параметр всегда 0. Первый параметр - адрес нами введенного значения флага. Второй параметр - адрес таблицы Transform. Я её так назвал. Её мы сдампим WinHex`ом:Code:mov ecx, [ebp+pbDebuggerPresent] push ecx push offset Transform lea edx, [ebp+Str] push edx call Encode
Да, её длина - 256 байт. Откуда взялся её размер? Из таких строк кода внутри неё:Code:unsigned char Transform[256] = { 0x4E, 0x3E, 0x08, 0x2B, 0x0F, 0x08, 0x6D, 0x0E, 0x7C, 0x0A, 0x64, 0x64, 0x0D, 0x7D, 0x4D, 0x2A, 0x20, 0x06, 0x19, 0x3D, 0x62, 0x52, 0x6E, 0x0A, 0x38, 0x47, 0x3C, 0x6F, 0x73, 0x1D, 0x06, 0x04, 0x4A, 0x5A, 0x01, 0x24, 0x66, 0x18, 0x0D, 0x04, 0x51, 0x59, 0x45, 0x3A, 0x58, 0x3B, 0x46, 0x79, 0x42, 0x10, 0x44, 0x1E, 0x11, 0x60, 0x29, 0x39, 0x45, 0x7A, 0x5D, 0x16, 0x80, 0x5A, 0x5D, 0x2F, 0x80, 0x18, 0x1F, 0x32, 0x15, 0x73, 0x66, 0x1C, 0x36, 0x42, 0x49, 0x38, 0x41, 0x7E, 0x58, 0x41, 0x04, 0x63, 0x5D, 0x13, 0x43, 0x56, 0x7F, 0x04, 0x12, 0x5D, 0x47, 0x51, 0x35, 0x1A, 0x6D, 0x1D, 0x0D, 0x3D, 0x65, 0x47, 0x18, 0x38, 0x4F, 0x30, 0x55, 0x27, 0x6B, 0x24, 0x5A, 0x5C, 0x24, 0x73, 0x27, 0x00, 0x45, 0x19, 0x7D, 0x76, 0x56, 0x35, 0x5B, 0x34, 0x41, 0x0E, 0x69, 0x0E, 0x44, 0x57, 0x6E, 0x47, 0x28, 0x77, 0x7B, 0x4C, 0x32, 0x55, 0x58, 0x41, 0x00, 0x00, 0x5A, 0x42, 0x49, 0x5A, 0x2F, 0x55, 0x58, 0x58, 0x50, 0x3D, 0x70, 0x1A, 0x36, 0x50, 0x70, 0x17, 0x7D, 0x70, 0x27, 0x55, 0x37, 0x58, 0x1A, 0x7C, 0x50, 0x55, 0x58, 0x50, 0x14, 0x70, 0x1A, 0x09, 0x2F, 0x70, 0x6C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, 0x4C, 0xF7, 0x59, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x4E, 0x00, 0x00, 0x00, 0x44, 0x33, 0x00, 0x00, 0x44, 0x1D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, 0x4C, 0xF7, 0x59, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x94, 0x33, 0x00, 0x00, 0x94, 0x1D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, 0x4C, 0xF7, 0x59, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x64, 0x02, 0x00, 0x00, 0xA8, 0x33, 0x00, 0x00 };
Из-за byte ptr конкретно. Далее особо не заморачиваемся и рипаем ассемблерный код этой функции. Я добавил в неё 4-й параметр - длину. Получилось как-то так:Code:mov ecx, [ebp+arg_0] add ecx, [ebp+CurrentSymbol] movsx edx, byte ptr [ecx]
Что в самом pirate проверяется? Наш введенный флаг дважды пропускается через Encode, потом полученное кодированное значение сравнивается с захардкоженным значением из бинарника. Дампим это значение тоже ВинХексом:Code:void Encode(unsigned char* buffer, unsigned char* table,unsigned int len, unsigned int xorvalue) { unsigned int var_4; __asm { mov var_4, 0 jmp short loc_401076 loc_40106D : mov eax, var_4 add eax, 1 mov var_4, eax loc_401076 : mov eax, len cmp var_4, eax jnb short loc_4010A5 mov ecx, buffer add ecx, var_4 movsx edx, byte ptr[ecx] mov eax, table movsx ecx, byte ptr[eax + edx + 0Ah] xor ecx, xorvalue mov edx, buffer add edx, var_4 movsx eax, byte ptr[edx] xor eax, ecx mov ecx, buffer add ecx, var_4 mov [ecx], al jmp short loc_40106D loc_4010A5 : } }
Где сравнение в коде? Вот:Code:unsigned char Encryptedflag[35] = { 0x5A, 0x42, 0x49, 0x5A, 0x2F, 0x55, 0x58, 0x58, 0x50, 0x3D, 0x70, 0x1A, 0x36, 0x50, 0x70, 0x17, 0x7D, 0x70, 0x27, 0x55, 0x37, 0x58, 0x1A, 0x7C, 0x50, 0x55, 0x58, 0x50, 0x14, 0x70, 0x1A, 0x09, 0x2F, 0x70, 0x6C };
byte_4031FC - начало закодированного флага.Code:mov ecx, [ebp+var_50] movsx edx, ds:byte_4031FC[ecx] mov eax, [ebp+var_50] movsx ecx, [ebp+eax+Str] cmp edx, ecx jz short loc_40128A
Как раскодировать? При кодировании каждый байт пропускается через Encode, получается как-то так: Encode(Encode(x)) = y. Причём есть соответсвие между х и у, но оно неоднозначное. Что это значит?
Как будет работать генератор флагов? Строить таблицу соответствий, искать закодированный символ в таблице, его индекс - первоначальное значение.Code:Encode(Encode(x)) = y Encode(Encode(z)) = y z != x
Строим таблицу для поиска:
Code:for (int i = 0; i < 256; i++) SwitchTable[i] = (unsigned char)(i & 0xFF); Encode(SwitchTable, Transform, 0x100, 0); Encode(SwitchTable, Transform, 0x100, 0);
Находим все варианты оригинальным символов по закодированным:
Как ищутся символы:Code:std::vector<std::vector<unsigned int>> brute; try { for (int i = 0; i < sizeof(Encryptedflag); i++) { auto number = get_number_by_symbol(Encryptedflag[i]); brute.push_back(number); } } catch (const std::string& error) { std::cout << error << std::endl; }
Обратите внимание на isprint - это ж цэтэфэ, могут попросить ввести флаг и не принимать на вход непечатаемые символы.Code:std::vector<unsigned int> get_number_by_symbol(unsigned char sym) { std::vector<unsigned int> result; for (unsigned int i = 0; i < sizeof(SwitchTable); i++) { if (SwitchTable[i] == sym) { if (isprint(i)) result.push_back(i); } } if (!result.size()) throw std::string("Not found ") + (char) sym; return result; }
Даже при таком раскладе получается куча вариантов - как перебрать их все? Есть черезжопный брутфорс, который я бы использовал только (АХТУНГ!) в цэтэфэ. Суть в том, что нужно выполнить генерацию кода переборщика скриптом. Вот две функции генерации:
Что это за фигня и куда её лепить? Ну вот представьте, что вам нужно перебрать все значения в векторе. Вы делаете что-то вроде:Code:def gen(x,y): if x == y: return print("for (auto val_" + str(x) + " : brute[" + str(x) + "])") print("{") gen(x + 1,y) print("}") def out(x,y): result = '' for a in range(x,y): result += " << (char) val_" + str(a) + " " return result
Если таких векторов два - делаете вложенный цикл, три - ещё один вложенный цикл. Здесь генерируется 35 вложенных циклов.Code:for (auto val_0 : brute[0]) { std::cout << (char) val_0 << std::endl; }
Да простят меня за эту жесть. Но в цэтэфэ вполне могут попросить какой-то конкретный флаг, где есть имя автора, кличка его кота, адрес проживания - что угодно. Потому генерируем всё в файл:Code:for (auto val_0 : brute[0]) { for (auto val_1 : brute[1]) { for (auto val_2 : brute[2]) { for (auto val_3 : brute[3]) { for (auto val_4 : brute[4]) { for (auto val_5 : brute[5]) { for (auto val_6 : brute[6]) { for (auto val_7 : brute[7]) { for (auto val_8 : brute[8]) { for (auto val_9 : brute[9]) { for (auto val_10 : brute[10]) { for (auto val_11 : brute[11]) { for (auto val_12 : brute[12]) { for (auto val_13 : brute[13]) { for (auto val_14 : brute[14]) { for (auto val_15 : brute[15]) { for (auto val_16 : brute[16]) { for (auto val_17 : brute[17]) { for (auto val_18 : brute[18]) { for (auto val_19 : brute[19]) { for (auto val_20 : brute[20]) { for (auto val_21 : brute[21]) { for (auto val_22 : brute[22]) { for (auto val_23 : brute[23]) { for (auto val_24 : brute[24]) { for (auto val_25 : brute[25]) { for (auto val_26 : brute[26]) { for (auto val_27 : brute[27]) { for (auto val_28 : brute[28]) { for (auto val_29 : brute[29]) { for (auto val_30 : brute[30]) { for (auto val_31 : brute[31]) { for (auto val_32 : brute[32]) { for (auto val_33 : brute[33]) { for (auto val_34 : brute[34]) { std::cout << (char)val_0 << (char)val_1 << (char)val_2 << (char)val_3 << (char)val_4 << (char)val_5 << (char)val_6 << (char)val_7 << (char)val_8 << (char)val_9 << (char)val_10 << (char)val_11 << (char)val_12 << (char)val_13 << (char)val_14 << (char)val_15 << (char)val_16 << (char)val_17 << (char)val_18 << (char)val_19 << (char)val_20 << (char)val_21 << (char)val_22 << (char)val_23 << (char)val_24 << (char)val_25 << (char)val_26 << (char)val_27 << (char)val_28 << (char)val_29 << (char)val_30 << (char)val_31 << (char)val_32 << (char)val_33 << (char)val_34 << std::endl; } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } }
После того, как файл стал более 400 Мб, я невзлюбил цэтэфэ и завершил эту жуть.Code:.\PirateFlagGenerator.exe > log.txt
Протестил парочку значений из файла:
РезультатыCode:PS C:\Reverse\pirate> .\pirate.exe Enter the flag please: M<EM':]0_m4ny_3xc4p78]n5_:0_h4nd'4} YES! PS C:\Reverse\pirate> .\pirate.exe Enter the flag please: M<EM'7]0_m3ny_4xc4b78]n5_:0_h3nd'4} YES!
Особо ничему не научились. Ида про теперь офигенно разворачивает Seh обработчики, что показывает, куда перейдёт поток выполнения при исключении и нехило тащит данный таск. Если это цэтэфэ для школьников, то школьники нехило залевелапились с момента моего обучения в школе. Я в школе чайник поднимал двумя руками, пустой, без крышки. А тут такое надо решать. Горжусь школами, ну а чё?



Reply With Quote
Thanks
