IDA 플러그인 컨테스트 참가기

글을 작년 4월쯤에 썼었군요... 감사하게도 IDA 플러그인 컨테스트에서 3등을 수상했습니다. 🙂 플러그인들을 만든 동기와, 삽질했던 부분에 대해 써보고자 했습니다.

프롤로그

제가 자주 쓰는 에디터는 서브라임 텍스트인데요, 두 가지 기능이 마음에 들었습니다.

  1. 커맨드 팔레트 및 파일 탐색기 (Ctrl+Shift+P, Ctrl+P)
  2. 패키지 설치 및 제거 (Package Control)

서브라임에서 원하는 기능이 있으면 대부분 Package Control로 검색해서 받을 수 있었습니다. 받은 기능들은 커맨드 팔레트로 목록화하고, 검색해서 쓸 수 있을 뿐더러, 파일 및 심볼 탐색도 팔레트 상에서 몇 가지 단축키로 할 수 있어서 좋았습니다. 검색기가 fuzzy search를 사용해서 비슷하게 입력해도 원하는 결과가 표시되더군요.

sublime-cmd

동시에 IDA 유저인 저로서는 이 두 기능이 IDA에는 없다는 것이 마음에 들지 않았습니다. 그래서 두 가지 플러그인을 만들기 시작했는데요,

ifred: Command Palette #

기존에 fuzzy match로 커맨드 및 심볼을 찾아주는 플러그인들은 몇 가지 있던 것으로 알지만, 사실 제가 원하는 것은 Sublime Text 모양의 커맨드 팔레트였습니다. 목표는 세 가지였습니다. CSS 지원, 파이썬 바인딩 지원, 그리고 쓸만한 속도입니다.

이름은 저와 같은 생각을 가지고 계셨던 다른 분이 지어주셨습니다. Mac OS X의 유명한 프로그램인 Alfred의 패러디로, IDA용 커맨드 팔레트입니다. (세한형 고마워요!)

1. css 등으로 스타일 지정이 가능할 것

Qt UI 라이브러리를 쓰는 IDA의 특성 상 CSS 적용이 가능했습니다. CSS3을 전부 지원하진 않았지만, 어느정도는 괜찮은 느낌입니다. 전용 언어로 스타일을 지정하는 것은 세부적인 UI의 쉬운 조정을 위해서 좋을 것 같아요. 후일담으로, zyantific이 배포하는 IDASkins도 쓰더군요.

당시에는 Visual Studio Code를 쓰고 있었기 때문에, 대충 비슷하게 css를 만들어서 내장하였습니다. 어려움이라면...

vs 위젯

QListView::item:selected {
    background: #062f4a !important;
}

QScrollBar::handle:vertical {
    border-radius: 6px;
}

Qt는 자체 CSS 엔진을 사용합니다. 위젯의 종류마다 지원하는 속성이 미묘하게 다르기 때문에 시행착오가 있었지만, 다행히 원하는 만큼의 스타일링이 가능했습니다.

Qt에서는 위젯을 사용자에게 보여주기 위해 사용되는 여백, 폰트 등 여러가지 정보들을 Style이라 부르는데요, 각 위젯들은 OS에 따라 다른 QStyle의 서브클래스들을 사용하고, Qt는 CSS의 내용을 반영시켜주는 QStyleSheetStyle을 별도로 구현합니다. 결과적으로, 각 위젯에 setStyleSheet 함수를 제공하여 CSS를 사용할 수 있습니다...

네, 시행착오가 많았습니다. 문서화가 완전하진 않아서 Qt 코드와 구글을 자주 오갔습니다.
다음번에는 다른 UI 프레임워크도 사용해보고 싶군요...

vs Rich Text (HTML)

em { /* Highlighted Text */
    font-weight: bold;
    color: rgb(0, 151, 251);
    font-style: normal;
}

td.name, td.description, td.shortcut { ... /* padding, etc */ }

또한 검색 결과들을 쉽게 꾸미기 위해 HTML 문법의 Rich Text를 써서 렌더링했습니다. 이곳 또한 다른 구현의 CSS 엔진을 지원하는데요, 하지만 원하는 레이아웃 관련 속성들은 거의 지원하지 않아서, padding, float 등으로 해결하던 것들을 table 태그로 표를 만들어서 (...) 해결했습니다.

2. C++, 파이썬 지원하기

파이썬 바인딩은 pybind11을 사용해서 만들었습니다.

// 클래스 선언
py::class_<PyPalette>(m, "Palette")
    .def(py::init<std::string, std::string, py::list>());

// 전역 메서드 선언
m.def("show_palette", [](PyPalette & palette) -> bool {
    show_palette(
        ...,
        [](const Action & action) {
            py::gil_scoped_acquire gil;
            ... // 액션 콜백을 실행해요!

            return true;
        });
    return true;
});

// 파이썬!
from __palette__ import *

entries = [Action(name="Hello!", handler=print, id='action')]
show_palette(Palette('mypalette', '> ', entries))

3. 쓸만한 속도

