요즘은 일을 하다보니 CTF에 참가하는 일이 줄었고 참가해도 주로 가벼운 문제만 쓰윽하고 나오는 것 같다..
우선 소스코드가 주어진 초심자용 문제이다.
ebeb@DESKTOP-44QI01G:/mnt/e/CTF/CyberSecurityRumble/baby_flag_checker$ ./chall
Flag not found, contact challenge authors.
일단 바이너리를 실행해보았다. 저번 출제에서도 든 생각인데 가끔 이러면 더미플래그도 같이 제공해야하나 고민이다.
ebeb@DESKTOP-44QI01G:/mnt/e/CTF/CyberSecurityRumble/baby_flag_checker$ nc challs.rumble.host 53921
Enter the flag: aaaaaaaaaaaaaaa
Wrong flag: aaaaaaaaaaaaaaa
입력하면 로컬의 FLAG파일과 비교하는 전형적인 문제이다.
주로 조건문 돌파나 스택에서 정보를 꺼내는 문제로 나오는 타입이다.
Enter the flag: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
HACKER!
오버플로우를 내면 싫어한다 ㅋㅋㅋ
[*] '/mnt/e/CTF/CyberSecurityRumble/baby_flag_checker/chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
기본적으로 모든 기본 보안이 걸려있다.
사실 check함수를 보자마자 취약점은 찾았다.
strncpy(guess, input, sizeof(guess));
strncpy(flag, secret_flag, sizeof(flag));
if (!strcmp(guess, flag)) {
printf("Well done! You got it: %s\n", flag);
}
else {
printf("Wrong flag: %s\n", guess);
}
strncpy를 저렇게 써서는 안된다. Target의 크기를 가득차게 복사하는 것이 아니라 -1을 하여 하나의 여유를 줄 필요가 있다.
기본적으로 프로그램은 문자열끼리의 구별을 null byte로 진행한다. 즉 null byte가 문자열들의 가림막 역할을 해주는 것이다.
예를들면 aaa\x00bbb\x00의 앞을 출력하려면 보통 aaa만 출력하려 할 것이다. 근데 \x00가 없으면? 분명 aaa의 주소를 지정해도
bbb까지 다 출력해버린다. 즉 null byte를 만날 때 까지 출력해버린다.
char guess[32], flag[64];
해당 트릭을 위해 check함수에서는 flag함수도 다시한번 배열로 선언하여 secret flag를 복사하는 것으로 guess와 flag의 위치를
이웃하게 조정했다. 이걸로 만약 guess의 끝의 null byte가 풀리면
else {
printf("Wrong flag: %s\n", guess);
}
여기서 guess를 출력할 때 플래그까지 출력해버린다.
그래서 이 잘못 쓰여진 strncpy가 null byte에 어떠한 영향을 주는가?
strncpy함수의 재밌는 특징은 만약 target과 크기가 “같거나” 작은 문자를 복사할 때는 null byte를 추가하지 않는다.
그리고 보다 큰 문자에서는 null byte를 사용하여 자른다. ( 아무튼 가득 채워준다 )
str1abcde\x00str2fghi\x00
만약 여기서 str1에서 str3로 3만큼 strncpy(str2,str1,3)을 한다고 생각해보자
str1abcde\x00str2abci\x00
null byte를 추가 안하는게 당연한 동작이다. 우리는 str2가 abc이후에 끊겨서 h가 안나오는 상황을 원하는게 아니다.
근데 만약 strncpy(str2,str1,5)를 한다고 생각해보자. str2는 null byte를 포함하면 크기가 5이다. 즉 “같거나” 작기 때문에
null byte를 추가하지 않는다.
str1abcde\x00str2abcde
그럼 만약에 str2 뒤에 str3가 있으면? 컴퓨터는 str2와 str3를 한 덩어리로 착각하게 된다.
그럼 이 문제를 다시 보자.
strncpy(guess, input, sizeof(guess));
strncpy(flag, secret_flag, sizeof(flag));
if (!strcmp(guess, flag)) {
printf("Well done! You got it: %s\n", flag);
}
else {
printf("Wrong flag: %s\n", guess);
}
하나 더 조심할 것은
fgets(input, sizeof(input), stdin);
입력이 fgets라서 자동으로 개행문자가 뒤에 하나 들어간다. 즉 abcde를 입력해도 총 길이는 6이된다.
그럼 우리가 31글자를 입력하면? 31글자+개행문자로 32글자가 되어 이것이 guess로 nullbyte가 없이 넘어간다.
Guessa…..a(31글자)\n(개행문자)FLAG\x00
이렇게 Guess에 Flag가 연결이 되어버린다.
from pwn import *
pc = connect("challs.rumble.host",53921)
pc.sendline(b"a"*31)
pc.interactive()
pwntools를 쓰거나 직접 31글자를 입력해도 괜찮다.
ebeb@DESKTOP-44QI01G:/mnt/e/CTF/CyberSecurityRumble/baby_flag_checker$ nc challs.rumble.host 53921
Enter the flag: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Wrong flag: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
CSR{should_have_used_strlcpy_instead}
예상대로 Wrong flag의 출력에서 개행문자 다음에 플래그가 나왔다.