By ccoma | May 25, 2018
Pwnable은 Windows로 시작하는거 아니다 -_- CH.01
(feat. 내가 이걸 왜 한다고 했을까…)
안녕하세요. 지난 4달 간 절 멘붕에 빠지게 했던 코드게이트가 드디어 끝나서 “신났었던” ccoma
입니다 ㅎㅎ
코드게이트 끝나서 이제 좀 여유롭겠지! 하고 출근한 제게 블로그 글을 쓰라는 청천벽력같은 업무가 주어졌습니다!
(“코드게이트도 끝나고 여유로우니 공부 좀 하고 그 김에 블로그 글로 환원좀 해볼까?^^” by.편집장)
흑 ㅠㅠ…
뭘 공부해서 글을 쓸까 고민하다가 Pwnable 문제
를 하나 잡고 풀어볼까- 몇년 간 생각만 하던 일을 실천해보기로 했어요.
그래서 이 주제를 선택하게 되었습니다. 제가 왜그랬을까요
전 그동안 FTZ
나 LOB
만 풀어봤었는데, 너무 오래전에 풀어보기도 했고 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
는 이전에 콘치언니가 엄청난 삽질, 멘붕과 함께 블로그에서 언급한 적이 있었습니다!
콘치 언니의 글에서 덧붙이자면!
이전 버전의 Visual Studio에서는 테스트를 안해봐서 모르겠지만, Visual Studio 2017 Community
버전에서 컴파일을 할 수 있었습니다.
그런데, 실행 후 클라이언트가 접속 시 수시로 접속이 끊기는 등 실행상의 오류가 많습니다.
때문에 저는 Rust
언어로 구현 된 업그레이드 버전의 AppJailLauncher
를 사용합니다!
Rust
버전의 AppJailLauncher
는 아래 링크에서 다운 받으실 수 있습니다.
물론 컴파일하기 위해선 Rust
와 Cargo
가 설치되어 있어야 합니다!
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
부터 살펴볼게요.
문제에서는 ASLR
과 DEP
라는 보호기법이 있다고 설명되어 있습니다.
ASLR
과 DEP
를 모르시는 분을 위해 간단히 설명을 드리자면, 다음과 같아요!
- ASLR(Address Space Load Randomization)
- 메모리 상의 공격을 어렵게 하기 위해 스택이나 힙, 라이브러리 등의 주소를 랜덤으로 프로세스 주소 공간에 배치함으로써 실행할 때마다 데이터의 주소가 바뀌게 하는 보호기법
- DEP(Data Execution Prevention)
- 데이터 영역에서 코드가 실행되는 것을 막는 보호기법
보호기법이 실제로 적용 되었는지 확인하기 위해서 저는 Process Explorer
를 사용했습니다.
압축을 해제한 후 procexp64.exe
를 실행시키면, 프로세스 목록에서 thing2_75d53bcedd5751724361ba26e9acfd60.exe
를 찾으실 수 있습니다.
해당 프로세스를 더블클릭해서 확인해보면 아래와 같습니다.
하단에 박스 친 곳을 확인해 보시면 이 바이너리에 적용 된 보호기법을 확인할 수 있습니다.
DEP
와 ASLR
이 적용되어 있는데 다행히 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는 1
과 2
두 가지입니다.
먼저 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]
제가 입력 한 aaaaa
가 rdx 레지스터
에 들어있는데 이를 rax 레지스터
로 옮기고, 중간 과정을 거친 후에는 rax + 8
을 call
합니다.
찾아보니 이는 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… 할 수 있겠지…? 흑ㅠㅠ