Pwnable은 Windows로 시작하는거 아니다-_- CH.01

By ccoma | May 25, 2018

Pwnable은 Windows로 시작하는거 아니다 -_- CH.01


(feat. 내가 이걸 왜 한다고 했을까…)



안녕하세요. 지난 4달 간 절 멘붕에 빠지게 했던 코드게이트가 드디어 끝나서 “신났었던” ccoma 입니다 ㅎㅎ
코드게이트 끝나서 이제 좀 여유롭겠지! 하고 출근한 제게 블로그 글을 쓰라는 청천벽력같은 업무가 주어졌습니다!
(“코드게이트도 끝나고 여유로우니 공부 좀 하고 그 김에 블로그 글로 환원좀 해볼까?^^” by.편집장)


흑 ㅠㅠ…


뭘 공부해서 글을 쓸까 고민하다가 Pwnable 문제를 하나 잡고 풀어볼까- 몇년 간 생각만 하던 일을 실천해보기로 했어요.

그래서 이 주제를 선택하게 되었습니다. 제가 왜그랬을까요


전 그동안 FTZLOB만 풀어봤었는데, 너무 오래전에 풀어보기도 했고 CTF에서 출제 된 Pwnable 문제는 한번도 풀어본 적이 없었어요.
그래서 약간 문제풀이가 야매임


그런 제가 Pwnable 중에서도 Windows Pwnable 문제를 선택한 이유는! 그냥… ㅎㅎ

Windows랑 Linux랑 Pwnable이 많이 다르겠어, (둘 다 못하지만) gdb 보다는 차라리 windbg가 사용하기가 좀 더 편하니까 Windows Pwnable을 풀어봐야겠다! 라고 생각하고 문제를 풀어봤는데…

네 많이 다르네요 하.. ㅠㅠ


어떤 부분에서 다른지는 다음에 기회가 된다면 더 자세히 알려드리기로 하고!(…)

일단은 이번 글에서는 2015년 Defcon 예선에 출제 된 thing2 문제를 풀이하려고 합니다.
시작 해 볼까요?




step.01 - 바이너리를 확인해봅시다(feat.AppJailLauncher)




thing2_e89e83e6cc343256f99fbfe6f434d788 라는 파일이 다운받아지실텐데, 파일명 뒤에 .zip을 붙여 주시면 압축을 풀 수 있습니다.
압축을 풀면 thing2 바이너리와 함께 AppJailLauncher.exe 라는 파일을 함께 받을 수 있습니다.


AppJailLauncher.exe는 이전에 콘치언니가 엄청난 삽질, 멘붕과 함께 블로그에서 언급한 적이 있었습니다!


윈도우충 콘치가 에러를 만났을 때(feat. SEH) – 번외 : 왕콘치 의식의 흐름


콘치 언니의 글에서 덧붙이자면!
이전 버전의 Visual Studio에서는 테스트를 안해봐서 모르겠지만, Visual Studio 2017 Community 버전에서 컴파일을 할 수 있었습니다.
그런데, 실행 후 클라이언트가 접속 시 수시로 접속이 끊기는 등 실행상의 오류가 많습니다.

때문에 저는 Rust 언어로 구현 된 업그레이드 버전의 AppJailLauncher를 사용합니다!



Rust 버전의 AppJailLauncher는 아래 링크에서 다운 받으실 수 있습니다.


GitHub - AppJailLauncher-RS


물론 컴파일하기 위해선 RustCargo가 설치되어 있어야 합니다!
Rust가 설치되어 있는 Windows에서 다음과 같이 입력하면 빌드 된 AppJailLauncher를 사용하실 수 있습니다.


git clone https://github.com/trailofbits/appjaillauncher-rs.git
cd appjaillauncher-rs
cargo build


그럼 아래와 같이 target 폴더에 appjaillauncher-rs.exe가 생성됩니다.



AppJailLauncher 사용법은 --help 명령어를 통해 확인하실 수 있습니다.

물론 thing2 문제에서는 친절하게 AppJailLauncher를 줬지만 문제 풀이에 필요한 AppJailLauncher를 성공적으로 깔았으니!
본론으로 돌아가서 thing2 문제가 출제됬을 당시의 정보는 다음과 같습니다.


  • 서버 : Windows 8.1 64bit
  • 보호기법 : ASLR + DEP


Windows 8.1이라니…

물론 iso 파일도 구할 수 있고 가상머신에 Windows 8.1 서버를 구축하면 되겠지만,
요즘 Windows 8.1을 사용하는 사람이 별로 없잖아요…?
그래서 전 Windows 10 64bit 환경에서 문제를 풀어볼거에요.
이게 제 많고 많은 실수 중에 2번째 실수임. 첫번째는 Windows Pwnable을 선택한거 ㅇㅇ




먼저 보호기법mitigation부터 살펴볼게요.

문제에서는 ASLRDEP라는 보호기법이 있다고 설명되어 있습니다.

ASLRDEP를 모르시는 분을 위해 간단히 설명을 드리자면, 다음과 같아요!


  • ASLR(Address Space Load Randomization)
    • 메모리 상의 공격을 어렵게 하기 위해 스택이나 힙, 라이브러리 등의 주소를 랜덤으로 프로세스 주소 공간에 배치함으로써 실행할 때마다 데이터의 주소가 바뀌게 하는 보호기법

  • DEP(Data Execution Prevention)
    • 데이터 영역에서 코드가 실행되는 것을 막는 보호기법


보호기법이 실제로 적용 되었는지 확인하기 위해서 저는 Process Explorer를 사용했습니다.


Process Explorer 다운로드


압축을 해제한 후 procexp64.exe를 실행시키면, 프로세스 목록에서 thing2_75d53bcedd5751724361ba26e9acfd60.exe 를 찾으실 수 있습니다.

해당 프로세스를 더블클릭해서 확인해보면 아래와 같습니다.



하단에 박스 친 곳을 확인해 보시면 이 바이너리에 적용 된 보호기법을 확인할 수 있습니다.
DEPASLR이 적용되어 있는데 다행히 Control Flow Guard는 적용되어 있지 않네요.


  • CFG(Control Flow Guard)
    • 메모리 손상 취약점을 제거하기 위해 만들어진 보안 기능.
    • 응용 프로그램이 코드를 실행할 수 있는 영역을 엄격하게 제한함으로써, Buffer OverFlow와 같은 취약점을 통해 악의적인 코드를 실행하는 것을 막는 기법.
    • 컴파일 시 컴파일러가 보호를 위한 코드를 삽입하여 만약 프로그램 실행 중 원래 의도했던 실행 흐름과 다른 코드가 실행 될 경우, 이를 탐지하고 프로그램을 강제 종료함.
    • 참고자료 : MSDN CFG 설명


적용되어 있는 보호기법까지 확인했으니! 이제 본격적으로 문제를 풀어볼게요.




step.02 - 바이너리를 분석 해 봅시다!


먼저 바이너리를 실행 해 봤습니다.
근데 뭥미? 암것도 안떠요.
입력을 마아아아아앙아아아악 입력 해 봤는데, 그래도 아무것도 출력을 안해줘요.



아무래도 뭔가 출력을 하게 해 주는 command가 있는 것 같네요.


IDA로 열어 도대체 무슨 값을 입력해야 하는건지 확인을 해볼게요.

IDA 파일을 열고 먼저 String을 확인 해 보았습니다.



제가 박스 친 부분을 보면 다음과 같은 문자열을 확인할 수 있습니다.


element
FINAL
ERROR THERE WAS A BAD KEY
Decompressed is %s\nDictSize:%d\n


즉, 출력할 무언가가 있네요!
이 문자열이 어디서 쓰이는지 확인을 해보겠습니다.



Decompressed is %s\nDictSize:%d\n 문자열을 찾아보니 sub_140025C0 함수에서 호출을 했습니다.

sub_140025C0를 확인해보니 main 함수에서 호출하는 함수임을 확인할 수 있습니다.



그런데, sub_140025C0 함수를 호출하고 반환된 값을 v24에 저장하는데, 바로 아래에서 printf 함수를 통해 그냥 출력을 하고 있습니다.

음… 어떠한 함수를 통과한 결과를 아무런 조건 없이 그대로 출력하다니.. 뭔가 취약점이 있을 것 처럼 생겼네요!

아직 어떤 취약점이 있는지는 확실히 모르겠으니, 일단은 main 함수를 차례대로 출력을 해 보겠습니다.



main 함수를 살펴보니, User로 부터 입력 값을 받아 v31에 저장하고 이를 비교하는 부분이 있네요.
사용자가 입력한 값이 1, 2, q 인지 검사를 합니다.
q는 아마 quit의 약자겠죠…?
역시나 q를 입력하니 프로그램이 종료됩니다.

그럼 thing2에서 사용할 수 있는 command는 12 두 가지입니다.


먼저 1을 입력하니 아래와 같은 결과를 얻을 수 있었습니다.



1만 입력 할 경우에는 공백만 출력되는데, 1abcde 와 같이 뒤에 특정 값을 넣어 입력하면, FINAL ?? element <마지막으로 입력한 문자> 형태로 문자열이 출력되고, 그 아래에서 숫자가 출력됩니다.

이 숫자들은 입력한 문자를 아스키 코드 10진수 값으로 바꾼 값입니다.


다른 값을 입력 해 볼게요!



헐 프로그램이 중단되었네요.
왜 중단되었는지 확인을 해 봐야겠죠…?


많은 디버깅 툴이 있겠지만 저는 windbg를 사용 할 거에요.


windbg를 열어 thing2.exe 프로세스를 Attach를 시키고 g를 입력 해 실행시킵니다.
아까와 같이 1aaaaaaaaaaaaaaa...를 입력하면 아래와 같이 windbg에서 오류가 난 부분을 보여줍니다.



박스 표시 한 부분이 보이시나요…?


mov rax, qword ptr [rdx]
mov rsi, rcx
mov rcx, rdx
mov rdi, rdx
call qword ptr [rax+8]


제가 입력 한 aaaaardx 레지스터에 들어있는데 이를 rax 레지스터로 옮기고, 중간 과정을 거친 후에는 rax + 8call 합니다.
찾아보니 이는 Virtual Function Call의 전형적인 형태라고 하네요!


그런데 여기서 제가 rdx를 조작할 수 있고, 이 rdx는 곧 rax가 되기 때문에, 제가 원하는 주소를 호출할 수 있겠죠?


일단 1 command는 두고, 2도 입력할 수 있으니 이를 확인 해 볼게요.
2도 입력 해 보니 아래와 같은 결과를 얻을 수 있었습니다.



2는 command이고, 5는 입력 할 숫자의 갯수, 이후 입력된 숫자 5개는 아스키코드로 변환되어 Decompressed is 다음에 출력됩니다.
아까 함수 sub_140025C0의 반환값이 main 함수에서 printf 를 통해 그대로 출력한다고 했었는데!
sub_140025C0에서는 Decompressed is~~ 형태의 문자열을 만들어주고,
Decompressed is 다음의 문자열은 User가 입력 한 값에서 나오게 되네요!


그렇다면 Format String Bug를 유발할 수 있을 것 같지 않나요?! ㅎㅎ 포맷스트링이라니!!


정말 가능한지 가설을 검증하기 위해 직접 해보겠습니다.

저는 아래처럼 값을 입력했습니다.


2       // command 2
12      // 입력 할 숫자의 갯수
37      // 아스키코드 %
112     // 아스키코드 p
32      // 아스키코드 공백
37      // 아스키코드 %
112     // 아스키코드 p
32      // 아스키코드 공백
37      // 아스키코드 %
112     // 아스키코드 p
32      // 아스키코드 공백
37      // 아스키코드 %
112     // 아스키코드 p
32      // 아스키코드 공백


즉, main 함수 내의 printf에는 printf("Decompressed is %p %p %p %p\nDictSize:??) 형태가 되겠죠.



역시, 뭔가 메모리의 값처럼 보이는 숫자들이 출력되었습니다!

이제 leak도 되고, 어딜 덮어야 하는지 주소도 찾았으니 exploit만 하면 될 것 되겠네요! ㅎㅎ




본격적으로 exploit를 하기 전에 제목 한 번 더 보시고 ㅎㅎ 마음의 준비를 하는 시간이 필요 할 것 같아요 ㅎㅎ
그럼 마음의 준비를 하시는동안 저는 다음편에 exploit을 완성시켜 오겠습니다!

exploit… 할 수 있겠지…? 흑ㅠㅠ

comments powered by Disqus