Расспаковка Skype средствами динамической инструментализации бинарного кода. Часть 2.

GDB Server

На наш взгляд одна из лучших возможностей поддерживаемых Intel PIN Toolkit является поддержка опции "-appdebug". Этот параметр указывает нашему фрэймворку подключиться к серверу GDB для удаленной отладки приложения. Мы можем использовать эту возможность для удаленной отладки в дизассемблере IDA с помощью дистанционного пульта управления предоставляемого отладчиком GDB. Единственной проблемой(по идее это даже не проблема, просто доставляет определенные неудобства в работе) является то, что мы не можем указать целевой порт в PIN который мы будем слушать, т.к он при инициализации выбирается случайным образом. При этом нам приходится каждый раз заменять его в параметрах отлаживаемого процесса в дебагере.
$ pin -appdebug -t source/tools/ManualExamples/obj-ia32/inscount0.so -- `which skype` Application stopped until continued from debugger. Start GDB, then issue this command at the (gdb) prompt: target remote :31337
И установливаем удаленную отладку GDB в IDA Free для чего указываем в параметрах подключения имя хоста и порта (Debugger -> Process Options):

После того как все настройки будут заданы, мы можем нажать кнопу OK и выбрать в меню IDA Debugger -> Attach to process. В следующем диалоговом окне, достаточно будет нажать также кнопку ОК при запросе к какому процессу мы хотим приаттачиться. А т.к процесс у нас уже был выставлен ранее мы можем приступить к отладке с PIN из IDA.

Простейший "write and exec" расспаковщик.

Давайте вернемся к основной цели нашей статьи которая заключается в написании расспаковщика для Skype средствами PIN Toolkit. Все что нам нужно будет делать это проверять, есть ли у нас в основном теле бинарного кода какие либо инструкции которые модифицируют какой либо сегмент нашего приложения(например, если инструкция пишет в секцию кода .text), сохраняют его и если приложение переходит к выполнению каких либо модифицированных секций, мы выставляем точки останова для того чтобы сообщить вовремя нашему отладчику процесс который кажется возможно расспакован. Впринципе это идея не нова и более того она очень проста и используется в большинстве простых да и сложных упаковщиках. При разработке Proof of Concept средствами фрэймворка PIN нам надо определиться с алгоритмом последовательности решения задачи, основная из которых заключается в установке параметров инструментализации кода в функции main() :
int main(int argc, char *argv[]) { // Инициализации библиотеки PIN. Принимаем аргументы , если не задано // по умолчанию вызывается с ключом -h(elp) которая в свою очередь вызывает// справку по аргументам if( PIN_Init(argc,argv) ) return Usage();// Регистроваяя функция вызывается при трассировке приложения TRACE_AddInstrumentFunction(trace_cb, 0); // Регистровая функция вызывается при старте приложения PIN_AddApplicationStartFunction(app_start_cb, 0); // Регистровая функция вызывается при выходе из приложения PIN_AddFiniFunction(fini_cb, 0); // Стартуем программу безвозвратно PIN_StartProgram(); return 0; }
В функции "app_start_cb" вызывается callback который мы должны буем сохранить в сегменте приложения std::map:
// опущен нерассматриваемый код // .... struct segdata_t { size_t size; ADDRINT check; bool written; }; typedef std::map segmap_t; segmap_t seg_bytes;// опущен нерассматриваемый код // .... //-------------------------------------------------------------------------- static VOID app_start_cb(VOID *v) { IMG img = APP_ImgHead(); for( SEC sec= IMG_SecHead(img); SEC_Valid(sec); sec = SEC_Next(sec) ) { ADDRINT sec_ea = SEC_Address(sec); // проверяем загружен ли сегмент в память процесса if ( sec_ea != 0 ) { ADDRINT check; // копируем первый DWORD/QWORD для проверки был ли он изменен size_t bytes = PIN_SafeCopy(&check, (void*)sec_ea, sizeof(ADDRINT)); if ( bytes == sizeof(ADDRINT) ) { if ( min_ea > sec_ea || min_ea == 0 ) min_ea = sec_ea; if ( max_ea < sec_ea || max_ea == (unsigned)-1 ) max_ea = sec_ea;segdata_t seg; seg.size = SEC_Size(sec); seg.check = check; seg.written = false; // сохраняем информацию о сегменте seg_bytes[sec_ea] = seg; } } } }  
Мы будем перебирать все сегменты в приложении которые будучи загруженными в память будут хранить информацию о себе. Теперь если код будет модифицировать память в пределах ранее записанных сегментов, или же процесс будет выполнять команду в ранее записанном сегменте приложения в коллбэке "trace_cb", мы будем проверять каждую последующую инструкцию в каждом блоке который будет выполняться в процессе исполнения нашей подопытной программы.
  ////-------------------------------------------------------------------------- static VOID trace_cb(TRACE trace, VOID *v) {

// Смотрим каждый след. простой блок при трассировке for ( BBL bbl = TRACE_BblHead(trace); BBL_Valid(bbl); bbl = BBL_Next(bbl) ) {

// Смотрим каждую след. инструкцию в блоке for( INS ins = BBL_InsHead(bbl); INS_Valid(ins); ins=INS_Next(ins) ) { ADDRINT ea = INS_Address(ins); if ( !valid_ea(ea) ) continue;

// если адресс был уже перезаписан и выполнен , скорее всего наш код уже расспакован if ( was_writen(ea) ) { INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)check_unpacked_cb, IARG_INST_PTR, IARG_CONST_CONTEXT, IARG_THREAD_ID, IARG_END); }

UINT32 mem_operands = INS_MemoryOperandCount(ins);

// Перебираем каждый операнд инструкции в памяти for ( UINT32 mem_op = 0; mem_op < mem_operands; mem_op++ ) { if ( INS_MemoryOperandIsWritten(ins, mem_op) ) { // модифицированы ли адресса в памяти? INS_InsertIfPredicatedCall(ins, IPOINT_BEFORE, (AFUNPTR)valid_ea, IARG_MEMORYOP_EA, mem_op, IARG_END);

> // если так, инструметируем код INS_InsertThenPredicatedCall( ins, IPOINT_BEFORE, (AFUNPTR)record_mem_write_cb, IARG_INST_PTR, IARG_MEMORYOP_EA, mem_op, IARG_END); } } } } }

 
В коллбэке "record_mem_write_cb" PIN Toolkit проверяет актуальность память которая потвержена перезаписи сегментов. Если это так, и на подлежит записи флаг соответствующего элемента сегмента выставляет в значение true:
//-------------------------------------------------------------------------- // Хэндл памяти что пишет записи VOID record_mem_write_cb(VOID * ip, VOID * addr) { ADDRINT ea = (ADDRINT)addr; segmap_t::iterator p; for ( p = seg_bytes.begin(); p != seg_bytes.end() && !p->second.written; ++p ) { ADDRINT start_ea = p->first; if ( ea >= start_ea ) { segdata_t *seg = &p->second; if ( ea size ) { fprintf(stderr, "%p: W %p\n", ip, addr); write_address.push_back((ADDRINT)addr); seg->written = true; break; } } } }
И, наконец, в функции обратного вызова "check_unpacked_cb", который мы установили в коллбэке "trace_cb", мы снова установим элемент записи в значение false, чтобы вызвать точку останова(breakpoint) который должен как раз таки и отловить наш дизассемблер IDA слушающий порт в ожидание отлова подобного рода исключения:
VOID check_unpacked_cb(VOID * ip, const CONTEXT *ctxt, THREADID tid) { ADDRINT ea = (ADDRINT)ip; addrdeq_t::iterator it = std::find(write_address.begin(), write_address.end(), ea); if ( it != write_address.end() ) write_address.erase(it); fprintf(stderr, "Layer unpacked: %p\n", ip); PIN_ApplicationBreakpoint(ctxt, tid, false, "Layer unpacked!"); }

Расспаковка Skype

Продолжение следует...

Об авторе: блог ведет Sanjar Satsura

Senior Security Analyst, Articles Writer at CyberSafe

Около 6 лет разработки программного обеспечения и реверс инженеринга закрытых программных продуктов на Windows Платформах, на *nix платформах - 4 год

Автор более 25 статей в журналах Xakep, Hakin9, "IT Sec" Magazine, "Software Developer's" Journal и 40 крупных программных продуктов и проектов.