• Приглашаем на KubanCTF

    Старт соревнований 14 сентября в 10:00 по москве

    Ссылка на регистрацию в соревнованиях Kuban CTF: kubanctf.ru

    Кодебай является технологическим партнером мероприятия

[Оригинал] How2 имитирует выполнение файла elf под другой архитектурой

Предисловие​

На форуме уже есть много статей об анализе формата ELF и моделировании библиотечных файлов. Эта статья не новая идея. Это просто личный отчет моего исследования. Надеюсь, мой опыт поможет всем.

Если есть какие-либо ошибки или упущения, пожалуйста, исправьте их в комментариях.

Введение​

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

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

Однако, если вы столкнетесь с двоичными файлами разных архитектур, например с архитектурой aarch64, как их можно легко отладить и смоделировать?

Распространенным подходом является запуск gdbserver или использование frida аналогичных инструментов для трассировки на соответствующей архитектуре. Но этот подход слишком громоздкий. Есть ли более простой способ имитировать запуск этого файла ELF?

Начните с удаления шифрования строк​

Начнем с нашего примера файла libkpk.so. Этот файл библиотеки содержит алгоритм шифрования, который необходимо отменить.

libkpk.soЭто файл библиотеки, извлеченный из установочного пакета Android. По данным декомпиляции можно обнаружить, что он должен содержать три функции, а именно fetch, isKpkи kpkJava вызывает эти функции через jni. Среди этих функций мы уделяем особое внимание kpkфункции, которая выступает в качестве функции шифрования.

1720959188767.png

Однако в этом двоичном файле не было обнаружено строк, связанных с шифрованием. Даже если строки и были, они выглядели в очень странном состоянии.

1720959207006.png

Наблюдая за этими странными строками, мы обнаружили, что эти строки будут изменены функциями init_arrayвнутри в начале .datadiv_decodexxx, поэтому мы можем разумно сделать вывод, что эти datadiv_decode функции являются функциями дешифрования, используемыми для расшифровки зашифрованных строк при загрузке библиотеки.

1720959258622.png

После поиска можно обнаружить datadiv_decode, что это функция дешифрования, которая появляется после обфускации строки Armariris.

Армаририс замешательство​

Armariris — это платформа обфускации на основе llvm, разработанная Шанхайским университетом Цзяо Тонг. Открытый исходный код находится по адресу https://github.com/GoSSIP-SJTU/Armariris .

Логика шифрования строк в Armariris относительно проста. «Для всех константных строк сначала создайте читаемую и записываемую глобальную переменную того же типа и размера и сохраните исходную строку xor случайное число в этом новом блоке.".

Расшифровка также относительно проста. Запишите логику расшифровки строки в соответствующую функцию расшифровки. Затем просто запустите функцию расшифровки при загрузке библиотеки.

подробные шаги​

Таким образом, метод расшифровки можно резюмировать так:

  1. Загрузить эльфа в память
  2. Имитировать запуск функции дешифрования
  3. Сохраните расшифрованную строку и перезапишите исходную зашифрованную строку.
  4. Удалить все функции дешифрования
Однако, поскольку этот двоичный файл aarch64 скомпилирован , а наш компьютер — amd64, его нельзя запустить напрямую. В это время мы можем использовать unicornдля имитации запуска программы.

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

Тогда мы можем начать возиться.

Шаг 1: Загрузите эльфа в память​

В первой части нам нужно загрузить файл elf в память. Для этого нам нужно PT_LOAD прочитать из файла области памяти, отмеченные всеми сегментами файла elf, и записать их в соответствующие адреса памяти.

В 64-битной версии заголовок программы определяется следующим образом. Когда p_type= PT_LOAD, это означает, что сегмент является загружаемым сегментом, а это означает, что этот сегмент будет загружен или отображен в память, где p_offset представляет положение сегмента в файле и p_filesz представляет длину сегмента. p_vaddr Это адрес, по которому данные отображаются в виртуальную память, p_flagsпредставляющий разрешения на чтение, запись и выполнение этой области. Конечно, поскольку мы выполняем под эмулятором ЦП, нас вообще не волнуют его разрешения, поэтому мы устанавливаем для них все значения rwx.

