개요
일반적으로 x86 과 x64용 프로세스는 소스코드단계부터 차이가 납니다. 사용하는 API나 레지스터도 그렇구요. 그렇다면 익스플로잇 등 쉘코드가 바로 인젝션되어 돌아가는 특수한 상황에서 CPU 아키텍처 별 호환성을 갖게 코딩할 수 있을까요?
DOUBLEPULSAR 공격툴 분석 중 이와 관련해 재밌는 부분을 찾았습니다. 출처는 여기입니다.
들어가기 앞서
아래 스크린샷은 부분은 최근 ShadowBrokers 의 문건 공개로 핫 이슈인 Vault 7 덤프중 하나인 DoublePulsar 공격툴에서 발생한 쉘코드 페이로드의 도입부입니다. Win Server 2008 R2 64bit 까지 노출된 이 SMB 취약점은 원격에서 쉘코드를 실행할 수 있는 취약점입니다. DoublePulsar는 이를 커널 백도어로 활용했습니다.
원래 분석 목표는 커널영역에 있는 DoublePulsar 백도어의 악성 행위로 유저영역 dll 인젝션을 시도하는데, 이 원리를 알아보고자 한 일이었지만, 분석도중 흥미로운 것이 있어 이렇게 글로 남깁니다.
분석
RCE 취약점을 활용하는 이것은, 동일 쉘코드 페이로드가 x86/x64 시스템 모두에서 동작 가능해야 할 것입니다. 원격 스캐닝으로 해당 시스템의 아키텍쳐를 알아와 case by case로 페이로드를 선택할 수도 있겠지만, 어쨋거나 손이 한번 더 가야 하는 일이니까요.
위 두 쉘코드는 31 C0 40 90 0F 84 B5 05 00 00 E8 00 00 00 00 58
의 동일 쉘코드를 x86 과 x64 instruction 로 디스어셈블 한 모습입니다. Online Disassebler 에 복붙해보시면 직접 편집해보며 쉽게 확인할 수 있습니다. CPU 아키텍처를 i386:x86-64:intel와 i386:x64-32:intel 로 교차 비교해 보세요.
노랗게 하이라이트 된 JE
명령어 위를 보시면, x86 에선 NOP
(0x90) x64에선 REX XCHG EAX, EAX
(0x40 90) 인 것을 확인할 수 있습니다. 어셈블리어에 익숙하신 분들이라면, 벌써 아~ 하고 계시겠죠?
x86
XOR EAX, EAX
는 EAX 레지스터를 0으로 만들면서 ZF(Zero Flag)를 1로 셋 합니다. (ZF 는 연산의 결과가 0이면, TRUE로 자동 세팅 됩니다) 이어 나오는 INC EAX
는 EAX 의 값을 1 증가합니다. 연산의 결과가 0이 아니기에 ZF 는 FALSE 가 되겠죠? 이로 인해 다음에 나오는 JE
(Jump Equal/zero) 에서 해당 주소로 분기하지 않고 바로 CALL
로 넘어갑니다. loc_0000000f 에 x86용 쉘코드를 작성하면 되겠군요.
x64
x64의 경우 x86에선 두 개의 다른 instruction 으로 디스어셈블 된 두 쉘코드 0x40과 0x90 이 하나의 instruction 0x4090, REX XCHG EAX, EAX
로 해석됩니다. XOR EAX, EAX
에서 세팅된 ZF 는 두 번째 연산 이후에도 TRUE 일 것이고, loc_000005bf 로 점프하게 됩니다. 물론 점프한 곳에 x64 용 쉘코드를 준비해야겠지요. 점프할 땐 EIP든 RIP든 동일하게 동작하도록 상대주소 점프를 써야함에 유의합니다.
x86 Instruction 검색은 x86 Instruction Set Reference에서 쉽게 할 수 있습니다. x64 Instruction 은… 그때그때 찾아서 하긴 하는데 좋은 곳 있으면 댓글로 소개 부탁드립니다.
POC
앞서 설명한 내용에 대한 POC입니다. POC 에선 JE 위치에 RET, CALL 위치엔 INT 3 이 있도록 수정해, 예외처리를 통해 아키텍처를 식별토록 했습니다. 첨부한 POC 를 x64용 x86용으로 소스수정 없이 컴파일해보면 의도대로 동작하는걸 확인할 수 있습니다.
#include <windows.h>
#include <stdio.h>
typedef VOID(WINAPI * Foo)();
int main()
{
Foo pFoo = NULL;
UCHAR shellcode[] = { 0x31, 0xC0, 0x40, 0x90, 0x0F, 0x84, 0x06, 0x00, 0x00, 0x00, 0xE8, 0x00, 0x00, 0x00, 0x00, 0xCC, 0xC3 };
pFoo =(Foo)VirtualAlloc(NULL, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(pFoo, shellcode, sizeof(shellcode));
__try
{
pFoo();
}
__except (GetExceptionCode() == EXCEPTION_BREAKPOINT)
{
printf("x86 Architecture Detected\n");
goto END;
}
printf("x64 Architecture Detected\n");
END:
system("pause");
}
결론
커널레벨 백도어라니… 흥미로운 내용이 아닐 수 없습니다. 알려진지 얼마 안된 따끈따끈한 아이기도 하구요. 좋은 자료나 공유하고 싶은 정보가 있으시다면 언제든 댓글이나 이메일 jw.reversenote@gmail.com 로 연락 부탁드립니다.