Кибервызов 2020 от Ростелеком-Солар. Студенческий уровень - весь Reverse
@ Rakovsky Stanislav | Monday, Aug 31, 2020 | 9 minutes read | Update at Monday, Aug 31, 2020

Черновик. Нашли опечатку - пишите @hexadec1mal, мау

Cobol

TTERM

TOTP

oPOSsum 1 и oPOSsum 2

reverse, 325 points (104 solves total) > Cobol

Мы просто не понимаем, что тут написано.

nc cobol.level-up.2020.tasks.cyberchallenge.ru 20004

cobol.cbl

random.cbl

Я тоже не понимаю, что тут происходит. Диалектов кобола очень много, на gnucobol 2.2 воспроизвести ошибку не удалось.

Подозреваю, что фича в строке 44 файла cobol.cbl, там идет неправильная проверка - input-buffer сравнивается не только с secret, но и с s.

Соответственно, загоняем 40 Z (ими заполнена переменная s) - получаем флаг:

Success
CC{IsuddenlyBecame40yearsOlder}

Кстати, если кому понадобится обвязка для решения hashcash:

from pwn import *
if 1:
    conn = remote('cobol.level-up.2020.tasks.cyberchallenge.ru', 20004)
    conn.recvuntil('hashcash -mb24 ')
    token = conn.recvuntil('> ').split(b"\n")[0]
    print(token)
    hc = process(['hashcash', '-mb24', token])
    t2 = hc.recvuntil("\n", drop=True).split(b" ")[-1]

UPD:

@kekov, [31.08.20 10:09]

Кстати, секрет кобола: там есть ограничение на максимальную длину строки, дальше все обрезается. И в проверке на равенство вместо secret получается s. Было на рустф 🙂

@finkrer, [31.08.20 10:10]

в вскоде очень хорошо видно, кстати

@ne_bknn, [31.08.20 10:11]

вим

reverse, 524 points (29 solves total) > TTERM

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

tterm.exe

tterm.pdb

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

Market:
  0: TSLA $1660.02 (-$1.32)
  1: INTC $48.36 (+$0.01)
  2: MSFT $209.68 (-$0.12)

Menu:
  1. Buy shares
  2. Sell shares
  3. Insights
  4. Exit

Choose menu option:

Интересная функция Insights. Trading Insights переводится как Торговая аналитика, поэтому изучим ее логику.

Автор таска заботливо приложил нам .pdb-файл, поэтому с удовольствием загрузим его.

Как это делается в Ida Pro:

  1. Соглашаемся с загрузкой pdb с сервера Microsoft.
  2. Ида ругается, что у мелкомягких не нашла нужного файла, и предлгает загрузить его с диска. Соглашаемся, указываем путь.
  3. ???
  4. Profit!

Пользы от этого будет много, ибо у нас в файле namespace std hell.

Залетаем в функцию handle_insights.

Мы видим интересную проверку, но простым патчием или изменением флага ZF делу не помочь - флаг некорректно расшифруется.

if ( (unsigned __int64)std::vector<unsigned char>::size(&history) <= 9 )

Если история наших операций составляет больше 9 переменных - мы вылетим из дальнейших проверок.

Попробуем провести операции:

BUY TSLA 2
SELL TSLA 2
BUY INTC 50

Вектор заполнился следующими значениями:

0x00 0x00 0x02 0x00 0x01 0x02 0x01 0x00 0x32

Убедимся, что в истории операции записываются друг за другом, и что сами операции представляют такой вид:

struct oper{
_BYTE option
_BYTE oper_type
_BYTE count
}

Также проверяем, что count - остаток от деления нашего количества на 256.

Разгребая std, становится понятной логика проверки: Берется вектор 0xCA 0xFE 0xBA 0xBE 0xDE 0xAD 0xBE 0xEF, его соединяют с нашим вводом, берут md5 (функция Hash) и сравнивают с захардкоженным значением.

Почему md5?

CryptCreateHash(phProv, 0x8003, 0i64, 0, &phHash)

Ищем доки по этой функции, видим, что второй патаметр задает хэш-функцию, и все enum-ы начинаются на CALG_. Наводим на 0x8003, жмем M - ищем нужное значение.

CryptCreateHash(phProv, CALG_MD5, 0i64, 0, &phHash)

Также можно заматчить по длине вшитого вектора.

Задача сводится к брутфорсу чит-кода покупок и продаж. Мы знаем, что длина истории должна быть не больше 9 единиц, что соответствует 1-3 операциям.

