본문 바로가기

Security

[Dice CTF 2021] flippidy

Binary 분석

바이너리 checksec 결과

Canary, NX가 설정되어있고, Full RELRO가 되어있다. Full RELRO이므로, GOT 영역을 쓸 수 없다. 다른 글들을 참고해보면 여러 방법이 있지만 __malloc_hook이나 __free_hook의 주소를 덮어서 공격을 했다.

쓰기 권한 설정 X

프로그램을 실행하면, notebook의 크기를 입력받고 2가지의 기능을 선택할 수 있다. "add notebook", "flip notebook"이다. 

프로그램 실행 결과

IDA로 각 기능을 분석하겠다.

add 기능 디컴파일

main 함수에서 notebook의 크기(=note_size)를 입력받았고, 해당 크기에 맞게 malloc( ) 함수를 통해 heap_addr를 저장한다. v1에 heap_addr + 8 * idx 값을 저장하고, 해당 주소에 0x30 만큼 동적 할당한다. 생성된 heap에는 0x30만큼 입력받는다. 

flip 기능 디컴파일

두 번에 나눠서 분석하겠다. note_size/2 만큼 for문을 반복한다. 이때, *(heap_addr + i)에 값이 존재하면 해당 위치의 값을 &s에 복사하고 그 주소의 메모리를 해제한다. 아니라면 v3에 1을 저장한다.

다음 if문에서는 *(heap_addr + note_size - i -1)에 값이 존재하면 해당 위치의 값을 &dest에 저장하고, free( )한다. 코드는 봐서는 정확히 이해가 안될 수 있지만 다음 표와 같이 동작한다.

flip 메모리 해제

note_size가 8인 heap 메모리 구조라고 예를 들어보자. 첫 번째 if문에서 *(heap_addr + i)에 0x0000이라는 값이 존재한다고 가정한다. 그럼 0x0000이라는 값을 &s에 복사하고 메모리를 해제할 것이다. 두 번째 if문에서는 *(heap_addr + note_size - i -1) 값을 검사하는데, 천천히 살펴보자. note_size는 8이고, i는 현재 0일 것이다. 그러면 가리키는 값은 0x7777의 값이 된다. 따라서 위의 표처럼 0x0000과 0x7777값의 메모리가 해제된다. 

flip 메모리 해제 2

그 다음에는 각 가리킨 값을 0으로 초기화한다. 만약 위에서 v3가 1로 설정되었다면 그 반대되는 주소인 *(heap_addr + note_size - i - 1)에 0을 넣는다. 아니라면, 반대되는 주소에 새롭게 0x30만큼 동적할당하고, &s에 저장했던 값을 strcpy( ) 함수를 통해 복사한다.

이를 통해 알 수 있는 것은, flip의 기능이 힙의 구조를 반전시키는 것을 알 수 있다. 위의 표를 계속해서 예시를 든다면 flilp( ) 함수가 수행되면 다음과 같이 구조가 반전될 것이다.

flip( ) 기능 실행 후 힙 메모리 구조

취약점 분석

소스코드 오디팅만으로는 취약점을 발견하지 못하고, 다양한 값을 직접 넣어보며 프로그램의 취약점을 분석했다. add( ) 기능으로 힙 오버플로우를 일으키는 것은 입력 길이가 제한되므로 불가능하다. 다양하게 해보다가 가장 처음, notebook의 크기를 입력하는 부분에 "1"을 넣고 content 값을 채우고 flip( ) 기능을 실행하니 Double Free bug가 발생했다. 막상 발생하고 나서야 해당 취약점이 있구나 하고 생각했다. 

flip( ) 함수 내에서, *(heap_addr + i)와 *(heap_addr + note_size - i - 1)을 해제한다. 이때, note_size가 1이라면 서로 같은 주소를 가리키고 DFB가 발생한다. 다시 생각해보면, 노트 사이즈가 홀수인 경우에는 이와 같은 취약점이 똑같이 발생한다.

