Когда разработчик пишет программу, он имеет возможность использовать такие достижения цивилизации, как структуры и классы. А вот реверсеру это лишь осложняет жизнь: ему ведь необходимо понимать, как компилятор обрабатывает высокоуровневые сущности и как с ними потом работает процессор. О способах нахождения в бинарном коде объектов и структур мы и поговорим.
После небольшой передышки продолжим сопоставлять дизассемблерные листинги для архитектуры x86-64 и конструкции языков высокого уровня (в наших примерах мы используем C/C++). Этим мы занимаемся (если ты по какой‑то нелепой причине не читал прошлые номера нашего журнала), чтобы точнее понять принцип работы программ, подвергнутых дизассемблированию, и освоить некоторые интересные приемы реверс‑инжиниринга.
C/C++ не единственный язык, на котором можно написать логику программы. Благодаря виртуальным машинам существуют более быстрые способы разработки хороших приложений, но модули безопасности программ по‑прежнему чаще всего создаются с помощью C/C++. А главная задача хакера — разгрызть модуль безопасности, чтобы нужная программа не требовала регистрационных ключей, ввода паролей или, того хуже, подключения к веб‑серверу разработчика.
Пятнадцать лет назад эпический труд Криса Касперски «Фундаментальные основы хакерства» был настольной книгой каждого начинающего исследователя в области компьютерной безопасности. Однако время идет, и знания, опубликованные Крисом, теряют актуальность. Редакторы «Хакера» попытались обновить этот объемный труд и перенести его из времен Windows 2000 и Visual Studio 6.0 во времена Windows 10 и Visual Studio 2019.
Ссылки на другие статьи из этого цикла ищи на странице автора.
Обращаю твое внимание на одну деталь: с текущей статьи я перехожу на Visual Studio 2019. Последняя версия датируется 17 сентября и имеет номер 16.7.5. Чтобы избежать возможных несостыковок, советую тебе тоже обновить «Студию».
Структуры очень популярны среди программистов. Позволяя объединить под одной крышей родственные данные, они делают листинг программы более наглядным и упрощают его понимание. Соответственно, идентификация структур при дизассемблировании облегчает анализ кода. К великому сожалению исследователей, структуры как таковые существуют только в исходном тексте программы и практически полностью «перемалываются» при ее компиляции, становясь неотличимыми от обычных, никак не связанных друг с другом переменных.
Рассмотрим пример, демонстрирующий уничтожение структур на стадии компиляции:
#include <stdio.h>#include <string.h>struct zzz{ char s0[16]; int a; float f;};void func(struct zzz y)// Понятное дело, передачи структуры по значению лучше избегать,// но здесь это сделано умышленно для демонстрации скрытого создания// локальной переменной{ printf("%s %x %fn", &y.s0[0], y.a, y.f);}int main(){ struct zzz y; strcpy_s(&y.s0[0], 14, "Hello,Sailor!"); // Для копирования строки y.a = 0x666; // используется безопасная версия функции y.f = (float)6.6; // Чтобы подавить возражение компилятора, func(y); // указываем целевой тип}
Результат компиляции этого кода с помощью Visual Studio 2019 для платформы x64 должен выглядеть так:
main proc near; Члены структуры неотличимы от обычных локальных переменныхvar_48 = xmmword ptr -48hvar_38 = qword ptr -38hDst = byte ptr -28hvar_18 = qword ptr -18hvar_10 = qword ptr -10h sub rsp, 68h mov rax, cs:__security_cookie xor rax, rsp mov [rsp+68h+var_10], rax ; Подготовка параметров для вызова функции lea r8, Src ; "Hello,Sailor!" mov edx, 0Eh ; SizeInBytes lea rcx, [rsp+68h+Dst] ; Dst ; Вызов функции для копирования строки из сегмента данных в локальную ; переменную call cs:__imp_strcpy_s
Следующая команда копирует одно вещественное число, находящееся в младших 32 битах источника, — константу __real@40d33333
(смотрим, чему она равна при объявлении в секции rdata: __real@40d33333 dd 6.5999999
, в формате float
она будет равна 6.6) в младшие 32 бита приемника — 128-битного регистра XMM1
. Напомню, восемь регистров XMM0 — XMM7
были добавлены в расширение SSE и поэтому впервые появились в процессоре Pentium III.
movss xmm1, cs:__real@40d33333 ; Помещаем указатель на строку в регистр RDX lea rdx, [rsp+68h+var_48]
Далее с использованием инструкции MOVUPS
из расширения SSE копируются невыровненные куски по 16 бит. Таким образом, за раз копируются сразу восемь символов Unicode. Однако количество символов в строке вполне может быть не кратно восьми, поэтому используется именно эта инструкция — все остальные инструкции из расширения SSE оперируют с переменными, выровненными по 16-битным границам памяти. В ином случае они вызывают исключение.
movups xmm0, xmmword ptr [rsp+68h+Dst] ; В регистр RCX помещаем форматную строку для функции printf lea rcx, _Format ; "%s %x %fn" ; Помещаем двойное слово (значение 0x666) в переменную типа DWORD mov dword ptr [rsp+68h+var_18], 666h ; --1
Следующая команда копирует строго двойное слово из памяти в регистр (у нас это XMM3
). Значение, сохраненное в копируемой области памяти: 6.599999904632568
, выровнено по границе 16 бит и на самом деле равно 6.6. В случае копирования из памяти в регистр (подобно нашему примеру) обнуляется старшее двойное слово источника.
movsd xmm3, cs:__real@401a666660000000 ; Помещаем значение 0x666 в 32-битный регистр mov r8d, 666h ; Из переменной (см. метку --1) копируем двойное слово в регистр movsd xmm2, [rsp+68h+var_18]
Далее учетверенное слово (64 бит) копируется из регистра XMM3
расширения SSE в регистр общего назначения R9
, добавленный вместе с расширением x86-64. Ведь AMD64, по сути, представляет собой такое же расширение процессорной архитектуры x86, как и SSE.
movq r9, xmm3
Инструкция shufps
посредством битовой маски комбинирует и переставляет данные в 32-битных компонентах XMM-регистра. Таким образом, если представить 0E1h
в бинарном виде, получим 11100001b
. В соответствии с этой маской происходит трансформация всех четырех 32-битных частей регистра XMM2
.
shufps xmm2, xmm2, 0E1h ; Копирование нижней 32-битной части источника в приемник movss xmm2, xmm1 ; Копирует 128 бит из регистра в переменную movaps [rsp+68h+var_48], xmm0 ; В соответствии с маской перемешивает содержимое регистра (см. выше) shufps xmm2, xmm2, 0E1h ; Две следующие инструкции помещают значение регистра в переменные, ; находящиеся в памяти movsd [rsp+68h+var_18], xmm2 movsd [rsp+68h+var_38], xmm2 ; Все параметры находятся на своих местах, вызываем функцию printf call printf xor eax, eax mov rcx, [rsp+68h+var_10] xor rcx, rsp ; StackCookie call __security_check_cookie add rsp, 68h retnmain endp
Компилятор сгенерировал довольно витиеватый код со множеством команд из расширения SSE. При этом он встроил функцию func
прямо в main
!
А теперь заменим структуру последовательным объявлением тех же самых переменных и рассмотрим пример, демонстрирующий сходство структур с обычными локальными переменными.
int main(){ char s0[16]; int a; float f; strcpy_s(&s0[0], 14, "Hello,Sailor!"); a = 0x666; f = (float)6.6; printf("%s %x %fn", &s0[0], a, f);}
И сравним результат компиляции с предыдущим:
main proc nearDst = byte ptr -28hvar_18 = qword ptr -18h; Есть различие! Компилятор избавился от ненужных для выполнения переменных,; однако от этого не становится понятнее, принадлежат переменные структуре или нет sub rsp, 48h mov rax, cs:__security_cookie xor rax, rsp mov [rsp+48h+var_18], rax ; Готовим параметры lea r8, Src ; "Hello,Sailor!" mov edx, 0Eh ; SizeInBytes lea rcx, [rsp+48h+Dst] ; Dst ; Вызываем функцию копирования строки call cs:__imp_strcpy_s ; В XMM3 помещается значение 6.599999904632568 (подробно мы говорили, ; когда разбирали предыдущий листинг) movsd xmm3, cs:__real@401a666660000000 ; Последующие инструкции продолжают готовить параметры для функции lea rdx, [rsp+48h+Dst] movq r9, xmm3 ; В регистр RCX помещаем форматную строку для функции printf lea rcx, _Format ; "%s %x %fn" ; Помещаем значение 0x666 в младшие 32 бита регистра R8 mov r8d, 666h ; Вызов функции printf call printf xor eax, eax mov rcx, [rsp+48h+var_18] xor rcx, rsp ; StackCookie call __security_check_cookie add rsp, 48h retnmain endp
Без вызова дополнительных функций и передачи параметров дизассемблерный листинг заметно сократился. Остальной код остался идентичным предыдущему листингу.
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
1 год7690 р. |
1 месяц720 р. |
Я уже участник «Xakep.ru»
Читайте также
Последние новости