Post

The Hidden Originals – Unveiling the Secrets of Go-based Malware Packers

Reverse engineering malware packing and obfuscation tools written in Go

The Hidden Originals – Unveiling the Secrets of Go-based Malware Packers

Introduction

2026 년 새해가 밝았다. 이직과 바뀐 생활과 환경에 적응하며 바쁜 시간을 보내며 하루하루 보냈다. 25 년 회고를 쓰는 것은 쌩 깠는지 .. 길지는 않지만 연휴를 맞이하여 그동안 수집했던 샘플 분석을 진행하였다.

해당 분석에 쓰인 파일은 Mach-O 파일 형식에 보호 메커니즘이 적용된 Malware 를 생성하는 특징을 가지고 있다. 기존에는 SECURE 한 보호 메커니즘 생성 기능을 가지고 있었는데 일부 바이너리 수준에서 악의적으로 코드 주입이 이뤄진거 같다. 따라서 추후 탐지 및 보안기술로 가공할 수 있는 수준이라 좀 더 심도있게 분석을 진행해보려고 한다.

이번에는 정적 분석에 사용할 수 있는 Sphinx 라는 엔진을 따로 만들어서 분석을 진행해보려고 한다. 요즘 AI 기술이 많이 발전해서 리버싱에도 활용할 수 있는 부분이 많아졌다. 그런데 단순하게 사용하는 게 아니라 AI 를 이용하여 좀 더 퀄리티 있는 분석을 위하여 분석 도구 개발을 시도하고 있다.

또 최근에는 악성코드에서 인상을 받아 Frida 와 비슷한 강력한 동적 분석 도구를 추가로 만들게 되었는데 아직 공개할 수 있는 수준은 아니지만 언젠가는 공개할 수 있도록 노력해보려고 한다.

본격적으로 바이너리 파일에 대하여 정리해보려고 한다.

먼저 들어가기 전에 Mach-O 파일에 대하여 그리고 컴파일 과정은 간략하게 아래를 참고할 수 있다.

1
2
3
4
5
6
7
8
MyApp.zip
 └── Payload/
      └── MyApp.app/
           ├── MyApp        ← (이게 Mach-O 실행파일)
           ├── Info.plist
           ├── embedded.mobileprovision
           ├── Assets.car
           └── ...

iOS 앱 패키지 안에는 내부에 Payload 디렉토리가 있고 그 안에 MyApp.app 디렉토리가 있다. MyApp.app 디렉토리 안에는 MyApp이라는 Mach-O 실행 파일이 존재한다.

1
2
3
4
5
6
7
8
9
10
11
+------------------------+
| Mach Header            |  (mach_header / mach_header_64)
+------------------------+
| Load Commands[]        |  (ncmds 개)
+------------------------+
| Segment / Section Data |  (__TEXT, __DATA, __LINKEDIT ...)
+------------------------+
| Linkedit Data          |  (symbol table, strings, dyld info ...)
+------------------------+
| Code Signature         |  (LC_CODE_SIGNATURE 가 가리킴)
+------------------------+

Mach-O 파일은 macOS 및 iOS에서 실행되는 바이너리 파일 형식으로, ELF 또는 PE와 유사한 역할을 한다. 이 파일 형식은 실행 파일, 라이브러리 및 드라이버를 포함한 다양한 유형의 바이너리를 지원한다.

해당 샘플은 Windows 에서 Mach-O 파일을 가공하는 Go 실행 파일이다.

TL;DR

이 샘플은 정적 재작성(static rewrite) + 재서명(orchestration) 도구에 가까우며 핵심 경로는 parse, protect, rename, resign 으로 총 4 개의 entry point 를 가지고 있다.

Go 기반 윈도우 바이너리는 _rt0_amd64_windows 가 시작점이며 main.main 이 실제 main 함수이다.

아래는 메인파일에 대한 정적 분석을 진행하였을 때 call graph table 의 일부분이다.

Call Graph Table

GroupSourceTargetExpectResultNote
bootstrapmain.main @0xb78fa0SIKfqUD... (root init) @0xb78b00directOK 
bootstrapSIKfqUD... (root init) @0xb78b00rpc_NewApiClient @0x918140directOK 
bootstrapSIKfqUD... (root init) @0xb78b00RVExOT... (rpc setup) @0x9183c0directOK 
bootstrapSIKfqUD... (root init) @0xb78b00cobra.(*Command).ExecuteC @0xb703c0directOK 
bootstrapcobra.(*Command).ExecuteC @0xb703c0cmd_parse_Run @0xb78000indirectINDIRECTCobra Run/RunE function pointer dispatch
bootstrapcobra.(*Command).ExecuteC @0xb703c0cmd_protect_Run @0xb78220indirectINDIRECTCobra Run/RunE function pointer dispatch
bootstrapcobra.(*Command).ExecuteC @0xb703c0cmd_rename_Run @0xb78880indirectINDIRECTCobra Run/RunE function pointer dispatch
parsecmd_parse_Run @0xb78000parse_AnalyzeIBIN @0xb3e9c0directOK 
parseparse_AnalyzeIBIN @0xb3e9c0validate_IbinPath @0xb42740directOK 
parseparse_AnalyzeIBIN @0xb3e9c0parse_InitIbinInfo @0xb43680directOK 
parseparse_InitIbinInfo @0xb43680kVadJQg... @0xb17fc0directOK 
parseparse_InitIbinInfo @0xb43680XfikXi... @0xb18f20directOK 
parseparse_InitIbinInfo @0xb43680XrjaDU... @0xb439a0directOK 
parseparse_InitIbinInfo @0xb43680ihAeIF... @0xb1c8e0directOK 
parseparse_AnalyzeIBIN @0xb3e9c0analyze_NewObfuscateSymbolInfoWithSets @0xb42ce0directOK 
parseparse_AnalyzeIBIN @0xb3e9c0analyze_NewObfuscateSymbolInfo @0xb42ea0directOK 
parseparse_AnalyzeIBIN @0xb3e9c0analyze_TextSourceScan @0xb41f80directOK 
parseanalyze_TextSourceScan @0xb41f80analyze_AddReference @0xb43060directOK 
parseparse_AnalyzeIBIN @0xb3e9c0analyze_AddReferencesFromSet @0xb431e0directOK 
parseparse_AnalyzeIBIN @0xb3e9c0analyze_AddCStringRef @0xb43120directOK 
parseparse_AnalyzeIBIN @0xb3e9c0nib_BuildInfoMap @0xb19520directOK 
parsenib_BuildInfoMap @0xb19520nib_ParseFile @0xb06320directOK 
parseparse_AnalyzeIBIN @0xb3e9c0nib_SelectorsToSet @0xb223e0directOK 
parseparse_AnalyzeIBIN @0xb3e9c0symbols_LoadSources @0xb1d140directOK 
parsesymbols_LoadSources @0xb1d140obfusc_BuildPaths @0xb1f300directOK 
parsesymbols_LoadSources @0xb1d140machO_LoadSymbolsSource @0xa10f00directOK 
parsesymbols_LoadSources @0xb1d140machO_LoadSymbolsSourceSysFramework @0xa118a0directOK 
parsesymbols_LoadSources @0xb1d140collect_SelectorsFromSource @0xb27be0directOK 
parsesymbols_LoadSources @0xb1d140collect_ClassNamesFromSource @0xb27960indirectINDIRECTNot direct; seen via ihAe… @0xb1c8e0
parseihAeIF... @0xb1c8e0collect_ClassNamesFromSource @0xb27960directOKActual direct edge
parsesymbols_LoadSources @0xb1d140CqUJL... @0xb1e0c0directOK 
parseparse_AnalyzeIBIN @0xb3e9c0dev_MergeExtraSymbols @0xb18880directOK 
parseparse_AnalyzeIBIN @0xb3e9c0plist_Decode @0x8fd740directOK 
parseparse_AnalyzeIBIN @0xb3e9c0HqEorPz... @0xb46540directOK 
parseparse_AnalyzeIBIN @0xb3e9c0lIjvkAq... @0xb465c0directOK 
parseparse_AnalyzeIBIN @0xb3e9c0sMhJpJp... @0xb44c60directOK 
parseparse_AnalyzeIBIN @0xb3e9c0jTxZbdR... @0x8fe920directOK 
parseparse_AnalyzeIBIN @0xb3e9c0RALipV... @0xb222a0directOK 
protectcmd_protect_Run @0xb78220msiz... (license wrapper) @0x916a60directOKLicense wrapper
protectmsiz... (license wrapper) @0x916a60license_ValidateAndRefresh @0x916e00directOK 
protectlicense_ValidateAndRefresh @0x916e00license_ReadFile @0x90bb80directOK 
protectlicense_ValidateAndRefresh @0x916e00license_VerifySignature @0x909e80directOK 
protectlicense_ValidateAndRefresh @0x916e00license_ParseData @0x90b8a0directOK 
protectlicense_ParseData @0x90b8a0license_DecryptAndUnmarshal @0x909d00directOK 
protectlicense_ValidateAndRefresh @0x916e00license_CheckByServer @0x918600directOK 
protectlicense_CheckByServer @0x918600rpc_VerifyDeviceLicense @0x9191a0directOK 
protectrpc_VerifyDeviceLicense @0x9191a0rpc_CallServiceRaw @0x9198c0directOK 
protectlicense_ValidateAndRefresh @0x916e00license_CreateOfflineTimer @0x90ba60directOK 
protectlicense_ValidateAndRefresh @0x916e00offlineTimer_Update @0x90b3a0directOK 
protectlicense_ValidateAndRefresh @0x916e00rpc_HttpGet @0x919040directOK 
protectcmd_protect_Run @0xb78220protect_ObfuscateIBIN @0xb4d4e0directOK 
protectprotect_ObfuscateIBIN @0xb4d4e0validate_IbinPath @0xb42740directOK 
protectprotect_ObfuscateIBIN @0xb4d4e0build_UniAppInfoFromConfig @0xb27e60directOK 
protectbuild_UniAppInfoFromConfig @0xb27e60plist_Decode @0x8fd740directOK 
protectbuild_UniAppInfoFromConfig @0xb27e60kgrugtd... @0xb28b80directOK 
protectbuild_UniAppInfoFromConfig @0xb27e60vrtaaMq... @0xb0c9c0directOK 
protectprotect_ObfuscateIBIN @0xb4d4e0copy_IbinToTemp @0x8fbcc0directOK 
protectcopy_IbinToTemp @0x8fbcc0copy_IbinToTemp_WalkFunc @0x8fbdc0indirectINDIRECTWalk callback/data-ref style path
protectprotect_ObfuscateIBIN @0xb4d4e0read_ImportSymbolConfig @0xb4f220directOK 
protectprotect_ObfuscateIBIN @0xb4d4e0build_RenameMapFromConfig @0xb4f500directOK 
protectprotect_ObfuscateIBIN @0xb4d4e0protect_JS @0xb4b380directOK 
protectprotect_JS @0xb4b380Cfvfx... @0xb4c9c0directOK 
protectprotect_JS @0xb4b380xMmuk... @0xb4c2c0directOK 
protectprotect_JS @0xb4b380pjXHY... @0xb4ce00directOK 
protectprotect_JS @0xb4b380npBB... @0xb4c660directOK 
protectprotect_JS @0xb4b380zZoWC... @0xb4c080directOK 
protectprotect_ObfuscateIBIN @0xb4d4e0cleanup_JSObfuscator @0xb4d240indirectINDIRECTCleanup called by pjXHY… @0xb4ce00
protectpjXHY... @0xb4ce00cleanup_JSObfuscator @0xb4d240directOKActual direct edge
protectprotect_ObfuscateIBIN @0xb4d4e0protect_ImageMD5 @0xb4b0e0directOK 
protectprotect_ImageMD5 @0xb4b0e0wFTY... @0x8ff660directOK 
protectprotect_ObfuscateIBIN @0xb4d4e0obfuscate_CoreStage @0xb4e4c0directOK 
protectobfuscate_CoreStage @0xb4e4c0obfuscate_CollectSymbols @0xb50a00directOK 
protectobfuscate_CollectSymbols @0xb50a00machO_ProcessImages @0xb25d20directOK 
protectmachO_ProcessImages @0xb25d20machO_AnalyzeSections @0xb22740directOK 
protectmachO_ProcessImages @0xb25d20machO_CaptureSectionData @0xb21ee0directOK 
protectmachO_ProcessImages @0xb25d20machO_ApplyReplacements @0xb26740directOK 
protectmachO_ApplyReplacements @0xb26740ADPoX... @0xb25680directOK 
protectmachO_ProcessImages @0xb25d20machO_WriteSections @0xb23860directOK 
protectmachO_ProcessImages @0xb25d20nib_PatchBytes @0xb24a80directOK 
protectmachO_ProcessImages @0xb25d20nib_ParseFile @0xb06320directOK 
protectobfuscate_CoreStage @0xb4e4c0plist_Decode @0x8fd740directOK 
protectobfuscate_CoreStage @0xb4e4c0plist_ReplaceRecursive @0x8fed60indirectINDIRECTVia oLAERM… wrapper @0x8fece0
protectobfuscate_CoreStage @0xb4e4c0oLAERM... (plist wrapper) @0x8fece0directOKWrapper call
protectoLAERM... (plist wrapper) @0x8fece0plist_ReplaceRecursive @0x8fed60indirectINDIRECTDecompiler-level wrapper call; not resolved in direct code-ref export
protectobfuscate_CoreStage @0xb4e4c0howett.net/plist.Encoder.Encode @0x78daa0directOK 
protectobfuscate_CoreStage @0xb4e4c0replace_TextSources @0xb4de40directOK 
protectprotect_ObfuscateIBIN @0xb4d4e0TFjZW... (cleanup temp dir) @0xb1c800directOK 
rename_resigncmd_rename_Run @0xb78880rename_Apply @0xb44d60directOK 
rename_resignrename_Apply @0xb44d60sequential rename (nuHe...) @0xb45f40directOK 
rename_resignrename_Apply @0xb44d60random rename (KsIiw...) @0xb3e760directOK 
rename_resignrename_Apply @0xb44d60plus rename (WNB...) @0xb45a80directOK 
rename_resignrename_Apply @0xb44d60rename_AIWorker @0xb45620indirectINDIRECTruntime.newproc goroutine dispatch
rename_resignrename_AIWorker @0xb45620rename_BuildAIMap @0xb45900directOK 
rename_resignrename_BuildAIMap @0xb45900rpc_WordReplace @0xb460c0directOK 
rename_resignresign_LogStart @0xb13e00resign_ExecIbinsign @0xb146e0directOK 
rename_resignresign_ExecIbinsign @0xb146e0os_exec.Command @0x769000directOK 
rename_resignresign_ExecIbinsign @0xb146e0os_exec.(*Cmd).Run @0x76a580directOK 