1
2
3
4
5
6
7
8
9
10
typedef struct {
Elf64_Word p_type;
Elf64_Word p_flags;
Elf64_Off p_offset;
Elf64_Addr p_vaddr;
Elf64_Addr p_paddr;
Elf64_Xword p_filesz;
Elf64_Xword p_memsz;
Elf64_Xword p_align;
} Elf64_Phdr;
Поняв основные понятия, мы можем загрузить в память сегмент, соответствующий эльфу. Как показано в следующем коде, get_mapping_address он вычислит, какой адрес памяти необходимо сопоставить для этой памяти, и выровняет его по размеру страницы, который равен 0x1000. Затем mmap просто запишите данные

1
2
3
4
5
6
for seg in lib.iter_segments_by_type('PT_LOAD'):
st_addr, size = get_mapping_address(seg)
# don't care, rwx everywhere
emulator.mem_map(lib.address + st_addr, size, UC_PROT_ALL)
emulator.mem_write(lib.address + seg.header.p_vaddr, seg.data())
log.info("loaded segment 0x%x-0x%x to memory 0x%x-0x%x", seg.header.p_vaddr,seg.header.p_vaddr+seg.header.p_memsz, lib.address + st_addr, lib.address + st_addr+size)

Шаг 2. Найдите все функции, начинающиеся с .datadiv_decode, и выполните их.​

Этот шаг относительно прост. Прочитав файл с помощью pwntools, найдите в таблице символов все функции, начинающиеся с .datadiv_decode, а затем выполните их.

В aarch64 регистр указателя возврата LRустанавливается перед входом в функцию LR, затем он возвращается назад, когда функция завершается LR. Здесь мы устанавливаем его в 0 LR, поэтому мы знаем, что когда программа достигнет 0, функция завершится.

1
2
3
4
5
6
7
8
9
datadivs = []
for name in lib.symbols:
if name.startswith(".datadiv_decode"):
datadivs.append(name)
for datadiv in datadivs:
log.info("[%s] Function %s invoke", hex(lib.symbols[datadiv]), datadiv)
emulator.reg_write(arm64_const.UC_ARM64_REG_LR, 0) # 把return pointer (LR) 设置为0
emulator.emu_start(begin=lib.symbols[datadiv], until=0)
log.info("[%s] Function return",hex(lib.symbols[datadiv]),)

Шаг 3. Используйте расшифрованные данные для исправления исходных зашифрованных данных.​

Этот шаг также относительно прост, поскольку весь текст находится в .data абзаце, просто .data закройте весь абзац.
1
2
3
4
log.info("Patch .data section")
new_data = emulator.mem_read(lib.address + data_section_header.sh_addr, data_section_header.sh_size)
libfile.seek(data_section_header.sh_offset)
libfile.write(new_data)

Шаг 4. Исправьте все функции дешифрования.​

Этот шаг также относительно прост, просто позвольте функции ret

1
2
3
4
5
6
7
8
9
10
log.info("Patch .datadiv_decode functions")
for datadiv in datadivs:
libfile.seek(lib.symbols[datadiv] & 0xFFFFFFFE)
ret = b''
try:
ret = asm(shellcraft.ret())
except:
# fallback to manual
ret = asm("ret")
libfile.write(ret)

Эффект​

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

1720959771165.png

Найдя функцию, соответствующую kpk, нам нужно подумать, как ее смоделировать и выполнить.

Как загрузить файлы ELF​

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

Краткое описание сегмента​

readelf -l libkpk.decrypt.so Мы можем прочитать файл ELF и получить некоторую базовую информацию. Прежде всего, мы можем знать, что этот файл библиотеки является динамическим файлом общей библиотеки.

Он содержит несколько ключевых сегментов.

PT_LOAD:

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

PT_DYNAMIC:

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

Например, большинство программ теперь включают PIE (независимый от позиции исполняемый файл) при запуске. После включения pie базовое значение программы больше не равно 0. В это время вам необходимо исправить правильный адрес символа посредством перенаправления.

1720959848706.png