from hashlib import md5
import binascii
import time
t = time.time()
expect = binascii.unhexlify("E7E9F6588A5A63233AFDB1C20AC78674")

# массив всех допустимых операций. Он небольшой: 2*3*256 = 1536.
# Всего комбинаций из одной, двух и трех комбинаций суммарно 3.626.239.488, что вполне брутабельно
all_vars_for_one_round = []
for operation in (0, 1):
    for opcion in (0,1,2):
        for value in range(256):
            all_vars_for_one_round.append(bytes([opcion, operation, value]))

# сразу брутим чит-код по три операции
for r0 in all_vars_for_one_round:
    print(all_vars_for_one_round.index(r0)/len(all_vars_for_one_round))
    for r1 in all_vars_for_one_round:
        for r2 in all_vars_for_one_round:
            ans = md5(b"\xCA\xFE\xBA\xBE\xDE\xAD\xBE\xEF"+r0+r1+r2).digest()
            if ans == expect:
                print("YAY", all_vars_for_one_round[r0], all_vars_for_one_round[r1], all_vars_for_one_round[r2])
                print(time.time() - t)
                break
print("Done")

Даже жутко “медленный” питон справился за 6 минут. Простора для оптимизации полно - переписать на более быстрый язык, построить дерево решений, сколько денег мне должно хватить на эти покупки.

Вводим наш чит-код (BUY INTC 42 - BUY MSFT 179 - BUY TSLA 13) в программу:

[INSIGHTS] Authorizing... success. Decrypting...

И затем нам раскрывают очень важные секреты:

1. Sleeping well is important
2. Brushing teeth is important too
3. CC{d3Mo-W4LL3T-15-fOR-NoO82-t1M3-To-tr4D3-U51N9-r34L-MON3y}
4. jk no insights here, police is already coming after you

UPD:

@Exie42, [31.08.20 10:28]

echo "000102,0001,CAFEBABEDEADBEEF?1?2?b?1?2?b?1?2?b" > mask.hcmask && .\hashcat.exe -a 3 E7E9F6588A5A63233AFDB1C20AC78674 .\mask.hcmask -v --hex-charset

@Daniil159x, [31.08.20 10:34]

hashcat пару секунд
./hashcat -m 0 -a 3 md5.txt --hex-wordlist --hex-charset -1 '000102' -2 '0001' 'cafebabedeadbeef?1?2?b?1?2?b?1?2?b'

reverse, 756 points (9 solves total) > TOTP

Наш инженер, Пётр, пропал. Мы знаем, что в последнее время
он начал работать над каким-то секретным проектом, но, чтобы
его посмотреть, требуется ввести ключ. Для генерации этих
ключей Пётр использовал свой самодельный токен на базе
Arduino Uno.

На приложенной записи можно увидеть, как работает
устройство. Мы смогли понять, что на записи устройство по
нажатию кнопки сгенерировало коды

881964
881964
011225
К сожалению, сейчас генерируемые устройством коды уже не
подходят. Мы предполагаем, что там могло сбиться время. К
счастью, мы смогли получить прошивку устройства, пожалуйста 
разберитесь с ней и узнайте подробности об этом секретном
проекте.

footage.mp4

totp.hex

Дана ссылка на секретный проект (возможно, в определенный момент протухнет).

Формат .hex спокойно воспринимает 010 Editor, можно экспортировать как бинарь.

Это прошивка под avr-микроконтроллеры. Спокойно открывается в IDA PRO (у кого-нибудь купленный hexrays с atmel avr?) в режиме декомпилятора, в Сutter в режиме декомпилятора и дизассемлера (Ghidra).

Повтыкав пару часиков в бинарь, можно увидеть цикл работы(IDA: ROM:1D73) бесконечной рутины (IDA: ROM:0A30), в рамках которой происходит ожидание нажатия кнопки (IDA: ROM:0A32 - ROM:0A3A), и после нее - вызов пяти фунок, отвечающих за шифрование и вывод информации на светодиоды.

Криптография распознана не была, увы и ах.

Вечером второго дня приходит хинт:

Обратите внимание на название задания.

Гуглим arduino totp, находим единственный проект.

В конце нашей прошивки можно увидеть строки nan, inf, ovf, %06ld, ayy lmao.

Заматчим четвертую строку с единственной в исходниках проекта - мы движемся в верном направлении:

Также видим, что в ходе работы проекта на вход принимается строка. Первые три перечисленные строки из прошивки относятся к математичесим примитивам, а вот последняя строка уже интересна.

Проект ссылается на RFC 6238 и в качестве демонстрации показывает видео с использованием мобильного приложения Google Authenticator. Это приложение и несколько подобных отказываются принимать ayy lmao, ожидая b32 строку. Не вопрос, мы люди не жадные, зашифровываем. По экрану весело бегают шестизначные коды, сменяя друг дружку каждые 30 секунд.

Переходим на сайт секретного проекта.

Вводим ключ.

Разумно. Один ключ можно было бы побрутить в вечном цикле надеясь ня удачу (длина - 6 цифр), а два подряд - уже почти нереально.

К сожалению, ничего более в исходниках страницы не припрятано.

reverse, 578 points (22 solves total) > oPOSsum 1

reverse, 597 points (20 solves total) > oPOSsum 2

К нам в банк привезли прототип нового POS-терминала.
Говорят, там можно запускать сторонние приложения
(ROM-образы), но они обязательно должны быть подписаны. Нам
нужно исследовать насколько эти новые терминалы безопасны,
но у нас, как всегда, не хватает на это ресурсов. Пока мы
только выяснили, что память устройства имеет следующий вид:

+-----------------------------+
|         BOOTLOADER          |
+-----------------------------+
|             ...             |
+-----------------------------+ -----> 0x80000000
|     ROM VALIDATOR STACK     |
+-----------------------------+ -----> 0x80000400
|     ROM VALIDATOR CODE      |
+-----------------------------+
|             ...             |
+-----------------------------+ -----> 0x80002000
|          ROM STACK          |
+-----------------------------+ -----> 0x80002400
|          ROM IMAGE          |
+-----------------------------+
Поможешь?

P.S. "Первый флаг укажет путь, второй же ждет в конце",
— Конфуций.

nc opossum.level-up.2020.tasks.cyberchallenge.ru 20003

rom

Фаззим заголовок

Нам доступна прошивка. Изучим ее структуру, вооружившись полуручным скриптом на фаззинг.

from pwn import *
from time import sleep
import struct
from base64 import b64encode
file = bytearray(open("download.dat", "rb").read())

FZ = 4

for i in range(256):
    conn = remote('dejavu.level-up.2020.tasks.cyberchallenge.ru', 20003)

    conn.recvuntil("(base64 encoded):")
    file[FZ] = i
    print("try", hex(i))
    send = b64encode(file)
    conn.send(send+b"\n")
    a = conn.recvuntil("0x0")
    print(a)

    conn.close()

010 + этот скрипт + выдаваемые ошибки на стороне сервера позволяют узнать структуру заголовка нашей прошивки:

// [ERROR] Wrong ROM image magic
_DWORD ROM_image_magic
// [ERROR] ROM image version is not supported
_WORD ROM_image_version
// [ERROR] Invalid ROM image size - если меньше 0x21
// [ERROR] ROM image is not aligned - если не кратен 16
// полный размер прошивки вместе с заголовком
_WORD ROM_image_size 
// [ERROR] Bad signature: <16 байт памяти>
_DWORD SIG_offset
_DWORD CODE_offset

Затем идет секция кода прошивки и секция сигнатуры.

Достаем прошивку

Уязвимость заключается в том, что нам выводят 16 байт памяти по указанному оффсету (он считается относительно базы ROM), поэтому мы можем прочитать память секции ROM VALIDATOR CODE, чтобы выяснить, как происходит проверка контрольной суммы.

Баффнем наш скрипт - добавим в него возможность дампить память.

Чтобы сохранить значение переменной ans, можно запустить python с параметром -i.

Следует отметить достаточно неудобный вывод ошибки, он может написать все 16 байт друг за другом, а может представить их в виде хексов, разделенных пробелом, поэтому нам нужно обрабатывать оба кейса.

from pwn import *
from time import sleep
import struct
from base64 import b64encode
file = bytearray(open("download.dat", "rb").read())

ans = b""