아래는 위에 나열된 call graph 를 중심으로 전체 흐름을 오프셋으로 매핑한 것이다. 각 함수는 해당 오프셋에서 시작하는 함수로, 역으로 보면 주요 기능과 역할을 간략히 설명한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
  main.main @0xb78fa0
    -> SIK... @0xb78b00
      -> rpc_NewApiClient @0x918140
      -> RVExOT... @0x9183c0
      -> github.com_spf13_cobra._ptr_Command.ExecuteC @0xb703c0
        -> (Run/RunE 함수포인터 디스패치)
          parse: cmd_parse_Run @0xb78000
            -> parse_AnalyzeIBIN @0xb3e9c0
              -> validate_IbinPath @0xb42740
              -> parse_InitIbinInfo @0xb43680
                -> kVad... @0xb17fc0
                -> Xfik... @0xb18f20
                -> Xrja... @0xb439a0
                -> ihAe... @0xb1c8e0
                  -> collect_ClassNamesFromSource @0xb27960
              -> analyze_NewObfuscateSymbolInfoWithSets @0xb42ce0
              -> analyze_NewObfuscateSymbolInfo @0xb42ea0
              -> analyze_TextSourceScan @0xb41f80
                -> analyze_AddReference @0xb43060
              -> analyze_AddReferencesFromSet @0xb431e0
              -> analyze_AddCStringRef @0xb43120
              -> nib_BuildInfoMap @0xb19520
                -> nib_ParseFile @0xb06320
              -> nib_SelectorsToSet @0xb223e0
              -> symbols_LoadSources @0xb1d140
                -> obfusc_BuildPaths @0xb1f300
                -> machO_LoadSymbolsSource @0xa10f00
                -> machO_LoadSymbolsSourceSysFramework @0xa118a0
                -> collect_SelectorsFromSource @0xb27be0
                -> CqUJL... @0xb1e0c0
              -> dev_MergeExtraSymbols @0xb18880
              -> plist_Decode @0x8fd740
              -> HqEor... @0xb46540
              -> lIjvk... @0xb465c0
              -> sMhJp... @0xb44c60
              -> jTxZb... @0x8fe920
              -> RALip... @0xb222a0

          protect: cmd_protect_Run @0xb78220
            -> msiz... @0x916a60
              -> license_ValidateAndRefresh @0x916e00
                -> license_ReadFile @0x90bb80
                -> license_VerifySignature @0x909e80
                -> license_ParseData @0x90b8a0
                  -> license_DecryptAndUnmarshal @0x909d00
                -> license_CheckByServer @0x918600
                  -> rpc_VerifyDeviceLicense @0x9191a0
                    -> rpc_CallServiceRaw @0x9198c0
                -> license_CreateOfflineTimer @0x90ba60
                -> offlineTimer_Update @0x90b3a0
                -> rpc_HttpGet @0x919040
            -> protect_ObfuscateIBIN @0xb4d4e0
              -> validate_IbinPath @0xb42740
              -> build_UniAppInfoFromConfig @0xb27e60
                -> plist_Decode @0x8fd740
                -> kgrugt... @0xb28b80
                -> vrtaa... @0xb0c9c0
              -> copy_IbinToTemp @0x8fbcc0
                -> copy_IbinToTemp_WalkFunc @0x8fbdc0
              -> read_ImportSymbolConfig @0xb4f220
              -> build_RenameMapFromConfig @0xb4f500
              -> protect_JS @0xb4b380
                -> Cfvfx... @0xb4c9c0
                -> xMmuk... @0xb4c2c0
                -> pjXHY... @0xb4ce00
                  -> cleanup_JSObfuscator @0xb4d240
                -> npBB... @0xb4c660
                -> zZoWC... @0xb4c080
              -> protect_ImageMD5 @0xb4b0e0
                -> wFTY... @0x8ff660
              -> obfuscate_CoreStage @0xb4e4c0
                -> obfuscate_CollectSymbols @0xb50a00
                  -> machO_ProcessImages @0xb25d20
                    -> machO_AnalyzeSections @0xb22740
                    -> machO_CaptureSectionData @0xb21ee0
                    -> machO_ApplyReplacements @0xb26740
                      -> ADPoX... @0xb25680
                    -> machO_WriteSections @0xb23860
                    -> nib_PatchBytes @0xb24a80
                    -> nib_ParseFile @0xb06320
                -> plist_Decode @0x8fd740
                -> oLAERM... @0x8fece0
                  -> plist_ReplaceRecursive @0x8fed60
                -> howett.net/plist.Encoder.Encode @0x78daa0
                -> replace_TextSources @0xb4de40
              -> cleanup temp dir TFjZW... @0xb1c800

          rename: cmd_rename_Run @0xb78880
            -> rename_Apply @0xb44d60
              -> nuHe... (sequential) @0xb45f40
              -> KsIiw... (random) @0xb3e760
              -> WNB... (plus) @0xb45a80
              -> (간접/고루틴) rename_AIWorker @0xb45620
                -> rename_BuildAIMap @0xb45900
                  -> rpc_WordReplace @0xb460c0

  resign 경로(로컬 툴링):
    -> resign_LogStart @0xb13e00
      -> resign_ExecIbinsign @0xb146e0