Если двоичный файл является не файлом библиотеки, а динамическим исполняемым файлом , то будет еще один важный сегмент: PT_INTERP

PT_INTERP

В этом сегменте хранится информация и расположение необходимого интерпретатора программы (то есть динамического компоновщика ). Роль динамического компоновщика заключается в загрузке общих библиотек исполняемого файла и разрешении символов, используемых в исполняемом файле, чтобы эти библиотечные функции можно было правильно вызывать во время работы программы. Он загружает каждую общую библиотеку в память при запуске программы и при необходимости преобразует символы в фактические адреса памяти, чтобы программа могла работать плавно.

1720959905399.png

Следует отметить, что если бинарный файл является статическим-static исполняемым файлом, проще говоря, это исполняемый файл с добавленными при сборке параметрами. Вообще говоря, сегмента PT_DYNAMIC нет PT_INTERP, потому что он не нужен.

1720959920625.png

Статическое связывание и динамическое связывание​

Мы знаем, что исполняемые файлы elf можно разделить на две категории в зависимости от метода компоновки во время компиляции: одна — статическая программа, которая использует статическую компоновку при компиляции, а другая — динамическая программа, которая при компиляции использует динамическую компоновку.

Статическая ссылка

Статическое связывание означает непосредственную упаковку целевого кода (то есть сгенерированного файла .o) и кода файла библиотеки, на который указывает ссылка, в исполняемый файл при компиляции и связывании. То есть сам исполняемый файл содержит весь необходимый код.

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

Динамическая ссылка

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

Время компиляции : компилятор создает объектный файл (файл .o) и встраивает справочную информацию динамической библиотеки в исполняемый файл вместо фактического кода библиотеки.

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

Время загрузки : при запуске программы указанный динамический компоновщик найдет и загрузит необходимые общие библиотеки на основе справочной информации в исполняемом файле и сопоставит их с адресным пространством процесса.

Разрешение символов . Динамический компоновщик отвечает за разрешение символов, используемых в программе (например, вызовы функций и глобальные переменные), сопоставляя их с фактическими адресами в загруженных общих библиотеках.

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

1720960060759.png


Загрузка файла elf также обсуждается в каждом конкретном случае. Если загружаемый двоичный файл статически связан, то процесс загрузки elf относительно прост.

  1. Сначала PT_LOAD загрузите все терминалы в память, настройте и инициализируйте стек.
  2. entry Наведите пк (или рипните) на адрес в эльфийском шапке
Таким образом, статический файл elf успешно выполняется.

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

  1. Сначала PT_LOAD загрузите все терминалы в память, настройте и инициализируйте стек.
  2. Разобрать PT_LOAD и завершить перенаправление всех символов в этой программе.
  3. Загрузите все зависимые динамические библиотеки
  4. В соответствии с таблицей перемещения ( .relили .rela) проанализируйте все зависимости символов, найдите фактический адрес каждого символа, переместите ссылку на символ, измените код или данные в памяти, чтобы они указывали на правильный адрес символа (включая немедленную привязку и отложенную привязку). .
  5. Настройте компьютер entryна запуск программы.
Конечно, в реальных приложениях, за исключением первого шага, остальные шаги не требуют тела программы. Эти шаги обычно выполняются динамическим компоновщиком системы. Поэтому после того, как динамически загружаемая программа завершит загрузку собственной программы, она сначала тем же методом загрузит динамический компоновщик в память, а затем передаст соответствующие данные динамическому компоновщику. После этого программе это делать не нужно. управляться Просто подождите, пока динамический компоновщик завершит все операции и перенаправит компьютер к самой программе entry.

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

перезагрузить​

Первый шаг — прочитать PT_DYNAMIC сегмент, чтобы получить всю необходимую информацию. В 64-битной версии PT_DYNAMIC структура данных в терминале может быть представлена следующей структурой данных.

Что d_tagэквивалентно идентификатору типа, d_un контролируемому d_tag, внутреннее значение d_tag представляет разные значения в зависимости от

1
2
3
4
5
6
7
typedef struct {
Elf64_Xword d_tag;
union {
Elf64_Xword d_val;
Elf64_Addr d_ptr;
} d_un;
} Elf64_Dyn;
В основном мы ориентируемся на следующие d_tag

