• Партнер

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

    Пос­ле неболь­шой передыш­ки про­дол­жим сопос­тавлять дизас­сем­блер­ные лис­тинги для архи­тек­туры 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»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


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