━━━━ ◇ ━━━━
해킹/SuNiNaTaS 모의해킹

20. 써니나타스 (Suninatas) 20번 문제풀이 Write-up

728x90
반응형

써니나타스 모의해킹 사이트

https://suninatas.com/challenges

 

써니나타스

웹해킹, 포렌식, 리버싱, 암호학, 해킹 워게임 제공.

www.suninatas.com


리버싱 문제고 힌트가 두 개 주어졌다. 

 

압축파일을 다운로드하여 해제하니 reverseme라는 파일이 생성 됐다. 

 

일단 Hex 에디터로 열어봤는데 유닉스, 리눅스에서 구동되는 ELF 파일이었다. 

 

 

https://hex-rays.com/ida-free/#download

 

IDA Free

IDA Free The free binary code analysis tool to kickstart your reverse engineering experience.

hex-rays.com

IDA를 통해서 파일을 뜯어보았다. 

correct함수가 호출될 수 있는 input 중 가장 짧은 것을 구하라는 것이 힌트 1번이었기 때문에 correct함수 부분을 살펴봤다.

코드를 살펴보니 eax와 0DEADBEEFh를 비교해서 같다면 축하메시지가 출력되는 구조 같았다. 

아래 mov 파트를 보면 dword ptr의 의미는 4바이트씩 접근한다는 뜻이다. 

다른 코드들도 살펴보다가 더 이상은 무리인듯해서 다른 툴을 사용하기로 했다. 

 

 

https://hackchang.tistory.com/88

 

[Ghidra] 기드라 다운로드 설치 및 사용법 v9.1.2

기드라 (Ghidra)는 미국 국가 안보국 (NSA)에서 만든 역어셈블리어 프레임워크로 오픈소스다. 기드라를 사용해서 프로그램을 디버깅과 분석을 할 수 있고, 무료라는 점에서 IDA Pro와 대적할 수 있다

hackchang.tistory.com

기드라는 미국 NSA에서 만든 역어셈블리어 framwork로 오픈소스이다. 

기드라를 통해서 디버깅과 분석을 할 수 있으며 무료이다. 그래서 IDA pro와 견줄만한  툴이라고 생각한다. 참고로 기드라는 자바로 구축되어서 자바 런타임으로 구동된다. 기드라 다운로드 설치 및 사용법은 위 링크를 참고해 보길 바란다. 

 

 

기드라를 다운로드 받고 압축해제를 하면 위와 같은 파일들을 볼 수 있는데 그중에 bat파일을 실행시킨다. 

 

 

간혹 프로그램 실행시키면 방화벽에서 자동으로 차단을 하는 경우가 있다. 그래서 차단해제를 먼저 진행해야 한다. 

 

 

1. ghidraRun.bat 우클릭

2. 일반탭 - 보안 - 차단해체 체크 - 적용 및 확인

 

https://www.oracle.com/java/technologies/downloads/#jdk20-windows

 

Download the Latest Java LTS Free

Subscribe to Java SE and get the most comprehensive Java support available, with 24/7 global access to the experts.

www.oracle.com

그리고 현재기준 기드라를 실행시키기 위해서는 JDK 17+(17버전 이상)이 필요하다. 그래서 혹시 실행이 안된다면 위의 링크에서 자신의 운영체제에 맞는 다운로드 파일을 다운로드하고 설치하면 된다. 

 

기드라를 실행시킨 뒤에 새로운 프로젝트를 만들고, reverseme 파일을 드래그앤 드롭을 하면 위와 같은 창을 볼 수 있다. 

좌측 첫번째부터 Program Trees, Symbol Tree(함수 이름 리스트), listing(어셈블리어 조회), Decomplie(함수의 소스코드 조회) 탭들로 나누어져 있다. 

 

 

main함수 코드

undefined4 main(int param_1,undefined4 *param_2,int param_3)