재서명하는 과정은 추후 다시 보겠지만 아래와 같이 진행되는 거 같다.

  • p12, mobileprovision, entitlements(옵션) 등을 입력으로 받음
  • 내부 번들(Framework/PlugIn/Watch 등)을 깊은 경로부터 먼저 재서명하고 마지막에 상위 app 재서명
  • ibinsign 실행(os/exec)으로 실제 서명 수행, stdout/stderr 수집
  • 성공/실패 로그 처리 후 IBIN 출력(옵션 경로)

다음으로 각 오프셋들의 기능들의 연결 체인을 자세하게 다뤄보려고 한다.

1) 최상위 제어 흐름

  flowchart TD
    E["_rt0_amd64_windows @0x47ca00"] --> M["main.main @0xb78fa0"]
    M --> S["SIKfqUD... @0xb78b00"]
    S --> R1["rpc_NewApiClient @0x918140"]
    S --> R2["RVExOT... @0x9183c0"]
    S --> C["cobra.Command.ExecuteC @0xb703c0"]

    C -. Run/RunE 함수 포인터 디스패치 .-> P["cmd_parse_Run @0xb78000"]
    C -. Run/RunE 함수 포인터 디스패치 .-> O["cmd_protect_Run @0xb78220"]
    C -. Run/RunE 함수 포인터 디스패치 .-> N["cmd_rename_Run @0xb78880"]

