R0 CREW

Автоматическое удаление мусорных инструкций методом отслеживания состояния

Оригинал: usualsuspect.re

Аннотация

Существует множество методов обфускации кода, но все они разделяют одну цель – сделать код менее читаемым и тем самым создать трудности для реверс-инженера. Самый распространённый метод – вставка мусорных инструкций, чтобы раздуть код и усложнить задачу выделения значимых инструкций среди кучи других, не делающих в конечном счете ничего.

Я предлагаю решение для автоматической фильтрации мусорных инструкций путем отслеживания состояния исполнения (execution state). Используя Triton мы статически анализируем трассу обфусцированного кода и пытаемся отследить какие инструкции имеют побочный эффект (например, пишут в регистр или стек).

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

Я реализовал PoC в виде скрипта на Python, который может быть запущен в IDA Pro и статически удалить мусорные инструкции начиная с заданной точки. Скрипт занимает всего около 200 строк кода.

Введение

Я уже давно искал возможность поиграться с обфусцированным/виртуализированым кодом. И недавно я обнаружил образец вредоносного ПО, который был значительно обфусцирован, что сделало практически невозможным анализ внутренней логики. Отчеты (прим: наверное, имеется в виду VirusTotal, или аналог) определили, что образец был упакован с помощью коммерческого ПО для защиты на основе виртуализации – VMProtect. Посмотрев образец в IDA, стало понятно, что код не только защищен ВМ, но и в значительной степени обфусцирован путем внедрения мусорных инструкций.

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

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

Но я все же надеюсь, что данная статья достаточно интересна сама по себе, хотя и включает только те методики, которые в конечном счете сработали.

Что такое мусорные инструкции

Если мы хотим избавиться от мусорных инструкций, для начала определим что мы считаем таковыми.

Очевидный пример:

lea eax, dword ptr [esp]
mov eax, 7

Инструкция lea – мусорная, так как она пишет в eax, который перезаписывается следующей же командой.

Тоже самое, но для случая с флагами:

clc
add eax, num

Инструкция clc (сбросить флаг переноса) мусорная, так как add выставляет флаг переноса в зависимости от аргументов.

Я остановился на следующем определении:

По существу, инструкция изменяет значение регистра или флага или стек или память, но при этом нигде этот результат никогда не используется. Примеры выше идеально демонстрируют это (сделана запись в регистр и модифицирован CF), но результат никак не используется до момента перезаписи новым значением.

Границы для выполнения деобфускации

Очень важный вопрос с практической точки зрения: Где я должен начать автоматический анализ кода и где его закончить? Каковы границы кода, который нужно проанализировать, чтобы удалить мусорные инструкции?

В предыдущем разделе вы могли заметить, что я сделал акцент на слове никогда. В такой формулировке это, вообще говоря, трудная задача, потому что мы должны начать и закончить свой анализ где-нибудь (а не анализировать весь код в исполняемом файле).

Для того, чтобы отбросить инструкцию как мусорную, мы обязаны быть уверены в том, что никакая другая во всем коде, не ожидает получить результат исполнения первой. Но это потребовало бы идеального представления о работе исследуемой программы и я уверен, что в конце концов при попытке достичь такого состояния мы потерпим неудачу, уперевшись в проблему остановки (halting problem). В любом случае, общеизвестно, что для компьютеров построенных на фон Неймановской архитектуре это трудно решаемая задача (NP).

Для очень простых программ (без условных переходов, только один блок кода) мы можем легко говорить о полном понимании, однако, что насчет программ в которых значение регистра изменяется и считывается только внутри блока, который исполняется при некотором условии. Или хуже некоторый блок кода находится внутри цикла и чтение происходит только на тысячной его итерации. Нам бы пришлось рассмотреть абсолютно все возможности пути исполнения и вся эта информация в одночасье свалилась бы на нас. Я попытался найти алгоритмы, которые бы позволили вычислить каждый возможный вариант потока исполнения в коде с циклами без повторяющихся блоков, такие алгоритмы существуют, однако количество путей просто поражает.

Это терзало меня некоторое время, но затем я понял. Трюк в том, чтобы поставить себя на место обфускатора. Как он определяет куда вставлять мусор? Потому что, чтобы не изменить логику оригинального кода, обфускатор обязан быть уверен в том, что какие бы побочные эффекты не вызывали его мусорные инструкции, их действие всегда будет скомпенсировано. Так что, если он хочет добавить инструкцию clc в код, то обязан удостовериться, что ни одна из полезных инструкций не полагается на значение этого флага, до тех пор пока оригинальный код сам его не модифицирует.

Так что в итоге, они имеют дело с той же самой проблемой требующей идеального понимания кода, если они хотят наиболее общего решения.

По этой причине я допустил, что они сводят эту проблему к чему-то вроде:

Попросту говоря, они делают бесполезные присвоения, точно также как и примеры выше.

Вопрос, который все еще требует ответа: где же нам остановить деобфускацию кода? Пусть начальной точкой будет абсолютно любое место в коде, где реверсер посчитает интересным для него. Мы очевидно не можем анализировать вообще всю программу. Допустим, что теория о бесполезных присвоениях верна, в таком случае мы можем остановиться на любой из модифицирующих инструкций и начать поиск мусорных. Это оставит только мусорные инструкции, перезапись побочных эффектов которых происходит после выбранной точки остановки.

Я решил пойти другим путем. Точка начала анализа – произвольная, точно так-же и окончание. Затем я допустил, что любой регистр, значение не стеке или в памяти в этот момент значимо. В последствии это будет очевидно. Это допущение может казаться слишком общим, потому что дальше по потоку исполнения кто-то может перезаписать текущее состояние, но это нормально.

Пока остановимся на этом, так как это было достаточно большое описание «где начать, где закончить», в любом случае.

Алгоритм

Окончательный вариант алгоритма, что я выбрал, работает так:

  1. В некоторой произвольной точке мы начинаем снимать статически трассу
  2. Для каждой инструкции, что встретили, мы отслеживаем изменённые ей состояния. Вместе с каждой инструкцией мы сохраняем структуру, в которой отмечаем какая переменная состояния была изменена какой инструкцией, включая изменения сделанные текущей.
  3. В некоторой произвольной точке мы останавливаем сбор трассы и объявляем все в структуре состояния как значимые.

После трассировки начинаем работать в обратную сторону.

  1. Структура состояния последней инструкции содержит каждую переменную состояния и то, кто записал в неё значение. Мы помечаем каждую инструкцию, записанную там, как значимую.
  2. Для каждой значимой инструкции мы проверяем, что инструкция читает (например, какой регистр или область памяти). Затем мы ищем инструкцию, которая последняя писала в эту переменную состояния и точно также помечаем её как значимую, так как значимая инструкция зависит от неё.
  3. Повторять, пока не будут помечены все косвенным образом значимые инструкции.

Это лучше всего продемонстрировать на примере специально сконструированного обфусцированного кода:

1    mov eax, 1337
2    mov ecx, eax
3    add ecx, edx
4    mov edi, 42
5    mov ebp, 3
6    lea ebp, dword ptr [eax+3]
7    xor ebp, ebp

Мы проверяем каждую инструкцию и записываем изменения состояний. В начале, состояние пусто и внутрь мы заносим номер инструкции, что последняя писала в него.

#    Instruction                    State after instruction
0    initial                        empty
1    mov eax, 1337                  eax:1
2    mov ecx, eax                   eax:1, ecx:2
3    add ecx, edx                   eax:1, ecx:3
4    mov edi, 42                    eax:1, ecx:3, edi:4
5    mov ebp, 3                     eax:1, ecx:3, edi:4, ebp:5
6    lea ebp, dword ptr [eax+3]     eax:1, ecx:3, edi:4, ebp:6
7    xor ebp, ebp                   eax:1, ecx:3, edi:4, ebp:7

Теперь мы проверяем последнее состояние и помечаем каждую инструкцию внутри как полезную, это будут 1,3,4,7. Затем мы проходим по каждой полезной инструкции и проверяем все, от которых они зависят.

  • Инструкция 1 не имеет зависимостей, завершено.
  • Инструкция 3 зависит от ecx и edx. Для edx у нас нет информации, но для ecx мы можем проверить, которая из предыдущих инструкций изменяла его и обнаружить, что ecx был записан инструкцией 2. Добавляем 2 в список значимых.
  • Инструкция 4 не имеет зависимостей, пропускаем.
  • Инструкция 7 не имеет зависимостей, пропускаем.

Эта итерация добавила 2 в список хороших, так что мы проверяем 2 еще раз:

  • Инструкция 2 читает eax, мы проверяем состояния предыдущих инструкций, которые изменяли его и находим 1, которая уже в списке значимых, так что алгоритм заканчивается.

Окончательный список значимых инструкций это (1,2,3,4,7) так что инструкции 5 и 6 оказываются отброшены, потому что нигде в коде их побочные эффекты не оказывают влияния, поэтому мы получаем следующий деобфусцированный код:

1    mov eax, 1337
2    mov ecx, eax
3    add ecx, edx
4    mov edi, 42
7    xor ebp, ebp

Мы конечно же не знаем, имеет ли в конечном счете значение наличие 42 в регистре edi, или то, что ebp равно 0, однако в данный момент времени инструкции 4 и 7 ответственны за их значение и мы не можем безопасно отбросить подобное.

Сложности реализации

Конечно же, на практике не все так просто (хотя, по крайней мере, достаточно близко к этому). На практике есть несколько вещей усложняющих процесс.

Проблема 1 - Пересекающиеся регистры (Register aliasing)

Пример:

mov eax, 0xFFFFFFFF
mov ax, 1337

Обе инструкции значимы. Вторая частично перезаписывает eax, но старшие 16 бит все еще зависят от первой. К счастью, я не нашел в обработанных VMProtect исполняемых файлах подобных примеров. По этой причине, я поленившись просто привожу каждый регистр к большей форме (ah -> eax) до того как отмечаю запись в него. VMProtect действительно использует в больших количествах di, ax, bh и тому подобное, но никогда не пытается их пересекать, так что все работает хорошо.

Проблема 2 - Что мы понимаем под состоянием?

Здесь уже немного сложнее. Например, нельзя считать каждый регистр значимым. Допустим eip значимый, в таком случае каждая инструкция будет помечена как значимая, так как каждая модифицирует eip. Поэтому я исключил eip.

Еще я исключил записи в память, пока что, но это можно будет в дальнейшем поправить естественным образом.

Что насчет esp? Очевидно, что записи на стек достаточно важны, но что по поводу изменений для самого esp?

Проблема 3 - Стек

Здесь также не все так просто. VMProtect любит генерировать мусорные инструкции, которые пишут на стек и позже отбрасывает их результат без использования.

Например:

pushad
lea esp, dword ptr [esp+xxx]

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

  1. esp полностью исключается из отслеживания состояния
  2. Записи на стек запоминаются как запись в регистр по своим адресам (stackXXX)
  3. Перед тем как добавить инструкцию в список я отбрасываю все записи в стек с отрицательными смещениями esp.

Это означает, что случай выше будет обрабатываться следующим образом:

Instruction                     State
pushad                          stack0:1, stack1:1, stack2:1 ... stack7:1
lea esp, dword ptr [esp+40]     empty

pushad пишет в 8 областей памяти на стеке и мы отслеживаем их точно также как регистры. lea затем модифицирует esp и сдвигает его на 40 байт. Теперь esp стал выше (прим. значимые переменные всегда вверху стека) всех ранее рассмотренных стековых локаций и поэтому удален из состояния (прим. lea стала мусорной инструкцией). Таким образом, инструкция pushad рассматривается как мусор.

Это сработало на удивление хорошо и не было никаких проблем с игнорированием мусорных инструкций, который затрагивали стек.

Разработанный инструмент

Наконец, я расскажу о своём коде. С самого начала я хотел получить инструмент для работы в статике, который я смогу использовать в IDA и который сгенерировал бы мне читабельный код. Я использую Triton, библиотеку-фреймворк для динамического анализа исполняемых файлов, чтобы сделать статическую трассировку кода. Она также позволяет получить всю необходимую мне информацию, такую как читаемые/записываемые инструкцией регистры, адреса памяти и тому подобное. Это без преувеличения потрясающий инструмент.

Теперь я опишу немного процесс эволюции моей программы.

Сперва я получаю следующую трассу замусоренных инструкций от начальной точки защищенного VMProtect исполняемого файла, используя Triton:

100a63e2    [001]    push 0x8beef346                          
100a63e7    [002]    push dword ptr [esp]                     
100a63ea    [003]    mov dword ptr [esp + 4], 0x6fdd8840      
100a63f2    [004]    mov byte ptr [esp], dh                   
100a63f5    [005]    pushal                                   
100a63f6    [006]    pushal                                   
100a63f7    [007]    mov dword ptr [esp + 0x40], 0x2383346b   
100a63ff    [008]    mov byte ptr [esp + 8], 0x5d             
100a6404    [009]    lea esp, dword ptr [esp + 0x40]          
100a6408    [010]    jmp 0x100c8e85                           
100c8e85    [011]    pushal                                   
100c8e86    [012]    pushfd                                   
100c8e87    [013]    pushfd                                   
100c8e88    [014]    mov dword ptr [esp + 0x24], eax          
100c8e8c    [015]    push eax                                 
100c8e8d    [016]    pushfd                                   
100c8e8e    [017]    mov dword ptr [esp + 0x28], edx          
100c8e92    [018]    pushfd                                   
100c8e93    [019]    mov byte ptr [esp + 4], 0x6f             
100c8e98    [020]    call 0x100c87bd                          
100c87bd    [021]    jmp 0x100c895e                           
100c895e    [022]    mov dword ptr [esp + 0x2c], ebp          
100c8962    [023]    pushfd                                   
100c8963    [024]    call 0x100c8a92                          
100c8a92    [025]    mov dword ptr [esp + 0x30], ebx          
100c8a96    [026]    mov byte ptr [esp], dh                   
100c8a99    [027]    mov byte ptr [esp], ah                   
100c8a9c    [028]    push edi                                 
100c8a9d    [029]    jmp 0x100c8d55                           
100c8d55    [030]    mov dword ptr [esp + 0x30], esi          
100c8d59    [031]    mov byte ptr [esp + 4], ch               
100c8d5d    [032]    mov dword ptr [esp + 0x2c], edi          
100c8d61    [033]    mov word ptr [esp], 0x79d1               
100c8d67    [034]    mov byte ptr [esp], cl                   
100c8d6a    [035]    mov dword ptr [esp + 0x28], ebx          
100c8d6e    [036]    push ebx                                 
100c8d6f    [037]    push ecx                                 
100c8d70    [038]    push edi                                 
100c8d71    [039]    lea esp, dword ptr [esp + 0x34]          
100c8d75    [040]    jmp 0x100c8c80                           
100c8c80    [041]    bswap si                                 
100c8c83    [042]    not si                                   
100c8c86    [043]    pushal                                   
100c8c87    [044]    pushfd                                   
100c8c89    [045]    pop dword ptr [esp + 0x1c]               
100c8c8d    [046]    shld si, di, cl                          
100c8c91    [047]    jmp 0x100c7dbd                           
100c7dbd    [048]    mov dword ptr [esp + 0x18], ecx          
100c7dc1    [049]    bsf di, si                               
100c7dc5    [050]    push dword ptr [0x100c6d15]              
100c7dcb    [051]    pop dword ptr [esp + 0x14]               
100c7dcf    [052]    movzx cx, bl                             
100c7dd3    [053]    xadd edi, ebp                            
100c7dd6    [054]    mov dword ptr [esp + 0x10], 0            
100c7dde    [055]    cmc                                      
100c7ddf    [056]    jmp 0x100c7c7b                           
100c7c7b    [057]    mov esi, dword ptr [esp + 0x40]          
100c7c7f    [058]    bts di, ax                               
100c7c83    [059]    btr di, 4                                
100c7c88    [060]    add esi, 0x1644be2b                      
100c7c8e    [061]    neg di                                   
100c7c91    [062]    pushal                                   
100c7c92    [063]    clc                                      
100c7c93    [064]    xor esi, 0x69d1f651                      
100c7c99    [065]    mov word ptr [esp], 0xfaa6               
100c7c9f    [066]    or edi, eax                              
100c7ca1    [067]    xchg di, bp                              
100c7ca4    [068]    not esi                                  
100c7ca6    [069]    and bp, 0x4b2b                           
100c7cab    [070]    rcr bl, cl                               
100c7cad    [071]    dec bp                                   
100c7cb0    [072]    stc                                      
100c7cb1    [073]    lea ebp, dword ptr [esp + 0x30]          
100c7cb5    [074]    bts bx, bp                               
100c7cb9    [075]    rcl bx, 4                                
100c7cbd    [076]    add di, di                               
100c7cc0    [077]    neg bx                                   
100c7cc3    [078]    sub esp, 0x90                            
100c7cc9    [079]    push esp                                 
100c7cca    [080]    lea edi, dword ptr [esp + 4]             
100c7cce    [081]    lea esp, dword ptr [esp + 4]             
100c7cd2    [082]    adc bl, dl                               
100c7cd4    [083]    mov ebx, esi                             
100c7cd6    [084]    dec al                                   
100c7cd8    [085]    xor cl, cl                               
100c7cda    [086]    cmc                                      
100c7cdb    [087]    cmp esi, 0xae51dbba                      
100c7ce1    [088]    add esi, dword ptr [ebp]                 
100c7ce4    [089]    clc                                      
100c7ce5    [090]    not cl                                   
100c7ce7    [091]    mov al, byte ptr [esi]                   
100c7ce9    [092]    shr cl, 3                                
100c7cec    [093]    rol cl, 6                                
100c7cef    [094]    add al, bl                               
100c7cf1    [095]    movsx cx, dl                             
100c7cf5    [096]    rcl ch, 2                                
100c7cf8    [097]    movzx cx, al                             
100c7cfc    [098]    pushal                                   
100c7cfd    [099]    rol al, 7                                
100c7d00    [100]    or ch, dl                                
100c7d02    [101]    shrd cx, di, 6                           
100c7d07    [102]    shl cl, cl                               
100c7d09    [103]    pushal                                   
100c7d0a    [104]    neg al                                   
100c7d0c    [105]    mov cl, 0x47                             
100c7d0e    [106]    call 0x100c6d19                          
100c6d19    [107]    clc                                      
100c6d1a    [108]    sub esi, -1                              
100c6d1d    [109]    test dh, 0x18                            
100c6d20    [110]    not al                                   
100c6d22    [111]    dec ecx                                  
100c6d23    [112]    add bl, al                               
100c6d25    [113]    rcl cx, 3                                
100c6d29    [114]    rcr ch, 7                                
100c6d2c    [115]    rcr ch, 2                                
100c6d2f    [116]    movzx eax, al                            
100c6d32    [117]    mov byte ptr [esp], bl                   
100c6d35    [118]    clc                                      
100c6d36    [119]    not cl                                   
100c6d38    [120]    mov ecx, dword ptr [eax*4 + 0x100c7e65]  
100c6d3f    [121]    lea esp, dword ptr [esp + 0x44]          
100c6d43    [122]    jb 0x100c8390                            
100c6d49    [123]    push 0x968f74f5                          
100c6d4e    [124]    push dword ptr [esp]                     
100c6d51    [125]    ror ecx, 0xc                             
100c6d54    [126]    test bx, ax                              
100c6d57    [127]    add ecx, 0                               
100c6d5d    [128]    mov byte ptr [esp + 4], bl               
100c6d61    [129]    mov dword ptr [esp + 4], ecx             
100c6d65    [130]    pushal                                   
100c6d66    [131]    push esp                                 
100c6d67    [132]    push dword ptr [esp + 0x28]              
100c6d6b    [133]    ret 0x2c          

И таков был вывод моего инструмента в самом начале разработки:

[003]    mov dword ptr [esp + 4], 0x6fdd8840
[007]    mov dword ptr [esp + 0x40], 0x2383346b
[014]    mov dword ptr [esp + 0x24], eax
[017]    mov dword ptr [esp + 0x28], edx
[022]    mov dword ptr [esp + 0x2c], ebp
[025]    mov dword ptr [esp + 0x30], ebx
[030]    mov dword ptr [esp + 0x30], esi
[032]    mov dword ptr [esp + 0x2c], edi
[035]    mov dword ptr [esp + 0x28], ebx
[041]    bswap si
[042]    not si
[043]    pushal
[044]    pushfd
[045]    pop dword ptr [esp + 0x1c]
[046]    shld si, di, cl
[048]    mov dword ptr [esp + 0x18], ecx
[049]    bsf di, si
[050]    push dword ptr [0x100c6d15]
[051]    pop dword ptr [esp + 0x14]
[052]    movzx cx, bl
[053]    xadd edi, ebp
[054]    mov dword ptr [esp + 0x10], 0
[057]    mov esi, dword ptr [esp + 0x40]
[058]    bts di, ax
[059]    btr di, 4
[060]    add esi, 0x1644be2b
[061]    neg di
[062]    pushal
[064]    xor esi, 0x69d1f651
[065]    mov word ptr [esp], 0xfaa6
[068]    not esi
[073]    lea ebp, dword ptr [esp + 0x30]
[080]    lea edi, dword ptr [esp + 4]
[083]    mov ebx, esi
[088]    add esi, dword ptr [ebp]
[091]    mov al, byte ptr [esi]
[094]    add al, bl
[099]    rol al, 7
[104]    neg al
[108]    sub esi, -1
[110]    not al
[112]    add bl, al
[116]    movzx eax, al
[120]    mov ecx, dword ptr [eax*4 + 0x100c7e65]
[125]    ror ecx, 0xc
[127]    add ecx, 0
[129]    mov dword ptr [esp + 4], ecx
[132]    push dword ptr [esp + 0x28]
[133]    ret 0x2c

Обфусцированный код состоял из 133 инструкций, деобфусцированный (который также выглядит более читабельным) всего из 49.

Если вы попытаетесь прочитать код, то столкнётесь с проблемой касательно понимания адресов на стеке. Как было замечено ранее, я не отслеживаю изменения esp, так что инструкции, что только модифицируют esp вроде:

sub esp, 0x40

не попадают в вывод. Таким образом код неработоспособен, так как мы никогда не сможем понять с каким конкретно адресом на стеке программа пыталась работать. Я размышлял над тем, как решить эту проблему и пришел к следующему методу.

Благодаря библиотеке Triton я могу узнать к какому адресу на стеке идет на самом деле обращение. Так что я просто присваиваю уникальное имя каждому адресу. Это даёт нам следующее:

[003]    mov dword ptr [svar0], 0x6fdd8840
[007]    mov dword ptr [svar1], 0x2383346b
[014]    mov dword ptr [svar2], eax
[017]    mov dword ptr [svar3], edx
[022]    mov dword ptr [svar4], ebp
[025]    mov dword ptr [svar5], ebx
[030]    mov dword ptr [svar6], esi
[032]    mov dword ptr [svar7], edi
[035]    mov dword ptr [svar8], ebx
[041]    bswap si
[042]    not si
[043]    pushal
[044]    pushfd
[045]    pop dword ptr [svar9]
[046]    shld si, di, cl
[048]    mov dword ptr [svar10], ecx
[049]    bsf di, si
[050]    push dword ptr [0x100c6d15]
[051]    pop dword ptr [svar11]
[052]    movzx cx, bl
[053]    xadd edi, ebp
[054]    mov dword ptr [svar12], 0
[057]    mov esi, dword ptr [svar0]
[058]    bts di, ax
[059]    btr di, 4
[060]    add esi, 0x1644be2b
[061]    neg di
[062]    pushal
[064]    xor esi, 0x69d1f651
[065]    mov word ptr [svar13], 0xfaa6
[068]    not esi
[073]    lea ebp, dword ptr [svar12]
[080]    lea edi, dword ptr [svar14]
[083]    mov ebx, esi
[088]    add esi, dword ptr [ebp]
[091]    mov al, byte ptr [esi]
[094]    add al, bl
[099]    rol al, 7
[104]    neg al
[108]    sub esi, -1
[110]    not al
[112]    add bl, al
[116]    movzx eax, al
[120]    mov ecx, dword ptr [eax*4 + 0x100c7e65]
[125]    ror ecx, 0xc
[127]    add ecx, 0
[129]    mov dword ptr [svar15], ecx
[132]    push dword ptr [svar15]
[133]    ret 0x2c