DT_STRTAB/ DT_STRSZ
: адрес и длина таблицы строк. Имена символов, имена зависимостей и другие строки, необходимые компоновщику среды выполнения, находятся в этой таблице.

967169_CY922HGWX7JHW9X.png


DT_SYMTAB: Адрес таблицы символов.

967169_5MKMZK9VRZ458DG.png


Таблицу символов в 64-битной версии можно представить следующей структурой. В основном мы ориентируемся на st_nameи st_value.

st_nameУказывает индекс символа в таблице строк. Вы можете получить имя строки символа через это значение и таблицу строк. Если st_nameзначение равно 0, это означает, что символ не имеет относительного имени строки.

st_infoПредставляет значение этого символа. В зависимости от контекста это значение может быть абсолютным значением или адресом (в зависимости от типа последующего перенаправления).

1
2
3
4
5
6
7
8
typedef struct {
Elf64_Word st_name;
unsigned char st_info;
unsigned char st_other;
Elf64_Half st_shndx;
Elf64_Addr st_value;
Elf64_Xword st_size;
} Elf64_Sym;

DT_RELA/ DT_REL: адрес таблицы перемещения. В двоичном файле может быть несколько разделов перемещения. Когда вы создаете таблицу перемещения для исполняемого или общего объектного файла, редактор ссылок объединяет разделы, чтобы сформировать таблицу. В 64-разрядной версии ELF имеет две структуры таблицы перемещения, REL и RELA, для DT_REL и DT_RELA соответственно.

2
3
4
5
6
7
8
9
10
typedef struct {
Elf64_Addr r_offset; // Address
Elf64_Xword r_info; // 32-bit relocation type; 32-bit symbol index
} Elf64_Rel;

typedef struct {
Elf64_Addr r_offset; // Address
Elf64_Xword r_info; // 32-bit relocation type; 32-bit symbol index
Elf64_Sxword r_addend; // Addend
} Elf64_Rela;
r_offsetПредставляет виртуальный адрес, который необходимо восстановить.

r_infoСохраняются значения r_info_symи r_info_type, что указывает на то, что перенаправление указывает на N-йr_info_sym элемент в таблице символов , который представляет тип перенаправления. Для разных архитектур тип перенаправления также различен. Подробности можно узнать на официальном сайте. АБИ.r_info_type

1
2
r_info_sym = r_info >> 8
r_info_type = r_info && 0xff
При запуске перенаправления мы сначала читаем все таблицы перемещения, затем r_info_symнаходим символ, соответствующий каждому элементу перемещения, на основе таблицы символов и, наконец, вычисляем его на основе таблицы символов st_value, таблицы перемещения r_info_typeи r_addendбазового адреса текущей программы. Просто извлеките адрес после перенаправления и запишите его в память.

Переезд под AArch64​

Поскольку наш тестовый двоичный файл — aarch64, мы будем использовать aarch64 в качестве примера, чтобы проиллюстрировать процесс перенаправления.

Прежде всего, нам нужно знать, как рассчитать значение перенаправления для разных категорий перенаправления r_info_type. Для конкретного метода расчета мы можем обратиться к официальному ABI Arm . Здесь давайте взглянем на некоторые из наиболее часто используемых категорий перенаправления.

R_AARCH64_ABS64: адрес символа + r_addendбазовое значение программы + st_value+r_addened

967169_UECGQEBKY4UZ4VU.png


R_AARCH64_GLOB_DAT: также является адресом символа + r_addend также является базовым значением программы + st_value+r_addened

967169_5WXZFYSZ2ZZWX72.png


R_AARCH64_RELATIVE: Базовая стоимость программы плюс + r_addend

967169_PQQ74S8YYJ9QVZG.png


R_AARCH64_JUMP_SLOT: Этот особенный и представляет собой стол для прыжков. Эта категория обычно актуальна при вызове внешних библиотек. Когда st_valueзначение равно 0, это означает, что символ импортируется извне и его необходимо найти и загрузить из загруженной библиотеки через коннектор. Конечно, это перенаправление также можно связать во время выполнения, то есть отложенную загрузку, когда этот символ необходим. В этом разделе вы можете выполнить поиск и обратиться к процессу **__dl_runtime_resolve()**.

Если st_valueзначение не равно 0, оно R_AARCH64_GLOB_DAT совпадает с символическим адресом +r_addend

Пример реализации кода перенаправления вручную

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
def get_symbol_table(elf: 'ELF') -> SymbolTableSection:
for section in elf.iter_sections():
if section.header.sh_type == "SHT_DYNSYM":
return section


def get_relocations(elf: 'ELF') -> Dict[int, Relocation]:
rel_sections: List[RelocationSection] = []
for section in elf.iter_sections():
if section.header.sh_type in ["SHT_REL", "SHT_RELA"]:
rel_sections.append(section)
if section.header.sh_type == "SHT_DYNSYM":
dynsym = section

relocs = dict()
#
for rel_section in rel_sections:
for reloc in rel_section.iter_relocations():
r_offset = reloc.entry.r_offset # Indicates where .text[r_offset] needs to be fixed
if r_offset in relocs:
raise Exception("wtf")
relocs[r_offset] = reloc
return relocs


# fix relocation
relocs = get_relocations(lib)
symtab = get_symbol_table(lib)
for addr, reloc in relocs.items():
# 0x4962a0
#
if reloc.entry.r_info_type == ENUM_RELOC_TYPE_AARCH64['R_AARCH64_JUMP_SLOT']:
name = symtab.get_symbol(relocs[addr].entry.r_info_sym).name
# need to import from external library
if symtab.get_symbol(relocs[addr].entry.r_info_sym).entry.st_value == 0:
print(name, hex(addr), symtab.get_symbol(relocs[addr].entry.r_info_sym).entry.st_value,
relocs[addr].entry.r_addend)
if reloc.entry.r_info_type in [
ENUM_RELOC_TYPE_AARCH64['R_AARCH64_ABS64'],
ENUM_RELOC_TYPE_AARCH64['R_AARCH64_GLOB_DAT'],
ENUM_RELOC_TYPE_AARCH64['R_AARCH64_JUMP_SLOT']]:
ql.mem.write(lib.address + addr,
(lib.address + symtab.get_symbol(relocs[addr].entry.r_info_sym).entry.st_value + relocs[
addr].entry.r_addend).to_bytes(8,
"little"))
elif reloc.entry.r_info_type in [ENUM_RELOC_TYPE_AARCH64['R_AARCH64_RELATIVE']]:
ql.mem.write(lib.address + addr,
(lib.address + relocs[addr].entry.r_addend).to_bytes(8, "little"))
else:
print(f"not handled r_info_type {reloc.entry.r_info_type}")
# exit(0)
После завершения перемещения мы можем указать компьютеру, где мы хотим начать работу, а затем начать выполнение кода.

Используйте Qiling для имитации​

После завершения перенаправления мы можем приступить к выполнению кода. Здесь я использовал фреймворк qiling для имитации запуска кода под aarch64.

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

Реализация импортированных функций​

После перемещения двоичного файла с использованием упомянутого выше метода перемещения нам все равно необходимо обработать импортированные извне функции. Здесь у нас может быть два метода: один — загрузить зависимую библиотеку в память, затем найти соответствующую экспортируемую функцию и записать адрес обратно. Или мы также можем использовать перехватчики для ручной реализации функций, которые необходимо импортировать в Python.

Например, libkpk.soесли нам нужно использовать strlenэту функцию, мы можем перехватить strlenадрес и реализовать функцию strlen, чтобы нам не нужно было искать соответствующую зависимую библиотеку, а также мы могли отслеживать параметры и результаты вызова функции.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def hook_strlen(ql: Qiling):
# The string address is in X0 for AArch64
string_address = ql.arch.regs.read(arm64_const.UC_ARM64_REG_X0)
length = 0
while True:
byte = ql.mem.read(string_address + length, 1)
if byte[0] == 0:
break
length += 1
stlogger.log(f"strlen called with {ql.mem.read(string_address, length)}")
# Write the result back to X0
ql.arch.regs.write(arm64_const.UC_ARM64_REG_X0, length)
stlogger.callstack.pop(-1)
ql.arch.regs.write(arm64_const.UC_ARM64_REG_PC, ql.arch.regs.read(arm64_const.UC_ARM64_REG_LR))

