Tools/Volatility

#3 Volatility - 간단 플러그인 만들기

geunyeong 2021. 12. 11. 17:50

Table of Contents

    Abstract

    Volatility3 Documentation에는 간단한 플러그인을 만들어보는 예제 글이 있다. 이를 통해 플러그인을 만드는 법을 알아보자.

    https://volatility3.readthedocs.io/en/latest/simple-plugin.html

    How to write a Simple Plugin

    Volatility3 Location: D:\Volatility\volatility3

    My Plugin Location: <Volatility3 Location>\volatility3\framework\plugins\myplugins

    My Plugin File Name: mydlllist.py

    Purpose

    본 글에서 만들어볼 플러그인은 시스템에서 실행 중인 프로세스들의 모듈 목록을 출력하는 플러그인이다. PID를 입력받으면 해당 PID를 가진 프로세스의 모듈 목록만 출력할 수도 있다. 이로 하여금 플러그인을 만드는 기본적인 방법에 대해 알아볼 것이다.

    Declare the Plugin Class

    우선 플러그인 클래스를 선언한다. 플러그인 클래스는 기본적으로 interfaces 라이브러리의 PluginInterface라는 인터페이스를 상속받아 구현된다.

    from volatility3.framework import interfaces
    
    class MyDllList(interfaces.plugins.PluginInterface):
        """My DLL List Plugin"""
    
        _required_framework_version = (1, 0, 0)

    클래스 이름은 MyDllList로 지었다. 클래스 설명을 나타낼 주석과 _required_framework_version을 현재 자신이 가지고 있는 Volatility3 프레임워크 버전을 잘 보고 그보다 작게 넣어주면 된다. 필자는 Volatility3 Framework 1.0.1을 사용하고 있어 1, 0, 0 값을 넣었다.

    Define Requirements

    Requirement는 플러그인이 실행되기 위해 필요한 데이터들을 Volatility3 Framework에게 알려주는 역할을 한다. Volatility3 Framework는 플러그인의 get_requirements 메소드를 호출해 해당 플러그인이 필요로 하는 데이터를 알아오고 이를 플러그인의 config 멤버에 담아준다. 아래 코드에서는 TranslationLayer, SymbolTable, (Other) Plugin, Option Parameter에 대한 Requirements를 나타내고 있다.

    from volatility3.framework.configuration import requirements
    
    from volatility3.plugins.windows import pslist
    
    # ...(중략)...
    
        @classmethod
        def get_requirements(self):
            return [
                requirements.TranslationLayerRequirement(
                    name='primary',
                    description='Memory layer for the kernel',
                    architectures=['Intel32', 'Intel64']
                ),
                requirements.SymbolTableRequirement(
                    name='nt_symbols',
                    description='Windows kernel symbols'
                ),
                requirements.PluginRequirement(
                    name='pslist',
                    plugin=pslist.PsList,
                    version=(2, 0, 0)
                ),
                requirements.ListRequirement(
                    name='pid',
                    element_type=int,
                    description='Process IDs to include (all other processes are excluded)',
                    optional=True
                )
            ]

    TranslationLayerRequirement는 Volatility3의 Translation 계층이 인텔 아키텍처에 있어야 함을 의미한다. architectures로 Intel 계층임을 알려주면 Volatility3 프레임워크가 인텔 메모리 매핑 알고리즘을 사용해 가상 주소를 물리 주소로 변환한다.

    SymbolTableRequirement은 말 그대로 심볼들을 의미한다. 주어진 메모리 덤프 파일과 TranslationLayerRequirement가 만족되면 적절한 심볼 파일을 가져와 config 속성의 nt_symbols라는 키 값으로 관련 데이터들을 담아준다.

    PluginRequirement는 본 플러그인이 다른 플러그인의 코드를 사용할 것임을 나타낸다. pslist 플러그인의 PsList 오브젝트를 만든 후 config 속성의 pslist라는 키 값으로 PsList 오브젝트를 담아준다. version 값은 사용하려는 다른 플러그인의 버전을 의미하는데, 주 버전(가장 앞자리)는 반드시 동일해야 하고, 부 버전(중간 자리)는 version 파라메터로 주어진 값보다 크거나 같아야 한다. 버전에 따라 PsList 오브젝트가 달라지진 않으며 플러그인을 실행하기 전에 기능이 동작할 수 있는지 확인 절차에 불과하다.

    주의할 것은 Volatility3의 심플 플러그인 제작 문서에는 pslist의 version을 (1, 0, 0)으로 써놓았는데, 현재 Volatility3 Framework 1.0.1 기준으로 windows.pslist 플러그인의 버전은 2.0.0이다. 그러므로 pslist에 대한 PluginRequirement의 version 값을 (2, 0, 0)으로 해야한다. 즉 내가 사용하려는 다른 플러그인의 버전을 보고 그에 맞추어 넣어줘야 한다.

    Volatility3 Framework 1.0.1의 windows.pslist plugin version

    ListRequirement는 해당 플러그인이 CLI에서 실행될 때 사용될 수 있는 옵션 항목을 나타낸다. 위 코드에서는 pid라는 옵션 항목이며, description은 pid라는 옵션에 대한 설명이다. element_type은 옵션 값의 타입을 말하고, optional은 해당 옵션이 필수인지 여부를 나타낸다. 이에 대한 내용은 해당 플러그인에 -h 옵션을 주면 확인할 수 있다. 아래 그림처럼 플러그인 뒤에 -h 옵션을 주면 ListRequirement로 넘겨준 pid에 대한 내용들이 나타난다.

    mydlllist 플러그인 help 메시지

    Write a run method

    이제 플러그인의 동작을 나타내는 run 메소드를 작성한다. run 메소드에 대한 설명은 앞선 글에서 설명한 적 있으니 필요하다면 읽어보길 바란다.

    https://geun-yeong.tistory.com/59
    from volatility3.framework import renderers
    from volatility3.framework.renderers import format_hints
    
    # ...(중략)...
    
        def run(self):
            flt_function = pslist.PsList.create_pid_filter(
                self.config.get('pid', None)
            )
    
            return renderers.TreeGrid(
                [
                    ('PID', int),
                    ('Process', str),
                    ('Base', format_hints.Hex),
                    ('Size', format_hints.Hex),
                    ('Name', str),
                    ('Path', str)
                ],
                self._generator(
                    pslist.PsList.list_processes(
                        self.context,
                        self.config['primary'],
                        self.config['nt_symbols'],
                        filter_func = flt_function
                    )
                )
            )

    앞서 requirements에서 ListRequirement는 플러그인의 옵션을 담당한다고 했다. Volatility3 Framework는 pid 옵션 값을 우리 플러그인의 config라는 멤버에 pid라는 키를 가진 형태로 넣어준다(즉 self.config는 dict 타입이다). 때문에 옵션으로 넘어온 pid 값은 self.config.get 메소드로 가져올 수 있다. 만약 pid 옵션 값이 있으면 해당 pid 값을 PsList 플러그인의 create_pid_filter 메소드로 넘겨주어 해당 PID를 가진 프로세스의 데이터만 가져온다. flt_function은 그러한 일을 하는 함수 객체로 뒤에 list_processes에서 조건에 맞는 프로세스만을 필터링할 때 사용한다.

    출력될 테이블은 PID, Process 이름, 모듈 시작 주소, 모듈 크기, 모듈 이름, 모듈 경로로 구성된 6개의 컬럼을 가진다. PID는 정수 타입, Process 이름과 모듈 이름, 모듈 경로는 문자열 타입, 모듈 시작 주소와 모듈 크기는 16진수 포맷으로 출력된다.

    run 메소드에서 모든 작업을 해도 되지만 보통 _generator라는 Private 메소드를 선언해 테이블의 내용에 들어갈 리스트를 만든다. pslist 플러그인의 PsList 클래스가 제공하는 list_processes를 이용해 조건에 맞는 프로세스 정보들만(pid가 주어졌다면 해당 pid를 가진 프로세스) 가져오도록 한 후 _generator 메소드에 넘겨주도록 했다. _generator는 이러한 프로세스 데이터들을 이용해 테이블에 들어갈 내용을 만들게 된다.

    Write a _generator method

    테이블의 내용을 만들어줄 메소드로, _generator 메소드를 선언해 주된 데이터 처리를 하는 형태가 일반적이다.

    from volatility3.framework import interfaces, renderers, exceptions
    
    # ...(중략)...
    
        def _generator(self, procs):    
            for proc in procs:
                try:
                    for entry in proc.load_order_modules():
                        BaseDllName = entry.BaseDllName.get_string()
                        FullDllName = entry.FullDllName.get_string()
    
                        yield (
                            0,
                            (
                                proc.UniqueProcessId,                        # pid
                                proc.ImageFileName.cast(                     # process
                                    'string',
                                    max_length=proc.ImageFileName.vol.count,
                                    errors='replace'
                                ),
                                format_hints.Hex(entry.DllBase),             # base
                                format_hints.Hex(entry.SizeOfImage),         # size
                                BaseDllName,                                 # name
                                FullDllName                                  # path
                            )
                        )
                except:
                    pass

    Volatility3 Documentation에 나온 Simple Plugin 예제의 _generator와는 조금 다르게 작성했다. 메모리 덤프의 상태에 따라 가상 주소를 참조할 때 에러가 발생하는 경우가 있는데, try-except 문으로 묶여있지 않아 플러그인이 죽어버리는 문제가 발생했었다. 그래서 try-except 문의 영역을 좀 더 넓혔다.

    _generator는 pslist.PsList 클래스의 list_processes로 받아온 프로세스 목록을 하나씩 처리하는데, 각 프로세스가 가진 모듈(entry) 목록을 가져온 후 해당 모듈의 이름(파일명만 존재), 경로(C:\Windows~), 모듈 시작 주소, 모듈 크기를 를 yield로 PID, Process 이름과 함께 보관하다가 반환한다. yield로 완성된 리스트는 아마 아래와 같은 모습일 것이다.

    # level, values(pid, process, module start address, module size, module name, module path)
    (0, (4052, "svchost.exe", 0x7ff7f5380000, 0x11000, "svchost.exe", "C:\Windows\System32\svchost.exe"))
    (0, (4052, "svchost.exe", 0x7fffe9350000, 0x1f5000, "ntdll.dll", "C:\Windows\SYSTEM32\ntdll.dll"))
    ...

    Run

    이제 실행해보자. 메모리 덤프는 Windows 10 20H2 x64 가상머신에서 DumpIt으로 덤프한 메모리다.

    MyDllList Plugin 실행 모습

    pslist.PsList는 Windows 메모리 덤프에서 EPROCESS를 찾아 프로세스 정보를 구성하다보니 프로세스 이름이 일부 잘리는 경우가 발생한다(프로세스 이름을 저장하는 영역이 16바이트밖에 안되다보니). 그래도 큰 에러 없이 모든 프로세스의 모듈 목록을 잘 출력하는 것을 볼 수 있다.

    PID 옵션으로 PID가 604인 프로세스의 모듈 목록을 출력해도 잘 동작한다.

    Close

    Volatility3의 Documentation에 있는 SImple Plugin 작성법을 따라 DLL 목록을 출력하는 플러그인을 작성해봤다. Volatility3의 플러그인을 만드는 데 필요한 최소한의 기본 소양을 모두 다룰 수 있어 Volatility3에서도 DLL 목록을 출력하는 플러그인으로 예제를 만든 것 같다.

    이제 더 다양한 플러그인을 만들어보자.

    Source Code

    전체 소스 코드

    더보기
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    
    __author__  = "Geunyeong Choi"
    __email__   = "lucete387@gmail.com"
    __version__ = "1.0.0"
    
    from volatility3.framework import interfaces, renderers, exceptions
    from volatility3.framework.renderers import format_hints
    from volatility3.framework.configuration import requirements
    
    from volatility3.plugins.windows import pslist
    
    class MyDllList(interfaces.plugins.PluginInterface):
        """My DLL List Plugin"""
    
        _required_framework_version = (1, 0, 0)
        _version = (1, 0, 0)
    
        @classmethod
        def get_requirements(self):
            return [
                requirements.TranslationLayerRequirement(
                    name='primary',
                    description='Memory layer for the kernel',
                    architectures=['Intel32', 'Intel64']
                ),
                requirements.SymbolTableRequirement(
                    name='nt_symbols',
                    description='Windows kernel symbols'
                ),
                requirements.PluginRequirement(
                    name='pslist',
                    plugin=pslist.PsList,
                    version=(2, 0, 0)
                ),
                requirements.ListRequirement(
                    name='pid',
                    element_type=int,
                    description='Process IDs to include (all other processes are excluded)',
                    optional=True
                )
            ]
    
        def _generator(self, procs):
            for proc in procs:
                try:
                    for entry in proc.load_order_modules():
                        BaseDllName = entry.BaseDllName.get_string()
                        FullDllName = entry.FullDllName.get_string()
    
                        yield (
                            0,
                            (
                                proc.UniqueProcessId,                        # pid
                                proc.ImageFileName.cast(                     # process
                                    'string',
                                    max_length=proc.ImageFileName.vol.count,
                                    errors='replace'
                                ),
                                format_hints.Hex(entry.DllBase),             # base
                                format_hints.Hex(entry.SizeOfImage),         # size
                                BaseDllName,                                 # name
                                FullDllName                                  # path
                            )
                        )
                except:
                    pass
        
        def run(self):
            flt_function = pslist.PsList.create_pid_filter(
                self.config.get('pid', None)
            )
    
            return renderers.TreeGrid(
                [
                    ('PID', int),
                    ('Process', str),
                    ('Base', format_hints.Hex),
                    ('Size', format_hints.Hex),
                    ('Name', str),
                    ('Path', str)
                ],
                self._generator(
                    pslist.PsList.list_processes(
                        self.context,
                        self.config['primary'],
                        self.config['nt_symbols'],
                        filter_func = flt_function
                    )
                )
            )