Теперь мы можем с легкостью читать код и стековые адреса выглядят корректными.

Мне так-же потребовалось добавить несколько костылей, так как Triton не поддерживает полностью семантику x86. Я создал Issue на GitHub с описанием проблемы. Например, инструкция rol не читает флаг переноса, но из-за реализации (на самом деле она считывает из него значение, которое сама и записала) Triton вернёт, что считывание осуществляется, и это приведет к тому, что появится зависимость, которой быть не должно.

Доказательство

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

В качестве доказательства я использую код выше и вычислю адрес, по которому порейдет управление в конце блока:

[003]    mov dword ptr [svar0], 0x6fdd8840
[007]    mov dword ptr [svar1], 0x2383346b
[014]    mov dword ptr [svar2], eax
[017]    mov dword ptr [svar3], edx
[022]    mov dword ptr [svar4], ebp
[025]    mov dword ptr [svar5], ebx
[030]    mov dword ptr [svar6], esi
[032]    mov dword ptr [svar7], edi
[035]    mov dword ptr [svar8], ebx
[041]    bswap si
[042]    not si
[043]    pushal
[044]    pushfd
[045]    pop dword ptr [svar9]
[046]    shld si, di, cl
[048]    mov dword ptr [svar10], ecx
[049]    bsf di, si
[050]    push dword ptr [0x100c6d15]
[051]    pop dword ptr [svar11]
[052]    movzx cx, bl
[053]    xadd edi, ebp
[054]    mov dword ptr [svar12], 0                  svar12 = 0
[057]    mov esi, dword ptr [svar0]                 esi = 0x6fdd8840
[058]    bts di, ax
[059]    btr di, 4
[060]    add esi, 0x1644be2b                        esi = 0x8622466B
[061]    neg di
[062]    pushal
[064]    xor esi, 0x69d1f651                        esi = 0xEFF3B03A
[065]    mov word ptr [svar13], 0xfaa6
[068]    not esi                                    esi = 100C4FC5
[073]    lea ebp, dword ptr [svar12]                ebp = pointer to svar12 (which is 0)
[080]    lea edi, dword ptr [svar14]
[083]    mov ebx, esi                               ebx = 100C4FC5
[088]    add esi, dword ptr [ebp]                   esi = 100C4FC5
[091]    mov al, byte ptr [esi]                     al = 0x41       (lookup done in IDA)
[094]    add al, bl                                 al = 0x06
[099]    rol al, 7                                  al = 3
[104]    neg al                                     al = 0xFD
[108]    sub esi, -1                                esi = 100C4FC6
[110]    not al                                     al = 2
[112]    add bl, al                     
[116]    movzx eax, al                              eax = 2
[120]    mov ecx, dword ptr [eax*4 + 0x100c7e65]    ecx = 0xc8bd0100 (lookup done in IDA)
[125]    ror ecx, 0xc                               ecx = 100c8bd0
[127]    add ecx, 0
[129]    mov dword ptr [svar15], ecx
[132]    push dword ptr [svar15]
[133]    ret 0x2c                                   jmp 100c8bd0

И чтобы подтвердить корректность, ниже представлен финальный адрес в OllyDbg:

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

Дальнейшая работа

Я достаточно быстро заметил, что моя система стековых переменных хоть и работает, но в момент когда мы переходим к следующему блоку кода, очевидно ни одно из присвоенных ранее имен больше не актуально. Так что для того чтобы сделать инструмент действительно юзабельным мне необходимо обдумать как решить проблему обработки стека глобально.

Как было замечено ранее, перекрытие регистров находится в моём списке ToDo, также как и общая чистка кода, так как пока что все выглядит “неопрятно”. Записи в память не считаются значимыми, это тоже нужно доделать.

И очевидно код работает только для архитектуры x86, но портирование на x64 не должно вызвать особых проблем.

Ссылки

Скрипт доступен в моём репозитории на GitHub.

Если вас интересует необработанный вывод инструмента как есть, то он доступен здесь.

© Translated by sysenter special for r0 Crew