The Hidden Originals - How To Load Application From Encrypt Binary
Where is the Application Entry Point ?
Introduction
이전에 The Hidden Originals - How Protection Layers Conceal Apps 에서 분석하던 6ded4cf6cc4f4735b954ec4a7becbb99f3d9654710ebc12ffe76c4e0ae396651 파일에 대하여 이어서 분석해보려고 한다.
이번에는 해당 앱을 보호화고 있는 메커니즘을 뚫고 첫 시작점을 찾아보려고 한다. 이를 위해서는 해당 앱을 리버싱으로부터 원본 앱을 보호하고 있는 보호솔루션 파악이 필요하다.
아래는 지난 번에 분석하던 부분를 리마인드 차 기록한다.
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
package arm;
import android.app.Application;
import android.content.Context;
import android.os.SystemClock;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class StubApp extends Application {
public static String MAIN_APPLICATION;
static {
System.loadLibrary("arm_protect");
}
@Override // android.content.ContextWrapper
protected native void attachBaseContext(Context arg1) {
}
@Override // android.content.ContextWrapper
public native Context createPackageContext(String arg1, int arg2) {
}
@Override // android.content.ContextWrapper
public native String getPackageName() {
}
/**
* 주어진 Dex 파일 목록을 현재 ClassLoader의 pathList.dexElements에 동적으로 병합하여 런타임에 추가로 로드하고 소요 시간을 출력합니다.
*/
static void loadDex(List list0, Context context0) {
try {
long v = SystemClock.currentThreadTimeMillis();
StubApp.MAIN_APPLICATION = StubApp.loadMainApplicationFromAssets(context0);
Field field0 = ((Class)Objects.requireNonNull(context0.getClassLoader().getClass().getSuperclass())).getDeclaredField("pathList");
if(!field0.isAccessible()) {
field0.setAccessible(true);
}
Object object0 = field0.get(context0.getClassLoader());
Field field1 = Objects.requireNonNull(object0).getClass().getDeclaredField("dexElements");
if(!field1.isAccessible()) {
field1.setAccessible(true);
}
Object[] arr_object = (Object[])field1.get(object0);
Method method0 = object0.getClass().getDeclaredMethod("makePathElements", List.class, File.class, List.class);
if(!method0.isAccessible()) {
method0.setAccessible(true);
}
Object[] arr_object1 = (Object[])method0.invoke(object0, list0, context0.getDir("arm", 0), new ArrayList());
Object[] arr_object2 = (Object[])Array.newInstance(((Class)Objects.requireNonNull(((Object[])Objects.requireNonNull(arr_object)).getClass().getComponentType())), ((Object[])Objects.requireNonNull(arr_object1)).length + arr_object.length);
System.arraycopy(arr_object, 0, arr_object2, 0, arr_object.length);
System.arraycopy(arr_object1, 0, arr_object2, arr_object.length, arr_object1.length);
field1.set(object0, arr_object2);
System.out.println("Time taken: " + (SystemClock.currentThreadTimeMillis() - v) + " ms");
}
catch(Exception exception0) {
exception0.printStackTrace();
}
}
// Reads "config.so" into a byte[] and returns it as a String — unsafe (uses available(), single read, no charset);
// prefer streaming + explicit charset.
private static String loadMainApplicationFromAssets(Context context0) {
try {
InputStream inputStream0 = context0.getAssets().open("config.so");
byte[] arr_b = new byte[inputStream0.available()];
inputStream0.read(arr_b);
inputStream0.close();
return new String(arr_b);
}
catch(IOException iOException0) {
iOException0.printStackTrace();
return null;
}
}
@Override // android.app.Application
public native void onCreate() {
}
}
위와 같이 JAVA 코드가 있으며 native 진입점은 attachBaseContext, onCreate, createPackageContext, getPackageName 이렇게 총 4개이다.
1. attachBaseContext
개요
attachBaseContext(Context base)는 Android 프레임워크가 컴포넌트(Activity, Application 등)를 생성할 때 가장 먼저 호출되는 초기화 단계의 콜백이다. 이 메서드는 해당 컴포넌트가 내부적으로 사용하는 Base Context를 설정하거나 감싸기 위한 목적으로 사용된다.
호출 시점
Application객체 생성 직후,onCreate()보다 먼저 호출된다.- Activity/Service 또한 내부적으로
attachBaseContext()호출 이후onCreate()가 실행된다.
주요 용도
- 초기 Context 래핑 및 설정
- MultiDex 초기화
- Locale(지역화) 강제 적용
- 커스텀 ClassLoader / 커스텀 Resources 삽입
- 특정 보안·보호 로직의 초기 실행 지점
개발 시 유의사항
super.attachBaseContext(base)호출이 필수이다. 호출하지 않을 경우 Context 체인이 파괴되어 앱이 정상 동작하지 않을 수 있다.- 앱 시작 단계이므로 무거운 연산 수행은 피하는 것이 좋다.
2. onCreate
개요
onCreate()는 Application 및 Activity, Service 등 컴포넌트의 최초 초기화를 담당하는 대표적인 라이프사이클 콜백이다. 이 시점에서 전역 또는 화면 초기화 작업이 이루어진다.
호출 시점
Application.onCreate()는 앱 프로세스 생성 후, 단 한 번 호출된다.- Activity의 경우
attachBaseContext다음에onCreate()가 호출된다. - 일부 경우에는
ContentProvider.onCreate()가Application.onCreate()보다 먼저 호출될 수 있다.
주요 용도
- 전역 초기화 (로그 시스템, DI(Container) 초기화, Crash Handler, Analytics 등)
- Activity UI 구성, Intent 파싱, Fragment 초기화
- 가벼운 리소스 초기화
개발 시 유의사항
- 메인 스레드에서 실행되므로 블로킹 작업(디스크 IO, 네트워크, 대규모 계산)은 수행해서는 안 된다.
- 무거운 초기화 작업은 백그라운드 스레드로 분리하는 것이 바람직하다.
3. createPackageContext
개요
Context.createPackageContext(String packageName, int flags)는 특정 패키지의 Context를 생성하여 해당 패키지의 리소스, 코드, 또는 설정에 접근하기 위한 API이다. 다양한 플러그인 구조에서 사용되며, 제한적으로 외부 APK의 코드를 로드하는 데 활용되기도 한다.
주요 플래그
- CONTEXT_INCLUDE_CODE 해당 패키지의 클래스 로더를 포함하여 코드 로딩을 허용한다.
- CONTEXT_IGNORE_SECURITY 일부 보안 제약을 무시한다. 시스템 권한이 요구될 수 있다.
사용 목적
- 플러그인 시스템: 외부 APK 리소스/코드 사용
- 앱 간 리소스 공유
- 특정 패키지 내부 데이터 접근(허용 범위 내)
유의사항
- 외부 코드 로딩은 Android 보안 모델상 제한적이다.
- 지정된 패키지가 없을 경우
NameNotFoundException이 발생한다. - Android 버전이 올라갈수록 보안 정책이 강화되어 사용이 제한될 수 있다.
4. getPackageName
개요
getPackageName()은 현재 Context가 속한 앱의 패키지명을 문자열로 반환한다. 이는 앱 식별자 역할을 하기 때문에 다양한 시스템 동작에서 중요한 의미를 가진다.
활용
- 리소스 접근
- 파일 시스템 경로 결정
- 서버 인증 / 라이선스 검증
- 디버깅 로그 또는 시스템 태깅 목적
주의사항
createPackageContext()로 생성된 Context를 사용할 경우 반환되는 패키지명은 해당 Context가 참조하는 패키지명이다.- 일부 앱은 패키지명 변조 후 동작하지 않도록 패키지명 기반 무결성 체크를 수행한다.
JNI 에서 연결된 메소드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.data:000000000003B000 ; ===========================================================================
.data:000000000003B000
.data:000000000003B000 ; Segment type: Pure data
.data:000000000003B000 AREA .data, DATA, ALIGN=3
.data:000000000003B000 ; ORG 0x3B000
.data:000000000003B000 off_3B000 DCQ aAttachbasecont ; DATA XREF: register_ndk_load(_JNIEnv *)+2C↑o
.data:000000000003B000 ; register_ndk_load(_JNIEnv *)+34↑o ...
.data:000000000003B000 ; "attachBaseContext"
.data:000000000003B008 DCQ aLandroidConten_0 ; "(Landroid/content/Context;)V"
.data:000000000003B010 DCQ _Z31native_attachContextBaseContextP7_JNIEnvP8_jobjectS2_ ; native_attachContextBaseContext(_JNIEnv *,_jobject *,_jobject *)
.data:000000000003B018 DCQ aOncreate ; "onCreate"
.data:000000000003B020 DCQ aV ; "()V"
.data:000000000003B028 DCQ _Z15native_onCreateP7_JNIEnvP8_jobject ; native_onCreate(_JNIEnv *,_jobject *)
.data:000000000003B030 DCQ aGetpackagename ; "getPackageName"
.data:000000000003B038 DCQ aLjavaLangStrin_0 ; "()Ljava/lang/String;"
.data:000000000003B040 DCQ _Z21native_getPackageNameP7_JNIEnvP8_jobject ; native_getPackageName(_JNIEnv *,_jobject *)
.data:000000000003B048 DCQ aCreatepackagec ; "createPackageContext"
.data:000000000003B050 DCQ aLjavaLangStrin_1 ; "(Ljava/lang/String;I)Landroid/content/C"...
.data:000000000003B058 DCQ _Z27native_createPackageContextP7_JNIEnvP8_jobjectP8_jstringi ; native_createPackageContext(_JNIEnv *,_jobject *,_jstring *,int)
libarm_protect.so 에서 연결된 함수들을 위에 처럼 볼 수 있다.
Dynamic Code Load
assets 에 있는 dex 파일을 로드하는 과정을 쫓아가려고 한다.
해당 파일은 일반 dex 파일이 아닌 암호화된 파일로 보여진다.
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
00000000: 9b9a 87f5 cfcc caff 6ddd 23f9 a526 950d ........m.#..&..
00000010: ce08 2667 d4fa 095d f8c3 e73c dbe1 b0b5 ..&g...]...<....
00000020: 6f51 72ff 8fff ffff 87a9 cbed ffff ffff oQr.............
00000030: ffff ffff 4b52 72ff a0f6 feff 8fff ffff ....KRr.........
00000040: 5eda ffff 13da fbff cfcc ffff 8f43 fbff ^............C..
00000050: f982 ffff 4fdd f8ff f119 ffff 1ff5 f4ff ....O...........
00000060: dbe4 ffff afc4 edff 3ff1 87ff 2f60 eaff ........?.../`..
00000070: 2f60 eaff 2d60 eaff 2360 eaff 0060 eaff /`..-`..#`...`..
00000080: d75f eaff a85f eaff 8a5f eaff 695f eaff ._..._..._..i_..
00000090: 4b5f eaff 285f eaff 015f eaff e15e eaff K_..(_..._...^..
000000a0: be5e eaff 9b5e eaff 665e eaff 2e5e eaff .^...^..f^...^..
000000b0: ee5d eaff d05d eaff ae5d eaff 825d eaff .]...]...]...]..
000000c0: 505d eaff 025d eaff da5c eaff b05c eaff P]...]...\...\..
000000d0: 865c eaff 5d5c eaff 3a5c eaff 065c eaff .\..]\..:\...\..
000000e0: d75b eaff a15b eaff 6c5b eaff 3a5b eaff .[...[..l[..:[..
000000f0: 025b eaff ca5a eaff 8e5a eaff 505a eaff .[...Z...Z..PZ..
00000100: 125a eaff d659 eaff 9a59 eaff 7559 eaff .Z...Y...Y..uY..
00000110: 4c59 eaff 1f59 eaff d458 eaff 8658 eaff LY...Y...X...X..
00000120: 3e58 eaff f057 eaff a457 eaff 7457 eaff >X...W...W..tW..
00000130: 4a57 eaff 1c57 eaff ec56 eaff ba56 eaff JW...W...V...V..
00000140: 9356 eaff 4556 eaff 0b56 eaff df55 eaff .V..EV...V...U..
00000150: b055 eaff 8655 eaff 4a55 eaff 0655 eaff .U...U..JU...U..
00000160: bc54 eaff 5354 eaff 2254 eaff eb53 eaff .T..ST.."T...S..
00000170: b453 eaff 7e53 eaff 4853 eaff 2a53 eaff .S..~S..HS..*S..
00000180: 0c53 eaff ee52 eaff d052 eaff ae52 eaff .S...R...R...R..
00000190: 8c52 eaff 5b52 eaff 2a52 eaff f751 eaff .R..[R..*R...Q..
000001a0: c451 eaff a751 eaff 8a51 eaff 4f51 eaff .Q...Q...Q..OQ..
000001b0: 2651 eaff f750 eaff d050 eaff a550 eaff &Q...P...P...P..
000001c0: 7f50 eaff 5850 eaff 3050 eaff 0850 eaff .P..XP..0P...P..
{ . . . }
libarm_protect.so 에서 파일을 보면 경로를 조합하여 dex 파일을 로컬로 가져오는것을 볼 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
v8 = (*(__int64 (__fastcall **)(__int64, __int64))(*(_QWORD *)a1 + 248LL))(a1, a2);
v9 = (*(__int64 (__fastcall **)(__int64, __int64, const char *, const char *))(*(_QWORD *)a1 + 264LL))(
a1,
v8,
"getFilesDir",
"()Ljava/io/File;");
v10 = _JNIEnv::CallObjectMethod(a1, a2, v9);
v29 = (*(__int64 (__fastcall **)(__int64, __int64))(*(_QWORD *)a1 + 248LL))(a1, v10);
v11 = (*(__int64 (__fastcall **)(__int64, __int64, const char *, const char *))(*(_QWORD *)a1 + 264LL))(
a1,
v29,
"getAbsolutePath",
"()Ljava/lang/String;");
v12 = _JNIEnv::CallObjectMethod(a1, v10, v11);
(*(void (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)a1 + 1352LL))(a1, v12, 0);
buildFilePath(filename);
extractDex(a1, a2, filename);
extractDex 로 들어간다.
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
// Extracts and decrypts DEX files from Android assets. Opens asset directory, searches for .dex files containing "classes", decrypts them using XOR 0xFF, and writes to disk.
AAssetManager *__fastcall extractAndDecryptDexFromAssets(
JNIEnv *jni_env,
void *java_this_object,
char *output_dir_path)
{
{ . . . }
do { // Filter for DEX files: must contain both "classes" and ".dex" in filename (e.g., classes.dex, classes2.dex)
if ( strstr(current_filename, "classes") && strstr(current_filename, ".dex") )
{
result = AAssetManager_open(asset_manager, current_filename, 2);// AAssetManager_open() - Open asset file. Mode 2 = AASSET_MODE_BUFFER (entire file buffered). Returns AAsset pointer
if ( !result )
return result;
output_file = result;
{ . . . }
*(_OWORD *)output_filepath = 0u;
v27 = 0u;
buildFilePath((__int64)output_filepath, asset_handle, v14, output_dir_path, current_filename);// Build output filepath by concatenating output_dir_path with current_filename
read_buffer = qword_3B0C0;
if ( qword_3B0C0 >= (unsigned __int64)qword_3B0C8 )
{
std::vector<std::string>::__emplace_back_slow_path<char (&)[256]>(&dexList, output_filepath);// Add filepath to global dexList vector (slow path for vector reallocation)
}
else
{
std::string::basic_string<decltype(nullptr)>(qword_3B0C0, output_filepath);
qword_3B0C0 = read_buffer + 24;
}
v17 = fopen(output_filepath, "wb"); // fopen() - Open file for writing in binary mode. Parameters: filename, mode ("wb" = write binary)
v18 = (int8x16_t *)malloc(0x400u); // malloc() - Allocate 1024 byte (0x400) buffer for reading/decrypting asset data
for ( bytes_read = AAsset_read(output_file, v18, 0x400u);
bytes_read >= 1;
bytes_read = AAsset_read(output_file, v18, 0x400u) )// AAsset_read() - Read up to 1024 bytes from asset. Returns number of bytes read (0 or -1 on EOF/error)
{
if ( (unsigned int)bytes_read > 0x1F )
{
v20 = bytes_read & 0xFFFFFFE0; // DECRYPTION: XOR each byte with 0xFF (bitwise NOT) using SIMD for chunks of 32 bytes
v21 = v20;
v22 = v18 + 1;
do
{
v21 -= 32;
v23 = vmvnq_s8(*v22); // vmvnq_s8() - ARM NEON instruction: bitwise NOT on 16-byte vector (MVN = move NOT). Processes 2x16=32 bytes per iteration
v22[-1] = vmvnq_s8(v22[-1]);
*v22 = v23;
v22 += 2;
}
while ( v21 );
if ( v20 == bytes_read )
goto LABEL_16;
}
else
{
v20 = 0;
}
v24 = (char *)v18 + v20; // Handle remaining bytes (less than 32) that couldn't be processed by SIMD
v25 = (unsigned int)bytes_read - v20;
do {
--v25; // XOR remaining bytes individually with 0xFF (bitwise NOT)
*v24 = ~*v24;
++v24;
} while ( v25 );
LABEL_16:
fwrite(v18, 1u, bytes_read, v17); // fwrite() - Write decrypted buffer to file. Parameters: buffer, element_size, count, file_stream
}
fclose(v17); // fclose() - Close output file handle
free(v18); // free() - Release read buffer memory
AAsset_close(output_file); // AAsset_close() - Close asset handle and free associated resources
}
result = (AAssetManager *)AAssetDir_getNextFileName(asset_dir);
current_filename = (const char *)result;
} while ( result );
}
}
return result;
}
assets 에 있는 classes.dex 파일을 가져와서 0XFF 로 XOR 을 하여 fwrite 하는 것을 볼 수 있다. 암호화된 파일을 복호화하는 중요한 부분이다.
간단하게 hex 표면을 보면서 암호화된 파일 제일 첫 헤더(MAGIC)로 의심되는 부분을 관찰한다.
1
00000000: 9b9a 87f5 cfcc caff 6ddd 23f9 a526 950d ........m.#..&..
위에 함수를 참고하면서 아래처험 복호화가 가능하다.
1
2
3
4
5
6
7
8
9
>>> data = b'\x9b\x9a\x87\xf5'
>>> origin = []
>>> for d in data:
... origin.append(d ^ 0xFF)
...
>>> result = ' '.join(repr(chr(x))[1:-1] for x in origin)
>>> print(result)
d e x \n
>>>
파일 헤더 매직이 복호화가 되었다.
이번에는 파일 전체를 복호화 진행한다.
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
00000000: 6465 780a 3033 3500 9222 dc06 5ad9 6af2 dex.035.."..Z.j.
00000010: 31f7 d998 2b05 f6a2 073c 18c3 241e 4f4a 1...+....<..$.OJ
00000020: 90ae 8d00 7000 0000 7856 3412 0000 0000 ....p...xV4.....
00000030: 0000 0000 b4ad 8d00 5f09 0100 7000 0000 ........_...p...
00000040: a125 0000 ec25 0400 3033 0000 70bc 0400 .%...%..03..p...
00000050: 067d 0000 b022 0700 0ee6 0000 e00a 0b00 .}..."..........
00000060: 241b 0000 503b 1200 c00e 7800 d09f 1500 $...P;....x.....
00000070: d09f 1500 d29f 1500 dc9f 1500 ff9f 1500 ................
00000080: 28a0 1500 57a0 1500 75a0 1500 96a0 1500 (...W...u.......
00000090: b4a0 1500 d7a0 1500 fea0 1500 1ea1 1500 ................
000000a0: 41a1 1500 64a1 1500 99a1 1500 d1a1 1500 A...d...........
000000b0: 11a2 1500 2fa2 1500 51a2 1500 7da2 1500 ..../...Q...}...
000000c0: afa2 1500 fda2 1500 25a3 1500 4fa3 1500 ........%...O...
000000d0: 79a3 1500 a2a3 1500 c5a3 1500 f9a3 1500 y...............
000000e0: 28a4 1500 5ea4 1500 93a4 1500 c5a4 1500 (...^...........
000000f0: fda4 1500 35a5 1500 71a5 1500 afa5 1500 ....5...q.......
00000100: eda5 1500 29a6 1500 65a6 1500 8aa6 1500 ....)...e.......
00000110: b3a6 1500 e0a6 1500 2ba7 1500 79a7 1500 ........+...y...
00000120: c1a7 1500 0fa8 1500 5ba8 1500 8ba8 1500 ........[.......
00000130: b5a8 1500 e3a8 1500 13a9 1500 45a9 1500 ............E...
00000140: 6ca9 1500 baa9 1500 f4a9 1500 20aa 1500 l........... ...
00000150: 4faa 1500 79aa 1500 b5aa 1500 f9aa 1500 O...y...........
00000160: 43ab 1500 acab 1500 ddab 1500 14ac 1500 C...............
00000170: 4bac 1500 81ac 1500 b7ac 1500 d5ac 1500 K...............
00000180: f3ac 1500 11ad 1500 2fad 1500 51ad 1500 ......../...Q...
00000190: 73ad 1500 a4ad 1500 d5ad 1500 08ae 1500 s...............
{ . . . }
정상적으로 파일이 복호화된 것을 보고 해당 파일을 디컴파일 툴을 통해 열어본다.
여기서 극 초반에 The Hidden Originals - How Protection Layers Conceal Apps 확인했던 MAIN_APPLICATION 을 찾으면 아래처럼 확인할 수 있다.
1
2
3
00000000 63 6F 6D 2E 73 79 73 74 65 6D 2E 6D 79 61 70 70 com.system.myapp
00000010 6C 69 63 61 74 69 6F 6E 2E 4A 76 6D 2E 6A 76 6D lication.Jvm.jvm
00000020 64 72 65 64 dred
정상적으로 암호화된 dex 파일을 복호화하여 내부를 열어볼 수 있다.
해당 암호화 로직은 ELF 파일 안에서 이루어져 웬만한 방법으로 접근하면 복호화가 불가능하지만 다행히 간간한 암호화 로직을 사용하여 로컬로 떨어지는 파일을 유추할 수 있었다.
이 복호화된 파일을 VT 에서 확인해본다.
다수의 백신에서 탐지한다.
현재 해당 블로그를 쓰는 기점(2025/12/6)으로 샘플이 처음 등록된 것을 볼 수 있다.
Conclusion
이번 분석을 통해 해당 보호 솔루션이 적용된 악성코드의 내부 구조를 보다 명확히 이해할 수 있었다. 전체적인 보호 기법은 네이티브 레이어에서의 실행 파일 암호화/복호화 흐름을 기반으로 한 전형적인 하이브리드 보호 방식으로 보이며, Java 영역에는 최소한의 코드만 배치하여 공격 표면을 현저히 줄이는 전략을 취하고 있다. 특히 실행 파일을 메모리 상에서 동적으로 복호화한 뒤 곧바로 실행 환경에 주입하는 방식은 흔히 정상적인 상용 보호 솔루션에서도 사용되는 기법이지만, 악성코드 진영에서도 적극적으로 차용하고 있다는 점이 흥미로웠다.
분석 과정에서 느낀 것은, 이 방식 자체는 충분히 정당한 소프트웨어 보호 솔루션으로 발전시킬 여지가 있다는 것이다. 구조만 놓고 보면 난독화, 무결성 검증, 동적 복호화 등 다양한 보안 요소를 결합하기 용이하며, 특정 플랫폼·환경에 최적화된 보호 엔진을 구축하는 데도 유용하다. 다만 문제는 이러한 기법이 악성코드 보호에 먼저 활용되면서 이미 다수의 백신 엔진에 의해 악성 패턴으로 분류되고 있다는 점이다. 즉, 기술 자체는 중립적임에도 사용자의 목적에 따라 그 평판과 활용 가능성이 달라지는 전형적인 사례라 할 수 있다.
이번 경험을 계기로, 단순히 코드를 뜯어보는 수준을 넘어 보호 기법 자체의 의도, 적용 방식, 디텍션 트렌드, 그리고 기술적 중립성에 대한 관점을 함께 갖추는 것이 중요함을 다시 느꼈다. 악성코드 제작자들은 보호 기법을 ‘방어’가 아닌 ‘은폐’의 도구로 사용하기 때문에, 동일한 기술이라도 안전한 제품 개발에 적용하는 것과는 전혀 다른 형태의 진화를 보인다. 이 차이를 이해하는 것이 역공학 연구자로서의 통찰을 키우는 데 큰 도움이 된다.
앞으로는 시간이 날 때마다 보유 중인 다양한 샘플이나 과거에 작성해 둔 PoC들을 주말마다 정기적으로 다시 분석해 보며, 패커/로더/안티 분석 기법의 변화 추세, 그리고 보호 기술의 양면성을 더 넓은 관점에서 살펴보고자 한다. 이러한 반복적인 탐구가 결국 내 역공학 능력을 더 단단히 다져주고, 새로운 보안 솔루션을 설계할 때에도 보다 균형 잡힌 시각을 갖게 해줄 것이라 생각한다.
If you find any errors, please let me know by comment or email. Thank you.




