본문 바로가기

Wargame/pwnable.kr

[Grotesque] lokihardt

오랜만에 문제를 풀었다. 그간 오랜 공백기간이 있던 것 같은데. 회사에서 이런저런 바쁜 일이 많다보니 문제 풀 시간이 잘 안났다.

 

 

lokihardt 문제는 실제 lokihardt(갓정훈님)가 Microsoft Edge의 Chakra 자바스크립트 엔진에서 일으킨 취약점(CVE-2016-0191)을 기반으로 만들어졌다. 이런 류의 취약점은 실제로 자주 일어난다고 한다.

 

이 문제는 아마  HeapSpray 기법을 학습해보라는 의도로 만들어진 듯 하다.

이틀간 문제 푸는동안 많은 공부가 되었다.

 

 - 400명 이상 푼 문제는 PoC 코드를 공개하겠다.

 - 이 글에는 문제 풀이, 힌트 등이 있으니 실력 증진을 원하는 분들은 충분히 삽질한 후 읽어주시길 바란다.

 

1. 코드 흐름

====================================================

먼저 맨 첫번째 라인을 보자. gcc -pie -Wl, -z,now

바이너리에 pie 보호기법과 full RELRO가 걸려있다. 첫번째 라인 안보고 삽질한 분들 꽤나 많을 것 같다.

이거 안보고 gdb에서 분석&테스트한대로 익스가 안된다고 실망하지 말자. 여러분이 보호기법을 못봤을 뿐이니까..(또륵)

 

_OBJ 구조체는 rdata 256바이트, 포인터형 wdata 8바이트, length 8바이트, 포인터형 type 8바이트로 구성되어 있다. 자주 쓰이게 될 것이니 반드시 저 구조를 알아두는 것이 좋다.

 

전역변수로는 g_buf[16]가 선언되어 있다.

 

AllocOBJ는 main의 Alloc, HeapSpray에서 불려지는 구조체형 함수이다. malloc으로 OBJ 공간을 할당한 뒤, memset으로 할당한 공간을 초기화한다. 이후 OBJ type을 "null"로 초기화하고, 사용자에게 read 256바이트, write 16바이트를 입력받는다. write는 g_buf의 주소를 받아오게 되므로, 사용자가 입력한 값은 g_buf에 들어간다.

 

gc()는 garbage collect 역할을 하는 함수이다. refCount가 0이고 OBJ가 null이 아닐 경우, theOBJ와 randomPadding 영역을 free한 후,

theOBJ를 NULL로 초기화한다.

 

Delete()는 선택한 idx의 arrayBuffer를 null로 초기화한 후 refCount를 감소시킨다. refCount 변수는 관리 중인 힙 영역을 카운트하는 역할인 듯 싶다.

 

Alloc()은 refCount가 0, 즉 관리 중인 힙 영역이 존재하지 않을 경우 "0xcc"로 채워진 0~1024바이트 사이의 randomPadding 영역을 생성하고 그 뒤에 OBJ 영역을 할당한다. 그리고 할당한 영역의 주소를 선택한 idx의 arrayBuffer에 채워넣는다.

이후 Alloc()을 실행하면 선택한 idx의 arrayBuffer를 이미 할당했던 OBJ 주소로 채워넣는다.

 

Use() 함수는 문제의 핵심이 되는 부분이다. (결국 모든 함수를 다 써야하긴 하지만.. 여기가 제일 중요하다.)

idx가 0~16 사이의 값이 아니라면 "out-of-range" 메시지를 출력하고, 선택한 idx의 arrayBuffer 값이 Null이라면 "ArrayBuffer가 null이래"라는 메시지를 출력한다. 즉 선택한 idx는 0~16 사이어야만 하고 해당 ArrayBuffer 값이 Null이 아니어야 한다.

 그리고 type이 "read"일 경우 선택한 idx의 OBJ 값 256바이트를 출력하고, type이 "write"일 경우 사용자에게 length만큼 데이터를 입력받는다. 

 

main 함수는 별거 없다. 그냥 "1"은 Alloc(), "2"는 Delete(), "3"은 Use(), "4"는 gc(), "5"는 HeapSpray()라는 것만 알아두면 된다.

그리고 Alarm이 100으로 설정되어 있다는 것 정도.

 

 

2. 고려 사항

====================================================

1. gcc -pie-Wl, -z,now. 정말 중요하다. 기억하자.

2. 반드시 문제서버와 동일한 환경인 Ubuntu 16.04 64bit 환경에서 분석하는 것이 좋다. 상위 버전에서 분석할 경우, Heap chunk 할당 방식이 다른지, 취약점이 안 보인다.

3. 100초 안에 익스코드를 짜서 공격해야 한다.

4. type은 항상 "null"로 초기화되는데, 왜 Use()에는 "read"와 "write"가 있는걸까?

 

3. 필요 지식

====================================================

1. 이 문제는 CVE-2016-0191 취약점을 기반으로 만들어졌다. (하지만 검색해도 관련 글은 잘 안나온다 ㅎ..)

 

2. pie 보호기법이 뭔지 알아야 한다.

관련 주소: bpsecblog.wordpress.com/2016/06/10/memory_protect_linux_4/

 

3. full RELRO 보호기법이 뭔지 알아야 한다.

관련 주소: bpsecblog.wordpress.com/2016/05/18/memory_protect_linux_2/

..관련주소 찾다보니 어째 블랙펄 글들이 이리 많이 보이지? '-'

 

4. UAF(Use After Free) 공격 기법의 원리를 알아야 한다.

관련 주소: shayete.tistory.com/entry/7-Use-After-Free?category=857069

간단하게 설명하자면, UAF는 해제(free) 후 재할당(alloc)할 시 이전에 사용했던 힙을 다시 재할당받게 되어 발생하는 취약점이다. 다시 사용할 때, 이전에 사용했던 영역을 write할 수 있다.

 

5. 자바스크립트엔진의 공격이 어떤 방식으로 이루어지는지 알면 좋다. fakeOBJ, addrof 등의 용어도 알아두면 좋다.

아래 주소는 널룻(null#root)에서 phrack 페이지의 "Attacking JavaScript Engine"을 번역한 글이 있다.

관련 주소: null2root.github.io/blog/2019/04/06/Attacking-JavaScript-Engines-kor.html

 

[번역] Attacking JavaScript Engine

출처 : http://www.phrack.org/papers/attacking_javascript_engines.html 번역 및 보완 : LiLi, y0ny0ns0n, powerprove, cheese @ null2root

null2root.github.io

6. Type Confusion을 이해하면 좋다. 말 그대로 타입을 혼동하는 취약점이다. 예를 들어 "A"라는 OBJ의 타입유형이 "read"인데, 특정 오류로 인해 "A" OBJ의 타입유형을 "write"나 "null" 등 다른 타입으로 혼동하여 "write", "null"에 대한 동작을 수행하게 되는 버그이다.

 

7. HeapSpray의 사용처를 알아두면 좋다. 여기 문제에서는 단순하게 이용되는 것 같지만.. 실제로는 Heap Grooming 등 해커가 예측 가능한 주소에 공격코드를 올려놓도록 사용된다.

 

4. 문제 풀이

====================================================

 

문제를 켜보면 위와 같다. 할당, 삭제, 사용, GC, HeapSpray 등의 메뉴가 보이는데.. 아마

1. Alloc과 4. GarbageCollect, 5. HeapSpray의 코드구조를 보면 단번에 UAF가 일어날 수 있겠다라는 걸 눈치채셨을 것이다.

간단하게  1. Alloc -> 2. Delete -> 4. GarbageCollect -> 5. HeapSpray를 실행해보자.

처음 Alloc을 실행했을 때 random byte의 randompadding과 OBJ가 힙에 할당된다. 이후 4. gc로 힙을 free하였고, 거기에 5. HeapSpray로 값을 덮었다. 즉 이걸 통해 처음 할당한 Alloc공간을 HeapSpray로 덮을 수 있다는 걸 확인했다.

 

그럼 이걸로 뭘 할 수 있을까. 잘 생각해보자. Heap에는 어느 특정 주소로 점프하는 함수 포인터도 없고.. 단순히 p->type, p->write, g_buf의 length만이 바꿀 수 있을 것 같다.

 

이 문제에서는 type을 "null"로만 초기화할 뿐이고. 우리가 타입을 임의로 바꿀 수도 없는데 3. Use에는 이상하게도 "read"와 "write"가 존재한다. '아. 이걸 써먹으라는 거구나.' 라는 느낌이 들었을 것이다.

 

잘 고민하여 "read" 혹은 "write"를 이용하도록 방안을 모색해보자.

 

근데 가장 중요한 문제가 있다. "write"를 이용하면, 도대체 뭘 할 수 있는거지? p->type을 우리가 임의로 바꿀 수 있다는 걸 알았다 쳐도, 그걸 활용할 방법을 모른다면 바이너리에 공격이 아닌 엄한 장난질만 칠 수 있을 뿐이다.

 

이 문제 뿐만 아니라, 모든 문제를 풀 때는 항상 공격 시나리오를 생각하자.

(뭐.. lokihardt 문제에 접근할 정도면 어느정도 해킹 공부를 하셨으니 시나리오쯤은 모두 생각하시겠지만;;)

 

먼저 바이너리의 "write" 권한을 획득했다고 가정해보자. 뭘 할 수 있을까? 우리가 문제를 해결하기 위해 필요한 건

1. system("/bin/sh") 혹은 system("cat flag") 함수를 실행하거나,

2. mprotect를 이용해 heap에 rwx 권한을 주고 system("/bin/sh") 쉘코드를 실행하는 것이다.

근데 소스코드를 훑어봤을 때, got_overwrite를 이용하여 mprotect를 호출하더라도, mprotect와 인자 구조가 비슷한 함수는 없는 것 같고.. 스택에 OOB write가 가능한 곳도 없다. 그렇다는건 일단 mprotect를 이용해 쉘코드를 실행하는 방법은 나가리됬다는 뜻이다.

 

1. system("/bin/sh") 혹은 system("cat flag") 함수를 실행하거나,

2. mprotect를 이용해 heap에 rwx 권한을 주고 system("/bin/sh") 쉘코드를 실행하는 것이다.

그럼 "write" 권한을 획득한다면  system 함수를 콜하여 쉘을 얻어내는 것을 목표로 공격 시나리오를 구성해봐야 한다.

얼핏 힙구조를 봤을 때 p->write 주소, length, p->type을 우리 마음대로 바꿀 수 있을 것처럼 보였다.

 

그 말은 즉, write 주소, length를 우리 임의로 바꾼다면 원하는 주소에 원하는 길이의 데이터를 밀어넣을 수 있다는 뜻이다.

got_overwrite를 이용해 system("/bin/sh")를 실행하려면 1개의 인자를 가진 함수가 필요하다.

 

그 함수를 찾아서 "write"를 이용해 got_overwrite를 한다면 시스템함수를 실행시켜 쉘을 얻을 수 있을 것이다.

 

지금까지 정리된 내용을 확인해보자면... 

1. UAF를 일으켜 p->type을 read든 write든 조작할 수 있을 것 같다.

2. 공격에 필요한 함수(system, mprotect)를 확인해봤더니 system 함수를 쓰는게 가장 좋아보인다.

3. got_overwrite를 이용해 system 함수로 쓸 수 있을만한 함수를 찾아야 한다.

4. null->write type으로 변환하여 got_overwrite를 하는게 공격의 최종 목표같다.

 

여기까지만 시나리오를 생각해봐도 공격 흐름(attack flow)이 대충 보인다.

최종 목표를 위해 해당 바이너리의 보호기법을 우회하는건 그 다음이다.

 

목표를 정했으니 이제 뭘하면 될까? 구체적인 공격 방법을 생각해보자.

"write"로 정확히 특정 주소의 값을 바꾸려면, 특정 주소의 값을 정확하게 알아야 하는데, 이 바이너리에는 pie 보호기법이 걸려있다. 즉 특정 메모리의 값이 leak되지 않는 이상 우리가 원하는 주소에 원하는 값을 넣기 힘들다는 의미다.

그럼 "write"를 하기 이전에 "read"를 이용해 메모리 leak을 유도해보자. 운이 좋다면 메모리 leak에 성공하여 원하는 주소값을 계산하기 위한 base address를 발견할 수 있을 것이고, 우리가 미리 계산해놓은 offset들을 이용해 원하는 주소들을 계산할 수 있을 것이다.

 

다음으로, 이 바이너리에는 full RELRO가 걸려있다. 즉 우리가 시도하려 했던 got_overwrite가 안된다는 뜻이다. 그럼 어떻게 system을 호출해야 할까. full RELRO 우회를 구글에 검색하다보면 __free_hook을 이용한 공격 기법이 있다.

그걸 잘 찾아보자.

 

여기까지 정리했을 때, pie 보호기법을 우회하기 위한 메모리 leak을 할 수만 있다면 어떻게든 공격 코드를 예쁘게 만들어서 쉘을 얻어낼 수 있을 것 같다.

 

이 메모리 leak을 위한 힌트를 주자면.. 모든 방법이 확률적이겠지만 2가지 방법이 있다.

 

1. 힙에 수십, 수백만 바이트를 때려넣고 공격자가 예측 가능한 주소에 취약점을 발생시키는 방법인 Heap Grooming을 이용한다. 말 그대로 힙을 공격자가 원하는대로 정리(Grooming)하는 기법?이다. 예측 가능한 주소까지 HeapSpray로 데이터를 뿌린 후 Alloc()을 이용해 UAF 취약점을 발생시키고, 이후 2번째 Alloc을 할당하여 p->rdata에 "read" 문자열을 넣는다. 다음으로 첫번째 Alloc() OBJ의 p->type에 2번째 Alloc() OBJ 시작주소를 가리키도록 잘 설정하고(arraybuffer[idx] = theOBJ를 이용), 첫번째 OBJ를 호출(Use)하여 메모리 leak을 일으킨다.

2. UAF를 일으키면서 HeapSpray를 여러번 시도하다보면 OBJ Structure의 구조상 취약점으로 인해 p->type의 주소가 특정 값으로 덮이게 된다. 그걸 이용해 "read"를 호출하고 메모리 leak을 일으킨다.

 

여기까지만 알아도 문제는 거의 다 풀린다.

이번에 글쓰면서 너무 많은 걸 알려준 것 같은데.. 짧게 설명하자니 너무 애매하고, 그림 그려서 설명하자니 너무 쉽게 풀릴 것 같다.

 

- 끗 -

 

'Wargame > pwnable.kr' 카테고리의 다른 글

[Grotesque] Starcraft  (2) 2022.02.20
[Grotesque] maze  (2) 2020.10.12
[Grotesque] aeg  (2) 2020.10.03
[Rookies] crypto1  (0) 2020.09.14
[Rookies] note  (0) 2020.09.10