Разбор моих заданий с финала КИБЕРТОНА 2020
Smol
Однажды Эрнест Хемингуэй поспорил, что напишет самый короткий рассказ, способный растрогать любого. Проверь сам, всего 228 байт!
Описание
Нам дан 32-битный эльф размером 228 байт. Попробовал открыть в Иде или любом другом дизассемблере, мы встречаем ряд интересных ошибок:
В итоге Иде не получается загрузить наш файл и она умирает в агониях.
Что же тут происходит?
Посмотрим на вывод команды readelf
:
$ readelf -a Smol
ELF Header:
Magic: 7f 45 4c 46 01 00 00 00 00 00 00 00 00 00 01 00
Class: ELF32
Data: none
Version: 0
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x10020
Entry point address: 0x10020
Start of program headers: 4 (bytes into file)
Start of section headers: 2201010819 (bytes into file)
Flags: 0x6eb30ec
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 1
Size of section headers: 3213 (bytes)
Number of section headers: 33572
Section header string table index: 960
readelf: Warning: The e_shentsize field in the ELF header is larger than the size of an ELF section header
readelf: Error: Reading 107866836 bytes extends past end of file for section headers
readelf: Error: Section headers are not available!
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x00010000 0x00030002 0x10020 0x10020 R 0x8330c283
There is no dynamic section in this file.
Чтобы понять что тут написано, давайте взглянем на структуру Эльф заголовка:
typedef struct {
unsigned char e_ident[EI_NIDENT]; // магический хэдер (.ELF)
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry; // Виртуальный Адрес точки входа
Elf32_Off e_phoff; // Оффсет до структуры program_header
Elf32_Off e_shoff; // Оффсет до структуры section_header
Elf32_Word e_flags;
Elf32_Half e_ehsize; // Размер Эльф заголовка
Elf32_Half e_phentsize; // Размер структуры program_header
Elf32_Half e_phnum; // Кол-во структур program_header
Elf32_Half e_shentsize; // Размер структуры section_header
Elf32_Half e_shnum; // Кол-во структур section_header
Elf32_Half e_shstrndx;
} Elf32_Ehdr;
Для загрузки и запуска файла, Линуксу нужно всего несколько значений из этих заголовков, это:
- Валидный e_entry
- Валидный program_header
Как видно из вывода readelf, Оффсет program_header равен 4, но размер структуры Elf32_Ehdr равна 52 байт. По сути program_header лежит в структуре Elf32_Ehdr что мешает Иде нормально пропарсить и загрузить этот файл. Рекомендую прочитать отличную статью по созданию самого маленького эльфа. http://muppetlabs.com/~breadbox/software/tiny/teensy.html
Кстати адрес точки входа лежит тоже в заголовке Elf32_Ehdr. 0x10020 - 0x10000 = 0x20
Попробуем открыть этот файл как raw и дизассемлировать с оффсета 0x20:
seg000:00000020 ; ---------------------------------------------------------------------------
seg000:00000020 add edx, 30h ; '0'
seg000:00000023 sub esp, 30h
seg000:00000026 jmp short loc_2E
seg000:00000026 ; ---------------------------------------------------------------------------
Получается валидный код. Теперь создадим функцию по оффсету 0x20
.
Важно отметить, что при старте программы почти все регистры равны 0.
Первым делом программа считывает 0x30
байт из stdin
и кладет их на стэк.
edx = 0x30 ; size
eax = 0x3 ; stdin
ecx = esp ; buffer
После чего проверяется длина введенной строки. (системный вызов read
возвращает кол-во прочитанных байт в регистре eax)
seg000:00000036 mov edi, esp
seg000:00000038 mov esi, esp
seg000:0000003A dec eax
seg000:0000003B cmp al, 23h ; '#'
seg000:0000003D jnz short fail
Значит мы уже точно знаем длину - 0x23 байта.
После чего идет алгоритм проверки:
edi = адрес начала введенной строки
esi = адрес начала введенной строки
ecx = длина введенной строки
И так, на стэк пушатся 3 значения - 0x1f
0x1e
0x1e
, далее key1
key2
key3
После чего идет цикл, который:
- считывает байт из нашей строки
- добавляет key1
- ксорит с key2
- вычитает key3
- кладет новое значение обратно в строку
затем к key1 добавляется 1 из key2 вычитается 1 и к key3 добавляется 1
и сохраняется на стэк в обратном порядке.
На питоне это будет выглядеть следующим образом:
for i in range(0x23):
c = input[i]
c += key1
c ^= key2
c -= key3
input[i] = c
key1 += 1
key2 -= 1
key3 += 1
temp = key1
key1 = key3
key3 = key2
key2 = temp
Решение
Для решения я использовал z3, но так же можно забрутить каждый символ вручную.
from z3 import *
flag = [94, 59, 33, 91, 53, 50, 41, 51, 43, 155, 84, 80, 126, 91, 136, 88, 147, 134, 124, 30, 154, 83, 126, 57, 42, 137, 151, 88, 144, 75, 139, 30, 142, 73, 100]
flag_len = len(flag)
key1 = 0x1f
key2 = 0x1e
key3 = 0x1e
s = Solver()
input = []
for i in range(flag_len):
input.append(BitVec('input_%d' % i, 8))
encoded = [i for i in input]
for i in range(flag_len):
c = encoded[i]
c += key1
c ^= key2
c -= key3
encoded[i] = c
key1 += 1
key2 -= 1
key3 += 1
temp = key1
key1 = key3
key3 = key2
key2 = temp
for i in range(flag_len):
s.add(encoded[i] == flag[i])
if s.check() == sat:
model = s.model()
out = ''
for i in range(flag_len):
out += chr(model[input[i]].as_long())
print(out)
CYBERTHON{50_5m4ll_Bu7_SO_p0w3rFu1}
The King of Bitness
Ходят слухи, что эту программу невозможно отлаживать.
Описание
В этом таске нам нужно проанализировать код, который выполняется в 64-битном контексте из 32-битный программы.
Спойлер: этот таск будет работать только на 64-битном процессоре.
как выглядит мейн:
int main()
{
void *v1;
int v2;
puts("I'm the King of bitness.");
v1 = calloc(0x100u, 1u);
if ( !v1 )
return -1;
printf("%s", "Prove yourself worthy:");
scanf("%255s", v1);
v2 = check(v1);
if ( v2 )
{
v2 = 0;
puts("WRONG!");
}
else
puts("My respect. You can submit the flag");
return v2;
}
Самый сок находится в функции check
.text:08048601 check proc near
.text:08048601
.text:08048601 arg_0 = dword ptr 8
.text:08048601
.text:08048601 push ebp
.text:08048602 mov ebp, esp
.text:08048604 mov eax, [ebp+arg_0]
.text:08048607 call far ptr 33h:8048612h
.text:0804860E mov esp, ebp
.text:08048610 pop ebp
.text:08048611 retn
.text:08048611 check endp
Все что делает эта функция это кладет адрес нашей строки в регистр eax и вызывает новую функцию по адресу 0x8048612 с помощью call far ptr 33h:8048612h
.
Погуглив немного про call far 33h
, можно понять, что перед нами ни что иное, как Heaven’s gate. Описание работы Heaven’s gate не будет описано в этой статье, но если вкратце:
call far 33h:address
переводит выполнение программы в 64-битный режим. call far 23h:address
переводит выполнение программы в 32-битный режим. тоже самое относится к инстукции retf
Подробнее про heavens gate можно почитать тут и тут.
Все что требуется от нас - открыть программу в Ida64 и создать новый 64-битный сегмент начиная с адреса 8048612h
.
Анализ 64-битного кода
Код состоит из несколько частей:
- Инициализация
- Алгоритм
- Проверка
- Выход
Инициализация
Вычисляется длина введенной строки (напомню, что адрес строки лежит в регистре rax):
Алгоритм
Переведя на си:
for (int i = 0; i < strlen(password); i++) {
char c1 = password[i];
char c2 = password[i + 1];
c1 = (c1 << 5) | (c1 >> 3);
c1 += 0xab;
c1 ^= 0x69;
c1 = ~c1;
c2 = (c2 << 7) | (c2 >> 1);
c2 = ~c2;
c2 += 0xba;
c2 ^= 0x69;
c1 ^= c2;
password[i] = c1;
}
Проверка
На стэке создается строка, после чего посимвольно сверяется с закодированным вводом. Кстати опять можно заметить длину флага - 0x23 байт.
Решение
Можно заметить, что не зная одного из вдух символов, невозможно найти исходные. К счастью мы знаем, что последний символ строки - “}“, а после него следует “\x00“
(си строки заканчиваются нулем).
Поэтому нахождение исходных значений флага нужно начинать с конца:
flag = [
0xe1, 0xb1, 0x1b, 0x3c,
0x85, 0x5f, 0x59, 0xf9,
0x77, 0xc5, 0x4e, 0x96,
0x2d, 0x51, 0xcc, 0xc8,
0xce, 0x0c, 0xf7, 0x11,
0x46, 0x29, 0x51, 0xcc,
0x97, 0xf3, 0xea, 0x44,
0x71, 0x73, 0x5e, 0x2a,
0xee, 0x1d, 0x1c]
out = ''
last_byte = 0
for i in range(len(flag) - 1, -1, -1):
for c in range(0, 255):
c1 = c
c2 = last_byte
c1 = (c1 << 5) | (c1 >> 3)
c1 += 0xab
c1 ^= 0x69
c1 = ~c1
c2 = (c2 << 7) | (c2 >> 1)
c2 = ~c2
c2 += 0xba
c2 ^= 0x69
c1 ^= c2
c1 &= 0xff
c2 &= 0xff
if c1 == flag[i]:
out += chr(c)
break
last_byte = c
print(out[::-1])
CYBERTHON{32B1t_0r_64b1t_wH0_C4R3s}
Parent and Child
У этой семьи очень натянутые отношения. Отец и шага не дает сделать своему сыну.
Описание
В этом таске создается два процесса - родитель и ребенок. Родитель отлаживает ребенка, а ребенок запрашивает ключ и проверяет его. Главная сложность этого задания - восстановить алгоритм проверки ключа.
Вот как он выглядит в дизассемблере:
Как видно, поток выполнения не содержит циклов и каких либо операций над ключом.
Как выглядит обработчик прерываний родителя:
void parent(__pid_t pid)
{
int i;
int stat_loc;
int _i;
int v4;
user_regs_struct regs;
v4 = 0;
_i = 0;
while ( 1 )
{
waitpid(pid, &stat_loc, 0);
if ( stat_loc != 127 || BYTE1(stat_loc) != 5 )
break;
ptrace(PTRACE_GETREGS, pid, 0, ®s);
if ( regs.eax == 0xFEFEFEFE )
{
regs.eax = LOBYTE(regs.ebx) - LOBYTE(regs.edx);
}
else if ( regs.eax > 0xFEFEFEFE )
{
if ( regs.eax == 0x1337 )
{
if ( regs.eflags & 0x40 )
regs.eip = regs.edx;
else
regs.eip = regs.ebx;
}
else if ( regs.eax == 0x69696969 )
{
regs.eax = (LOBYTE(regs.ebx) ^ LOBYTE(regs.edx));
}
}
else if ( regs.eax == 0xCAFEBABE )
{
i = _i++;
regs.eax = flag[i];
}
else if ( regs.eax == 0xF0F0F0F0 )
{
regs.eax = LOBYTE(regs.ebx) + LOBYTE(regs.edx);
}
ptrace(PTRACE_SETREGS, pid, 0, ®s);
ptrace(PTRACE_CONT, pid, 0, 0);
}
}
Решение
У нас есть 5 обработчиков:
Magic | Operation |
---|---|
0xFEFEFEFE | eax = ebx - edx |
0x69696969 | eax = ebx ^ edx |
0xCAFEBABE | eax = flag[i++] |
0xF0F0F0F0 | eax = ebx + edx |
0x1337 | jz edx else jmp ebx |
Все что осталось сделать - восстановить алгоритм и отреверсить его. В данном случае я просто пропатчил все вызовы родителя его операциями.
Что получилось:
int check(const char *input)
{
const char *input_;
unsigned int input_len;
int v5;
char c;
input_ = input;
input_len = strlen(input) + 1;
v5 = input_len - 40;
if ( input_len == 40 )
{
while ( 1 )
{
c = *input_++;
if ( ((flag[v5] + c) ^ 0x69) != 0x13 )
break;
if ( ++v5 == 39 )
return 0;
}
}
return -1;
}
Переведя на питон:
flag = [0x37, 0x21, 0x38, 0x35, 0x28, 0x26, 0x32, 0x2b, 0x2c, 0xff, 0x2d, 0x1, 0x1b, 0x34, 0x46, 0x6, 0x32, 0x47, 0x8, 0x1b, 0x23, 0x46, 0x6, 0x17, 0x32, 0x47, 0x7, 0x1b, 0x35, 0x4, 0x47, 0x8, 0x21, 0x1b, 0x27, 0x43, 0x47, 0xa, 0xfd]
out = ''
for i in range(len(flag)):
for c in range(32, 127):
if ((flag[i] + c) ^ 0x69) & 0xff == 0x13:
out += chr(c)
break
print(out)
CYBERTHON{My_F4tH3r_W4tcH3s_Ev3rY_S73p}
Конец.