• Партнер

  • Се­год­ня мы с тобой научим­ся находить любые типы цик­лов в коде реаль­ных прог­рамм на язы­ках высоко­го уров­ня — пос­тро­енных с опти­миза­цией и без нее. Это шаг, с которым ты стол­кнешь­ся при обратной раз­работ­ке при­ложе­ний. При этом опре­деле­ние цик­ла – это толь­ко пол­дела. Далее нуж­но разоб­рать­ся в его механиз­ме, выделить исполь­зуемые дан­ные и кор­рек­тно деком­пилиро­вать, прев­ратив в строй­ный код на язы­ке высоко­го уров­ня. Без это­го точ­ное понима­ние алго­рит­ма прог­раммы невоз­можно.

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

    Фундаментальные основы хакерства

    Пят­надцать лет назад эпи­чес­кий труд Кри­са Кас­пер­ски «Фун­дамен­таль­ные осно­вы хакерс­тва» был нас­толь­ной кни­гой каж­дого начина­юще­го иссле­дова­теля в области компь­ютер­ной безопас­ности. Одна­ко вре­мя идет, и зна­ния, опуб­ликован­ные Кри­сом, теря­ют акту­аль­ность. Редак­торы «Хакера» попыта­лись обно­вить этот объ­емный труд и перенес­ти его из вре­мен Windows 2000 и Visual Studio 6.0 во вре­мена Windows 10 и Visual Studio 2019.

    Ссыл­ки на дру­гие статьи из это­го цик­ла ищи на стра­нице авто­ра.

     

    Циклы while/do

     

    Visual C++ 2022 с отключенной оптимизацией

    Для зак­репле­ния прой­ден­ного в прош­лой статье матери­ала рас­смот­рим нес­коль­ко живых при­меров. Нач­нем с самого прос­того — иден­тифика­ции цик­лов while/do:

    #include <stdio.h>
    int main()
    {
    int a = 0;
    while (a++ < 10)
    printf("Оператор цикла while\n");
    do {
    printf("Оператор цикла do\n");
    } while (--a > 0);
    }

    От­компи­лиру­ем этот код с помощью Visual C++ 2022 с отклю­чен­ной опти­миза­цией.

    Результат выполнения примера while-do
    Ре­зуль­тат выпол­нения при­мера while-do

    Ре­зуль­тат ком­пиляции дол­жен выг­лядеть при­мер­но так:

    ; int __cdecl main(int argc, const char **argv, const char **envp)
    main proc near ; CODE XREF: __scrt_common_main_seh+107↓p
    ; DATA XREF: .pdata:0000000140004018↓o
    var_18 = dword ptr -18h
    var_14 = dword ptr -14h
    ; резервируем память для двух локальных переменных,
    ; только откуда взялась вторая?
    sub rsp, 38h
    ; заносим в переменную var_18 значение 0
    ; следовательно, это переменная "a"
    mov [rsp+38h+var_18], 0

    Ни­же сле­дует перек­рес­тная ссыл­ка - loc_1400010EC, нап­равлен­ная вниз. Это говорит нам о том, что перед нами начало цик­ла. Пос­коль­ку перек­рес­тная ссыл­ка нап­равле­на вниз, то переход, ссы­лающий­ся на этот адрес, будет нап­равлен вверх!

    loc_1400010EC: ; CODE XREF: main+31↓j
    ; Загружаем в EAX значение переменной "a" (var_18)
    mov eax, [rsp+38h+var_18]
    ; Загружаем в var_14 значение переменной "a", вот, мы нашли,
    ; где используется вторая переменная
    mov [rsp+38h+var_14], eax
    ; Зачем-то снова загружаем то же значение в регистр EAX
    mov eax, [rsp+38h+var_18]
    ; Увеличение значения в регистре EAX на 1
    inc eax
    ; Загружаем значение из регистра EAX в переменную var_18 ("a")
    mov [rsp+38h+var_18], eax
    ; Сравниваем старое (до обновления) значение переменной "a",
    ; ранее сохраненное в var_14, с числом 0xA
    cmp [rsp+38h+var_14], 0Ah

    Ес­ли (var_14 >= 0xA), дела­ем пры­жок «впе­ред», непос­редс­твен­но за инс­трук­цию безус­ловно­го перехо­да, нап­равлен­ного «назад». Если выпол­няет­ся пры­жок «назад», зна­чит это цикл, а пос­коль­ку усло­вие выхода из цик­ла про­веря­ется в его начале, то это цикл с пре­дус­лови­ем.

    Для его отоб­ражения на цикл while необ­ходимо инверти­ровать усло­вие выхода из цик­ла на усло­вие про­дол­жения цик­ла, дру­гими сло­вами, заменить >= на <.

    Сде­лав это, мы получа­ем: while (a++ < 0xA)....

    jge short loc_140001113
    ; Начало тела цикла:
    ; заносим ссылку на строку "Оператор цикла while\n"
    lea rcx, _Format ; _Format
    ; выводим на консоль
    call printf
    ; безусловный переход, направленный назад, на метку loc_1400010EC - в начало цикла,
    ; в область подготовки переменных для проверки
    jmp short loc_1400010EC

    Меж­ду loc_1400010EC и jmp short loc_1400010E есть толь­ко одно усло­вие выхода из цик­ла: jge short loc_140001113. Зна­чит, исходный код цик­ла выг­лядел так:

    while (a++ < 0xA) printf("Оператор цикла while\n");

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

    loc_140001113: ; CODE XREF: main+23↑j
    ; main+4E↓j

    Ага, никако­го усло­вия в начале цик­ла не при­сутс­тву­ет, зна­чит, это цикл с усло­вием в кон­це или в середи­не.

    ; заносим ссылку на строку "Оператор цикла do\n"
    lea rcx, byte_140002278 ; _Format
    ; печатаем в консоли
    call printf
    ; тело цикла
    ; загружаем в EAX значение переменной var_18 ("a")
    mov eax, [rsp+38h+var_18]
    ; уменьшаем значение в EAX на 1
    dec eax
    ; возвращаем значение из EAX в переменную "a" - var_18
    mov [rsp+38h+var_18], eax
    ; сравниваем переменную "a" с нулем
    cmp [rsp+38h+var_18], 0
    ; Если (a > 0), делаем переход в начало цикла
    jg short loc_140001113

    Пос­коль­ку усло­вие рас­положе­но в кон­це цик­ла, это цикл do:

    do printf("Оператор цикла do\n");
    while (--a > 0);
    ; возвращаем 0
    xor eax, eax
    ; восстанавливаем стек
    add rsp, 38h
    retn
    main endp
     

    Visual C++ 2022 с включенной оптимизацией

    Сов­сем дру­гой резуль­тат получит­ся, если вклю­чить опти­миза­цию. Откомпи­лиру­ем тот же самый при­мер с клю­чом /O2 (мак­сималь­ная опти­миза­ция: при­ори­тет ско­рос­ти) и пос­мотрим на резуль­тат, выдан­ный ком­пилято­ром:

    ; int __cdecl main(int argc, const char **argv, const char **envp)
    main proc near ; CODE XREF: __scrt_common_main_seh+107↓p
    ; DATA XREF: .pdata:000000014000400C↓o
    ; сохраняем регистр в стеке
    push rbx
    ; подготавливаем стек, ни одной локальной переменной не объявлено
    sub rsp, 20h
    ; в EBX кладем число 0xA. Для чего, пока не ясно.
    mov ebx, 0Ah
    nop dword ptr [rax+rax+00h]
    ; Судя по следующей перекрестной ссылке, направленной вниз, это цикл!
    loc_140001080: ; CODE XREF: main+20↓j
    ; заносим в регистр RCX ссылку на строку "Оператор цикла while\n"
    lea rcx, _Format ; _Format
    ; выводим строку на терминал
    call printf
    ; Если это тело цикла, то где же предусловие?!
    ; Вычитаем из RBX число 1.
    sub rbx, 1
    ; Получается, что число 0xA, помещенное в EBX ранее, являлось начальным значением

    Инс­трук­ция SUB подоб­но CMP изме­няет сос­тояние фла­га нуля. Если в резуль­тате вычита­ния получа­ется 0, флаг нуля воз­водит­ся в еди­ницу. Сле­дующая инс­трук­ция совер­шает пры­жок назад, ког­да флаг не воз­веден, то есть в резуль­тате вычита­ния регистр RBX не стал равен нулю.

    jnz short loc_140001080

    Ком­пилятор в порыве опти­миза­ции прев­ратил неэф­фектив­ный цикл с пре­дус­лови­ем в более ком­пак­тный и быс­трый цикл с пос­тусло­вием. Имел ли он на это пра­во? А почему нет?! Про­ана­лизи­ровав код, ком­пилятор понял, что этот цикл выпол­няет­ся, по край­ней мере, один раз. Сле­дова­тель­но, скор­ректи­ровав усло­вие про­дол­жения, его про­вер­ку мож­но вынес­ти в конец цик­ла.

    Так­же в исходном тек­сте был инкре­мент счет­чика цик­ла от нуля до 0xA, а в под­готов­ленном тран­сля­тором коде мы видим обратный эффект: дек­ремент счет­чика от 0xA до нуля. Таким обра­зом, ком­пилятор заменил: while ((int a=0)+1) < 10) printf(...) на do printf(...) while ((int a=10)-1) > 0).

    При­чем, что инте­рес­но, он не срав­нивал перемен­ную цик­ла с кон­стан­той, а помес­тил кон­стан­ту в регистр и умень­шал его до тех пор, пока тот не стал равен нулю! Зачем? А затем, что так короче, да и работа­ет быс­трее.

    Хо­рошо, но как нам деком­пилиро­вать этот цикл? Непос­редс­твен­ное отоб­ражение на язык C/C++ дает сле­дующую инс­трук­цию:

    var_RBX = 0xA;
    do {
    printf("Оператор цикла while\n");
    var_RBX--;
    } while (var_RBX > 0);

    Впол­не кра­сивый и опти­маль­ный цикл с одной перемен­ной.

    ; Значение 0xB помещаем в регистр EBX. Это подготовка к следующему циклу.
    ; Этот код выполняется после завершения предыдущего цикла.
    mov ebx, 0Bh
    nop word ptr [rax+rax+00000000h]
    ; Перекрестная ссылка, направленная вниз, говорит нам о том, что это начало цикла
    loc_1400010A0: ; CODE XREF: main+40↓j
    ; Предусловия нет, значит, это цикл do
    ; заносим в регистр RCX ссылку на строку "Оператор цикла do\n"
    lea rcx, byte_140002278 ; _Format
    ; выводим строку на терминал
    call printf
    ; уменьшаем значение, загруженное в EBX, на единицу
    dec ebx
    ; проверяем EBX на равенство нулю
    test ebx, ebx
    ; Продолжаем выполнение цикла, пока EBX > 0
    jg short loc_1400010A0

    Этот цикл пря­миком отоб­ража­ется в конс­трук­цию язы­ка C/C++:

    var_EBX = 0xB;
    do { printf("Оператор цикла do\n"); }
    while (--var_EBX > 0);
    ; возвращаем ноль
    xor eax, eax
    ; восстанавливаем стек
    add rsp, 20h
    ; восстанавливаем регистр
    pop rbx
    retn
    main endp
     

    C++ Builder 10 без оптимизации

    Нес­коль­ко ина­че обра­баты­вает цик­лы ком­пилятор Embarcadero C++ Builder 10.4. Смот­ри при­мер while-do_cb:

    ; int __cdecl main(int argc, const char **argv, const char **envp)
    public main
    main proc near ; DATA XREF: __acrtused+29↑o
    ; объявляем шесть переменных
    var_1C = dword ptr -1Ch
    var_18 = dword ptr -18h
    var_14 = dword ptr -14h
    var_10 = qword ptr -10h
    var_8 = dword ptr -8
    var_4 = dword ptr -4
    ; сохраняем в стеке RBP
    push rbp
    ; резервируем память для локальных переменных
    sub rsp, 40h
    ; помещаем в RBP указатель на дно стека
    lea rbp, [rsp+40h]
    ; инициализируем переменные:
    ; в var_4 записывает 0, вероятно это переменная a из исходного кода
    mov [rbp+var_4], 0
    mov [rbp+var_8], ecx
    mov [rbp+var_10], rdx
    ; еще одна переменная, изначально равная нулю, возьмем на заметку
    mov [rbp+var_14], 0
    ; Ниже перекрестная ссылка, направленная вниз, значит, это начало какого-то цикла
    loc_40141F: ; CODE XREF: main+3E↓j
    ; в начале цикла условие не обнаружено, видимо, цикл с постусловием,
    ; хотя не будем спешить с выводами
    ; в регистр EAX копируем значение из переменной var_14
    mov eax, [rbp+var_14]
    ; копирование EAX в ECX
    mov ecx, eax
    ; увеличиваем значение в регистре ECX на 1
    add ecx, 1
    ; увеличенное значение из регистра ECX копируем в переменную var_14,
    ; из которой берется значение для счетчика в начале итерации
    mov [rbp+var_14], ecx
    ; сравнение не увеличенного значения с 0хА
    cmp eax, 0Ah
    ; если это значение больше или равно константе,
    ; тогда выполняем прыжок за пределы цикла в область старших адресов
    jge short loc_401440
    ; в случае продолжения выполнения помещаем ссылку на строку в регистр
    ; и выводим ее на консоль
    lea rcx, aOperatorIklaWh ; "Оператор цикла while\n"
    call printf
    ; зачем-то сохраняем текущее значение регистра EAX в переменной var_18...
    mov [rbp+var_18], eax
    ; ... и выполняем безусловный переход в начало цикла
    jmp short loc_40141F

    Вот так‑то C++ Builder опти­мизи­ровал код! Началь­ный цикл с пре­дус­лови­ем выпол­нения он прев­ратил в бес­конеч­ный цикл с усло­вием выхода посере­дине (за под­робнос­тями обра­тись к прош­лой статье)! Как мы можем деком­пилиро­вать этот цикл? Нап­рашива­ется такой вари­ант:

    int var_14 = 0;
    do {
    int var_EAX = var_14;
    int var_ECX = var_EAX;
    var_ECX++;
    var_14 = var_ECX;
    if (var_EAX >= 0xA) break;
    printf("Оператор цикла while\n");
    } while (TRUE);

    Этот вари­ант кар­диналь­но отли­чает­ся от пер­воначаль­ного, и я очень сом­нева­юсь, что в луч­шую сто­рону! Что ж, издер­жки про­изводс­тва...

    ; --------------------------------
    loc_401440: ; CODE XREF: main+2D↑j
    ; сюда происходит переход при выходе из предыдущего цикла
    ; как мы знаем, эта инструкция только переводит управление через себя
    jmp short $+2
    ; --------------------------------
    loc_401442: ; CODE XREF: main:loc_401440↑j
    ; main+5D↓j
    ; Новый цикл!
    ; Как видим, он начинается с вывода строки, нет условия, значит цикл с постусловием.
    lea rcx, aOperatorIklaDo ; "Оператор цикла do\n"
    call printf

    Про­маты­ваем дизас­сем­блер­ный лис­тинг вверх, что­бы вспом­нить, какое зна­чение находит­ся в регис­тре EAX. Зна­чит, в этом мес­те прог­раммы зна­чение в регис­тре EAX рав­но 0хА. Записы­ваем это зна­чение в перемен­ную var_1C (непонят­но для каких целей, ведь в будущем она не исполь­зует­ся). Выходит, локаль­ную перемен­ную a исходной прог­раммы пред­став­ляет регис­тро­вая перемен­ная EAX.

    mov [rbp+var_1C], eax
    ; Записываем в регистр EAX значение переменной var_14.
    ; А в ней содержится значение на 1 больше, чем в EAX! То есть, 0xB.
    mov eax, [rbp+var_14]
    ; Какой хитрый C++ Builder!
    ; Вместо реального вычитания он прибавляет к значению в EAX -1
    add eax, 0FFFFFFFFh
    ; Присваивает результат переменной var_14
    mov [rbp+var_14], eax
    ; И сравнивает уменьшенное значение с нулем
    cmp eax, 0
    ; Если (EAX > 0), то мы прыгаем назад к началу "нового цикла"
    ; и осуществлению очередной итерации
    jg short loc_401442

    Во что C++ Builder прев­ратил изна­чаль­ный цикл с пос­тусло­вием? В целом, никаких изме­нений он не внес, оста­вив все на сво­их мес­тах. И деком­пилиро­ван­ный лис­тинг это­го цик­ла дол­жен выг­лядеть при­мер­но так:

    var var_14 = 0xB;
    do
    {
    int var_EAX = var_14;
    var_EAX--;
    var_14 = var_EAX;
    printf("Оператор цикла while\n");
    } while (var_EAX > 0);
    ; В ином случае, когда (EAX <= 0), пропускаем переход
    ; и продолжаем выполнение кода программы
    mov [rbp+var_4], 0
    ; Возвращаем ноль
    mov eax, [rbp+var_4]
    ; Восстанавливаем стек
    add rsp, 40h
    ; Восстанавливаем регистр
    pop rbp
    retn
    main endp

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

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

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

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

    Подписаться
    Уведомить о
    2 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии