Главная > Реверсивная инженерия > Реассемблирование. Full version.

Реассемблирование. Full version.


Издавна предпринимались попытки по реассемблированию. Например раз, два и три. Но ни разу не описывался опыт полного, и главное удачного реассемблирования exe файла. Их есть у меня.

Для чего это нужно
Захотели вы, к примеру, скажем так, написать “полный аналог” любимой программы/игры/операционной системы. Если садиться за декомпиляцию(hexrays не предлагать), то восстановление функции за функцией займет годы (скорость где-то 512 кб кода в год). К тому же узнать о том, правильно ли вы все декомпилировали, вы сможете только когда декомпилируете достаточно много кода. Т.е. первый раз месяца через 2-3. За это время можно наплодить столько багов… Отладка будет та еще.

И чтобы таким делом не заниматься, можно полностью реассемблировать бинарь. То есть сделать полный ассемблерный листинг, привести его к компиляберному состоянию(чтобы на выходе получался работоспособный бинарь). А потом полученный ассемблер можно нужными порциями заменять своим сишным кодом. Благодаря такому подходу баги видны сразу, результаты работы тоже.

Как это сделать

how do you do – как вы это делаете?
alright – всегда правой.

Понадобятся довольно прямые руки и понимание как стыковать ассемблер с си. Но до того как начать декомпиляцию предстоит еще 4 этапа.

Этап №1. Создание базы
Нельзя реассемблировать просто так любую базу иды. Нужна девственная, а не та в которой проводится реверсинг. Иначе это будет весьма веселое занятие, как в статье у Касперски. Наша цель получить компилируемый ассемблер, а не ассемблер похожий на тот, что в IDA (с примененными на нем энумами, структурами, коментами). Поэтому создаем базу с ноля.

Перед тем как создавать базу, я рекомендую изменить в файле ida.cfg настройку

ASCII_PREFIX            = "a"

на

ASCII_PREFIX            = "ga"

Это префикс, который ида проставляет строкам. Если использовать просто “a”, то строка “%d%d” получает имя aDD, что является ассемблерной инструкцией. Оно нам надо?

В окне “Load new file”:

  • Отключаем автоанализ: убрать птичку “Analysis” – “Enabled”
  • В “kernel options 1″ отключаем использование сигнатур “Use filrt signatures”
  • В “kernel options 2″ отключаем использование уникода “check for unicode strings” и “create function tails”
  • Все. Теперь можно грузить.

    Этап №2. Подготовка базы к генерации ассемблера
    База создалась, но анализ не начался, потому что отключен. Так и должно быть.
    Перед тем как отпустить анализ, нужно удалить в окне Type Libraries (shift-F11) все библиотеки типов. Иначе затянет кучу структур и энумов, которые нам при реассембировании будут только проблемы создавать. Отпускаем автоанализ и ждем завершения.

    Теперь:
    Раскрываем все hidden функции. Иначе в ассемблер попадет текст:

    [00000005 BYTES: COLLAPSED FUNCTION RegisterClassA(x). PRESS KEYPAD "+" TO EXPAND]

    А не сам код.

    Удаляем align’ы. Если ида случайно промаркирует данные кодом, то она запросто может вставить там align директивы, тем самым просто отбросит изначальные данные. И константные указатели в ассемблере начнут указывать в никуда.
    Все align директивы нужно удалить. Вот в помощь IDC скрипт:

    auto ea,f;
    for (ea=0x400000;ea<"ваш адрес";ea++)
    {
    if (isAlign(GetFlags(ea)))
       MakeUnkn(ea,DOUNK_EXPAND);
    }
    

    Обязательно нужно отключить деманглинг, иначе MASM потом будет возмущаться.

    Импорт. Это проблема с которой сталкиваешься, когда уже казалось бы все проблемы этапа №3 решены. А приходится все начинать заново. Все потому, что ida проставляет импортируемым функциям имена из секции импорта. Она понятия не имеет какие там нужны имена функций, чтобы импорты смог потом подхватить линкер. В двух словах на примере:
    Чтобы линкер подхватил импорт win api, например функцию CreateWindowExA импорт должен выглядеть вот так:

    extrn __imp__CreateWindowExA@48:dword

    А ида генерит вот так:

    extrn __imp_CreateWindowExA:dword

    или так:

    extrn CreateWindowExA:dword

    а если импорт встречается дважды, то еще и вот так:

    extrn __imp_CreateWindowExA_0:dword

    Чтобы не повеситься с переделкой вручную, вот облегчающий жизнь кусочек плагина:

    	for (ea_t ea=НАЧАЛО СЕКЦИИ ИМПОРТА;ea<КОНЕЦ СЕКЦИИ ИМПОРТА;ea+=4)
    	{
    		int purgedBytes=0;
    		if (netnode_supval(ea,0xf,&purgedBytes,sizeof(purgedBytes),'A')!=-1)
    		{
    			
    			if (purgedBytes)
    				purgedBytes--; // это не хак, там действительно всегда на 1 байт больше
    
    			char name[250]={0};
    			get_true_name(BADADDR,ea,name,sizeof(name));
    
    			char * pname=name;
    			if (!strncmp(name,"__imp_",6))
    				pname+=6;
    
    			char newName[250]={0};
    			sprintf(newName,"__imp__%s@%d",pname,purgedBytes);
    			set_name(ea,newName);
    		}
    		
    	}

    Этот код вычитывает размер “purged bytes” для каждой из функции и заменяет имя импорта на правильное. В моем случае exe файл использовал только библиотеки windows. Соответственно там везде был stdcall, и фокус проканал. Для повторяющися импортов, где в имени был суффикс “_0″, пришлось руками удалить дубликат в сгенеренном ассемблере.

    В самом конце обязательно нужно проверить, чтобы не было кода в данных. Чтобы не было приколов типа как с align’ами.

    Все, можно генерить ассемблер.

    Этап №3. Работа напильником, приведение ассемблера в компиляберный вид
    Ассемблер, который генерирует IDA раньше был заточен под TASM, сейчас слава богу есть выбор, и “Generic for Intel 80×86″ это почти MASM. Поэтому напильником теперь пользоваться приходится гораздо меньше.
    Что придется изменить перед тем как пытаться собрать через ML:

  • Инструкции repnz movXX на rep movXX
  • Слова Name и Size. Если какая-то переменная или строка будет иметь такое имя – не скомпилится
  • Префикс large. Просто удаляем везде.
  • Префиксы short для jxx. Иногда почему-то приходится делать длиный, хотя в оригинале вроде как расстояние до инструкции позволяло использовать short
  • Могут быть еще какие-то мелкие проблемы несовместимости синтаксиса иды и MASM. Но не думаю, что будут какие-то особые сложности.
    Наконец можно пробовать скомпилировать.

    Я все также собираю командой: ML.EXE /coff /Fl /Sf /I. /Zi /c /Cp /Zp1 /Zm /Ta
    Потому что мне нужен obj, который я буду потом линковать к моим сишным сорцам. В моем случае перечисленных выше действий оказалось достаточно. Мой asm файл, размером 32 мб, скомпилился в красивый obj.

    Этап №4. Сборка в Visual studio
    Я создавал пустой проект типа “windows application” и добавлял туда конечный asm файл. Дальше прописывал для asm файла custom build tool и командную строку для ML. В настройках линкера я отключал DEP и ASLR, и указывал имя start в поле Entry Point. Этого достаточно, чтобы собрался EXE файл. Таким образом будет получен бинарь который прямо с энтрипоинта двинет в функцию start, которая была точкой входа первоначального EXE. Благодаря этому оригинальный runtime init отработает как надо и (о чудо!) программа запустится.

    Что придется доделать
    TLS колбэки. В новособранном EXE их нет, придется самостоятельно вызывать их перед запуском рантайма.

    Как выносить код из ассемблера
    Предположим нашел функцию, которую готов переписать как сишный код, или даже уже ее переписал, но непонятно как теперь ее правильно убрать из ассемблера, так чтобы сохранилась работоспособность. Я делал следующим образом:
    1) написал свою функцию в си
    2) написал wrapper над своей функцией, чтобы соблюсти конвенцию вызова и сохранить все регистры не измененными. Да, оказывается мало просто call перенаправить, нужно еще и окружение не испортить, чтобы по возвращению из вашей функции код смог продолжить выполнение.
    пример враппера, который я использовал для перенаправления fastcall в stdcall:

    extern "C" __declspec(naked) int * jMyCFunc(Object *object)
    {
    	static void * result;
    	__asm pushad
    	__asm push eax
    	__asm call MyCFunc
    	__asm mov result, eax
    	__asm popad
    	__asm mov eax, result
    	__asm ret
    }
    

    3) создавал extern декларацию в ассемблере

    extern _jMyCFunc : proc

    4) удалял оригинальную функцию в ассемблере и заменял все call sub_XXXX на call _jMyCFunc

    1. fntdv
      Февраль 21, 2012 в 17:54 | #1

      Спасибо автору за статью. Данный метод работает.
      В ходе реассемблирования столкнулся с рядом проблем. Некоторые из них помог решить Hex. Несколько слов о том что это были за проблемы и как они решались. Возможно это сэкономит немного времени для всех реассемблирующих. итак.

      ————————————————————————-
      Этап №2. Подготовка базы к генерации ассемблера

      А еще обязательно проверьте отсутствие нераспознанного IDA’ой кода. У Вас не должно быть “красных” участков кода. В противном случае при попытке сгенерить *.obj файл можно получить ошибку

      undefined symbol : loc_10015B93

      для совершенно безобидного участка кода.

      loc_10015B93: ; CODE XREF: .text:10015B8Fj
      mov eax, [ebp-4]
      mov ecx, dword_10031A30
      mov [eax], ecx
      mov eax, [ebp+8]
      mov dword_10031A30, eax
      call loc_1001A988

      Проблема в том. что этот код не является частью функции.

      ————————————————————————-
      Этап №3. Работа напильником, приведение ассемблера в компиляберный вид

      Здесь у меня возникло еще несколько проблем.

      IDA 6.2 нагенерила мне вот таких вот инструкций.

      dd rva sub_1000C4F8

      на что ml.exe мне ответил

      error A2206:missing operator in expression

      вобщем пофиксилось все простой заменой rva на offset.

      Остальные ошибки скорее всего были незначительными и не запомнились совсем. Но обо всех их можно узнать из сообщений компилятора.

      ————————————————————————-
      Этап №4. Сборка в Visual studio

      Здесь я наигрался и со студией, и с линковкой.
      Убедитесь в том, что студия знает где лежит сгенеренный *.obj файл, иначе рискуете получить подобные ошибки

      error LNK2001: unresolved external symbol _DllEntryPoint,
      если в опциях линкера прописать “DllEntryPoint” или же
      LNK2001: unresolved external symbol __DllMainCRTStartup@12,
      если ничего нигде не прописать.

      Еще один финт ушами пришлось сделать. Скорее всего у Вас не будет необходимых *.lib – файлов. сгенерить их можно с помощью двух утилит из пакета masm32. Делаем следующим образом:
      1. dumpbin.exe /EXPORTS D:\tmp.dll > tmp.def. потом допиливаем *.def файл(о ег оформате читаем здесь http://msdn.microsoft.com/en-us/library/28d6s79h(v=VS.80).aspx)
      2. lib.exe /DEF:D:\tmp.def
      в итоге получим то, что уже можно слинковать с нашим *.obj

      И последняя проблема. с которой пришлось повозиться.
      картина такая.
      в асме сгенерилиись такие вызовы для импортируемых функций:

      call ds:_DoSomeActions.
      Но на ее вызове мы падаем с Access Violation. Проблема в том. что мы лезем по какому-то стремному адресу.

      смотрим на него в IDA

      call dword ptr _DoSomeActions

      и… проблема ясна. при подобном вызове берется адрес с _DoSomeActions, а там лежит совсем не то, что нам нужно.

      В сгенеренных нами либах для каждой функции

      DoSomeActions

      сгенерятся следующие имена

      _DoSomeActions
      __imp__DoSomeActions

      заменяю в *.asm файле первый вариант(_DoSomeActions) на второй(__imp__DoSomeActions) и вызов работает успешно.

      И после этого всего реассемблированный модуль заработал без проблем.
      Т.е.

    2. Hex
      Февраль 22, 2012 в 09:55 | #2

      Хорошее дополнение, потому что я рассматривал пример с exe, а тут dll.

    1. Нет обратных ссылок.

    Добавить комментарий

    Fill in your details below or click an icon to log in:

    Логотип WordPress.com

    You are commenting using your WordPress.com account. Log Out / Изменить )

    Фотография Twitter

    You are commenting using your Twitter account. Log Out / Изменить )

    Фотография Facebook

    You are commenting using your Facebook account. Log Out / Изменить )

    Connecting to %s

    Follow

    Get every new post delivered to your Inbox.