ql.hook_address(hook_strlen, base_address + 0x0013b970)

Трассировка вызовов функций​

С помощью перехватчика qiling мы также можем отслеживать вызовы каждой функции и распечатывать стек вызовов функции.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class StackTracerLogger:
def __init__(self,
addr_to_fname,
base_address=0,
print_func_with_symbol_only=False,
print_exit=True,
printer=print
):
self.addr_to_fname = addr_to_fname
self.base_address = base_address
self.printer = printer
self.print_func_with_symbol_only = print_func_with_symbol_only
self.print_exit = print_exit
self.callstack = []

def call(self, func_addr: int, call_from: int):
self.callstack.append(func_addr)
fname = ""
if func_addr in self.addr_to_fname:
fname = self.addr_to_fname[func_addr]
if fname == "" and self.print_func_with_symbol_only:
return
elif fname == "":
fname = f"func_{hex(func_addr - base_address)}"
self.printer(
f"[{len(self.callstack)}]{' ' * len(self.callstack)}calls {fname} from {hex(call_from - self.base_address)}")

def exit(self, exit_addr):
if self.print_exit:
self.printer(f"[{len(self.callstack)}]{' ' * len(self.callstack)}exit at {hex(exit_addr - base_address)}")
if self.callstack:
self.callstack.pop(-1)

def log(self, msg):
self.printer(f"[{len(self.callstack)}]{' ' * (len(self.callstack) + 1)}{msg}")

def select_qiling_backtrace(self, arch_type: QL_ARCH):
if arch_type == QL_ARCH.ARM64:
return self.ql_aarch64_backtrace

def ql_aarch64_backtrace(self, ql: Qiling, address, size):
# Read the code at the current address
code = ql.mem.read(address, size)
# Decode the instruction (simple detection based on opcode; consider using Capstone for complex cases)
if size == 4:
opcode = int.from_bytes(code, 'little')
# Detect BL or BLX (0x94000000 for BL, check mask for lower bits)
if (int.from_bytes(code, 'little') & 0xFC000000) == 0x94000000:
# Calculate target address (offset is 26 bits, shift left and sign extend)
offset = int.from_bytes(code, 'little') & 0x03FFFFFF
if offset & 0x02000000: # Sign bit of 26-bit offset
offset -= 0x04000000 # 2's complement negative offset
target = address + (offset << 2) # left shift to account for instruction size
self.call(target, address)
# blr
elif (opcode & 0xFFFFFC1F) == 0xD63F0000:
reg_num = (opcode >> 5) & 0x1F
reg_val = ql.arch.regs.read(reg_num)
self.call(reg_val, address)
elif opcode == 0xd65f03c0: # RET
self.exit(address)

addr_to_fname = dict((v, k) for k, v in lib.symbols.items())
stlogger = StackTracerLogger(
addr_to_fname, lib.address, print_func_with_symbol_only=True, print_exit=False,
printer=log.info
)
ql.hook_code(stlogger.select_qiling_backtrace(ql.arch.type))


Эффект​

Мы видим, что программа работает отлично и распечатывает соответствующий стек вызовов.

967169_AKT4YM3TZG5ZTSE.png


трассировка стека вызовов обнаружила, что в качестве метода шифрования используется aec ecb
967169_BVY736TZAFBRYFT.png


Шифрование было успешно завершено, и зашифрованный URL-адрес был прочитан.

Заключение​

Благодаря приведенному выше подробному анализу и пошаговой демонстрации мы успешно смоделировали выполнение определенной функции библиотеки динамической компоновки в системе с различной архитектурой. Этот процесс не только предполагает глубокое понимание формата файла ELF, включая детали его загрузки и связывания, но также включает в себя кросс-платформенное моделирование с использованием передовых инструментов, таких как unicornи .qiling

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

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

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

Обучение наступательной кибербезопасности в игровой форме. Начать игру!