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

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

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

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

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

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

Вариант 2. Открой один материал

Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии