笔者在此前从未接触过Postscript及Ghostscript(甚至不闻其名),该文权当笔者在学习过程中的一篇学习笔记,其中如有不当或错误之处,望读者不吝赐教,笔者感激不尽。
读者可以先行安装Ghostscript
,之后便可于其中运行下列示例。
注:读者若要详细了解,见参考链接。
PostScript
是一种图灵完全的编程语言,通常PostScript
程序不是人为生成的,而是由其他程序生成的。然而,仍然可以使用手工编制的PostScript程序生成图形或者进行计算。
PostScript
是一种基于堆栈的解释语言(例如stack language),它类似于Forth语言但是使用从Lisp语言派生出的数据结构。这种语言的语法使用逆波兰表示法,这就意味着不需要括号进行分割,但是因为需要记住堆栈结构,所以需要进行训练才能阅读这种程序。
1 2 add
:1+2
3 4 add 5 1 sub mul
:(3 + 4) × (5 - 1)
/x1 15 def
:定义一变量x1
,其值为15
/x1 x1 2 add def
:x1+=2
x1 0 eq { 0 } if
:{}
可以简单理解为定义一过程
%!PS-Adobe-3.0 EPSF-3.0
:注释语句以%
开头
for
语句语法:initial increment limit proc for
。
它会维护一个control variable
,初始值设为initial
。然后,在每次重复之前,会先比较control variable
与limit
。若未超过limit
,则
将control variable
入栈,执行proc
之后再将increment
添加到control variable
。
示例如下:
可以用C语言写成(仅仅为表示其功能):
pstack
打印当前栈中所有元素。
交换堆栈顶部的两个元素:
可以用来给变量赋值:
定义数组:
为数组/字典/字符串中某个元素赋值:
index
语句语法:anyn … any0 n index
。
复制第n个元素到栈顶:
与for
及put
语句结合使用,可以为整个数组赋值:
可以用C语言写成(仅仅为表示其功能):
与put
语句相反,取出数组/字典/字符串中某个元素:
将数组元素及其自身入栈:
取出栈顶两个元素进行比较,结果(前者小于后者,为true
;反之为false
)入栈:
与le
语句比较规则相反:
repeat
语句语法:int proc repeat
。
重复执行proc
指定次数:
笔者上述介绍的语句均在POC中出现,若读者未完全理解,可进一步查阅官方参考文档。
笔者分析环境:Ubuntu 18.04、Ghostscript 9.21、GDB+pwndbg
可以用C语言写成(仅仅为表示其功能):
其功能为定义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如下:
如此一来,可直接在zprint()
函数处设断。(若在zaload()
函数处设断,无法一次断下)
启动GDB后设置参数如下:
实现aload
操作的函数zaload()
[位于/psi/zarray.c]是第一个关键点:
b zprint
设置断点,r
开始执行后,成功在zprint()
函数处断下:
查看osp及osbot(变量名osbot,osp和ostop代表operator stack的栈底、栈指针和栈顶):
根据ref_s
结构(位于/psi/iref.h)的定义:
可知0x00005555575d44e9
地址处存储的应该是buffers
字符串,验证之:
那么0x00005555572d5e60
地址处存储的是buffers
数组,根据POC Part2能够得知buffers[n]
为buffersizes[n] string
,且每个buffers[n]
的最后16位均为0xFF
,验证之:
b zaload
于zaload()
函数处设断,c
继续执行,于zaload()
函数处成功断下后,s
单步执行到if (asize > ostop - op)
:
IF条件成立,那么调用ref_stack_push()
函数(位于/psi/istack.c)重新分配栈空间:
之后的操作是向重新分配的栈空间中写入内容,b zarray.c:71
于修改osp
语句设断,c
继续执行到断点处:
x /222gx 0x5555572d5e60
查看buffers
数组的每一项地址:
注意:osp(0x5555575006f8)位于上图箭头所指数组项下方。
实现.eqproc
操作的函数zeqproc()
(位于/psi/zmisc3.c)是第二个关键点。.eqproc
是取出栈顶两个元素进行比较之后入栈一个布尔值(<proc1> <proc2> .eqproc <bool>
):
可以看出其在取出两个操作数时并未检查栈中元素数量,且并未检查两个操作数类型,如此一来,任意两个操作数都可以拿来进行比较。其修复方案即是针对此两种情况:
b zeqproc
设断后,c
继续执行,于zeqproc()
函数处成功断下。接下来b zmisc3.c:112
于make_false(op - 1);
设断:
可以看到make_false()
修改之处。之后的pop(1);
将栈指针上移,如此一来.eqproc
与loop
结合便可导致栈指针上溢。
下面来看POC Part3:
其通过buffersearchvars
数组来检索buffers[N]
(修改项见图片25)字符串后16位是否被make_false()
修改,进而判断osp
是否到达可控范围,并通过buffersearchvars
数组来保存位置。
于POC中254 le {
后添加(Overwritten) print
,并将之前添加的print
语句全部注释掉。重新启动GDB,设置参数见上,b zprint
设断后,r
开始运行,成功断下后:
如此一来,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如下,便于设断:
于zprint
断下后,查看上移前osp:
c
继续向下执行:
可以看到currentdevice
已经覆盖掉之前的字符串buffers[N],接下来的三条语句修改其属性:
关于属性各字段定义见tas_s结构(位于/psi/iref.h)):
修改完成:
查看此时的LockSafetyParams
值:
可以看到偏移0x3e8
处值为1(另外两处偏移应该是针对其他系统或版本)。LockSafetyParams
属性见gx_device_s
结构(位于\base\gxdevcli.h)。
最后通过.putdeviceparams
(实现位于/psi/zdevice.c)设置/OutputFile
为(%pipe%echo vulnerable > /dev/tty)
,.outputpage
完成调用。
样本名称:라자루스_에어컨계약.hwp
MD5:EC0C543675374A0EE9A83A4D55CA1A6C
使用HwpScan2打开文档,可以看到其中的PS脚本:
导出解压后的PS脚本,其中Y101
变量存储加密后Shellcode,直接改写该脚本将Y101
变量解密并写入一EPS文件中:
EPS脚本中有如下语句:
可以看出其确实利用了CVE-2017-8291。
继续分析解密后EPS脚本可以看到其调用了VirtualProtect()
函数:
x32dbg
中打开gbb.exe
,最新的HWP已经移除该组件,笔者分析时使用的HWP版本如下:
之后修改命令行,其参数为打开文档后于Temp目录下释放的PS脚本(即HwpScan2中的BIN0001.ps)完整路径:
于VirtualProtect()
函数处设断后F9运行,成功断下:
通过0xAABBCCDD
标志确定ECX指向:
由ECX给函数传递参数,获取系统函数调用地址:
判断当前进程是否运行在WOW64环境中:
获取当前系统内所有进程的快照:
获取第一个进程的句柄:
通过Process32Next()
枚举进程,并传递给sub_026AF131函数判断是否为explorer.exe
:
返回explorer.exe
进程ID:
之后将Shellcode注入到explorer.exe
进程中:
x64dbg附加到explorer.exe
上,分析其Shellcode功能。同样是通过0xAABBCCDD
标志确定RCX指向:
由ECX给函数传递参数,获取系统函数调用地址:
之后调用sub_4890EE0判断当前进程是否为explorer.exe
进程:
移动指针指向,并将gozdeelektronik[.]net提取出来:
载入WinInet.dll
:
获取即将调用函数调用地址:
之后从gozdeelektronik[.]net下载第二阶段载荷movie.jpg:
样本名称:2020년 연구ㆍ전문원 및 수자원분야 경력사원 선발 모집요강.hwp
MD5:F90770D4A320BF15E51FDD770845DCE5
同样是先使用HwpScan2查看该文档:
tomato
变量存储的是未加密的EPS脚本,可直接将其内容复制出来查看。其与上一利用脚本不同之处在于其采用拼接方式来定义名称字符串:
调试方法同上,不再赘述。可以成功在VirtualProtect()
函数处断下:
获取GetProcAddress()
调用地址:
获取LoadLibrary()
调用地址:
载入msvcrt.dll
并获取system()
函数调用地址:
通过call 02250806
指令来为system()
函数传递参数:
其执行指令的功能是于TEMP目录下创建一名为adsutil.vbs
的VBS脚本,写入内容并执行该脚本:
该VBS脚本经整理后内容如下:
该脚本功能是于https[:]//matteoragazzini[.]it下载第二阶段载荷,解码后写入svchost.exe
中并执行之。
样本名称:(첨부2)20-0206법인운영상황평가표서식(법인작성용).hwp
MD5:8AD471517E7457EB6EEA5E3039A3334F
HwpScan2查看该文档,会发现该样本不同于Lazarus组织的两个样本在于其EPS脚本最后部分:
同样是在VirtualProtect()
函数处断下:
通过ECX给sub_02544D7D传递参数获取系统函数调用地址:
调用GetComputerName()
获取计算机名并于其后添加经过计算的十六进制值,之后通过异或及指定运算来为即将创建的文件命名:
于临时目录下创建文件:
之后再次计算一文件名并创建文件:
调用ZwQuerySystemInformation()
遍历系统所有打开的句柄,此时SystemInformationClass=SystemHandleInformation
,若缓冲区不足则把申请内存的大小扩大一倍之后调用RtlReAllocateHeap()
再次申请,直至成功为止:
接下来调用ZwQueryObject()
查询对象的类型,找到打开的EPS文件:
使用CreateFileMapping()
和MapViewOfFile()
函数将EPS文件映射到进程内存空间中:
映射完成:
移动指针指向EPS脚本最后部分:
调用VirtualAlloc()
函数为其开辟内存空间:
解密并写入到分配的内存空间中:
实际上解密后的该部分将被注入到HimTrayIcon.exe
进程中,详见下文分析。
获取当前系统内所有进程的快照之后通过Process32Next()
枚举进程:
遍历线程,找到HimTrayIcon.exe
之后打开并挂起线程:
将解密出来的Shellcode写入到进程:
之后调用RtlCreateUserThread()
函数恢复线程的执行。最终释放内存空间并退出:
其注入Shellcode可以附加HimTrayIcon.exe
之后调试,亦可将Shellcode转成exe之后调试,笔者选择转成exe之后再进行调试。解密内存中的PE文件:
获取系统文件夹并拼接路径:
创建进程:
调用GetThreadContext()
函数,若失败则直接TerminateProcess
:
获取系统版本信息,以此来判断下一步如何执行:
多次调用WriteProcessMemory()
函数于创建的进程中写入PE文件内容:
恢复线程执行:
0
1
1
10
{
pop
1
add
}
for
0
1
1
10
{
pop
1
add
}
for
int
a
=
0
;
int
i;
for
(i
=
1
; i <
=
10
; i
+
+
)
{
a
+
=
1
:
}
int
a
=
0
;
int
i;
for
(i
=
1
; i <
=
10
; i
+
+
)
{
a
+
=
1
:
}
int
tmp[
10
];
int
i;
int
a
=
0
;
for
(i
=
1
; i <
=
10
; i
+
+
)
{
tmp[a]
=
i;
a
+
=
1
;
}
int
tmp[
10
];
int
i;
int
a
=
0
;
for
(i
=
1
; i <
=
10
; i
+
+
)
{
tmp[a]
=
i;
a
+
=
1
;
}
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
;
}
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
;
}
/
buffersearchvars [
0
0
0
0
0
]
def
/
sdevice [
0
]
def
buffers
%
+
+
(buffers)
print
%
+
+
pop
%
+
+
enlarge array aload
(after aload)
print
%
+
+
/
buffersearchvars [
0
0
0
0
0
]
def
/
sdevice [
0
]
def
buffers
%
+
+
(buffers)
print
%
+
+
pop
%
+
+
enlarge array aload
(after aload)
print
%
+
+
set
args
-
q
-
dNOPAUSE
-
dSAFER
-
sDEVICE
=
ppmraw
-
sOutputFile
=
/
dev
/
null
-
f
/
home
/
test
/
exp.eps
set
args
-
q
-
dNOPAUSE
-
dSAFER
-
sDEVICE
=
ppmraw
-
sOutputFile
=
/
dev
/
null
-
f
/
home
/
test
/
exp.eps
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
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
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;
};
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;
};
gdb
-
peda$ p asize
$
37
=
0x3e8
gdb
-
peda$ p ostop
-
op
$
38
=
0x31f
gdb
-
peda$ p asize
$
37
=
0x3e8
gdb
-
peda$ p ostop
-
op
$
38
=
0x31f
/
*
*
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
;
}
/
*
*
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;
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2021-2-18 16:54
被erfze编辑
,原因: