The Hidden Originals – Unveiling the Secrets of Go-based Malware Packers
Reverse engineering malware packing and obfuscation tools written in Go
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
| Group | Source | Target | Expect | Result | Note |
|---|---|---|---|---|---|
| bootstrap | main.main @0xb78fa0 | SIKfqUD... (root init) @0xb78b00 | direct | OK | |
| bootstrap | SIKfqUD... (root init) @0xb78b00 | rpc_NewApiClient @0x918140 | direct | OK | |
| bootstrap | SIKfqUD... (root init) @0xb78b00 | RVExOT... (rpc setup) @0x9183c0 | direct | OK | |
| bootstrap | SIKfqUD... (root init) @0xb78b00 | cobra.(*Command).ExecuteC @0xb703c0 | direct | OK | |
| bootstrap | cobra.(*Command).ExecuteC @0xb703c0 | cmd_parse_Run @0xb78000 | indirect | INDIRECT | Cobra Run/RunE function pointer dispatch |
| bootstrap | cobra.(*Command).ExecuteC @0xb703c0 | cmd_protect_Run @0xb78220 | indirect | INDIRECT | Cobra Run/RunE function pointer dispatch |
| bootstrap | cobra.(*Command).ExecuteC @0xb703c0 | cmd_rename_Run @0xb78880 | indirect | INDIRECT | Cobra Run/RunE function pointer dispatch |
| parse | cmd_parse_Run @0xb78000 | parse_AnalyzeIBIN @0xb3e9c0 | direct | OK | |
| parse | parse_AnalyzeIBIN @0xb3e9c0 | validate_IbinPath @0xb42740 | direct | OK | |
| parse | parse_AnalyzeIBIN @0xb3e9c0 | parse_InitIbinInfo @0xb43680 | direct | OK | |
| parse | parse_InitIbinInfo @0xb43680 | kVadJQg... @0xb17fc0 | direct | OK | |
| parse | parse_InitIbinInfo @0xb43680 | XfikXi... @0xb18f20 | direct | OK | |
| parse | parse_InitIbinInfo @0xb43680 | XrjaDU... @0xb439a0 | direct | OK | |
| parse | parse_InitIbinInfo @0xb43680 | ihAeIF... @0xb1c8e0 | direct | OK | |
| parse | parse_AnalyzeIBIN @0xb3e9c0 | analyze_NewObfuscateSymbolInfoWithSets @0xb42ce0 | direct | OK | |
| parse | parse_AnalyzeIBIN @0xb3e9c0 | analyze_NewObfuscateSymbolInfo @0xb42ea0 | direct | OK | |
| parse | parse_AnalyzeIBIN @0xb3e9c0 | analyze_TextSourceScan @0xb41f80 | direct | OK | |
| parse | analyze_TextSourceScan @0xb41f80 | analyze_AddReference @0xb43060 | direct | OK | |
| parse | parse_AnalyzeIBIN @0xb3e9c0 | analyze_AddReferencesFromSet @0xb431e0 | direct | OK | |
| parse | parse_AnalyzeIBIN @0xb3e9c0 | analyze_AddCStringRef @0xb43120 | direct | OK | |
| parse | parse_AnalyzeIBIN @0xb3e9c0 | nib_BuildInfoMap @0xb19520 | direct | OK | |
| parse | nib_BuildInfoMap @0xb19520 | nib_ParseFile @0xb06320 | direct | OK | |
| parse | parse_AnalyzeIBIN @0xb3e9c0 | nib_SelectorsToSet @0xb223e0 | direct | OK | |
| parse | parse_AnalyzeIBIN @0xb3e9c0 | symbols_LoadSources @0xb1d140 | direct | OK | |
| parse | symbols_LoadSources @0xb1d140 | obfusc_BuildPaths @0xb1f300 | direct | OK | |
| parse | symbols_LoadSources @0xb1d140 | machO_LoadSymbolsSource @0xa10f00 | direct | OK | |
| parse | symbols_LoadSources @0xb1d140 | machO_LoadSymbolsSourceSysFramework @0xa118a0 | direct | OK | |
| parse | symbols_LoadSources @0xb1d140 | collect_SelectorsFromSource @0xb27be0 | direct | OK | |
| parse | symbols_LoadSources @0xb1d140 | collect_ClassNamesFromSource @0xb27960 | indirect | INDIRECT | Not direct; seen via ihAe… @0xb1c8e0 |
| parse | ihAeIF... @0xb1c8e0 | collect_ClassNamesFromSource @0xb27960 | direct | OK | Actual direct edge |
| parse | symbols_LoadSources @0xb1d140 | CqUJL... @0xb1e0c0 | direct | OK | |
| parse | parse_AnalyzeIBIN @0xb3e9c0 | dev_MergeExtraSymbols @0xb18880 | direct | OK | |
| parse | parse_AnalyzeIBIN @0xb3e9c0 | plist_Decode @0x8fd740 | direct | OK | |
| parse | parse_AnalyzeIBIN @0xb3e9c0 | HqEorPz... @0xb46540 | direct | OK | |
| parse | parse_AnalyzeIBIN @0xb3e9c0 | lIjvkAq... @0xb465c0 | direct | OK | |
| parse | parse_AnalyzeIBIN @0xb3e9c0 | sMhJpJp... @0xb44c60 | direct | OK | |
| parse | parse_AnalyzeIBIN @0xb3e9c0 | jTxZbdR... @0x8fe920 | direct | OK | |
| parse | parse_AnalyzeIBIN @0xb3e9c0 | RALipV... @0xb222a0 | direct | OK | |
| protect | cmd_protect_Run @0xb78220 | msiz... (license wrapper) @0x916a60 | direct | OK | License wrapper |
| protect | msiz... (license wrapper) @0x916a60 | license_ValidateAndRefresh @0x916e00 | direct | OK | |
| protect | license_ValidateAndRefresh @0x916e00 | license_ReadFile @0x90bb80 | direct | OK | |
| protect | license_ValidateAndRefresh @0x916e00 | license_VerifySignature @0x909e80 | direct | OK | |
| protect | license_ValidateAndRefresh @0x916e00 | license_ParseData @0x90b8a0 | direct | OK | |
| protect | license_ParseData @0x90b8a0 | license_DecryptAndUnmarshal @0x909d00 | direct | OK | |
| protect | license_ValidateAndRefresh @0x916e00 | license_CheckByServer @0x918600 | direct | OK | |
| protect | license_CheckByServer @0x918600 | rpc_VerifyDeviceLicense @0x9191a0 | direct | OK | |
| protect | rpc_VerifyDeviceLicense @0x9191a0 | rpc_CallServiceRaw @0x9198c0 | direct | OK | |
| protect | license_ValidateAndRefresh @0x916e00 | license_CreateOfflineTimer @0x90ba60 | direct | OK | |
| protect | license_ValidateAndRefresh @0x916e00 | offlineTimer_Update @0x90b3a0 | direct | OK | |
| protect | license_ValidateAndRefresh @0x916e00 | rpc_HttpGet @0x919040 | direct | OK | |
| protect | cmd_protect_Run @0xb78220 | protect_ObfuscateIBIN @0xb4d4e0 | direct | OK | |
| protect | protect_ObfuscateIBIN @0xb4d4e0 | validate_IbinPath @0xb42740 | direct | OK | |
| protect | protect_ObfuscateIBIN @0xb4d4e0 | build_UniAppInfoFromConfig @0xb27e60 | direct | OK | |
| protect | build_UniAppInfoFromConfig @0xb27e60 | plist_Decode @0x8fd740 | direct | OK | |
| protect | build_UniAppInfoFromConfig @0xb27e60 | kgrugtd... @0xb28b80 | direct | OK | |
| protect | build_UniAppInfoFromConfig @0xb27e60 | vrtaaMq... @0xb0c9c0 | direct | OK | |
| protect | protect_ObfuscateIBIN @0xb4d4e0 | copy_IbinToTemp @0x8fbcc0 | direct | OK | |
| protect | copy_IbinToTemp @0x8fbcc0 | copy_IbinToTemp_WalkFunc @0x8fbdc0 | indirect | INDIRECT | Walk callback/data-ref style path |
| protect | protect_ObfuscateIBIN @0xb4d4e0 | read_ImportSymbolConfig @0xb4f220 | direct | OK | |
| protect | protect_ObfuscateIBIN @0xb4d4e0 | build_RenameMapFromConfig @0xb4f500 | direct | OK | |
| protect | protect_ObfuscateIBIN @0xb4d4e0 | protect_JS @0xb4b380 | direct | OK | |
| protect | protect_JS @0xb4b380 | Cfvfx... @0xb4c9c0 | direct | OK | |
| protect | protect_JS @0xb4b380 | xMmuk... @0xb4c2c0 | direct | OK | |
| protect | protect_JS @0xb4b380 | pjXHY... @0xb4ce00 | direct | OK | |
| protect | protect_JS @0xb4b380 | npBB... @0xb4c660 | direct | OK | |
| protect | protect_JS @0xb4b380 | zZoWC... @0xb4c080 | direct | OK | |
| protect | protect_ObfuscateIBIN @0xb4d4e0 | cleanup_JSObfuscator @0xb4d240 | indirect | INDIRECT | Cleanup called by pjXHY… @0xb4ce00 |
| protect | pjXHY... @0xb4ce00 | cleanup_JSObfuscator @0xb4d240 | direct | OK | Actual direct edge |
| protect | protect_ObfuscateIBIN @0xb4d4e0 | protect_ImageMD5 @0xb4b0e0 | direct | OK | |
| protect | protect_ImageMD5 @0xb4b0e0 | wFTY... @0x8ff660 | direct | OK | |
| protect | protect_ObfuscateIBIN @0xb4d4e0 | obfuscate_CoreStage @0xb4e4c0 | direct | OK | |
| protect | obfuscate_CoreStage @0xb4e4c0 | obfuscate_CollectSymbols @0xb50a00 | direct | OK | |
| protect | obfuscate_CollectSymbols @0xb50a00 | machO_ProcessImages @0xb25d20 | direct | OK | |
| protect | machO_ProcessImages @0xb25d20 | machO_AnalyzeSections @0xb22740 | direct | OK | |
| protect | machO_ProcessImages @0xb25d20 | machO_CaptureSectionData @0xb21ee0 | direct | OK | |
| protect | machO_ProcessImages @0xb25d20 | machO_ApplyReplacements @0xb26740 | direct | OK | |
| protect | machO_ApplyReplacements @0xb26740 | ADPoX... @0xb25680 | direct | OK | |
| protect | machO_ProcessImages @0xb25d20 | machO_WriteSections @0xb23860 | direct | OK | |
| protect | machO_ProcessImages @0xb25d20 | nib_PatchBytes @0xb24a80 | direct | OK | |
| protect | machO_ProcessImages @0xb25d20 | nib_ParseFile @0xb06320 | direct | OK | |
| protect | obfuscate_CoreStage @0xb4e4c0 | plist_Decode @0x8fd740 | direct | OK | |
| protect | obfuscate_CoreStage @0xb4e4c0 | plist_ReplaceRecursive @0x8fed60 | indirect | INDIRECT | Via oLAERM… wrapper @0x8fece0 |
| protect | obfuscate_CoreStage @0xb4e4c0 | oLAERM... (plist wrapper) @0x8fece0 | direct | OK | Wrapper call |
| protect | oLAERM... (plist wrapper) @0x8fece0 | plist_ReplaceRecursive @0x8fed60 | indirect | INDIRECT | Decompiler-level wrapper call; not resolved in direct code-ref export |
| protect | obfuscate_CoreStage @0xb4e4c0 | howett.net/plist.Encoder.Encode @0x78daa0 | direct | OK | |
| protect | obfuscate_CoreStage @0xb4e4c0 | replace_TextSources @0xb4de40 | direct | OK | |
| protect | protect_ObfuscateIBIN @0xb4d4e0 | TFjZW... (cleanup temp dir) @0xb1c800 | direct | OK | |
| rename_resign | cmd_rename_Run @0xb78880 | rename_Apply @0xb44d60 | direct | OK | |
| rename_resign | rename_Apply @0xb44d60 | sequential rename (nuHe...) @0xb45f40 | direct | OK | |
| rename_resign | rename_Apply @0xb44d60 | random rename (KsIiw...) @0xb3e760 | direct | OK | |
| rename_resign | rename_Apply @0xb44d60 | plus rename (WNB...) @0xb45a80 | direct | OK | |
| rename_resign | rename_Apply @0xb44d60 | rename_AIWorker @0xb45620 | indirect | INDIRECT | runtime.newproc goroutine dispatch |
| rename_resign | rename_AIWorker @0xb45620 | rename_BuildAIMap @0xb45900 | direct | OK | |
| rename_resign | rename_BuildAIMap @0xb45900 | rpc_WordReplace @0xb460c0 | direct | OK | |
| rename_resign | resign_LogStart @0xb13e00 | resign_ExecIbinsign @0xb146e0 | direct | OK | |
| rename_resign | resign_ExecIbinsign @0xb146e0 | os_exec.Command @0x769000 | direct | OK | |
| rename_resign | resign_ExecIbinsign @0xb146e0 | os_exec.(*Cmd).Run @0x76a580 | direct | OK |
아래는 위에 나열된 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 실행 래퍼 역할이 커보인다.
핵심 결론 (메인 바이너리)
- 보호/난독화는 런타임 안티디버깅보다 “IBIN 내부 파일 재작성(심볼/리소스 치환)”에 초점이 있다.
- 재서명은 외부 signer 실행 경로와 내부 Mach-O codesign 구현 경로가 둘 다 존재한다.
- 보호 대상은 Mach-O 심볼군, nib/plist/text, JS, 이미지(MD5 변경)이다.
IBIN 보호/난독화 흐름 (어떻게)
- cmd_protect_Run(0xb78220) -> protect_ObfuscateIBIN(0xb4d4e0).
- protect_ObfuscateIBIN에서 validate_IbinPath(0xb42740) -> build_UniAppInfoFromConfig(0xb27e60) -> 임시 디렉토리 생성 (*_tempObf) -> copy_IbinToTemp(0x8fbcc0).
- 설정 있으면 read_ImportSymbolConfig(0xb4f220) + build_RenameMapFromConfig(0xb4f500) 후 obfuscate_CoreStage(0xb4e4c0).
- obfuscate_CoreStage는 obfuscate_CollectSymbols(0xb50a00) + machO_ProcessImages(0xb25d20) + replace_TextSources + plist_Decode 경로로 진행.
- JS 옵션 시 protect_JS(0xb4b380) 실행, 이미지 옵션 시 protect_ImageMD5(0xb4b0e0) 실행.
- 마지막에 TFjZW…(0xb1c800) -> nbLIm…(0x903fa0)로 재패키징(path/filepath.Walk 기반 zip 작성).
무엇을 보호/변형하는지
- obfuscate_CollectSymbols(0xb50a00) 로그/포맷 기준:
- ObjC methods/classes.
- Swift selectors/classes.
- File name symbols.
- machO_ProcessImages(0xb25d20) callee 기준:
- nib_ParseFile(0xb06320) + nib_PatchBytes(0xb24a80) + machO_ApplyReplacements(0xb26740).
- replace_TextSources와 plist decode/encode 경로로 텍스트·plist 계열 치환.
- protect_ImageMD5(0xb4b0e0)에서 “alter pic md5” 로그와 wFTY…(0x8ff660) 호출 확인.
JS 난독화 (무엇으로)
- protect_JS(0xb4b380)가 유효 JS 수집/복사 후 외부 도구 실행.
- xMmuk…(0xb4c2c0)에서 os_exec.Command + Cmd.Run 호출.
- pjXH…(0xb4ce00)에서 obfuscator_win_x64.exe 및 obfuscator_win_x6420250802.exe 경로/리네임 처리 확인.
재서명 흐름 (어떻게)
- resign_LogStart(0xb13e00)에서 signer 인자 구성 후 resign_ExecIbinsign(0xb146e0) 호출.
- 인자 구성은 -c(P12), -p(password), -m(mobileprovision), -e(entitlements, optional) 패턴이 확인됨.
- resign_ExecIbinsign는 os_exec.Command + Cmd.Run, stdout/stderr 버퍼 수집.
- lkASVG…(0x904e60)는 재서명 실행파일 경로 준비/파일 생성 로직.
- KyJa…(0x905340)는 Cmd.Start/Cmd.Wait 기반 실행 경로.
- UrIP…(0xb162e0)에서 SignFlag/P12/Password/Profiles/Install 요약 문자열 생성(SignFlag: %s … 포맷).
내부 코드서명 구현 (무엇으로)
- bvNR…(0xa01ec0)가 fat-arch 단위로 LKc…(0x99f5e0) 호출.
- LKc… 시그니처에 _ptr_macho_File, _ptr_codesign_Config 타입이 드러남.
- LKc… 내부에서 WTLQ…(0x925560) 호출해 서명 blob 구성.
- WTLQ…에서 CodeDirectory/slot 처리 흔적 확인.
- xref 증거:
- 0xddffb4(“failed to create CodeDirectory”) -> 0x925bd2(inside WTLQ).
- 0xde7291(“failed to get CodeDirectory blob data”) -> 0x925df9(inside WTLQ).
- 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)로 나뉘어 있고, 각 경로가 수행하는 역할이 비교적 명확해서 전체적인 구조를 이해하는 데 도움이 되었다.
다음에는 코어 부분을 좀 깊게 봐보려고 한다. 언제 가능할지 모르겠지만 전체적인 구도는 잡혔으니 좀 봐보려고 한다.
If you find any errors, please let me know by comment or email. Thank you.