검색 및 정렬 속도를 어느정도 높이고 싶었기 때문에 전체 구현에 C++를 사용했습니다. fuzzy match 라이브러리는 fts_fuzzy_match.h를 사용하고, QRegularExpression (JIT)/QRegExp 등의 정규식 클래스들은 키워드가 빠르게 바뀌는 특성 상 컴파일 시간이 길어 O(n)인 해당 헤더의 fuzzy_match_simple을 사용하였습니다.

작업 전체에 QSortFilterProxyModel을 써보려고도 했지만 용도가 맞지 않아서 부모 클래스에서 다시 구현하게 되었어요. Qt 고수들이 많다는 생각이 들었습니다.

결과

ida-palette

이제 어느 정도 쓸만해진 것 같아요. 기본 테마는 vscode입니다.

단축 키는 Ctrl/Command+Shift+P이며, Ctrl+P로는 심볼 검색을 실행할 수 있어요. 제목 근처에 있는 #(샵) 링크에서 받으실 수 있습니다.

idapkg: Package management #

사실 처음 시작할 땐 플러그인을 직접 로딩할 수 있는지조차 몰랐지만, 덕분에 IDA의 여러 기능들에 익숙해지더군요. 구현에 여러 방식을 생각해보다가 선택한 방법은 아래와 같습니다.

%IDAUSR%

IDAUSR라는 환경 변수를 써서 패키지 매니저를 만드려 했는데요, 아래와 같은 특성이 있습니다.

  • IDAUSR는 :로 구분된 경로들의 목록입니다 (윈도에서는 ; 문자).
  • 지정되었을 경우
    • IDA에서 plugins/procs/loaders/... 디렉토리를 스캔 시 참고합니다.
      가령, IDAUSR=/etc/ida일 경우 /etc/ida/plugins에 있는 플러그인들도 로딩됩니다.
  • 미지정되었을 경우
    • 유닉스의 경우 ~/.idapro, 윈도는 %APPDATA%\Hex-Rays\IDA Pro이 쓰입니다. (idaapi.get_user_idadir() 참고)

이쯤 되면 각 패키지를 ~/idapkg/에 각 폴더로 저장 후, IDAUSR만 바꿔줘도 모든 플러그인 로딩이 가능해질텐데... 문제가 있다면, 이 경로들이 IDA 시작 시 캐시된다는 점이였습니다.

1. 시작 시 캐시가 된다는 점

IDA에서는 시작 시 IDAUSR를 구분자로 나눈 뒤 전역 변수에 qvector 배열로 저장합니다. 문제는, 한 번 로딩한 후에는 더 이상 이 환경 변수를 파싱하지 않는다는 점인데요, 따라서 putenv로 업데이트해도 반영이 되질 않습니다. 이를 해결하기 위해 몇 가지 방법을 생각해봤습니다.

1-1. 어떻게던 해당 배열의 주소를 얻어 와서 size를 0으로 만들기

해당 배열의 주소를 리턴하는 API는 없었습니다. 프로세서 모듈은 get_idp_descs() 함수가 리턴하는 전역 벡터 포인터를 얻어와서 C단에서 clear하면 되니 조작이 편하지만, IDAUSR 자체에는 이런 API가 없었습니다. 따라서 오프셋을 하드코딩하거나 ida.dll를 자동으로 분석한다던지 하는 작업을 해야 합니다. 일단은 하드코딩해서 PoC를 만들어보았습니다.

cf) 결국 이 방법을 썼습니다. 의외로? 안정적이더군요!

def invalidate_idausr():
    global __possible_to_invalidate

    if not __possible_to_invalidate:
        return False

    IDADIR_OFFSETS = {
        (7, 0, 171130): {"win": [0x5e9dc8, 0x5f20d8]},
        (7, 0, 170914): {"mac": [0x5be118, 0x5c7428]}
    }

    # Now this is tricky part
    _, lib = ida_lib()
    base = get_lib_base(lib)

    try:
        offset = IDADIR_OFFSETS[version_info][current_os][current_ea == 64]
    except KeyError:
        logger.info(
            "Loading processors/loaders are not supported in this platform.")
        __possible_to_invalidate = False
        return False

    # qvector<qstring> *ptr(getenv("IDAUSR").split(";" or ":"))
    # ptr.len = ptr.cap = 0
    ptr = ctypes.cast(base + offset, ctypes.POINTER(ctypes.c_size_t))
    # Memory leak here, but not too much.
    ptr[1] = ptr[2] = 0
    return True

어쨌거나 작동은 했습니다. 단 버전마다 전부 하드코딩하긴 힘들고, 바이너리 분석을 한다고 해도 얼마나 잘 작동할지는 모르겠습니다...

이 건에 대해 Hex-Rays 측에 문의를 해 보았는데요, IDA로 시작하는 환경변수를 프로그램 내에서 바꾸는 것은 의도하지 않은 동작이라고 하는군요. 바뀌진 않을 것 같습니다.

