Разбор виртуальной машины отборочного этапа Я-Профи 2019 для магистров
@ Rakovsky Stanivlav, Unicorn CTF | Friday, Dec 27, 2019 | 13 minutes read | Update at Friday, Dec 27, 2019


Данное задание подготовлено сообществом SPbCTF https://vk.com/spbctf


Один из участников тренировок SPbCTF прислал нам программу и сказал, что применил в ней инновационные приёмы, чтобы усложнить анализ. Помогите убедить его, что они не работают.

Linux: ./main 1234567890qwerty
(ссылка)
Windows: main.exe 1234567890qwerty
(ссылка)

Проанализируйте, как она это делает, и найдите верный ключ.
Формат ответа: yaprofi{...}`

main

main.exe

Как выглядит главная функция:

Приведем ее немного в порядок:

Заглянем в 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_{}.

  1. 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.

  1. 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]

  1. 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]

  1. 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

  1. 401671 -> fun4:
.text:0000000000401671                 pop     rbx
.text:0000000000401672                 mov     cs:ret_val, rbx
.text:0000000000401679                 retn

1 параметр. ret_val = var{arg0}

  1. 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}

  1. 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}

  1. 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}

  1. 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}

  1. 4016E8 -> fun9
.text:00000000004016E8                 pop     rbx
.text:00000000004016E9                 add     rsp, rbx
.text:00000000004016EC                 retn

1 параметр. jump(arg0//8)

  1. 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)

  1. 4016FE -> fun11
.text:00000000004016FE                 nop
.text:00000000004016FF                 retn

0 параметров. nop

  1. 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 раза запустить программу кажется проще, чем полчаса разбираться, как можно попробовать собрать математическую решалку.

Для этого:

  1. закомментируем вывод других функций;
  2. создадим в idapython глобальную переменную counter = 0;
  3. поставим обнуление счетчика с помощью точки останова где-нибудь в начале функи flag_check:
  4. и внесем изменения в функции 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

VM
Save as image

Cats!

Under construction

Wow! Flipable!

Hello from another side of the Moon!

Looking for a flag? Okay, take this:
LFXXKIDBOJSSA43XMVSXIIC6LYQCAIA=

About me

Under construction. You can try to contact me and fill this field… haha… ha…