핵심은 ExecuteC 이후가 직접 call graph가 아니라 함수 포인터 디스패치라는 점이다. 즉, 정적 caller/callee만 보면 일부 엣지가 비어 보일 수 있다.

2) parse 경로: 난독화 입력 데이터 수집기

  flowchart LR
    P["cmd_parse_Run @0xb78000"] --> A["parse_AnalyzeIBIN @0xb3e9c0"]
    A --> V["validate_IbinPath @0xb42740"]
    A --> I["parse_InitIbinInfo @0xb43680"]

    I --> I1["kVad... @0xb17fc0"]
    I --> I2["Xfik... @0xb18f20"]
    I --> I3["Xrja... @0xb439a0"]
    I --> I4["ihAe... @0xb1c8e0"]

    A --> S1["analyze_NewObfuscateSymbolInfoWithSets @0xb42ce0"]
    A --> S2["analyze_NewObfuscateSymbolInfo @0xb42ea0"]
    A --> T["analyze_TextSourceScan @0xb41f80"]
    T --> AR["analyze_AddReference @0xb43060"]
    A --> RS["analyze_AddReferencesFromSet @0xb431e0"]
    A --> CR["analyze_AddCStringRef @0xb43120"]

    A --> NB["nib_BuildInfoMap @0xb19520"]
    NB --> NP["nib_ParseFile @0xb06320"]
    A --> NS["nib_SelectorsToSet @0xb223e0"]

    A --> L["symbols_LoadSources @0xb1d140"]
    L --> BP["obfusc_BuildPaths @0xb1f300"]
    L --> MS["machO_LoadSymbolsSource @0xa10f00"]
    L --> MF["machO_LoadSymbolsSourceSysFramework @0xa118a0"]
    L --> CS["collect_SelectorsFromSource @0xb27be0"]
    L -. xref/간접 연계 .-> CC["collect_ClassNamesFromSource @0xb27960"]
    L --> CQ["CqUJL... @0xb1e0c0"]

    A --> ME["dev_MergeExtraSymbols @0xb18880"]
    A --> PD["plist_Decode @0x8fd740"]

이 단계는 ‘보호 실행’보다 치환 대상 심볼/문자열/셀렉터 집합 구축에 가까운 거 같다. 즉 protect를 위한 데이터 준비 단계이다.

3) protect 경로: 라이선스 게이트 + Binary 정적 변환 엔진

3-1. 라이선스 검증 체인

  flowchart TD
    CP["cmd_protect_Run @0xb78220"] --> MW["msiz... wrapper @0x916a60"]
    MW --> LV["license_ValidateAndRefresh @0x916e00"]

    LV --> LR["license_ReadFile @0x90bb80"]
    LV --> VS["license_VerifySignature @0x909e80"]
    LV --> LP["license_ParseData @0x90b8a0"]
    LP --> DU["license_DecryptAndUnmarshal @0x909d00"]
    LV --> LC["license_CheckByServer @0x918600"]
    LC --> RV["rpc_VerifyDeviceLicense @0x9191a0"]
    RV --> RC["rpc_CallServiceRaw @0x9198c0"]
    LV --> CT["license_CreateOfflineTimer @0x90ba60"]
    LV --> OT["offlineTimer_Update @0x90b3a0"]
    LV --> HG["rpc_HttpGet @0x919040"]

