R0 CREW

Pirate from StartCTF или почему я не люблю цэтэфэ

Wellcome, уважаемая аудитория.

Не так давно у нас опять появились задания с очередного ещё одного цтф. Осё здесь ссылочка

Для примера, возьмём заданьице полегче. Pirate. Из инструментов IDA Pro 7.0 + Visual Studio 2017 + WinHex (да-да, можно без него, но я вот так привык). Тут нам обещают антиотладку. Плагинов для сокрытия нет никаких, работаем.

Загрузив файл, видим приглашение ввести флаг:

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

Ниже чтение флага и сравнение его длины с 35:

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

Антиотладка

  1. GetTickCount
  2. CheckRemoteDebuggerPresent
  3. int 3
  4. int 1
  5. segment register mod + pushfd
  6. ud2

Кратко по приколам - ничего нового. GetTickCount - делает замеры тиков для выявления трейса/остановки на брейкпоинте. CheckRemoteDebuggerPresent - вернёт по адресу второго параметра инфу, есть ли дебаггер или нет. int 1/int 3/ud2 сгенерят исключение без дебаггера. segment register mod + pushfd достанет установленный в 1 флаг трассировки, если была трассировка.

Понатыкать-то этого всего понатыкали, а что оно защищает и как? Сердце этого всего - функция Encode:

push    ecx
push    offset Transform
lea     edx, [ebp+Str]
push    edx
call    Encode

Первый вызов вообще наглядно показывает суть всей антиотладки:

mov     ecx, [ebp+pbDebuggerPresent]
push    ecx
push    offset Transform
lea     edx, [ebp+Str]
push    edx
call    Encode

Видите, как IDA дала имя переменной - pbDebuggerPresent. Если дебаггера нет - третий параметр всегда 0. Первый параметр - адрес нами введенного значения флага. Второй параметр - адрес таблицы Transform. Я её так назвал. Её мы сдампим WinHex`ом:

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
};

Да, её длина - 256 байт. Откуда взялся её размер? Из таких строк кода внутри неё:

mov     ecx, [ebp+arg_0]
add     ecx, [ebp+CurrentSymbol]
movsx   edx, byte ptr [ecx]

Из-за byte ptr конкретно. Далее особо не заморачиваемся и рипаем ассемблерный код этой функции. Я добавил в неё 4-й параметр - длину. Получилось как-то так:

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 :
	}
}

Что в самом pirate проверяется? Наш введенный флаг дважды пропускается через Encode, потом полученное кодированное значение сравнивается с захардкоженным значением из бинарника. Дампим это значение тоже ВинХексом:

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
};

Где сравнение в коде? Вот:

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

byte_4031FC - начало закодированного флага.

Как раскодировать? При кодировании каждый байт пропускается через Encode, получается как-то так: Encode(Encode(x)) = y. Причём есть соответсвие между х и у, но оно неоднозначное. Что это значит?

Encode(Encode(x)) = y
Encode(Encode(z)) = y
z != x

Как будет работать генератор флагов? Строить таблицу соответствий, искать закодированный символ в таблице, его индекс - первоначальное значение.

Строим таблицу для поиска:

for (int i = 0; i < 256; i++)
		SwitchTable[i] = (unsigned char)(i & 0xFF);
	Encode(SwitchTable, Transform, 0x100, 0);
	Encode(SwitchTable, Transform, 0x100, 0);

Находим все варианты оригинальным символов по закодированным:

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;
	}

Как ищутся символы:

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;
}

Обратите внимание на isprint - это ж цэтэфэ, могут попросить ввести флаг и не принимать на вход непечатаемые символы.

Даже при таком раскладе получается куча вариантов - как перебрать их все? Есть черезжопный брутфорс, который я бы использовал только (АХТУНГ!) в цэтэфэ. Суть в том, что нужно выполнить генерацию кода переборщика скриптом. Вот две функции генерации:

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

Что это за фигня и куда её лепить? Ну вот представьте, что вам нужно перебрать все значения в векторе. Вы делаете что-то вроде:

for (auto val_0 : brute[0])
{
  std::cout << (char) val_0 << std::endl;
}

Если таких векторов два - делаете вложенный цикл, три - ещё один вложенный цикл. Здесь генерируется 35 вложенных циклов.

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;
																																			}
																																		}
																																	}
																																}
																															}
																														}
																													}
																												}
																											}
																										}
																									}
																								}
																							}
																						}
																					}
																				}
																			}
																		}
																	}
																}
															}
														}
													}
												}
											}
										}
									}
								}
							}
						}
					}
				}
			}
		}
	}

Да простят меня за эту жесть. Но в цэтэфэ вполне могут попросить какой-то конкретный флаг, где есть имя автора, кличка его кота, адрес проживания - что угодно. Потому генерируем всё в файл:

.\PirateFlagGenerator.exe > log.txt

После того, как файл стал более 400 Мб, я невзлюбил цэтэфэ и завершил эту жуть.

Протестил парочку значений из файла:

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 обработчики, что показывает, куда перейдёт поток выполнения при исключении и нехило тащит данный таск. Если это цэтэфэ для школьников, то школьники нехило залевелапились с момента моего обучения в школе. Я в школе чайник поднимал двумя руками, пустой, без крышки. А тут такое надо решать. Горжусь школами, ну а чё?

Да, коллизии есть и в более сложных заданиях на CTF. И это, своего рода, бич. Особенно если речь идет о решении на время.
Пост выше несколько утрирует ситуацию, которая имеет место быть. Соглашусь лишь с тем, что задания имеют отдаленное отношение к реверсу реальных приложений, но владения инструментарием все же требуют. На современных СTF (уровень сложности которых постоянно растет) зачастую остаются задания не решенные в течении отведенного времени, даже при участии маститых команд. И это при том, что азиаты решают ВСЕ с учетом того что команды в полсотни тел. Тут мы имеем задание школьного уровня и по сложности и по качеству. Что касается тезиса вынесенного в титлы, то “на вкус и цвет все фломастеры разные” и кто-то присыщен реверсом в каждодневной деятельности по месту работы, а для кого-то это наоборот хобби и смена рода деятельности.

Что касается рассматриваемого задания, то не обязательно генерить выхлоп в 400+ Мб. :slight_smile:

master = [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, 0x9, 0x2f, 0x70, 0x6c]

data   = [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] 

o = ''
for i in range(35):
   for j in range(32,128):
     k = j^data[j]
     if k < 128:
       if k^data[k] == master[i]:
         o += chr(j)
   o += ' '
print o

При этом генерится строка с учетом всех коллизий

MS <I E MS '),l{ 7: 0] 0] _ m 34 n y _ 34 Wx c 34 bp 7: 18 0] n 5 _ 7: 0] _ h 34 n d '),l{ 34 } 

из которой легко лепится “осмысленный” флаг MIEM{700_m4ny_3xc3p710n5_70_h4ndl3}

3xc3p710n5_70_h4ndl3 - сорри, но осмысленным я бы его не назвал.

Потому осмысленный и взято в кавычки.

Ок, до меня пока дойдёт …