i = -5
while 1:
    i+=1
    conn = remote('dejavu.level-up.2020.tasks.cyberchallenge.ru', 20003)

    conn.recvuntil("(base64 encoded):")
    print("try", hex(i))
    
    sig_offset = struct.pack("<i", -0x2000 + i*16)
    file[0x8] = sig_offset[0]
    file[0x8+1] = sig_offset[1]
    file[0x8+2] = sig_offset[2]
    file[0x8+3] = sig_offset[3]
    
    send = b64encode(file)
    conn.send(send+b"\n")
    a = conn.recvuntil("0x0")
    print(a)

    conn.close()
    
    if b"Bad signature" in a:
        sig1 = b"signature: \x00\n"
        sig2 = b"\nExited with code 0x0"
        ind1 = a.find(sig1)
        ind2 = a.find(sig2)
        app = a[ind1+len(sig1):ind2]
        print("app", app)
        if len(app.split(b" ")) == 17:
            print("type 1")
            ans+=bytes([int(j, 16) for j in app.decode().split(" ")[:-1]])
        else:
            print("type 2")
            ans+=app
        
        print("ans", ans)
    else:
        print("F")
        exit()
    conn.close()

Таким образом, достаем прошивку, в ней зашит ключ от первого таска:

Правильная подпись

Где же второй флаг? В нашей прошивке есть неоднозначный намек на то, что нам нужно запустить ее с инструкцией int 22h. Хозяин-барин.

Для удобства, чтобы не смотреть на красные MEMORY[...], создаем в базе Иды секцию с ROM (View - Open Subviews - Segments - Add Segment).

Изучаем функции из прошивки валидатора, находим алгоритм хэширования:

  LOWORD(a1) = SIZE - 16;
  i = 0i64;
  xorry1 = ROM_offset_14 ^ ROM_image_magic;
  xorry2 = ROM_offset_18 ^ ROM_image_version__size;
  xorry3 = ROM_offset_1C ^ SIG_offset;
  xorry4 = ROM_offset_10 ^ CODE_offset;
  if ( (signed __int16)(SIZE - 16) > 32 )
  {
    LOWORD(a1) = SIZE - 48;
    for ( i = (_DWORD *)&unk_20; ; LOWORD(i) = (_WORD)i + 16 )
    {
      xorry1 ^= i[0x20000901];
      xorry2 ^= i[0x20000902];
      xorry3 ^= i[0x20000903];
      xorry4 ^= i[0x20000900];
      if ( (signed __int16)a1 <= 16 )
        break;
      LOWORD(a1) = a1 - 16;
    }
  }
  sub_154(a1, i);

Алгоритм хэширования на двордовых ксорах (в целом - разницы нет, будь это дворд или байт). Видно, что первые и вторые шестнадцать байт ксорятся в немного зашаффленном порядке, но это не страшно.

Заглянем в исполняемую часть нашей прошивки:

ROM:0000000080002410 ROM_offset_10:
ROM:0000000080002410                 jmp     short loc_8000241E
ROM:0000000080002412 ; ---------------------------------------------------------------------------
ROM:0000000080002412
ROM:0000000080002412 loc_80002412:                           ; CODE XREF: ROM:loc_8000241E↓p
ROM:0000000080002412                 pop     rax
ROM:0000000080002413                 mov     ebx, 22h ; '"'
ROM:0000000080002418
ROM:0000000080002418 ROM_offset_18:                          ; DOS - PROGRAM TERMINATION
ROM:0000000080002418                 int     20h             ; returns to DOS--identical to INT 21/AH=00h
ROM:000000008000241A ; ---------------------------------------------------------------------------
ROM:000000008000241A                 xor     eax, eax
ROM:000000008000241C
ROM:000000008000241C ROM_offset_1C:                          ; DOS - PROGRAM TERMINATION
ROM:000000008000241C                 int     21h
ROM:000000008000241E ; ---------------------------------------------------------------------------
ROM:000000008000241E
ROM:000000008000241E loc_8000241E:                           ; CODE XREF: ROM:ROM_offset_10↑j
ROM:000000008000241E                 call    loc_80002412
ROM:000000008000241E ; ---------------------------------------------------------------------------
ROM:0000000080002423 aJustRememberNe db 'Just remember: NEVER use int 22h!',0

Для удобства представления я поместил прошивку в секцию через idapython:

rom = open("download.dat", "rb").read()
for i in range(len(rom)):
    idc.PatchByte(ord(rom[i]), 0x80002400+i)

Заменим байт по адресу 80002419 (смещение 0x19 в прошивке) на 0x22.

Подсчитаем ожидаемый байт по смещению 0x19 + 0x10 = 0x29:

>>> старый_байт_19 ^ новый_байт_19 ^ старый_байт_29
>>> hex(0x20^0x22^0x65)
'0x67'

Заливаем, проверяем.

CC{17_w4s_4_l0ng_j0urn3y_n0w_y0u_c4n_r3st}
Exited with code 0x0

author, editor: Rakovsky Stanislav, Unicorn CTF

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…