즉 로컬 검증 + 서버 검증 + 오프라인 타이머 업데이트가 함께 동작하며

서버와 검증을 하거나 서버에서 처리하는 것이 있는지 살펴보았는데 아직까지는 보이지 않는다.

3-2. 실제 보호/치환 파이프라인

  flowchart TD
    OB["protect_ObfuscateIBIN @0xb4d4e0"] --> VI["validate_IbinPath @0xb42740"]
    OB --> UI["build_UniAppInfoFromConfig @0xb27e60"]
    UI --> PD["plist_Decode @0x8fd740"]
    UI --> KG["kgrugt... @0xb28b80"]
    UI --> VR["vrtaa... @0xb0c9c0"]

    OB --> CI["copy_IbinToTemp @0x8fbcc0"]
    CI --> CW["copy_IbinToTemp_WalkFunc @0x8fbdc0"]
    OB --> RI["read_ImportSymbolConfig @0xb4f220"]
    OB --> BR["build_RenameMapFromConfig @0xb4f500"]

    OB --> JS["protect_JS @0xb4b380 (옵션)"]
    JS --> J1["Cfvfx... @0xb4c9c0"]
    JS --> J2["xMmuk... @0xb4c2c0"]
    JS --> J3["pjXHY... @0xb4ce00"]
    JS --> J4["npBB... @0xb4c660"]
    JS --> J5["zZoWC... @0xb4c080"]
    J3 -.-> CJ["cleanup_JSObfuscator @0xb4d240"]

    OB --> IM["protect_ImageMD5 @0xb4b0e0 (옵션)"]
    IM --> WF["wFTY... @0x8ff660"]

    OB --> CORE["obfuscate_CoreStage @0xb4e4c0"]
    CORE --> COL["obfuscate_CollectSymbols @0xb50a00"]
    COL --> MP["machO_ProcessImages @0xb25d20"]

    CORE --> PR["plist_ReplaceRecursive @0x8fed60"]
    CORE --> PE["howett.net/plist.Encoder.Encode @0x78daa0"]
    CORE --> RT["replace_TextSources @0xb4de40"]

    OB --> CL["cleanup temp dir TFjZW... @0xb1c800"]

3-3. Mach-O/NIB 세부 패치 엔진

  flowchart LR
    MP["machO_ProcessImages @0xb25d20"] --> MA["machO_AnalyzeSections @0xb22740"]
    MP --> MC["machO_CaptureSectionData @0xb21ee0"]
    MP --> MR["machO_ApplyReplacements @0xb26740"]
    MR --> AD["ADPoX... @0xb25680"]
    MP --> MW["machO_WriteSections @0xb23860"]
    MP --> NB["nib_PatchBytes @0xb24a80"]
    MP --> NF["nib_ParseFile @0xb06320"]

결론적으로 protect는 런타임 로더가 아니라 오프라인 파일 재작성 루틴이다.

4) rename 경로: 규칙형 + AI형 혼합

  flowchart LR
    CR["cmd_rename_Run @0xb78880"] --> RA["rename_Apply @0xb44d60"]
    RA --> SQ["sequential rename: nuHe... @0xb45f40"]
    RA --> RD["random rename: KsIiw... @0xb3e760"]
    RA --> PL["plus rename: WNB... @0xb45a80"]
    RA -. AI 경로 .-> AI["rename_AIWorker @0xb45620"]
    AI --> BM["rename_BuildAIMap @0xb45900"]
    BM --> WR["rpc_WordReplace @0xb460c0"]

rename는 단일 알고리즘이 아니라 모드 기반이며, AI 경로에서는 RPC를 통해 단어 치환 맵을 생성한다.

5) resign 경로: 외부 서명 도구 오케스트레이션

  sequenceDiagram
    participant RS as resign_LogStart @0xb13e00
    participant EX as resign_ExecIbinsign @0xb146e0
    participant OS as os_exec_Command
    participant KX as ibinsign (external)

    RS->>EX: 재서명 시작
    EX->>OS: signer 실행 커맨드 구성
    OS->>KX: ibinsign sign ...
    KX-->>OS: stdout/stderr + exit code
    OS-->>EX: 실행 결과 반환
    EX-->>RS: "re-sign success"/실패 로그

즉, 내부 구현이 모든 서명을 처리한다기보다 외부 signer 실행 래퍼 역할이 커보인다.

핵심 결론 (메인 바이너리)

  1. 보호/난독화는 런타임 안티디버깅보다 “IBIN 내부 파일 재작성(심볼/리소스 치환)”에 초점이 있다.
  2. 재서명은 외부 signer 실행 경로와 내부 Mach-O codesign 구현 경로가 둘 다 존재한다.
  3. 보호 대상은 Mach-O 심볼군, nib/plist/text, JS, 이미지(MD5 변경)이다.

IBIN 보호/난독화 흐름 (어떻게)

  1. cmd_protect_Run(0xb78220) -> protect_ObfuscateIBIN(0xb4d4e0).
  2. protect_ObfuscateIBIN에서 validate_IbinPath(0xb42740) -> build_UniAppInfoFromConfig(0xb27e60) -> 임시 디렉토리 생성 (*_tempObf) -> copy_IbinToTemp(0x8fbcc0).
  3. 설정 있으면 read_ImportSymbolConfig(0xb4f220) + build_RenameMapFromConfig(0xb4f500) 후 obfuscate_CoreStage(0xb4e4c0).
  4. obfuscate_CoreStage는 obfuscate_CollectSymbols(0xb50a00) + machO_ProcessImages(0xb25d20) + replace_TextSources + plist_Decode 경로로 진행.
  5. JS 옵션 시 protect_JS(0xb4b380) 실행, 이미지 옵션 시 protect_ImageMD5(0xb4b0e0) 실행.
  6. 마지막에 TFjZW…(0xb1c800) -> nbLIm…(0x903fa0)로 재패키징(path/filepath.Walk 기반 zip 작성).

무엇을 보호/변형하는지

  1. obfuscate_CollectSymbols(0xb50a00) 로그/포맷 기준:
  2. ObjC methods/classes.
  3. Swift selectors/classes.
  4. File name symbols.
  5. machO_ProcessImages(0xb25d20) callee 기준:
  6. nib_ParseFile(0xb06320) + nib_PatchBytes(0xb24a80) + machO_ApplyReplacements(0xb26740).
  7. replace_TextSources와 plist decode/encode 경로로 텍스트·plist 계열 치환.
  8. protect_ImageMD5(0xb4b0e0)에서 “alter pic md5” 로그와 wFTY…(0x8ff660) 호출 확인.

JS 난독화 (무엇으로)

  1. protect_JS(0xb4b380)가 유효 JS 수집/복사 후 외부 도구 실행.
  2. xMmuk…(0xb4c2c0)에서 os_exec.Command + Cmd.Run 호출.
  3. pjXH…(0xb4ce00)에서 obfuscator_win_x64.exe 및 obfuscator_win_x6420250802.exe 경로/리네임 처리 확인.

재서명 흐름 (어떻게)

  1. resign_LogStart(0xb13e00)에서 signer 인자 구성 후 resign_ExecIbinsign(0xb146e0) 호출.
  2. 인자 구성은 -c(P12), -p(password), -m(mobileprovision), -e(entitlements, optional) 패턴이 확인됨.
  3. resign_ExecIbinsign는 os_exec.Command + Cmd.Run, stdout/stderr 버퍼 수집.
  4. lkASVG…(0x904e60)는 재서명 실행파일 경로 준비/파일 생성 로직.
  5. KyJa…(0x905340)는 Cmd.Start/Cmd.Wait 기반 실행 경로.
  6. UrIP…(0xb162e0)에서 SignFlag/P12/Password/Profiles/Install 요약 문자열 생성(SignFlag: %s … 포맷).

내부 코드서명 구현 (무엇으로)

  1. bvNR…(0xa01ec0)가 fat-arch 단위로 LKc…(0x99f5e0) 호출.
  2. LKc… 시그니처에 _ptr_macho_File, _ptr_codesign_Config 타입이 드러남.
  3. LKc… 내부에서 WTLQ…(0x925560) 호출해 서명 blob 구성.
  4. WTLQ…에서 CodeDirectory/slot 처리 흔적 확인.
  5. xref 증거:
  6. 0xddffb4(“failed to create CodeDirectory”) -> 0x925bd2(inside WTLQ).
  7. 0xde7291(“failed to get CodeDirectory blob data”) -> 0x925df9(inside WTLQ).
  8. 0xdded9a(“failed to write CodeDirectory”) -> 0x926be9.

재서명하는 과정 (서명 바이너리)

재서명하는 바이너리는 Windows에서 IBIN/.app을 재서명하는 CLI가 맞고, 핵심 재서명 로직은 바이너리 내부(정적으로 링크된 Go 코드)에서 수행된다. 완전 자체구현 only 라기보다, 내부 코드 + 외부 오픈소스 라이브러리(컴파일 시 포함) 조합이다.

동작 흐름

1.엔트리 실행

  • main.main 0x85d1e0 -> SkzMcHINAuqaAywryNkpyT 0x85c280 호출.
  • 여기서 Cobra 루트 커맨드 실행: github.com_spf13_cobra._ptr_Command.ExecuteC 0x7e9c00.
  • panic 복구/로그는 main.main.func1 0x85d240에서 처리.

2.CLI 명령 구성

  • 루트/서브커맨드 문자열 확인: ibinsign, sign, info, install.
  • sign 플래그 등록 함수: rZNnYQBfXhAAMctxCAxPA 0x85c340.
  • 매핑 확인:
    • -c certificate -> qword_D2A4C0
    • -m mobileprovision -> qword_D2A4D0
    • -p password -> qword_D2A4E0
    • -e entitlements -> qword_D2A4F0
    • -z zip -> qword_D2A500
    • -i install(bool) -> BYTE2(stru_DB1980.len)
    • verbose(글로벌) -> BYTE1(stru_DB1980.len)

3.sign 실제 처리

  • 핵심 핸들러: KmpBjuroWNHqKChnVTsmpNlWz 0x85c560.
  • 순서:
    • 입력 경로 존재 확인.
    • 인증서 로드: OruhuJNUDQPUNjJGVsCbwic 0x79cda0에서 os_ReadFile 후 golang_org_x_crypto_pkcs12_Decode 호출.
    • mobileprovision 파일 읽기.
    • entitlements 옵션 있으면 추가 로드.
    • 입력이 .app이면 번들/Framework 순회 후 재서명(CYEPAT… 0x798420).
    • 입력이 .zip이면 임시 Payload 생성/압축해제/서명/재패키징.
    • -z 지정 시 결과 IBIN 저장.
    • -i면 설치 루틴 호출.

4.설치 경로(-i)

  • crEFJbSRaKPewdpJKdjGWzkcw 0x85c060에서 zipconduit_Connection 사용.

5.수정된 IBIN가 Windows에서 다시 동작하는 이유 (핵심)

  • iOS는 설치/실행 시 Mach-O 코드서명(해시+CMS) 검증을 수행한다.
  • 이 툴은 수정된 바이너리/리소스 기준으로 코드서명 데이터를 다시 계산하고 새 인증서로 CMS 서명을 생성해 덮어쓴다.
  • 근거 심볼/문자열:

    • CSMAGIC_CODEDIRECTORY, CSSLOT_CODEDIRECTORY_SHA256, CSMAGIC_EMBEDDED_ENTITLEMENTS, CSMAGIC_EMBEDDED_SIGNATURE
    • ibinsign/sign.(*SignInfo).GetInfoPlistHash
    • ibinsign/sign.(*SignInfo).GetCodeResourcesHash
    • ibinsign/sign.PaddingSuperBlob
    • ibinsign/macho.(*Macho).GetSignatureCommand, AddLoadCommand

즉 “수정됐지만 검증 기준 자체를 새로 만들어” 다시 유효하게 만드는 구조이다. 단, mobileprovision의 Team/AppID/Entitlements와 서명 인증서가 맞아야 설치/실행된다.

외부 프레임워크 의존 여부로는 재서명 핵심은 실행 시 외부 프레임워크(DLL)를 따로 요구하지 않고, 바이너리에 정적으로 포함된 Go 패키지로 처리하는 형태이다. 다만 내부적으로는 외부 오픈소스 라이브러리를 사용한다(컴파일 포함):

  • github.com/spf13/cobra
  • golang.org/x/crypto/pkcs12
  • github.com/github/smimesign/ietf-cms
  • gitee.com/kxapp/goios/ios/zipconduit (설치 경로)
  • PE import는 주로 kernel32만 보이지만, 설치(-i)는 USB/Lockdown 서비스 환경 영향(예: usbmux/Apple Mobile Device 계열)이 있다.

정리하면

IBIN 파일인데 윈도우에서 실행 파일을 다룰 수 있는 이유는 파일 포맷(IBIN=zip, Mach-O, plist, CMS 서명) 을 로컬에서 다루기 때문에 가능하다. 즉 macOS/Xcode 없이도 정적 수정 + 재서명 + IBIN 재생성이 가능하다는 점이다.

Conclusion

연휴 때 분석한 Go 기반 CLI 의 주요 기능과 실행 흐름을 정리하였다. 다른 거 하지 않고 해당 파일만 분석하면서 시간 가는 줄 몰랐는데, 정리하다 보니 내용이 꽤 많았다. Go 바이너리 구조와 정적 분석에 익숙하지 않으면 진입 장벽이 있을 수 있다. 또한 분석 대상이 되는 함수들이 명확히 구분된 서브커맨드(parse/protect/rename)로 나뉘어 있고, 각 경로가 수행하는 역할이 비교적 명확해서 전체적인 구조를 이해하는 데 도움이 되었다.

다음에는 코어 부분을 좀 깊게 봐보려고 한다. 언제 가능할지 모르겠지만 전체적인 구도는 잡혔으니 좀 봐보려고 한다.

This post is licensed under CC BY 4.0 by the author.
If you find any errors, please let me know by comment or email. Thank you.

© Ruffalo. Some rights reserved.

I'm

Using the Chirpy theme for Jekyll.