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

Пос­ле неболь­шой передыш­ки про­дол­жим сопос­тавлять дизас­сем­блер­ные лис­тинги для архи­тек­туры 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. Что­бы избе­жать воз­можных несос­тыковок, советую тебе тоже обно­вить «Сту­дию».

 

Идентификация структур

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

Мастер создания приложения в VS’19
Мас­тер соз­дания при­ложе­ния в VS’19

Рас­смот­рим при­мер, демонс­три­рующий унич­тожение струк­тур на ста­дии ком­пиляции:

#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 -48h
var_38 = qword ptr -38h
Dst = byte ptr -28h
var_18 = qword ptr -18h
var_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 %f\n"
; Помещаем двойное слово (значение 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
retn
main endp

Ком­пилятор сге­нери­ровал доволь­но вити­ева­тый код со мно­жес­твом команд из рас­ширения SSE. При этом он встро­ил фун­кцию func пря­мо в main!

А теперь заменим струк­туру пос­ледова­тель­ным объ­явле­нием тех же самых перемен­ных и рас­смот­рим при­мер, демонс­три­рующий сходс­тво струк­тур с обыч­ными локаль­ными перемен­ными.

IDA PRO
IDA PRO
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 near
Dst = byte ptr -28h
var_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 %f\n"
; Помещаем значение 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
retn
main endp

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

Продолжение доступно только участникам

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

Крис Касперски

Крис Касперски

Известный российский хакер. Легенда ][, ex-редактор ВЗЛОМа. Также известен под псевдонимами мыщъх, nezumi (яп. 鼠, мышь), n2k, elraton, souriz, tikus, muss, farah, jardon, KPNC.

Юрий Язев

Юрий Язев

Широко известен под псевдонимом yurembo. Программист, разработчик видеоигр, независимый исследователь. Старый автор журнала «Хакер».

Check Also

Эксперты обнаружили еще одну малварь для атак на Docker

Специалисты китайской компанией Qihoo 360 обнаружили вредоноса Blackrota, который атакует …

Оставить мнение