Данное задание подготовлено сообществом SPbCTF https://vk.com/spbctf
Один из участников тренировок SPbCTF прислал нам программу и сказал, что применил в ней инновационные приёмы, чтобы усложнить анализ. Помогите убедить его, что они не работают.
Linux: ./main 1234567890qwerty
(ссылка)
Windows: main.exe 1234567890qwerty
(ссылка)
Проанализируйте, как она это делает, и найдите верный ключ.
Формат ответа: yaprofi{...}`
Как выглядит главная функция:
Приведем ее немного в порядок:
Заглянем в sub_4015DF
, ее сразу можно переименовать в flag_check
void __fastcall flag_check(__int64 a1)
{
__int64 v1; // [rsp-40h] [rbp-58h]
qword_403010 = (__int64)&v1;
qword_408BA0 = a1;
}
Фмм, похоже, хексрейс лукавит. Как это выглядит в асме?
.text:00000000004015DF flag_check proc near ; CODE XREF: sub_401550+3B↑p
.text:00000000004015DF push rbp
.text:00000000004015E0 push rbx
.text:00000000004015E1 push rcx
.text:00000000004015E2 push rdx
.text:00000000004015E3 push rdi
.text:00000000004015E4 push rsi
.text:00000000004015E5 push r8
.text:00000000004015E7 push r9
.text:00000000004015E9 push r10
.text:00000000004015EB push r12
.text:00000000004015ED push r13
.text:00000000004015EF mov cs:qword_403010, rsp
.text:00000000004015F6 mov cs:qword_408BA0, rcx
.text:00000000004015FD mov rdx, 403010h
.text:0000000000401607 add rdx, 8
.text:000000000040160B mov rsp, rdx
.text:000000000040160E retn
.text:000000000040160E flag_check endp
Сохраняется состояние регистров, а заодно и старое значение вершины стека (инструкция по 4015EF), имеет смысл посмотреть другие места использования:
.text:00000000004015C0 sub_4015C0 proc near ; DATA XREF: .pdata:000000000040C06C↓o
.text:00000000004015C0 mov rsp, cs:qword_403010
.text:00000000004015C7 pop r13
.text:00000000004015C9 pop r12
.text:00000000004015CB pop r10
.text:00000000004015CD pop r9
.text:00000000004015CF pop r8
.text:00000000004015D1 pop rsi
.text:00000000004015D2 pop rdi
.text:00000000004015D3 pop rdx
.text:00000000004015D4 pop rcx
.text:00000000004015D5 pop rbx
.text:00000000004015D6 pop rbp
.text:00000000004015D7 mov rax, cs:qword_408B98
.text:00000000004015DE retn
Это точно конечная инструкция с восстановлением вершины стека и значений регистров.
Назовем функу check_end
. qword_408B98
- возвращаемое значение, можно переименовать
соответствующе (ret_val
).
Что есть mov cs:qword_408BA0, rcx
по адресу 4015F6
? Это введенное нами значение,
оно было передано через регистр rcx
. Теперь она с легкого нажатия хоткея N будет
our_input
.
Теперь насчет последних пяти инструкций из flag_check
. Заглянем туда,
где будет находиться вершина нового стека (я собрал байты в кворды):
.data:0000000000403018 dq offset sub_40160F
.data:0000000000403020 dq 968h
.data:0000000000403028 dq 4281h
.data:0000000000403030 dq offset sub_401671
.data:0000000000403038 dq 290h
.data:0000000000403040 dq offset sub_401693
.data:0000000000403048 dq 968h
.data:0000000000403050 dq offset sub_401658
.data:0000000000403058 dq 150h
.data:0000000000403060 dq offset sub_40160F
.data:0000000000403068 dq 858h
.data:0000000000403070 dq 946Fh
.data:0000000000403078 dq offset sub_401671
.data:0000000000403080 dq 2C87h
.data:0000000000403088 dq offset sub_401693
.data:0000000000403090 dq 858h
.data:0000000000403098 dq offset sub_401658
.data:00000000004030A0 dq 1B0h
.data:00000000004030A8 dq offset sub_40160F
...
Press D, это похоже на инструкции виртуальной машины с параметрами.
Окей, последовательно рассмотрим сами инструкции. Предлагаю считать адрес, располагающийся
в rcx
, нулевым относительно памяти виртуальной машины.
Переменные в такой памяти будем называть с помощью префикса var_{}
.
40160F
->fun0
:
.text:000000000040160F fun0 proc near ; DATA XREF: .data:0000000000403018↓o
.text:000000000040160F ; .data:0000000000403060↓o ...
.text:000000000040160F pop rbx
.text:0000000000401610 pop rax
.text:0000000000401611 mov rcx, 408BA0h
.text:000000000040161B mov [rcx+rbx], rax
.text:000000000040161F retn
.text:000000000040161F fun0 endp
2 параметра. Помещает в переменную var_{arg0}
значение arg1
.
401620
->fun1
:
.text:0000000000401620 pop rbx
.text:0000000000401621 pop rax
.text:0000000000401622 mov rcx, 408BA0h
.text:000000000040162C add rbx, rcx
.text:000000000040162F mov rbx, [rbx]
.text:0000000000401632 movzx rax, byte ptr [rbx+rax]
.text:0000000000401637 mov cs:ret_val, rax
.text:000000000040163E retn
2 параметра. Помещает в ret_val
байт по адресу [arg0+arg1]
40163F
->fun2
:
.text:000000000040163F pop rbx
.text:0000000000401640 mov rcx, 408BA0h
.text:000000000040164A add rbx, rcx
.text:000000000040164D mov rax, [rbx]
.text:0000000000401650 mov cs:ret_val, rax
.text:0000000000401657 retn
1 параметр. Помещает в ret_val
байт по адресу [arg0]
401658
->fun3
:
.text:0000000000401658 pop rbx
.text:0000000000401659 mov rcx, 408BA0h
.text:0000000000401663 add rbx, rcx
.text:0000000000401666 mov rax, cs:ret_val
.text:000000000040166D mov [rbx], rax
.text:0000000000401670 retn
1 параметр. var{arg0} = ret_val
401671
->fun4
:
.text:0000000000401671 pop rbx
.text:0000000000401672 mov cs:ret_val, rbx
.text:0000000000401679 retn
1 параметр. ret_val = var{arg0}
401671
->fun5
:
.text:000000000040167A pop rbx
.text:000000000040167B mov rcx, 408BA0h
.text:0000000000401685 add rbx, rcx
.text:0000000000401688 mov rbx, [rbx]
.text:000000000040168B sub cs:ret_val, rbx
.text:0000000000401692 retn
1 параметр. ret_val -= var{arg0}
401693
->fun6
.text:0000000000401693 pop rbx
.text:0000000000401694 mov rcx, 408BA0h
.text:000000000040169E add rbx, rcx
.text:00000000004016A1 mov rbx, [rbx+0]
.text:00000000004016A4 add cs:ret_val, rbx
.text:00000000004016AB retn
1 параметр. ret_val += var{arg0}
4016AC
->fun7
.text:00000000004016AC pop rbx
.text:00000000004016AD mov rcx, 408BA0h
.text:00000000004016B7 add rbx, rcx
.text:00000000004016BA mov rbx, [rbx]
.text:00000000004016BD xor cs:ret_val, rbx
.text:00000000004016C4 retn
1 параметр. ret_val ^= var{arg0}
4016C5
->fun8
.text:00000000004016C5 pop rbx
.text:00000000004016C6 mov rcx, 408BA0h
.text:00000000004016D0 add rbx, rcx
.text:00000000004016D3 mov rbx, [rbx]
.text:00000000004016D6 mov rax, cs:ret_val
.text:00000000004016DD imul rbx
.text:00000000004016E0 mov cs:ret_val, rax
.text:00000000004016E7 retn
1 параметр. ret_val *= var{arg0}
4016E8
->fun9
.text:00000000004016E8 pop rbx
.text:00000000004016E9 add rsp, rbx
.text:00000000004016EC retn
1 параметр. jump(arg0//8)
4016ED
->fun10
.text:00000000004016ED pop rbx
.text:00000000004016EE mov rax, cs:ret_val
.text:00000000004016F5 test rax, rax
.text:00000000004016F8 jz short fun11
.text:00000000004016FA add rsp, rbx
.text:00000000004016FD retn
1 параметр. if not ret_val: jump(arg0//8)
4016FE
->fun11
.text:00000000004016FE nop
.text:00000000004016FF retn
0 параметров. nop
401700
->fun12
.text:0000000000401700 pop rbx
.text:0000000000401701 pop rax
.text:0000000000401702 mov rcx, 403010h ; local const
.text:000000000040170C mov rdx, 408B90h ; exit_vm
.text:0000000000401716 ; если это не адрес RBX
.text:0000000000401716
.text:0000000000401716 loc_401716: ; CODE XREF: func12+2F↓j
.text:0000000000401716 add rcx, 8
.text:000000000040171A cmp rbx, [rcx]
.text:000000000040171D jnz short loc_401724
.text:000000000040171F mov [rcx], rax
.text:0000000000401722 jmp short loc_40172C
.text:0000000000401724 ; ---------------------------------------------------------------------------
.text:0000000000401724 ; если это не адрес RAX
.text:0000000000401724
.text:0000000000401724 loc_401724: ; CODE XREF: func12+1D↑j
.text:0000000000401724 cmp rax, [rcx]
.text:0000000000401727 jnz short loc_40172C
.text:0000000000401729 mov [rcx], rbx
.text:000000000040172C ; если это инструкция выхода
.text:000000000040172C
.text:000000000040172C loc_40172C: ; CODE XREF: func12+22↑j
.text:000000000040172C ; func12+27↑j
.text:000000000040172C cmp rcx, rdx
.text:000000000040172F jnz short loc_401716
.text:0000000000401731 retn
Это сложная инструкция. Она принимает на вход адрес двух других инструкций, затем меняет их местами
во всем стеке. Это должно затруднить последовательный анализ машины, вкупе с условными переходами.
vm.swap(dfun0, dfun1)
.
Окей, мы молодцы. Но из четырех часов, которые уходят на все-все задания, после решения таска на
шеллкод и разбор 12 инструкций виртуальной машины, у нас остается, допустим, около получаса. Писать свой парсер уже
не вариант: нам нужно будет следить за джампами, за модификациями fun12
.
Поступим старым дедовским методом: с помощью условных бряк получим контекст программы:
- Напишем сниппет, запустим:
import struct
def state():
rsp = GetRegValue("rsp")
rbx = GetManyBytes(rsp, 8)
rax = GetManyBytes(rsp+8, 8)
rbx = struct.unpack("<Q", rbx)[0]
rax = struct.unpack("<Q", rax)[0]
ret_val = GetManyBytes(0x408B98, 8)
ret_val = struct.unpack("<Q", ret_val)[0]
ans = { "rax": rax,
"rbx": rbx,
"rsp": rsp,
"ret_val": ret_val}
#print ans
return ans
- Создадим обработчики для функций виртуальной машины:
def var(num):
return "var"+hex(num).replace("L", "").replace("0x", "")
def func0():
s = state()
print "#F0"
print var(s["rbx"]),"=",hex(s["rax"])
def func1():
s = state()
print "#F1"
print "ret_val = byte from", s["rax"]+s["rbx"]
def func2():
s = state()
print "#2"
print "ret_val = byte from", var(s["rbx"])
def func3():
s = state()
print "#F3"
print var(s["rbx"]), "=", "ret_val"
def func4():
s = state()
print "#F4"
print "ret_val", "=", hex(s["rbx"])
def func5():
s = state()
print "#F5"
print "ret_val", "-=", var(s["rbx"])
def func6():
s = state()
print "#F6"
print "ret_val", "+=", var(s["rbx"])
def func7():
s = state()
print "#F7"
print "ret_val", "^=", var(s["rbx"])
def func8():
s = state()
print "#F8"
print "ret_val", "*=", var(s["rbx"])
def func9():
s = state()
print "#F9"
print "jump? ", s["rbx"]/8
def func10():
s = state()
print "#F10"
print "if ret_val: jump", s["rbx"]/8
def func11():
s = state()
print "#F11"
print "nop"
def func12():
s = state()
print "#F12"
print "SWAP TWO TYPES OF INSTRUCTIONS"
-
Ставим по точке останова на первую инструкцию в асме для каждой функи. В условиях вызываем функцию.
-
Запускаем, любуемся. Ниже очень много текста:
#F0
var968 = 0x4281
#F4
ret_val = 0x290
#F6
ret_val += var968
#F3
var150 = ret_val
#F0
var858 = 0x946f
#F4
ret_val = 0x2c87
#F6
ret_val += var858
#F3
var1b0 = ret_val
#F0
var900 = 0x56dfd84
#F4
ret_val = 0x1537330b
#F5
ret_val -= var900
#F3
var2c8 = ret_val
#F1
ret_val = byte from 0
#F3
var3d8 = ret_val
#F8
ret_val *= var3d8
#F8
ret_val *= var150
#F3
var488 = ret_val
#2
ret_val = byte from var3d8
#F8
ret_val *= var1b0
#F6
ret_val += var488
#F5
ret_val -= var2c8
#F3
varc80 = ret_val
#F12
SWAP TWO TYPES OF INSTRUCTIONS
#F12
SWAP TWO TYPES OF INSTRUCTIONS
#F12
SWAP TWO TYPES OF INSTRUCTIONS
#F12
SWAP TWO TYPES OF INSTRUCTIONS
#F12
SWAP TWO TYPES OF INSTRUCTIONS
#F0
var988 = 0x35c4
#F4
ret_val = 0xd76b
#F5
ret_val -= var988
#F3
vare0 = ret_val
#F0
var910 = 0x8931
#F4
ret_val = 0x156eb
#F5
ret_val -= var910
#F3
var218 = ret_val
#F0
var858 = 0x14a681c
#F4
ret_val = 0x1638e645
#F6
ret_val += var858
#F3
var298 = ret_val
#F1
ret_val = byte from 1
#F3
var3c8 = ret_val
#F8
ret_val *= var3c8
#F8
ret_val *= vare0
#F3
var460 = ret_val
#2
ret_val = byte from var3c8
#F8
ret_val *= var218
#F6
ret_val += var460
#F5
ret_val -= var298
#F3
varc88 = ret_val
#F12
SWAP TWO TYPES OF INSTRUCTIONS
#F12
SWAP TWO TYPES OF INSTRUCTIONS
#F12
SWAP TWO TYPES OF INSTRUCTIONS
#F12
SWAP TWO TYPES OF INSTRUCTIONS
#F12
SWAP TWO TYPES OF INSTRUCTIONS
#F0
var820 = 0xfad
#F4
ret_val = 0x984
#F6
ret_val += var820
#F3
varf0 = ret_val
#F0
var8c0 = 0x7733
#F4
ret_val = 0xaa3
#F6
ret_val += var8c0
#F3
var1d0 = ret_val
#F0
var880 = 0x20d7afa
#F4
ret_val = 0x2fdb3a6
#F6
ret_val += var880
#F3
var2e0 = ret_val
#F1
ret_val = byte from 2
#F3
var370 = ret_val
#F8
ret_val *= var370
#F8
ret_val *= varf0
#F3
var410 = ret_val
#2
ret_val = byte from var370
#F8
ret_val *= var1d0
#F6
ret_val += var410
#F5
ret_val -= var2e0
#F3
varc90 = ret_val
#F12
SWAP TWO TYPES OF INSTRUCTIONS
#F12
SWAP TWO TYPES OF INSTRUCTIONS
#F12
SWAP TWO TYPES OF INSTRUCTIONS
#F12
SWAP TWO TYPES OF INSTRUCTIONS
#F12
SWAP TWO TYPES OF INSTRUCTIONS
#F0
var8d8 = 0x2a7
#F4
ret_val = 0x3b8
*2000 инструкций спустя*
#F6
ret_val += var8
#F3
var8 = ret_val
#F9
jump? 0
#2
ret_val = byte from vard60
#F10
if ret_val: jump 12
#F4
ret_val = 0x1
#F3
var1650 = ret_val
#2
ret_val = byte from var1650
#F6
ret_val += var8
#F3
var8 = ret_val
#F9
jump? 0
#2
ret_val = byte from vard68
#F10
if ret_val: jump 12
#F4
ret_val = 0x1
#F3
var1650 = ret_val
#2
ret_val = byte from var1650
#F6
ret_val += var8
#F3
var8 = ret_val
#F9
jump? 0
#2
ret_val = byte from vard70
#F10
if ret_val: jump 12
#F4
ret_val = 0x1
#F3
var1650 = ret_val
#2
ret_val = byte from var1650
#F6
ret_val += var8
#F3
var8 = ret_val
#F9
jump? 0
#2
ret_val = byte from vard78
#F10
if ret_val: jump 12
#F4
ret_val = 0x1
#F3
var1650 = ret_val
#2
ret_val = byte from var1650
#F6
ret_val += var8
#F3
var8 = ret_val
#F9
jump? 0
#F1
ret_val = byte from 32
#F10
if ret_val: jump 10
#F11
nop
#F6
ret_val += var1650
#F10
if ret_val: jump 4
#F4
ret_val = 0x1
В самом начале ищут блоки, разделенные пятью инструкциями F12.
И в рамках этих блоков есть инструкция F1, которая берет байты нашего флага (!). Всё, триггер.
Перед пятью F12 есть инструкция F3, которая записывает в переменные var_c80…var_d78, то есть 32 ячейки. Это длина нашего флага. Давайте дополнительно будем выводить содержание параметров F3, если происходит запись в одну из наших 32 ячеек:
def func3():
s = state()
print "#F3"
if s["rbx"] in range(0xc0, 0xd78+1):
print "### ret_val", s["ret_val"]
print var(s["rbx"]),"=","ret_val"
И посмотрим на первый блок проверки:
...
#F3
### ret_val 18446744073489579520
varc80 = ret_val
#F12
SWAP TWO TYPES OF INSTRUCTIONS
#F12
SWAP TWO TYPES OF INSTRUCTIONS
#F12
SWAP TWO TYPES OF INSTRUCTIONS
#F12
SWAP TWO TYPES OF INSTRUCTIONS
#F12
SWAP TWO TYPES OF INSTRUCTIONS
Мы знаем формат флага: yaprofi{...}
. Изменится ли значение, если мы передадим валидный первый символ?
...
#F3
### ret_val 0
varc80 = ret_val
#F12
SWAP TWO TYPES OF INSTRUCTIONS
#F12
SWAP TWO TYPES OF INSTRUCTIONS
#F12
SWAP TWO TYPES OF INSTRUCTIONS
#F12
SWAP TWO TYPES OF INSTRUCTIONS
#F12
SWAP TWO TYPES OF INSTRUCTIONS
Отлично! Мы нашли способ проверить правильность нашего байта. Как поступит благоразумный магистр, у которого в запасе осталось около 30 минут свободного времени? Полагаю, переберет значения: ответ уже в руках, а 52 раза запустить программу кажется проще, чем полчаса разбираться, как можно попробовать собрать математическую решалку.
Для этого:
- закомментируем вывод других функций;
- создадим в idapython глобальную переменную
counter = 0
;
- поставим обнуление счетчика с помощью точки останова где-нибудь в начале функи
flag_check
:
- и внесем изменения в функции F3 и F12:
def func12():
s = state()
#print "#F12"
global counter
counter+=1
def func3():
s = state()
#print "#F3"
if s["rbx"] in range(0xc0, 0xd78+1):
global counter
if s["ret_val"] == 0:
print "array.update({",counter//5, ":_})"
Посмотрим вывод на фразе yaprofi{abcdefghijklmnopqrstuvw}
:
Прекрасно, оно работает. У нас есть 20 минут, чтобы сдать флаг. Пишем еще одну программку в отдельном терминале:
for i in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ":
... print("_ = '{}'".format(i))
... print("#", i*32)
... while len(input()):
... pass
18 минут! Живей! Копируем строку с 32 символами, пихаем из в параметры командной строки в Иде, запускаем, получаем ответ.
Я замерил, мне удалось пробрутить ручками за 9 минут 38 секунд:
>>> for i in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ":
... print("_ = '{}'".format(i))
... print("#", i*32)
... while len(input()):
... pass
...
_ = 'a'
# aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
array.update({ 1 :_})
array.update({ 18 :_})
_ = 'b'
# bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
array.update({ 19 :_})
_ = 'c'
# cccccccccccccccccccccccccccccccc
_ = 'd'
# dddddddddddddddddddddddddddddddd
_ = 'e'
# eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
array.update({ 20 :_})
_ = 'f'
# ffffffffffffffffffffffffffffffff
array.update({ 5 :_})
array.update({ 10 :_})
_ = 'g'
# gggggggggggggggggggggggggggggggg
array.update({ 30 :_})
_ = 'h'
# hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh
array.update({ 12 :_})
_ = 'i'
# iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii
array.update({ 6 :_})
_ = 'j'
# jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjj
array.update({ 25 :_})
_ = 'k'
# kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk
array.update({ 23 :_})
array.update({ 26 :_})
_ = 'l'
# llllllllllllllllllllllllllllllll
array.update({ 9 :_})
_ = 'm'
# mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm
_ = 'n'
# nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn
_ = 'o'
# oooooooooooooooooooooooooooooooo
array.update({ 4 :_})
_ = 'p'
# pppppppppppppppppppppppppppppppp
array.update({ 2 :_})
_ = 'q'
# qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq
_ = 'r'
# rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr
array.update({ 3 :_})
array.update({ 13 :_})
_ = 's'
# ssssssssssssssssssssssssssssssss
_ = 't'
# tttttttttttttttttttttttttttttttt
array.update({ 22 :_})
_ = 'u'
# uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu
_ = 'v'
# vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
_ = 'w'
# wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww
_ = 'x'
# xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
_ = 'y'
# yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
array.update({ 0 :_})
array.update({ 14 :_})
_ = 'z'
# zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
array.update({ 28 :_})
_ = 'A'
# AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
_ = 'B'
# BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
array.update({ 16 :_})
_ = 'C'
# CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
_ = 'D'
# DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD
_ = 'E'
# EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE
_ = 'F'
# FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
array.update({ 8 :_})
_ = 'G'
# GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG
_ = 'H'
# HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
_ = 'I'
# IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII
_ = 'J'
# JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJ
_ = 'K'
# KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK
_ = 'L'
# LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL
_ = 'M'
# MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
array.update({ 24 :_})
_ = 'N'
# NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN
_ = 'O'
# OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
array.update({ 11 :_})
_ = 'P'
# PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP
_ = 'Q'
# QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ
_ = 'R'
# RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR
array.update({ 21 :_})
array.update({ 29 :_})
_ = 'S'
# SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS
array.update({ 27 :_})
_ = 'T'
# TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT
_ = 'U'
# UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU
array.update({ 15 :_})
array.update({ 17 :_})
Теперь открываем третий терминал:
>>> array = {}
>>> array.update({7:"{"})
>>> array.update({31:"}"})
>>> _ = 'a'
>>> # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
... array.update({ 1 :_})
>>> array.update({ 18 :_})
>>>
>>> _ = 'b'
>>> # bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
# *и так далее - это копипаста с предыдушего скрипта*
# и получение ответа
>>> for i in range(32):
... print(array[i], end="")
Флаг: yaprofi{FlfOhryUBUabeRtkMjkSzRg}
. Проверяем - работает.
Отлично, есть 5 минут, чтобы сдать флаг. Магистры, которым дают всего лишь 4 часа на решение, должны быть действвительно очень сильными, а ведь там помимо двух реверсов есть еще задачи, и баллы с реверса не покрывают и половины всех баллов. Но это мысль в сторону.
Если нам не нравится решение за полчаса с помощью ручного брутфорса и использованияем IDA PRO и сниппетов на питоне
в качестве системы поддержки выдвижения суждения о виде флага - можно предложить тому же питону побрутить каждый из 32
математических блоков, ведь каждый из них находит решение независимо от другого.
author, editor: Rakovsky Stanislav, Unicorn CTF