위의 내용에 이어서, 이를 자동으로 수행하기 위해 kaitai struct로 ida.dll / dylib를 파싱하고, 해당 명령어를 위한 아주 간단한 디스어셈블러를 짰습니다. 이전 버전에서는 파일 파싱에 LIEF를 사용했지만, 패키지 의존성을 줄이기 위해 바꿨습니다. (둘 다 엄청 편합니다!)

1-2. ida 실행 파일을 바꿔치기하기

또는 IDA 시작 시 DLL 인젝션을 시도하는 방법이 있습니다. IDA 바이너리는 플러그인을 위해 의도적으로 코드 사이닝이 되있지 않으므로, 가능한 이야기입니다.

또는 실행 파일을 바꿔치기하거나 idapkg 전용 바로가기를 만드는 방법도 있습니다. 현재로서는 이 방법이 제일 깔끔해보이네요. 하지만, 설치 후 IDA 재시작 전까지 즉시 사용은 안 되는 단점이 있습니다. 약간 애매하네요.

2. IDAUSR 쓰지 않고 해결하기

IDAUSR는 확실히 기능이 좋았지만, 프로세스 내에서 바꾸기는 약간 애매하군요. 각 폴더의 파일을 기본 IDADIR(또는 get_user_idadir) 폴더로 복사해도 되겠지만, 그러면 파일 간 디펜던시가 깨진다는 단점이 있습니다.

wrapper 만들기

언어별로 plugins/procs/loaders 모듈을 감싸는 가짜 모듈을 만드는 방법도 있습니다. 이 방법의 단점은, til/sig/ids에는 적용하기 힘들다는 점이 있겠네요.

IDA의 동작 오버라이드하기

plugins/procs/... 관련 루틴을 IDA에서 제공하는 UI Hook이나 코드 패치로 후킹하면 어떨까 싶기도 합니다. 이를 위해 추가적으로 구현해야 하는 목록을 정리해보았습니다.

loaders: loader modules
 - build_loaders_list 후킹
plugins: loadable plugins
 - w/o PLUGIN_FIX: plugins.cfg + ida_loader.load_plugin(path)
 - w/  PLUGIN_FIX: plugins.cfg + IDA 재시작 (PLUGIN_FIX 플래그가 있는 플러그인을 load_plugin으로 로딩하는 경우 버그가 있었습니다)
procs: processor modules
 - get_idp_descs hook or proccache modifying after get_idp_descs().clear()
til: type libraries
 - 1. load_til 후킹 후 디렉터리 스캐닝 지점 추가
 - 2. 그냥 패키지 디렉토리에서 복사하기
 - 3. TIL 로딩 UI는 ida.exe에 있으며, 직접 디렉토리 스캐닝을 구현했으므로, 창을 재구현 후 기존 창을 대체해도 되긴 됨
sig: FLIRT signature
 - til과 동일
ids/idt: known functions for static/dynamic libraries
 - database open ui hook to find and load available ids/idt
idc, python: include path for each language
 - 패키지 루트에 추가할지, 해당 폴더를 각각 include path에 추가할 지는 고민이 됩니다.

많네요! 두 방법을 섞어쓰지 않을까 싶습니다.

에필로그

ifred는 어느 정도 안정화가 되었지만, idapkg 구현을 위해서는 아직 문제들이 남아있습니다. 각 패키지를 pip 패키지로 제작해도 된다고 생각은 하지만, 그러면 idc/dll을 지원하기 힘듭니다. Sublime Text의 Package Control과 pip 패키지들의 관계라고 생각할 수도 있겠지만, 사실 그분들이 어떻게 이 문제를 생각하고 있는지는 잘 모르겠어요.

더 고민해보고, 어느정도 안정화 된다면 플러그인 컨테스트에 내야겠어요...

항상 그렇듯이, 읽어주셔서 감사합니다!

추신

2020년 1월이 되었군요. idapkg의 경우 https://idapkg.com 에 테스트 레포 및 매뉴얼을 마련했습니다. 저도 이 레포가 얼마나 안전한진 몰라서... 🤪 적어도 올라온 패키지들은 전부 검증했으니까요, 이와 상관없이 ifred는 계속 쓰고 있습니다. 데비안 저장소 이상의 보안성을 가지지 않는 이상, 아직까지는 PoC 정도로만 생각해야 할 것 같아요.

뿌듯한 점이 하나 있다면, IDA측에서 IDAUSR 환경 변수에 대한 내용을 더 보강해주셨는데요, 사실 위에서 언급했듯이, 이 환경변수에 영향을 받는 부분이 IDA에는 많기 때문에 API를 바꿀 예정은 없다는 피드백을 받았습니다. 그런데, 대회 후에 확인해보니 런타임 수정까진 아니더라도 이를 이용해서 사용할 수 있는 기능들에 대한 내용을 매뉴얼에 추가해줘서, 꽤 재밌는 경험이였습니다. [링크] (NOTE가 여러개 추가되었어요!)

또한 올해도 재미있는 플러그인들이 많이 섭밋되었으니, 한 번 보고 가세요!