최근 Javascript 의 기본 개념 학습과 버그바운티를 진행하며 깨달은 것은 어느정도 CTF 문제 풀이가 필요하다는 것이다. 완벽하게 순위권에 드는 사람은 아니더라도 어느정도 익숙하게 할 필요성을 깨달았다.
첫 문제를 스근하게 풀고나서 두 번째 문제를 진행해보고자 마음먹고 진행했는데... 그냥 보이는 것만 확인하고 풀면 되지만 본인 성격 상 그러지 못하여 어느정도 시간이 더 걸리게 되었다.
해당 문제의 이름은 'collision'.
단어의 뜻은 '충돌' 이라는 뜻으로 문제 내용을 미루어볼 때, 쿨-한 MD5 해시가 충돌이났고, 그것과 연관된 문제라고 추측해보았다.
그럼 문제에 접속해서 한번 풀어보자.
해당 문제에서 확인 가능한 파일은 총 3개로 col, col.c, flag 파일이다. 이전 문제와 마찬가지로 flag 파일에 대한 실행은 권한 문제로 인하여 해당 파일 내 값에 대한 확인이 불가하다.
flag 파일의 실행을 위한 바이너리 파일로 col 파일이 보이니, 이번엔 실행하지 않고 소스코드를 분석해보자.
#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] )){
system("/bin/cat flag");
return 0;
}
else
printf("wrong passcode.\n");
return 0;
}
크게 두 구역으로 나누어 분석해보면 전 역 구간 (hashcode, check_passcode) 과 메인 함수 (main() ) 구간으로 나눌 수 있다.
전 역 구간을 확인해보면 long 변수 형태의 hashcode 와 입력된 값을 5번 반복, 포인터 크기인 4바이트씩 입력 값을 끊어 덧셈을 실행하여 res 에 저장, 그 이후 반환해주는 check_password 함수가 보인다.
메인 함수 구간에선 문자열을 사용자에게 입력받고, 입력된 인자가 2개 미만인 경우 사용방법을 출력하고, 입력된 문자열의 길이가 20 이 되지 않는 경우 passcode의 길이는 20바이트라고 알려준다. 마지막으로 전 역 구간에서 선언된 hashcode 변수의 값과 입력된 문자열 값을 check_password 함수 대입 시 나온 반환 값이 동일한 경우 flag 값을 읽어준다.
flag 값을 읽어들이기 위해선 check_password 함수에 입력되는 값이 필요하다. 그러니까 col 프로그램 실행 시 if 조건에 들어맞아 flag 값을 읽어들일 수 있도록 하는 입력 인자 값이 이번 플래그 값을 찾는데 핵심이 되는 값이다.
전 역 구간 내 선언된 hashcode 값은 0x21DD09EC 이며, 해당 값은 main 함수 내 선언된 if 의 check_password 함수에서 계산된 값에 대한 결과 값이라고 볼 수 있다. if 문에에서 check_password 함수의 결과와 hashcode 값을 비교하여 flag 값을 출력시키기 때문.
check_password 함수를 확인해보면 해당 함수는 입력된 값을 포인터 바이트 수 (4바이트) 만큼 나눠, 나누어진 값 들을 비어있는 res 변수에 5번 덧셈연산을 수행하여 저장한다. 그 말은 결과 값인 hashcode 값을 5번 나누면 본래 입력되어야 할 사용자 값이 나온다고 볼 수 있다.
해당 값을 5로 나눈 값은 '0x6C5CEC8' 이된다.
그런데 여기에서 재밌는 점은 분명 같은 값으로 5개를 더한 값이나, 해당 값을 5로 나누면 나머지가 생긴다는 것이다.
필자 본인은 '어짜피 5개를 더해서 나온 결과 값이니, 나머지가 없을거고 그렇다면 5로 나눈 값인 '6C5CEC8' 이 값이겠구나 생각하였고 리틀 엔디언을 적용하여 0xC8 0xCE 0xC5 0x06 을 문자열로 치환해보았다.
해당 문자열 치환 이후, 치환 값을 이용하여 프로그램에 입력하면 당연히 flag 값이 나올 줄 알았다. 당연히 문제에서 문자열이라고 출제 되었으므로 사용 가능한 문자라고 생각하기도 하였고, 무엇보다도 나누었을 때 나머지가 있을 것이라 생각을 못했으니까. (하지만 낭낭하게 나머지가 있었다.)
여기서 본인이 간과한 사실이 있는데, 입력되어야 할 값은 치환된 '문자열' 값이 아닌 치환되지 않은 16진수 문자형식으로 작성된 값이다. 때문에 해당 값이 정답 문자열 자체가 아니더라도 4바이트씩 5개로 끊어 계산하는 수식이 'hashcode' 값처럼 16진수 형태의 문자열 값이어야 하기 때문에 위 증적에서 입력된 문자열은 20바이트보다 작기 때문에 정답이 아니다.
- 해당 부분을 간과하여 며칠동안 왜 안되지 라는 생각을 하고 있었다. 입력되는게 숫자 값이라고 생각한 것도 문제였다.
되돌아가서 해당 플래그 값을 얻기 위해 입력되어야 하는 값은 문자열이며, 20바이트고 포인터 길이인 4바이트씩 끊어서 5번 나눠 한 변수에 더하여 저장하고 그 값이 hashcode 와 동일해야한다 가 조건이다.
그렇다면 본래 값은
C8CEC506 + C8CEC506 + C8CEC506 + C8CEC506 +(C8CEC506 + 4) → C8CEC506 + C8CEC506 + C8CEC506 + C8CEC506 + CCCEC506 → ( C8CEC506 )*4 + CCCEC506 |
가 되며, 이를 정규식으로 치환하면
(0xC8 0xCE 0xC5 0x06)*4 + (0xCC 0xCE 0xC5 0x06)
이 된다. 이를 해당 문제에 입력하면 /bin/cat flag 명령이 실행되어 flag 값을 얻을 수 있다.
문제 풀이 결과 ▼
flag : daddy! I just managed to create a hash collision :)
'Training > Pwnable.kr' 카테고리의 다른 글
File Description (0) | 2024.06.13 |
---|