하지만 이 취약점은 패치가 되지 않은 2.27버전 이하 라이브러리 버전에서만 exploitable하므로 다른 취약점을 찾고 싶었지만 찾지 못했다. 다른 분들의 write-up도 찾아봤는데 익스플로잇 방법의 차이였지 취약점 자체는 해당 취약점을 이용하여 공격했다.

Exploit 작성

우선, system( ) 함수의 주소를 구하는 게 첫 번째 목표다. 어떤 방법으로 leak 할지 고민하다가 menu를 출력하는 함수에서 특이한 점을 발견했다.

메뉴 출력 함수

프로그램의 기능을 설명하는 함수는 문자열을 직접 하드 코딩한 반면, 해당 함수에서는 for문을 돌며 off_404020[i]을 출력한다. 그렇다면 0x404020이 가리키는 값을 특정 함수의 GOT로 덮어쓰면 그 함수의 실제 주소를 얻을 수 있고, offset 계산을 통해 system( ) 함수의 주소를 구하면 된다.

어려웠던 점은, 내가 원하는대로 malloc, free를 하는 것이 아닌, add( )와 flip( ) 기능에서 사용하는 것을 컨트롤 하는 것이 힘들었다. 나의 경우, 처음부터 힙 구조가 어떻게 될지 예측하고 공격 순서를 작성하지 않고, 값을 직접 넣어보고 디버깅해보며 공격 방법을 생각했다.

0x404020을 입력한 힙 메모리 상황

add( ) 기능에서 content에 0x404020을 입력한 힙 메모리 상황이다. 0x17a9260에는 content를 가리키는 포인터가 저장되어있다.

첫 번째 free( ) 후, tcache_entry

flip( ) 함수 내에서, 첫 번째 free( ) 하고 직후의 tcache_entry 모습이다. 0x17a9280의 주소가 해제된 것을 확인할 수 있다.

두 번째 strcpy( ), 0x0 값을 복사

두 번째, strcpy( ) 함수에서는 같은 주소를 복사할 것이므로 0x17a9280의 주소를 &dest (위의 디컴파일 참고)로 복사하는데 이미 해제된 영역이므로 0x0값이 복사된다.

tcache_entry에서 같은 포인터 double free

두 번째 free( ) 이후에 tcache_entry 모습이다. 같은 주소를 해제해서 같은 주소가 들어가 있고, 이 때문에 0x17a9280 주소에 0x17a9280 값이 들어갔다.

malloc(0x30) 이후, tcache_entry 상황

두 번의 strcpy( )와 free( ) 이후, malloc(0x30)을 진행한다. tcache_entry에서 같은 크기인 0x17a9280 주소가 할당되었지만, 0x17a9280에는 0x17a9280값이 존재하므로 tcache에서는 위와 같이 나온다.

다시 strcpy( )

새로운 힙을 할당하고, 위에서 스택에 저장했던 값을 다시 strcpy( ) 함수를 통해서 복사한다. 처음에 0x404020 값이 있었으므로 0x17a9280에 0x404020이 복사될 것이다.

0x17a9280에 복사된 0x404020 값
복사 이후, 수정된 tcache_entry 모습

strcpy( ) 이후, 원래 tcache_entry에 0x21f7280 값이 있었는데 해당 값에 0x404020이 복사되어서 위와 같이 수정된 것을 확인할 수 있다. (주소값이 바뀐 이후는 캡쳐를 하지못해 다시 디버깅함.)

한 번 더, malloc(0x30) 

한 번 더, malloc(0x30)이 진행되고, 최종적인 tcache_entry의 모습은 위와 같을 것이다. 해당 tcache에서 0x30만큼 메모리를 할당하고 값을 입력하면, 0x404020에 데이터를 입력할 수 있다. 이 값은 menu를 출력할 때 값이므로 함수의 GOT로 덮어버리면 원래 menu가 출력되야 하지만, 원하는 함수의 주소를 leak 할 수 있을 것이다.

현재 tcache에서 add( ) 기능을 통해, 0x404020에 puts( ) 함수의 GOT(=0x403fd8)로 덮어쓸 것이다. 그럼 puts( ) 함수의 주소를 얻을 수 있고, offset 계산을 통해 system( ) 함수의 주소까지 구할 수 있다. 그 다음에는 보통 GOT를 system( )으로 덮어써서 해당 함수를 실행해서 쉘을 획득하지만, Full RELRO가 걸려있어서 위에서 언급한대로 __free_hook( )를 system( )으로 덮어쓸 것이다. 

tcache에는 아까 동적할당 요청이 들어와서 0x404020 주소를 할당해줬을꺼고, "0x404040 --> 0x654d..." 이 있을 것이다. 여기서 어떻게 할지 고민했는데 0x404020을 덮을 때, 좀만 더 덮으면 0x404040까지 덮을 수 있다. 0x404040 주소를 다시 0x404040으로 덮으면 다시 컨트롤할 수 있게 된다.

덮었다고 가정했을 때, 현재 tcache는 "0x404040 --> 0x404040"일 것이다. 이 상태에서 add( ) 기능을 이용하여 malloc(0x30)으로 동적할당을 요청하고, content에는 &__free_hook 을 입력한다. 그럼 0x404040 주소에는 __free_hook의 주소가 입력될 것이다. 그럼 tcache는 "0x404040 --> &__free_hook"이 될 것이다. 두 번의 malloc( ), 동적할당 요청을 하고 system( ) 함수의 주소를 입력하면 쉘을 획득할 수 있다. 디버깅하면서 다시 살펴보도록 하자.

0x404020에 입력하며 0x404040까지 overwrite

malloc(0x30)을 요청하여 0x404020은 할당된 상태고, content를 입력할 때, puts( )의 got를 입력하고, 0x404040에는 0x404040까지 덮어쓴 tcache 상황이다.

한 번 더, malloc(0x30) 요청 후, __free_hook으로 덮음

그 다음, add( ) 기능을 이용하여 0x404040 주소에 __free_hook의 주소를 입력했다.

__free_hook 주소를 system( ) 주소로 덮음

앞에서는 dummy 값으로 malloc(0x30)을 입력하고, 한 번 더, 요청하여 입력하면 위와 같이 __free_hook 주소에 입력할 수 있다. system( ) 주소를 입력하고, 다시 add( ) 기능을 이용하여 "/bin/sh\x00"을 입력하고 flip( ) 기능을 이용하면 "/bin/sh\x00"이 저장된 메모리를 free( ) 하는데 이미 system으로 덮어썼으므로 쉘을 획득할 수 있게 된다. 작성한 공격코드는 다음과 같다.

from pwn import *
context.log_level='debug'
p = process('./flippidy', env={"LD_PRELOAD":"./libc.so.6"})
pause()
p.sendlineafter(': ', '1')

def malloc(idx, content):
    p.sendlineafter(': ', '1')
    p.sendlineafter(': ', str(idx))
    p.sendlineafter(': ', content)

def flip():
    p.sendlineafter(': ', '2')

malloc(0, p64(0x404020))
flip()
malloc(0, p64(0x403f98)*4+p64(0x404040))

tmp = b''
for i in range(0, 8):
    tmp+=p.recv(1)

puts_addr = u64(tmp) >> 0x10
#puts_addr = u64(p.recv(8)) >> 0x10
system_addr = puts_addr - 0x31580
binsh_addr = puts_addr + 0x1334da
free_hook = puts_addr + 0x36cf28

log.info("puts()'s addr: 0x%x" % puts_addr)
log.info("system()'s addr: 0x%x" % system_addr)
log.info("/bin/sh's addr: 0x%x" % binsh_addr)
log.info("&__free_hook: 0x%x" % free_hook)

malloc(0, p64(free_hook))
malloc(0, '1111')
malloc(0, p64(system_addr))
malloc(0, "/bin/sh\x00")
flip()

p.interactive()

'Security' 카테고리의 다른 글

[Reversing.kr] Replace  (0) 2021.04.01
[Reversing.kr] Easy Unpack  (0) 2021.03.28
[Dice CTF 2021] babyrop  (0) 2021.02.23
[pwnable.kr] ascii_easy  (0) 2021.02.21
[pwnable.kr] brain fuck  (0) 2021.02.15