본문 바로가기
IT생활

컴파일 후에도 주석을 유지하는 방법 (꼼수 및 우회법)

by 우물 밖 개구리. 2025. 3. 15.
반응형

컴파일 후에도 주석을 유지하는 방법 (꼼수 및 우회법)

일반적으로 주석은 컴파일 과정에서 제거되기 때문에, 실행 파일에서 직접 확인할 수 없다. 하지만 몇 가지 우회 방법을 활용하면 주석과 유사한 정보를 실행 파일에 남길 수 있다.

다음과 같은 기법을 사용하면 디컴파일 시에도 주석에 해당하는 정보를 유지할 수 있다.


1. 문자열 리터럴(String Literal)로 주석 유지

가장 간단한 방법은 주석을 문자열로 변환하여 코드에 포함하는 것이다. 컴파일러는 실행되지 않는 주석을 제거하지만, 문자열 리터럴은 유지된다.

예제: C/C++에서 문자열 리터럴 활용

const char* comment = "이 코드는 두 수를 더하는 함수입니다.";
int add(int a, int b) {
    return a + b;
}
  • comment 변수는 코드 실행에는 영향을 미치지 않지만, 바이너리 내 문자열 섹션(.rodata)에 저장되므로 디컴파일 시 확인할 수 있다.
  • 디컴파일러(Ghidra, IDA Pro 등)로 분석하면 "이 코드는 두 수를 더하는 함수입니다." 같은 문자열이 노출된다.

2. asm 또는 volatile을 이용한 강제 유지

일부 최적화 수준에서는 사용되지 않는 문자열 상수가 최적화로 인해 제거될 수 있다. 이를 방지하려면 volatile 또는 asm을 활용할 수 있다.

예제: volatile을 이용한 주석 유지 (C/C++)

volatile const char* comment = "이 코드는 두 수를 더하는 함수입니다.";
  • volatile을 사용하면 컴파일러가 최적화하지 않고 강제로 변수 유지
  • 디컴파일 시 문자열이 남아 있음

예제: asm을 이용한 주석 유지 (GCC, Clang)

__asm__("이 코드는 두 수를 더하는 함수입니다.");
  • __asm__(inline assembly)을 이용하면, 해당 문자열이 어셈블리 코드에 포함됨
  • 실행 파일을 분석하면 특정 어셈블리 섹션에 문자열이 남아 있음

3. 로그(Log) 또는 디버깅 메시지를 활용

로그 출력 함수를 활용하면 주석처럼 정보를 남길 수 있다.

예제: Python에서 print를 이용

print("DEBUG: 이 함수는 두 수를 더합니다.")  # 주석을 남기는 효과
def add(a, b):
    return a + b
  • 실행 파일을 디컴파일하면 print 출력 문자열이 남아 있음
  • 단점: 실행 시 로그가 출력될 수 있음

예제: C/C++에서 printf 또는 fprintf 활용

#include <stdio.h>

int add(int a, int b) {
    printf("DEBUG: 이 함수는 두 수를 더합니다.\n");
    return a + b;
}
  • 실행 바이너리를 분석하면 "DEBUG: 이 함수는 두 수를 더합니다." 가 남아 있음
  • 디컴파일 시 로그 출력이 포함됨

4. 매크로(Macro) 또는 애노테이션(Annotation) 사용

C/C++에서는 매크로를, Java/Python에서는 애노테이션을 활용하여 주석을 유지할 수 있다.

예제: C/C++ 매크로 활용

#define COMMENT(x) static const char* comment_##__LINE__ = x;

COMMENT("이 함수는 두 수를 더합니다.")
int add(int a, int b) {
    return a + b;
}
  • COMMENT() 매크로를 사용하여 각 라인의 주석을 문자열 변수로 변환
  • 디컴파일 시 comment_23 = "이 함수는 두 수를 더합니다." 같은 형태로 남아 있음

예제: Java 애노테이션 활용

@Retention(RetentionPolicy.RUNTIME)
@interface Comment {
    String value();
}

@Comment("이 메서드는 두 수를 더하는 역할을 합니다.")
public int add(int a, int b) {
    return a + b;
}
  • @Retention(RetentionPolicy.RUNTIME)을 적용하면 리플렉션을 통해 실행 중에도 주석 조회 가능
  • 디컴파일 시 애노테이션이 남아 있음

5. 코드 난독화(Obfuscation) 기법을 활용

컴파일러가 최적화 과정에서 문자열을 제거하는 것을 막기 위해, 난독화 기법을 사용할 수도 있다.

예제: 문자열 인코딩 후 실행 시 복호화 (C)

#include <stdio.h>

void decode_and_print() {
    char comment[] = { 73, 32, 108, 111, 118, 101, 32, 67, 111, 100, 101, 0 }; // "I love Code"
    printf("%s\n", comment);
}

int main() {
    decode_and_print();
    return 0;
}
  • 디컴파일 시 주석이 보이지 않지만, 실행 시 복구 가능
  • 리버스 엔지니어링을 어렵게 만드는 효과

결론: 주석을 유지하는 꼼수는 가능하지만, 목적에 따라 방법이 다르다

 

방법  주석 유지 여부  장점  단점
문자열 리터럴 활용 O 간단하고 효과적 최적화 시 제거될 수도 있음
volatile 또는 asm 사용 O 최적화 방지 가능 코드 가독성이 떨어질 수 있음
로그(Log) 활용 O 디버깅에 유용 실행 시 로그가 남을 수 있음
매크로 또는 애노테이션 사용 O 코드 분석 시 유용 언어에 따라 적용 방식이 다름
난독화 기법 활용 O 리버스 엔지니어링 방지 복잡하고 유지보수 어려움

가장 쉬운 방법은 문자열 리터럴을 활용하는 것이며, 디컴파일 시에도 보이게 하려면 volatile 또는 로그 출력 방식을 사용할 수 있다.
반면, 리버스 엔지니어링을 어렵게 만들고 싶다면 난독화 기법을 활용하는 것이 효과적이다.

반응형