문제의 이름이 BOF인 만큼 fgets() 함수에서 입력받는 과정에서 버퍼 사이즈를 잘 못 지정해서 취약점이 발생한 것 같다.
fgets() 함수에 대해 알아보자.
fgets(char * str, int num, FILE * stream) - 첫 번째 인자는 읽어들인 문자열을 저장할 char 배열을 가리키는 포인터이다. - 두 번재 인자는 마지막 NULL문자를 포함하여 읽어들일 최대 문자 수이다. - 세 번재 인자는 문자열을 읽어들일 스트림의 FILE 객체를 가리키는 포인터이다. - 리턴값은 성공했을 경우 char 배열을 리턴하고 아무런 문자가 없거나 오류가 발생했을 때는 NULL이 리턴된다.
shell() 함수가 존재한다. gdb로 좀 더 살펴보자.
빨간 네모박스를 잘 보자.
ebp-0xc 위치에 0x80484b4 가 들어간다. 그 다음 eax에 ebp-0xc 위치의 값이 들어간 뒤 call 된다.
그렇다면 ebp-0xc 위치에 shell 함수의 주소를 입력하면 call 에서 shell 함수가 호출될 것이다.
해보자.
먼저, fgets로 입력된 값이 어느 위치에 들어가는지 알아보자.
fgets가 호출된 다음 부분에 bp를 걸고 실행해보자.
0xffffd0cc에 입력을 받는 것을 알았다.
두 번째로 ebp-0xc의 주소를 알아내보자.
$ebp의 주소에서 0xc를 빼주면 ebp-0xc의 주소인 0xffffd14c이 나온다.
세 번째로 0xffffd14c 주소에 넣을 shell 함수의 주소를 알아내 보자.
마지막으로 입력되는 주소에서 ebp-0xc의 거리를 계산해보자.
위의 네모박스는 입력받는 곳이고, 밑은 ebp-0xc의 자리이다.
0xffffd14c - 0xffffd0cc = 0x80
거리가 나왔다.
이젠 필요한 재료(?)들이 다 모였다. 익스를 짜보자.
입력받는 곳으로부터 A로 128byte를 채우면 ebp-0xc의 자리에 위치한다. 이 곳에 shell 함수의 주소를 넣는다.
128byte + 4byte = 132byte 이다. 위의 fgets 함수의 버퍼 사이즈를 넘지 않는다.
Format String Bug (FSB) : printf() 함수 계열을 사용할 때, 인자로 포맷 스트링과 포맷 스트링에 대응하는 인자를 사용하지 않고 바로 변수를 인자로 지정했을 때, 악의적인 공격자가 포맷 스트링을 입력하여 스택의 값을 leak 하거나, RET나 _DTOR_END__, GOT와 같은 프로그램의 흐름을 제어할 수 있는 메모리 주소값을 변조하여 악의적인 행동을 할 수 있게 된다.
윈도우와 우분투에 파일을 다운받고 IDA로 코드를 확인해보자.
메인함수에선 따로 확인할건 없어보인다.
vuln 함수를보자.
snprintf 함수와 printf 함수에서 서식문자를 따로 지정해주지 않고 바로 넘겨버린다.
이 함수에서 FSB 취약점이 발생한다는 것을 알 수 있다. 두 함수에 대해 알아보자.
sprintf(char * buffer, const char * format, ...)
- ... 은 가변 파라미터를 뜻 한다. - 첫 번째 인자에 두 번째 인자의 내용이 문자열로써 담기게 된다.
snprintf(char * buffer, int buf_size, const char * format, ...) - sprintf 함수에서 두 번째 인자로 size가 추가된 함수로, buffer overflow를 방지하기위해 출력할 문자열의 길이를 지정하였다.
gdb를 이용해 basic_fsb에 flag 함수가 있는걸 확인했다.
이 함수를 이용하면 될 것 같다.
우분투에서 파일을 실행해서 서식문자를 넣어보자.
두 번째 서식문자에는 입력된 AAAA의 hex값인 41414141이 출력된다.
%n : 서식문자 앞까지의 바이트 수를 계산 후 출력, 출력했던 값을 주소로 생각하고 계산한 값을 주소에 넣는다.
그렇다면 위의 서식문자를 바탕으로 이렇게 사용할 수 있을 것 같다.
-> printf_GOT 주소에 %n 이 오게한 다음, %n이 flag 함수의 주소를 계산하게 만들면 문제가 해결될 것이다!!
GOT는 PLT가 참조하는 테이블이다. 쉽게 말하면 함수의 실제 주소가 들어있다.
자! 이제 printf_GOT의 주소와 flag 함수의 주소를 찾아보자.
모두 찾았다. python으로 익스를 짜보자.
flag 함수의 주소값 0x80485b4을 10진수로 바꾼만큼 %x 포맷을 이용해 출력 해준다.
그리고 위에서 포맷스트링 두 번째부터 입력값이 출력됐기 때문에 -4byte를 해줘야 한다.
import java.util.*;
class VaultDoor3 {
public static void main(String args[]) {
VaultDoor3 vaultDoor = new VaultDoor3();
Scanner scanner = new Scanner(System.in);
System.out.print("Enter vault password: ");
String userInput = scanner.next();
String input = userInput.substring("picoCTF{".length(),userInput.length()-1);
if (vaultDoor.checkPassword(input)) {
System.out.println("Access granted.");
} else {
System.out.println("Access denied!");
}
}
// Our security monitoring team has noticed some intrusions on some of the
// less secure doors. Dr. Evil has asked me specifically to build a stronger
// vault door to protect his Doomsday plans. I just *know* this door will
// keep all of those nosy agents out of our business. Mwa ha!
//
// -Minion #2671
public boolean checkPassword(String password) {
if (password.length() != 32) {
return false;
}
char[] buffer = new char[32];
int i;
for (i=0; i<8; i++) {
buffer[i] = password.charAt(i);
}
for (; i<16; i++) {
buffer[i] = password.charAt(23-i);
}
for (; i<32; i+=2) {
buffer[i] = password.charAt(46-i);
}
for (i=31; i>=17; i-=2) {
buffer[i] = password.charAt(i);
}
String s = new String(buffer);
return s.equals("jU5t_a_sna_3lpm18gb41_u_4_mfr340");
}
}
아래쪽에 입력받은 패스워드를 검사하는 함수의 for문들을 차례대로 따라가보자.
"jU5t_a_sna_3lpm18gb41_u_4_mfr340" 길이가 32byte이기 때문에 비교에 사용될 것 같다.
첫 번째 for 문이다.
for (i=0; i<8; i++) {
buffer[i] = password.charAt(i);
}
"jU5t_a_s" 이 저장된다.
두 번째 for 문이다.
for (; i<16; i++) {
buffer[i] = password.charAt(23-i);
}
i = 8 이다.
"1mpl3_an" 이 저장된다.
세 번째 for 문이다.
for (; i<32; i+=2) {
buffer[i] = password.charAt(46-i);
}
i = 16 이다.
"4?r?m?4?u?1?b?8?" 이 저장된다.
네 번째 for 문이다.
for (i=31; i>=17; i-=2) {
buffer[i] = password.charAt(i);
}
i = 31 이다.
"?g?4?_?_?_?f?3?0" 이 저장된다.
3번째와 4번째 for문에서 저장되는 값은 i가 교차하기 때문에 엇갈리게 해줘야 한다. (주의)
for문을 전부 차례대로 따라가 보았다.
이어 붙이면 플래그가 완성될 것 같다.
플래그는 picoCTF{jU5t_a_s1mpl3_an4gr4m_4_u_1fb380} 이다.