{
  uint uVar1;
  int iVar2;
  uint uVar3;
  undefined4 uStack_40;
  undefined auStack_3a [30];
  uint uStack_1c;
  uint uStack_18;
  int iStack_14;
  
  memset(auStack_3a,0,0x1e);  //30바이트
  //param1이 2 보다 작고 param_2가 ./suninatas여야 한다.
  if ((param_1 < 2) && (iVar2 = strcmp(*param_2,&UNK_080d9dbd), iVar2 == 0)) {
    for (iStack_14 = 0; *(int *)(param_3 + iStack_14 * 4) != 0; iStack_14 = iStack_14 + 1) {
      for (uStack_18 = 0; uVar1 = uStack_18,
          uVar3 = strlen(*(undefined4 *)(param_3 + iStack_14 * 4)), uVar1 < uVar3;
          uStack_18 = uStack_18 + 1) {
        *(undefined *)(uStack_18 + *(int *)(param_3 + iStack_14 * 4)) = 0;
      }
      //dword ptr이기 때문에 모두 4바이트 단위이다 -> *4
    }
    printf(&UNK_080d9dc9);
    __isoc99_scanf(&UNK_080d9dd9,auStack_3a);
    memset(input,0,0xc); //12바이트
    uStack_40 = 0;
    uStack_1c = Base64Decode(auStack_3a,&uStack_40);  //입력 시 Base64 encode로
    if (uStack_1c < 0xd) {  //입력값이 13보다 작아야 한다. 
      memcpy(input,uStack_40,uStack_1c);
      iVar2 = auth(uStack_1c);  //auth 함수에서 인증을 받으면 iVar2가 1이 되고 correct 함수 호출
      if (iVar2 == 1) {
        correct();
      }
    }
  }
  return 0;
}

 

 

 

Auth함수 코드

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

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

 

 

Correct함수 코드

void correct(void)

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

 

Auth함수의 인자는 디코드 된 입력 값이고 이 값을 md5 해시 함수를 통해서 hashing을 진행한다.

그리고 이 값이 f87cd601aa7fedca99018a8be88eda34라면 1을 리턴하고 다르면 0을 리턴한다. 

만약 1을 리턴한 경우, correct 함수를 호출한다. 그리고 input의 첫 4바이트의 주소가 0xDEADBEEF면 풀리는 프로그램. 

 

그리고 Auth함수에서 주목할 점은 버퍼오버플로우 공격이 가능한 memcpy 함수를 사용했다는 것이다. 

 


KALI LINUX

아까 main함수에서 첫 번째 조건문을 잘 살펴보면 strcmp를 통해서 ./suninatas와 비교를 했다.

main함수에서 param_2는 파일이름이기 때문에 reverseme 파일 이름을 suninatas로 변경해 줬다. 

그리고 4바이트를 동일하게 입력해도 계속 다른 해쉬값이 나오고 12바이트를 입력하면 Segmentation fault가 발생했다. 

 

일단 처음 4바이트는 0xDEADBEEF이기 때문에 EF BE AD DE. 즉 Little Edian이다. 

 

과정에 따라 작성을 하다보니 정리가 안된 것 같아서 정리를 하고 넘어가야겠다. 

 

정리

1. argv 인자로 파일명을 검색한다. 파일 명은 suninatas이어야 실행된다.

2. Authenticate에 입력된 값을 받아서 auth 함수에서 md5 함수로 암호화되어 출력된다. 

int MD5_Final(uchar *md,MD5_CTX *c)

{
  size_t __n;
  uint uVar1;
  uint uVar2;
  uint *puVar3;
  bool bVar4;
  byte bVar5;
  
  bVar5 = 0;
  uVar1 = c->num;
  puVar3 = c->data;
  uVar2 = uVar1 + 1;
  *(undefined *)((int)c->data + uVar1) = 0x80;
  if (uVar2 < 0x39) {
    __n = 0x37 - uVar1;
  }
  else {
    memset((void *)(uVar2 + (int)puVar3),0,0x3f - uVar1);
    md5_block_asm_data_order(c,puVar3,1);
    __n = 0x38;
    uVar2 = 0;
  }
  memset((void *)(uVar2 + (int)puVar3),0,__n);
  c->data[0xe] = c->Nl;
  c->data[0xf] = c->Nh;
  md5_block_asm_data_order(c,puVar3,1);
  bVar4 = ((uint)puVar3 & 1) != 0;
  uVar2 = 0x40;
  c->num = 0;
  if (bVar4) {
    *(undefined *)c->data = 0;
    puVar3 = (uint *)((int)c->data + 1);
    uVar2 = 0x3f;
  }
  if (((uint)puVar3 & 2) != 0) {
    *(undefined2 *)puVar3 = 0;
    uVar2 = uVar2 - 2;
    puVar3 = (uint *)((int)puVar3 + 2);
  }
  for (uVar1 = uVar2 >> 2; uVar1 != 0; uVar1 = uVar1 - 1) {
    *puVar3 = 0;
    puVar3 = puVar3 + (uint)bVar5 * -2 + 1;
  }
  if ((uVar2 & 2) != 0) {
    *(undefined2 *)puVar3 = 0;
    puVar3 = (uint *)((int)puVar3 + 2);
  }
  if (bVar4) {
    *(undefined *)puVar3 = 0;
  }
  *(uint *)md = c->A;
  *(uint *)(md + 4) = c->B;
  *(uint *)(md + 8) = c->C;
  *(uint *)(md + 0xc) = c->D;
  return 1;
}

3. 위 MD5final 함수에서는 입력된 값이 랜덤 한 값으로 변하기 때문에 특정 출력에 대한 입력값을 찾아내는 것은 불가능하다. 즉 auth함수에서 확인했던 부분인데, 해쉬값과 동일해야 correct함수로 넘어가는 것이 무의미 해진 상황이라는 것이다. 그래서 값으로 접근하기보다는 메모리 주소를 덮어서 풀어야 한다. 

 

4. 입력받은 값은 base64decode 함수를 통해서 디코딩 과정을 거치는데, 이는 입력할 때 base64 인코딩이 완료된 상태로 넣어야 문제없이 진행될 수 있다. 

 

5. 버퍼오버플로우 취약점이 있는 부분은 auth함수의 memcpy함수다. 이 함수는 문자열 길이 제한 없이 복사를 하게 되는데 auth함수 8바이트 영역에 12바이트를 더해서 SFP(Stack Fram Pointer: 함수 프롤로그 부분에 EBP 레지스터 위치를 보존하기 위해 스택에 저장되는 값)가 덮인다. 

문제는 auth함수의 SFP값이 EBP로 사용된다는 것이다. 

 

6. 12바이트 중에 마지막 4바이트 값으로는 EBP가 변조되어 있다.

그래서 처음 4바이트 값은 0xDEADBEEF로, 가운데 4바이트는 correct함수 주소, 마지막 4바이트는 input주소 값을 넣어야 한다. 

 

 

먼저 IDA를 통해서 input을 검색한 다음 input의 주소를 알아냈다. 

 

같은 방법으로 correct의 주소도 알아냈다. 

그러면 처음 비교하게되는 input값인 0xDEADBEFF를 참고해서 

base 64로 인코딩을 해주면 된다.

(\xEF\xBE\xAD\xDE + correct 주소 값) + \x5F\x92\x04\x08 + (input 주소값 + \xEC\xC9\x11\x08)

 

 

pwntools를 이용해서 코드를 작성할텐데 먼저 KALI에서 Pwntools를 설치해야 한다. (ubuntu)

pwntools 설치 명령어

sudo apt update

sudo apt install python3 python3-pip python3-dev git libssl-dev libffi-dev build-essential

sudo python3 -m pip install --upgrade pip

sudo python3 -m pip install --upgrade pwntools

 

 

pwntools를 활용한 코드작성

from pwn import *
import base64

correct_input=p32(0xdeadbeef)
correct_addr=p32(0x0804925f)
input_addr=p32(0x0811c9ec)

payload=correct_input+correct_addr+input_addr
payload=base64.b64encode(payload)
p=process('./suninatas')
p.sendline(payload)
print(payload)
print(p.recvall())

 

결과를 살펴보면 입력값으로 사용된 페이로드가 보인다. 

즉 인증키인 것이다. 

 

인증키를 입력하고 축하메시지와 함께 20번을 클리어 했다. 

20번은 시간도 오래 걸렸고 문제를 풀면서 새로운 도구들을 사용하고 이해하는 데에 시간이 많이 소요됐다. 

갑자기 난도가 확 올라간 느낌이었다. 한번 더 나의 부족함을 느끼며 더 발전해야겠다는 생각이 든다. 

 

문제 20번

 

성공

728x90
반응형
COMMENT