인기 검색어

Research

ComRaceConditionSeeker

  • -

서론

COM(Component Object Model)은 마이크로소프트에서 개발한 소프트웨어 아키텍처로, 다양한 소프트웨어 컴포넌트들이 서로 통신하고 데이터를 교환할 수 있게 하는 기술입니다. 윈도우 운영 체제에서 널리 사용되며, 개발자들이 모듈식 컴포넌트를 통해 애플리케이션을 빌드할 수 있도록 지원합니다. COM은 언어 독립적인 방식으로 설계되어 있어, 다양한 프로그래밍 언어로 작성된 컴포넌트들이 서로 상호 작용할 수 있습니다.

COM의 주요 개념은 객체지향 프로그래밍의 원칙에 기반을 두고 있으며, 인터페이스와 구현의 분리를 통해 소프트웨어 컴포넌트의 재사용성과 유지보수성을 높입니다. COM 객체는 정의된 인터페이스를 통해서만 접근할 수 있으며, 이 인터페이스들은 고유한 식별자인 GUID(Global Unique Identifier)로 식별됩니다.

COM 기술은 윈도우 프로그래밍의 여러 영역에서 활용됩니다. 예를 들어, OLE(Object Linking and Embedding)와 ActiveX 컨트롤은 COM을 기반으로 하며, 이를 통해 문서의 임베딩이나 인터넷 익스플로러와 같은 애플리케이션에서의 동적 컨텐츠 제공 등의 기능을 구현합니다.

COM은 또한 분산 컴포넌트 객체 모델(DCOM)의 기반이 되어, 네트워크를 통한 컴포넌트 간 통신을 가능하게 합니다. 이를 통해 개발자들은 네트워크상의 다양한 컴퓨터에서 실행되는 애플리케이션들 사이에서 데이터와 서비스의 공유를 실현할 수 있습니다.

ComRaceConditionSeeker는 이런 COM에서 Race-Condition이 가능한 후보 함수들을 빠르게 선별할 수 있도록 도와줍니다. 대부분의 Race-Condition의 Patch Diff를 살펴보면 함수 시작과 끝에 원자 Lock(예를 들면 SRWLock, CrticalSection등)으로 해당 함수가 동기적으로 실행될 수 있게 합니다.

그렇기에, ComRaceConditionSeeker(이하 CRSeeker)는 COM의 실질적 구현이 이루어지는 DLL에서 함수 중 Lock이 없는 함수 후보군만을 선별하여 해당 함수에서 참조되는 공유 자원(COM은 객체 지향 언어임으로 동일한 Structure을 가진 this 객체)를 식별하여 사용자에게 알려줄 수 있도록 해, 더 빠른 Race-Condition 가능성을 가진 함수를 찾을 수 있도록 합니다.

본론

COM은 windows 10 x64 1909 기준만 해도 “BUILT-IN”에서만 8300개 이상의 Object를 노출하고 있고 이중 27000개의 interface를 노출하고 있습니다. 이는 정말 많은 공격 벡터를 함유하고 있다고 해석할 수 있습니다.

MTA의 경우 Multi Thread Apartment로 여러 쓰레드에서 Target Method에 접근할 수 있음을 의미합니다. Race-Condition은 다수의 쓰레드에서 데이터의 조작으로 이루어지는 것이기 때문에 타겟으로 하는 COM에서 당연히 MTA를 지원해야합니다.

STA의 경우 Single Thread Apartment로 말 그대로 한 쓰레드만 Method에 접근할 수 있습니다. Marshaling을 통해 타 쓰레드에서 접근 가능하지만, 현재 연구에서의 범위에 어긋나기에 이 부분은 생략하고 넘어 가겠습니다.

Race-Condition Case

MTA에서 발생 가능한 Race-Condition에 대해 상세히 설명한 논문인 COMRace https://www.usenix.org/conference/usenixsecurity22/presentation/gu-fangming를 참고하여 COM에서의 Race-Condition이 어떻게 발생하는지 알아보겠습니다.

CVE-2020-1394

해당 CVE에 대해 논문의 분석을 보면, 2번라인과 10번라인을 보면 똑같이 (void**)(this + 104)을 사용하고 있고, 해당 필드를 사용하는 부분에서 Race-Condition이 일어난다고 합니다.

proc3에서는 멤버필드 (다른 COM 객체를 가리키고 있는)를 읽어서 AddRef 함수를 통해 reference count를 증가시키게 됩니다. proc6에서는 17번라인에서 해당 필드 값을 읽고나서 18번에서 Release를 호출해서 ref Count가 null인 경우 Release를 하게됩니다. 그 후 20번을 보면 a2(객체)로 v2의 값을 바꾸게 됩니다. 여기서 눈여겨 봐야할 점은 Proc6에서 v4를 Release하게 되면 Proc3에서 ptr을 Release(free)하면 UAF가 발생하게 됩니다.

How…?

위 분석에서 ‘그래서 위 CVE로 알아낼 수 있는게 무엇인가?’라는 의문을 품고, COM 객체의 특징과 COM Race-Condition Case를 분석하면서 어떤 특징이 있을까 생각해봤습니다. 그리고 도출해낸 결과는 아래와 같습니다.

  1. COM의 Interface로 호출되는 기능들은 dll에서 첫번째 인자로 this를 받게된다(자신이 정의 된 class의 주소) 아래는 ComRCSeeker로 찾을 수 있었던 함수의 하나입니다.

  1. COM의 Race-Condition은 타 프로그램이나 DLL에서 사용되는 전역변수로 일어나는 Race-Condition이 아닌, this 객체의 공유 자원의 경쟁 조건을 두고 이루어진다</aside>
  2. 💡 똑같이 (void)(this + 104)을 사용하고 있고**, 해당 필드를 사용하는 부분에서 Race-Condition이 일어난다고 합니다.
  3. Race-Condition이 일어나기 위해 Lock 관련 함수들이 없어야한다.

COM에서만 볼 수 있는 위와 같은 특징들을 잘 생각해보면, COM의 Race-Condition이 발생 가능한 패턴을 쉽게 정리할 수 있습니다.

  1. 함수에서 this를 인자로 받아 내부에서 처리하는지
  2. LOCK함수가 없는지
  3. this에서 어떤 필드에 접근하는지

위의 정형화 된 패턴을 사용하여, idapython을 사용하여 모든 함수 중 여러 쓰레드에서 접근했을 때 비동기를 지원하는 함수에서, 동일한 this 구조체를 사용하며 동일한 this 구조체에서 어떤 필드에 접근하는지 알아낼 수 있다면, 하나하나 인터페이스를 살펴보고 method를 살펴볼 시간을 대폭 단축할 수 있을 것입니다.

개발 과정

이미 ComRace의 논문을 보니, Unsafe Method를 탐지하는 기본적인 Skeleton이 있었습니다.

💡 이는 모든 멤버 필드가 이 포인터를 통해 상수 오프셋으로 액세스되므로 이 포인터([rcx]에 저장된 규칙 동맹)의 사용을 추적하여 실현됩니다. - ComRace 논문 중 발췌

기본 아이디어는 Lock이 없는지 확인하고, RCX(첫번째 인자 this)에 어느 오프셋이 더해지는지를 통해 this 구조체에 얼만큼의 오프셋이 더해지는지를 통해 unsafe한지 아닌지를 찾는다고 합니다.

처음에는 COMRace의 위 논문을 보고, ‘이미 완벽하게 만들어졌구나’라는 생각을 하고 다른 것을 개발하려고 했으나, 생각보다 lea A, [rcx+offset]으로 this를 복사하는 경우는 적었습니다.

위는 this+15를 Set 함수에 넣어 this+15를 Free시키고 v9에 복사하는 COM method 중 일부입니다. 이는 디스어셈블러로 보면

lea시 rcx+offset 형태로 들어가지 않고, rcx를 rbx로 옮기고 rbx + 78을 한 결과를 인자로 넘겨줍니다. COMRace논문에서는 Symbolic Execution을 통해 Instructor의 Offset에 따라 Read / Write로 구분하여 동일한 데이터가 공유되는지 체크한다고 했는데, 실제로 COMRace에서 찾은 CVE 중 한개를 살펴보면 this + 40이 공유됨으로서 SKIP 과 NEXT 함수를 호출하며 Race-Condition이 일어납니다.

그리고 이를 disassembler로 살펴보면 실제로 mov,lea A, [rcx+offset]의 형태로 이루어진 것을 볼 수 있습니다.

이것을 보고 나서 Symbolic Execution이 아니라 다른 레지스터에 복사된 값도 추적하여 this로 변환할 수 있는 idapython을 사용하여 this 객체의 rw를 추적해보면 어떨까라는 생각을 하게 되었습니다.

실제로 COMRace에서 사용한 Symbolic Execution을 통한 rcx 오프셋 추적으로 찾은 unsafe method는 COM에서 약 170개로, COMRace에서 분석을 시도한 10420개의 COM 인터페이스 중 170개에서밖에 찾지 못했다는 것은 생각보다 이상하다고 생각했습니다.

그렇기에, rcx 오프셋으로 추적하는 방식이 아닌 IDApython을 사용하여 pseudocode로 찾아보면 더 많은 this 객체 사용을 찾을 수 있다고 판단하여 개발을 진행했습니다.

함수의 첫번째 인자가 this인지 확인

아래의 코드는 ida에서 resolve한 모든 함수를 가져와 pseudocode(Cfunc)로 변환하여 함수의 첫번째 인자의 이름을 갖고와, 이름이 this인지를 확인하는 함수입니다. 처음에는 모든 인자를 가져와 iterate했었는데, 지금까지 발견한 것 중 그 어느것도 첫번째 외에 this를 받는 함수가 없었기에 첫번째 인자만 체크하게 됩니다.

import idautils
import idc
import ida_name
import ida_hexrays
import ida_funcs
import re

func_resolve_dict = []
banlist = ['__imp__Mtx_unlock',' __imp__Mtx_lock','__imp_AcquireSRWLockExclusive', '__imp_ReleaseSRWLockShared', '__imp_InitializeSRWLock','__imp_TryAcquireSRWLockShared','__imp_ReleaseSRWLockExclusive','__imp_AcquireSRWLockExclusive','__imp_TryAcquireSRWLockExclusive','__imp_AcquireSRWLockShared','__imp_TryAcquireSRWLockShared','__imp_EnterCriticalSection','__imp_LeaveCriticalSection']
# 모듈 내의 모든 함수를 순회합니다.

def get_function_pseudocode(ea):
    # Hex-Rays Decompiler가 사용 가능한지 확인
    if not ida_hexrays.init_hexrays_plugin():
        print("Hex-Rays Decompiler is not available.")
        return None

    # 주어진 주소에서 함수를 찾음
    f = ida_funcs.get_func(ea)
    if not f:
        print("Function not found at the given address.")
        return None

    # 함수를 디컴파일
    cfunc = ida_hexrays.decompile(f.start_ea)
    if not cfunc:
        print("Failed to decompile function.")
        return None

    # 디컴파일된 함수의 pseudocode를 문자열로 변환
    pseudocode = str(cfunc)
    return pseudocode

def get_this_pseudo(code_snippet):
    pattern_corrected = r"this \\+ (0x[0-9a-fA-F]+|\\d+)"
    matches_corrected = re.findall(pattern_corrected, code_snippet)
    return matches_corrected

def get_this_xref(function_name):
    function_start = idc.get_name_ea_simple(function_name)
    function_end = idc.find_func_end(function_start)
    # 함수 내에서 'rcx' 레지스터를 사용하는 모든 명령어를 찾습니다.
    for head in idautils.Heads(function_start, function_end):
        a = idc.generate_disasm_line(head, 0)
        start_index = a.find("[rcx")
        end_index = a.find("]", start_index) + 1 # ']'의 위치를 찾음
        # '['와 ']' 사이의 문자열 추출
        substring = a[start_index:end_index]
        if(start_index != -1):
            print(substring)
    
def get_all_func_resolve():
    func_dict = []
    for func in idautils.Functions():
        # 함수의 시작 주소를 얻습니다.
        current_function_start = idc.get_func_attr(func, idc.FUNCATTR_START)
        # 함수의 이름을 얻습니다.
        current_function_name = ida_name.get_name(current_function_start)

        # 현재 함수의 모든 주소를 순회하며 함수 호출을 찾습니다.
        for instr in idautils.FuncItems(current_function_start):
            # 현재 주소에서 참조하는 모든 코드 참조를 순회합니다.
            for ref in idautils.CodeRefsFrom(instr, False):
                # 참조된 주소의 세그먼트 이름을 얻습니다.
                seg_name = idc.get_segm_name(ref)
                # 참조된 주소의 함수 이름을 얻습니다.
                ref_func_name = ida_name.get_name(ref)
                # 참조된 주소가 현재 분석 중인 함수의 범위 외부에 있고, `.idata` 세그먼트에 있으면 외부 함수로 간주합니다.
                if ref_func_name and seg_name == '.idata':
                    func_dict.append(['out',current_function_name,ref_func_name,instr])
                # 참조된 주소가 현재 분석 중인 함수의 범위 외부에 있지만, `.idata` 세그먼트에는 없는 경우, 내부 함수로 간주합니다.
                elif ref_func_name and idc.get_func_attr(ref, idc.FUNCATTR_START) != current_function_start:
                    func_dict.append(['in',current_function_name,ref_func_name,instr])
    return func_dict

def get_specific_func_resolve(funcDo):
    func_dict = []
    for func in funcDo:
        # 함수의 시작 주소를 얻습니다.
        current_function_start = func[1]
        # 함수의 이름을 얻습니다.
        current_function_name = ida_name.get_name(current_function_start)

        # 현재 함수의 모든 주소를 순회하며 함수 호출을 찾습니다.
        for instr in idautils.FuncItems(current_function_start):
            # 현재 주소에서 참조하는 모든 코드 참조를 순회합니다.
            for ref in idautils.CodeRefsFrom(instr, False):
                # 참조된 주소의 세그먼트 이름을 얻습니다.
                seg_name = idc.get_segm_name(ref)
                # 참조된 주소의 함수 이름을 얻습니다.
                ref_func_name = ida_name.get_name(ref)
                # 참조된 주소가 현재 분석 중인 함수의 범위 외부에 있고, `.idata` 세그먼트에 있으면 외부 함수로 간주합니다.
                if ref_func_name and seg_name == '.idata':
                    func_dict.append(['out',current_function_name,ref_func_name,instr])
                        #print("{} 함수에서 외부 함수 참조: {}".format(current_function_name, ref_func_name))
                # 참조된 주소가 현재 분석 중인 함수의 범위 외부에 있지만, `.idata` 세그먼트에는 없는 경우, 내부 함수로 간주합니다.
                elif ref_func_name and idc.get_func_attr(ref, idc.FUNCATTR_START) != current_function_start:
                    func_dict.append(['in',current_function_name,ref_func_name,instr])
    return func_dict

def get_all_func():
    func_dict = []
    for func in idautils.Functions():
        # 함수의 시작 주소를 얻습니다.
        current_function_start = idc.get_func_attr(func, idc.FUNCATTR_START)
        # 함수의 이름을 얻습니다.
        current_function_name = ida_name.get_name(current_function_start)
        func_dict.append([current_function_name,current_function_start])
    return func_dict

def get_func_calls(func_name):
    func_dict = []  # 함수 호출 정보를 저장할 리스트
    # 입력된 함수 이름으로부터 함수 시작 주소를 가져옵니다.
    func_ea = idc.get_name_ea_simple(func_name)
    if func_ea != idc.BADADDR:
        # 함수가 발견되었을 때만 처리합니다.
        # 함수의 내부 호출 및 외부 호출 정보를 수집합니다.
        for instr in idautils.FuncItems(func_ea):
            # 현재 주소에서 참조하는 모든 코드 참조를 순회합니다.
            for ref in idautils.CodeRefsFrom(instr, False):
                # 참조된 주소의 세그먼트 이름을 가져옵니다.
                seg_name = idc.get_segm_name(ref)
                # 참조된 주소의 함수 이름을 가져옵니다.
                ref_func_name = ida_name.get_name(ref)
                # 참조된 주소가 내부 함수인지 외부 함수인지 확인합니다.
                if ref_func_name and seg_name == '.idata':
                    # 외부 함수로 간주됩니다.
                    func_dict.append(['out', func_name, ref_func_name, instr])
                elif ref_func_name and idc.get_func_attr(ref, idc.FUNCATTR_START) == func_ea:
                    # 내부 함수로 간주됩니다.
                    func_dict.append(['in', func_name, ref_func_name, instr])
    return func_dict

filtered_func = []

my_list = get_all_func()
for check in my_list:
    func_ea = idc.get_name_ea_simple(check[0])
    cfunc = ida_hexrays.decompile(func_ea)
    if cfunc:
        for arg in cfunc.arguments:
            if(arg.name == "this"):
                filtered_func.append(check)

함수 내부에서 Lock 함수가 호출되는지 확인

func_resolve_dict = get_specific_func_resolve(filtered_func)
my_list = [sub_list for sub_list in func_resolve_dict if not any(item in banlist for item in sub_list)]
only_all_func = get_all_func()
filtered_func_name_addr = []
for get in only_all_func:
    for checklist in my_list:
        if(checklist[1] == get[0]):
            filtered_func_name_addr.append(get)
            break

함수 내부에서 Lock과 관련 된 함수가 호출되는지 확인합니다. 처음에 this를 호출하는 함수들만 filter를 하고, filter된 함수를 iterate하면서 만약 lock이 있다면 제외합니다. 여기서 Idapython의 약간의 문제점이 있는데, 특히 Lock과 관련 된 함수는 전부 Import된 함수들이라 IAT에 존재하게 됩니다. 그래서 함수를 가져올 때 포인터로 호출되는지의 여부에 따라 달라집니다. .idata의 포인터로 호출된다면, 외부함수입니다.

def get_specific_func_resolve(funcDo):
    func_dict = []
    for func in funcDo:
        # 함수의 시작 주소를 얻습니다.
        current_function_start = func[1]
        # 함수의 이름을 얻습니다.
        current_function_name = ida_name.get_name(current_function_start)

        # 현재 함수의 모든 주소를 순회하며 함수 호출을 찾습니다.
        for instr in idautils.FuncItems(current_function_start):
            # 현재 주소에서 참조하는 모든 코드 참조를 순회합니다.
            for ref in idautils.CodeRefsFrom(instr, False):
                # 참조된 주소의 세그먼트 이름을 얻습니다.
                seg_name = idc.get_segm_name(ref)
                # 참조된 주소의 함수 이름을 얻습니다.
                ref_func_name = ida_name.get_name(ref)
                # 참조된 주소가 현재 분석 중인 함수의 범위 외부에 있고, `.idata` 세그먼트에 있으면 외부 함수로 간주합니다.
                if ref_func_name and seg_name == '.idata':
                    func_dict.append(['out',current_function_name,ref_func_name,instr])
                        #print("{} 함수에서 외부 함수 참조: {}".format(current_function_name, ref_func_name))
                # 참조된 주소가 현재 분석 중인 함수의 범위 외부에 있지만, `.idata` 세그먼트에는 없는 경우, 내부 함수로 간주합니다.
                elif ref_func_name and idc.get_func_attr(ref, idc.FUNCATTR_START) != current_function_start:
                    func_dict.append(['in',current_function_name,ref_func_name,instr])
    return func_dict

함수의 this 객체의 구조체 이름 확인, 필드 확인

for filt in filtered_func_name_addr:
    dec = get_function_pseudocode(filt[1])
    pseduo = get_this_pseudo(dec)
    filt.append(pseduo)
    func_ea = idc.get_name_ea_simple(filt[0])
    cfunc = ida_hexrays.decompile(func_ea)
    if cfunc:
        arg = cfunc.arguments[0]
        filt.append(arg.type().dstr())

함수가 Lock이 없고, this를 사용한다면 this 객체의 구조체를 확인해야합니다. 왜냐하면 같은 구조체를 사용하는 함수들끼리 쌍을 지어주어, method내에서 field를 공유할 때 서로 같은 구조체를 사용하여 침범하는지 침범하지 않는지를 확인해야하기 때문입니다. 그렇기에 idapython의 cfunc argument를 사용하여 구조체의 정보를 불러와줍니다.

아래의 코드를 보면 get_this_pseudo를 사용하여 정규식을 사용해 pseudo코드에서 this 필드에 접근하는 오프셋들을 전부 가져와줍니다.

def get_function_pseudocode(ea):
    # Hex-Rays Decompiler가 사용 가능한지 확인
    if not ida_hexrays.init_hexrays_plugin():
        print("Hex-Rays Decompiler is not available.")
        return None

    # 주어진 주소에서 함수를 찾음
    f = ida_funcs.get_func(ea)
    if not f:
        print("Function not found at the given address.")
        return None

    # 함수를 디컴파일
    cfunc = ida_hexrays.decompile(f.start_ea)
    if not cfunc:
        print("Failed to decompile function.")
        return None

    # 디컴파일된 함수의 pseudocode를 문자열로 변환
    pseudocode = str(cfunc)
    return pseudocode

def get_this_pseudo(code_snippet):
    pattern_corrected = r"this \\+ (0x[0-9a-fA-F]+|\\d+)"
    matches_corrected = re.findall(pattern_corrected, code_snippet)
    return matches_corrected

또 아래 코드는 ComRace와의 결과 비교를 위해 RCX로 가져오는 코드인데, 위에서 제시한 예시 코드에서 [rcx+18h], [rcx+40h]를 출력하는 반면, pseudocode를 사용한 코드는 +5, +13, +20, +0x58을 정상적으로 다 가져오는 것을 볼 수 있습니다.

def get_this_xref(function_name):
    function_start = idc.get_name_ea_simple(function_name)
    function_end = idc.find_func_end(function_start)
    # 함수 내에서 'rcx' 레지스터를 사용하는 모든 명령어를 찾습니다.
    for head in idautils.Heads(function_start, function_end):
        a = idc.generate_disasm_line(head, 0)
        start_index = a.find("[rcx")
        end_index = a.find("]", start_index) + 1 # ']'의 위치를 찾음
        # '['와 ']' 사이의 문자열 추출
        substring = a[start_index:end_index]
        if(start_index != -1):
            print(substring)

같은 Class끼리 묶기, this + offset Xref 출력

0xnumber 과 같은 숫자를 파싱하고, 오름차 순으로 Xref Offset을 정렬합니다.

같은 class끼리 Category를 묶어두어 가시성을 올려줍니다.

categorized_list = {}
for item in filtered_func_name_addr:
    category = item[-1]
    ref_string = list(dict.fromkeys(item[2]))
    converted_numbers = [int(x, 16) if 'x' in x else int(x) for x in ref_string]
    sorted_numbers = sorted(converted_numbers)
    item[2] = sorted_numbers
    if category not in categorized_list:
        categorized_list[category] = []
    categorized_list[category].append(item)

for category, items in categorized_list.items():
    print(f"Category: {category}")
    for item in items:
        print(f"  - {item}")
    print()

실제 적용 사례 및 출력

CVE-2020-1146

  • UserMgr.dll에서 발생하는데, 아래 함수를 여러 쓰레드에서 실행하게 되면 동시간에 Microsoft::WRL::Wrappers::HString::Set(this + 14, &v3);가 호출되게 되고, 이는 내부적으로 WindowsDeleteString(*newString(this+14)); 를 호출하게 되면서, this+14가 Double Free가 된다.
패치 후
AcquireSRWLockExclusive을 사용하여 공유 자원에 Lock을 걸고 있는 것을 확인할 수 있다!!
  AcquireSRWLockExclusive((PSRWLOCK)this + 10);
  v4 = Microsoft::WRL::Wrappers::HString::Set((HSTRING *)this + 15, &v8);
  v5 = v4;
  if ( v4 >= 0 )
  {
    if ( v2 )
      ReleaseSRWLockExclusive(v2);
    return 0i64;
  }
  else
  {
    wil::details::in1diag3::Return_Hr(
      retaddr,
      (void *)0x93,
      (int)"onecore\\\\ds\\\\security\\\\umstartup\\\\usermgr\\\\lib\\\\signincontext.cpp",
      (const char *)(unsigned int)v4);
    if ( v2 )
      ReleaseSRWLockExclusive(v2);
    return v5;
  }
}

패치전
__int64 __fastcall Windows::System::Internal::SignInContext::put_AuthData(HSTRING *this, HSTRING a2)
{
  HSTRING v3; // [rsp+38h] [rbp+10h] BYREF

  v3 = a2;
  Microsoft::WRL::Wrappers::HString::Set(this + 14, &v3);
  return 0i64;
}

__int64 __fastcall Microsoft::WRL::Wrappers::HString::Set(HSTRING *newString, HSTRING *a2)
{
  unsigned int v2; // ebx

  v2 = 0;
  if ( !*a2 || *a2 != *newString )
  {
    WindowsDeleteString(*newString);
    *newString = 0i64;
    return (unsigned int)WindowsDuplicateString(*a2, newString);
  }
  return v2;
}

ComRaceConditionSeeker로 usermgr.dll 분석

**?get_AuthData@SignInContext@Internal@System@Windows@@**를 성공적으로 가져와, 공유 자원의 Offset과 함수 주소를 제대로 가져오는 것을 볼 수 있다. 이로서 취약한 함수들을 성공적으로 가져오는 것을 확인할 수 있다.

출력결과 :

Category: Windows::System::UserStatics *
  - ['?GetNonRoamableIdForUserAndApp@UserStatics@System@Windows@@UEAAJPEAUIUser@23@PEAUHSTRING__@@PEAPEAU5@@Z', 6442468736, [18, 72, 144], 'Windows::System::UserStatics *']
  - ['?RemoveUserAndFireUserRemoved@UserStatics@System@Windows@@UEAAJII@Z', 6442492416, [6, 14, 18, 20, 27, 35, 44, 72, 88, 112, 144, 160, 176, 216, 240, 280, 296, 360, 384], 'Windows::System::UserStatics *']
  - ['?GetUserById@UserStatics@System@Windows@@UEAAJIIPEAPEAUIUser@23@@Z', 6442494496, [14, 16, 72, 112, 128], 'Windows::System::UserStatics *']
  - ['?ChangeSessionActiveShellUser@UserStatics@System@Windows@@UEAAJII@Z', 6442496944, [56], 'Windows::System::UserStatics *']
  - ['?GetSessionActiveShellUser@UserStatics@System@Windows@@UEAAJIPEAPEAUIUser@23@@Z', 6442510688, [27, 176, 216], 'Windows::System::UserStatics *']
  - ['?CreateInternalWatcher@UserStatics@System@Windows@@UEAAJPEAPEAUIUserWatcher@23@@Z', 6442518672, [312, 384], 'Windows::System::UserStatics *']
  - ['?RemoveSessionActiveShellUser@UserStatics@System@Windows@@UEAAJI@Z', 6442587520, [176, 216], 'Windows::System::UserStatics *']
  - ['??1UserStatics@System@Windows@@UEAA@XZ', 6442948672, [1, 4, 5, 6, 7, 21, 23, 25, 27, 34, 42, 44, 51, 52, 53, 54, 55, 56, 57, 128, 168, 184, 200, 216, 232, 296, 336, 352, 368], 'Windows::System::UserStatics *']
  - ['?CreateWatcher@UserStatics@System@Windows@@UEAAJPEAPEAUIUserWatcher@23@@Z', 6442958384, [328, 376], 'Windows::System::UserStatics *']
  - ['?FireSignOutStarted@UserStatics@System@Windows@@UEAAJPEAUIUser@23@EPEAPEAUIUserAuthenticationStatusChangingEventArgs@23@@Z', 6442965184, [45, 46, 48, 49, 312, 360, 384], 'Windows::System::UserStatics *']
  - ['?RemovePartialUserAndFireUserRemoved@UserStatics@System@Windows@@UEAAJII@Z', 6442978240, [16, 20, 44, 72, 160, 384], 'Windows::System::UserStatics *']
  - ['?TryFindUser@UserStatics@System@Windows@@UEAAJIIPEAEPEAPEAUIUser@23@@Z', 6442981648, [14, 16, 72], 'Windows::System::UserStatics *']
  - ['?TryFindUserByContextWithCache@UserStatics@System@Windows@@UEAAJ_KPEAPEAUIUser@23@@Z', 6442982848, [20, 72, 160], 'Windows::System::UserStatics *']

.
.
.
중략
Category: Windows::System::UserAuthenticationStatusChangingEventArgs *
  - ['?GetDeferral@UserAuthenticationStatusChangingEventArgs@System@Windows@@UEAAJPEAPEAUIUserAuthenticationStatusChangeDeferral@23@@Z', 6443400416, [2, 15, 80], 'Windows::System::UserAuthenticationStatusChangingEventArgs *']

Category: Windows::System::Internal::SignInContext *
  - ['**?get_AuthData@SignInContext@Internal@System@Windows@@UEAAJPEAPEAUHSTRING__@@@Z**', 6443452176, [10, 15, 80], 'Windows::System::Internal::SignInContext *']
  - ['?get_CorrelationId@SignInContext@Internal@System@Windows@@UEAAJPEAPEAUHSTRING__@@@Z', 6443452384, [8, 16, 64], 'Windows::System::Internal::SignInContext *']
  - ['?get_Credentials@SignInContext@Internal@System@Windows@@UEAAJPEAPEAUICredentialSerialization@234@@Z', 6443452592, [10, 16, 80], 'Windows::System::Internal::SignInContext *']
  - ['?put_AuthData@SignInContext@Internal@System@Windows@@UEAAJPEAUHSTRING__@@@Z', 6443453216, [10, 15, 80], 'Windows::System::Internal::SignInContext *']
  - ['?put_CorrelationId@SignInContext@Internal@System@Windows@@UEAAJPEAUHSTRING__@@@Z', 6443453376, [8, 16, 64], 'Windows::System::Internal::SignInContext *']
  - ['?put_Properties@SignInContext@Internal@System@Windows@@UEAAJPEAUIPropertySet@Collections@Foundation@4@@Z', 6443453680, [10, 17], 'Windows::System::Internal::SignInContext *']

Category: WsiEnvironmentAccountManagerTraceProvider::FunctionCall *
  - ['?StartActivity@FunctionCall@WsiEnvironmentAccountManagerTraceProvider@@QEAAXXZ', 6443602172, [6, 8], 'WsiEnvironmentAccountManagerTraceProvider::FunctionCall *']

Category: CredProvUtils *
  - ['?GetLogonUIKeyPath@CredProvUtils@@YAJPEAPEAG@Z', 6443608696, [], 'CredProvUtils *']

CVE-2020-1211

ComRaceConditionSeeker을 통해 this+3과 this+40에 접근하는 것을 확인할 수 있고

Skip에서는 this+7의 포인터를 계속 증가시키고 Next에서 this+7에 접근하면서 OOB가 발생하게 됩니다.

결론 & 한계

ComRaceConditionSeeker

결론

  • ComRace라는 Com의 Race-condition을 연구하는 논문에서 RCX로만 this를 추적하기에 이를 보완함
  • idapython을 사용하여 this의 멤버 사용을 모두 추적
  • 실제로 rcx를 사용하면 2개밖에 얻지 못하는 결과를 5개가 넘는 결과로 확장
  • COM과 관련 된 1-day CVE 중 winrt 포함 5개중 5개를 Repro할 수 있었음.
  • ComRace가 놓친 취약점이나 윈도우 패치 후 변경 된 코드를 ComRaceConditionSeeker을 통해 찾을 수 있음
  • 2개의 추가 Race-Condition을 발견하고 제보예정

한계

  • Lock함수가 총 49개가 있는데 더 추가된 쌍이 있기 때문에 분석가가 Banlist에 추가해줘야함
  • this 참조중 정말 드물게 this[offset]으로 참조하는 경우가 있는데, 이 경우의 분석을 추가할 예정

'Research' 카테고리의 다른 글

SeedSeeker  (1) 2024.02.18
CodeQL Summary  (1) 2024.02.06
What Is Windows Search DB?  (1) 2023.12.26
Twice the Bits, Twice the Trouble: Vulnerabilities Induced by Migrating to 64-Bit Platforms  (2) 2023.11.22
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.