Window Backdoor - 1

안상준·2026년 2월 10일

Reversing

목록 보기
13/16
post-thumbnail

Ghidra Study

Background

MOTHRA(Modern Technique Harmless program for Reversing and Analysis)란
리버스 엔지니어링 학습 및 분석 연습을 위해 만들어진 교육용 샌드박스

RAT(Remote Access Trojan)란
공격자가 상대방의 컴퓨터를 내 컴퓨터처럼 원격으로 조작할 수 있게 해주는 악성코드

.gdt(Global Descriptor Table)란
커널 드라이버나 로우레벨 시스템을 분석할 때, 특정 메모리 영역의 권한을 확인하기 위해 이 테이블 정보를 덤프 떠서 .gdt 파일로 저장

실습에서는 분석 보조를 위하여 활용, Decompile 결과를 가독성 있게 만들어 줌.

FIDB(Function ID Database)란
Ghidra로 바이너리를 불러왔을 때, 정정 링크된 라이브러리 함수들은 이름 없이 표시된느 경우가 많다. 이 때 FIDB가 자동으로 이름을 복구해 준다.

Backdoor 분석

이번 실습은 책에서 제공하는 실습 파일 MOTHRA_RAT.exe파일(윈도우 백도어)을 이용하여 분석하였다.
앞서 배경지식에서 설명한 gdt 파일과 fidb 파일을 임포트한 뒤 분석을 진행하였다.
먼저 script를 이용하여 main 함수의 위치를 찾았다.

Main

#TODO WinMain
#@author Sangjun
#@category _NEW_
#@runtime Jython

import __main__ as flatapi
from ghidra.app.services import DataTypeManagerService
from ghidra.program.model.address import AddressSet
from ghidra.program.model.listing import ParameterImpl
from ghidra.program.model.listing import ReturnParameterImpl
from ghidra.program.model.listing.Function.FunctionUpdateType import DYNAMIC_STORAGE_FORMAL_PARAMS
from ghidra.program.model.scalar import Scalar
from ghidra.program.model.symbol.FlowType import UNCONDITIONAL_CALL, UNCONDITIONAL_JUMP
from ghidra.program.model.symbol.SourceType import USER_DEFINED

def get_winmain_address():
    current_program = flatapi.getCurrentProgram()
    symbol_iter = current_program.getSymbolTable().getSymbols('entry')
    
    if not symbol_iter.hasNext():
        print("Could not find symbol 'entry'")
        return None
    
    entry = symbol_iter.next()
    print("Found entry at: {}".format(entry.getAddress()))
    
    inst = flatapi.getInstructionAt(entry.getAddress())
    if inst is None or inst.getFlowType() != UNCONDITIONAL_CALL:
        print("Entry does not start with a CALL instruction.")
        return None
    
    next_inst = inst.getNext()
    if next_inst is None or next_inst.getFlowType() != UNCONDITIONAL_JUMP:
        return None

    image_base = current_program.getImageBase().getOffset()

    search_start_address = next_inst.getFlows()[0]
    search_end_address = search_start_address.add(0x200)
    search_range = AddressSet(search_start_address, search_end_address)
    
    # PUSH 0x... (ImageBase);
    hits = flatapi.findBytes(search_range, '\x68\x00.{3}\xe8', 0, 1)
    for hit_address in hits:
        inst_push = flatapi.getInstructionAt(hit_address)
        if inst_push is None: continue
        
        op_objects = inst_push.getOpObjects(0)
        if not op_objects: continue
        
        val = op_objects[0]
        if isinstance(val, Scalar) and val.getValue() == image_base:
            inst_call = inst_push.getNext()
            if inst_call and inst_call.getFlowType() == UNCONDITIONAL_CALL:
                return inst_call.getFlows()[0]
    return None

def apply_winmain_signature(address):
    func = flatapi.getFunctionAt(address)
    if func is None:
        print("No function found at {}".format(address))
        return

    current_program = flatapi.getCurrentProgram()
    service = flatapi.getState().getTool().getService(DataTypeManagerService)
    
    all_managers = service.getDataTypeManagers()
    dt_manager = None
    for manager in all_managers:
        if 'winapi_32' in manager.getName() or 'windows_vs12_32' in manager.getName():
            dt_manager = manager
            break
            
    if dt_manager is None:
        print("Could not find Windows Data Type Archive. Please open 'winapi_32' or 'windows_vs12_32' first.")
        return

    print("Using Data Type Manager: {}".format(dt_manager.getName()))
    
    dt_hinstance = dt_manager.getDataType('/WinDef.h/HINSTANCE')
    dt_lpstr = dt_manager.getDataType('/winnt.h/LPSTR')
    dt_int = dt_manager.getDataType('/int')

    ret_type = ReturnParameterImpl(dt_int, current_program)
    param1 = ParameterImpl('hInstance', dt_hinstance, current_program, USER_DEFINED)
    param2 = ParameterImpl('hPrevInstance', dt_hinstance, current_program, USER_DEFINED)
    param3 = ParameterImpl('lpCmdLine', dt_lpstr, current_program, USER_DEFINED)
    param4 = ParameterImpl('nShowCmd', dt_int, current_program, USER_DEFINED)
    
    func.updateFunction('__stdcall', ret_type, DYNAMIC_STORAGE_FORMAL_PARAMS, True, USER_DEFINED, [param1, param2, param3, param4])
    print("Successfully applied WinMain signature!")

def main():
    winmain_address = get_winmain_address()
    if winmain_address:
        print('Found WinMain at {}'.format(winmain_address))
        flatapi.createLabel(winmain_address, 'WinMain', False, USER_DEFINED)
        apply_winmain_signature(winmain_address)
    else:
        print("WinMain address not found.")

if __name__ == '__main__':
    main()

jython으로 작성한 ghidra scipt로 Main함수를 찾는 script다.

Main함수의 디컴파일 결과다. 여기서 FUN_00402da0함수를 호출하고 특정 값이 아니면 아무 동작을 하지 않는다.

Main에서 호출하는 함수다. MessageBoxW함수를 호출한다. 메시지 박스에서 사용자가 클릭하는 버튼에 따라 반환값이 달라진다.
정리하면, 메시지 박스에서 yes를 클릭하게 되면 Main함수에서 if문에 있는 내용이 실행되게 된다.
그 다음 다시 Main함수로 돌아가 다음으로 호출되는 함수인 FUN_00402d00를 분석하였다.

뮤텍스 확인


함수 내부에서 CreateMutexW를 호출하는 것을 볼 수 있다. CreateMutexW 함수는 윈도우 운영체제에서 뮤텍스(Mutex)라고 불리는 동기화 객체를 생성하거나 기존에 만들어진 뮤텍스를 여는 역할을 한다. 여기서는 뮤텍스가 존재하는지, 즉 자신이 실행 중인지 확인하기 위하여 사용하는 것 같다.

환경 감지

Main함수에서 다음으로 호출하는 함수를 분석하였다.

내부에서 또 함수를 호출하고 반환값이 0이면 프로세스를 종료한다. 호출하는 함수를 따라가 보자.

RegOpenKeyExW함수를 호출한다. 이는 이미 존재하는 레지스트리 키와 핸들을 검색하는 윈도우 API다.
레지스트리란, 윈도우 운영체제와 설치된 프로그램들의 설정 정보, 사용자 옵션 등을 저장하는 거대한 데이터베이스다.
인자를 순서대로 하나씩 살펴보면

  • hKey (0x80000002): HKEY_LOCAL_MACHINE을 의미하여, 시스템 전체에 적용되는 설정을 담는 루트키다.
  • lpSubKey (L"SOFTWARE\MOTHRA"): HKLM 아래에 있는 SOFTWARE\MOTHRA 라는 경로의 키를 연다는 의미다.
  • samDesired (0): 예약된 옵션으로 보통 0을 넣는다.
  • phkResult (&local_8): 키가 성공적으로 열리면 해당 키를 제어할 수 있는 핸들 값을 여기에 저장한다.

정리하면 이는 이전에 내가 감염시켰는지 확인(레지스트리가 존재하면 0반환), 감염 됐다면 열였던 레지스트리 키를 다시 닫기.

Config 로드

Config는 악성코드가 동작하는 데 필요한 핵심 설정값들의 집합을 의미한다. 이는 보통 데이터 영역에 보존되어 있으며 암호화되어 있다.
다음 함수인 FUN_00402cb0를 분석해 보자.

보면 FID 덕분에 함수명이 memcpy로 돼있는 것을 확인할 수 있다.
_Dst에 포인터 반환값을 사용하는데 다음 함수에서 memcpy의 인자로 사용되는 것을 보아 malloc으로 추축된다.
그 다음 DAT_00416008에 있는 값을 복사하고 복사된 값을 인자로 사용하여 함수를 호출한다.
DAT_00416008영역을 확인하면 엔트로피가 높은 랜덤값을 확인할 수 있으며 암호화된 데이터일 것이라 한다.

앞서 malloc으로 추측한 함수를 분석하면, Heap 공간을 할당하는 동작을 수행하므로, malloc이 맞는 것을 확인할 수 있다.
다시 돌아가 FUN_004016d0함수를 분석해 보자

RC4


아주 간단하게 함수 2개를 호출한다.

상당히 복잡한데 RC4 알고리즘의 초기화 단계라고 한다.
그 다음 호출되는 함수를 살펴보면,

동일하게 복잡한데 이건 RC4 암호화 알고리즘의 두 번째 단계인 PRGA라고 한다. 직전 함수가 준비단계이며 이 함수가 복호화의 핵심 단계이다.

정리

내용이 많아 새 포스트에 C2 서버부터 작성하려고 한다.
실제 악성코드를 가지고 분석을 한 것은 처음이며, 굉장히 어렵지만, 하나씩 분석해 나가는 과정이 재밌는 것 같다.

0개의 댓글