Фундаментальные основы хакерства. Как идентифицировать структуры и объекты в чужой программе

Содержание статьи

  • Идентификация структур
  • Идентификация объектов
  • Классы и объекты
  • Мой адрес — не дом и не улица!
  • Заключение

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

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

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

#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!

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

IDA PROint 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

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *