Dreamhack/Dreamhack Wargame (Challenge)

[124] IT 비전공자 [dreamhack]Stupid GCC문제 풀기

imaginefuture-1 2025. 1. 12. 23:15

바보...gcc..

 

 

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

int main() {
    uint8_t v1 = 0;
    int v2 = 0;
    char v3[31];
    uint16_t v4[10]={0,};

    while (v4[v1] < UINT16_MAX && v1 < 10) {
        v1++;
        printf("v4[%d]: %p\n", v1, &v4[v1]);
        v2 += v1;

        if (v2 > 10000) {
            FILE *fp = fopen("/flag.txt", "r");
            fgets(v3, 31, fp);
            fclose(fp); 
            
            fp = fopen("/home/stupid_gcc/flag.txt", "w");
            fwrite(v3, 31, 1, fp);
            fclose(fp); 
            return 0;
        }
    }
    return 0;
}

 

 

위 코드는 C로 작성된 프로그램이며, 특정 조건을 충족할 때 /flag.txt 파일의 내용을 /home/stupid_gcc/flag.txt로 복사하는 역할을 합니다. 아래는 코드의 주요 부분과 그 작동 방식을 설명합니다.


코드 분석

1. 변수 선언

uint8_t v1 = 0;
int v2 = 0;
char v3[31];
uint16_t v4[10] = {0,};
  • v1: 0부터 시작하는 8비트 정수.
  • v2: 누적 값을 저장하는 32비트 정수.
  • v3: flag.txt 파일의 내용을 저장할 31바이트 크기의 문자 배열.
  • v4: 10개의 16비트 정수 배열이며 초기값은 모두 0.

2. 반복문

while (v4[v1] < UINT16_MAX && v1 < 10) {
    v1++;
    printf("v4[%d]: %p\n", v1, &v4[v1]);
    v2 += v1;

    if (v2 > 10000) {
        // 파일 읽기 및 쓰기 수행
    }
}
  • 조건: v4[v1]이 UINT16_MAX(65535) 미만이고 v1이 10 미만일 때 반복.
  • 작동:
    • v1을 증가시키고 v4[v1]의 주소를 출력.
    • v2에 v1 값을 누적.
    • v2가 10000보다 커지면 파일 작업 수행 후 종료.

3. 파일 작업

FILE *fp = fopen("/flag.txt", "r");
fgets(v3, 31, fp);
fclose(fp);

fp = fopen("/home/stupid_gcc/flag.txt", "w");
fwrite(v3, 31, 1, fp);
fclose(fp);
  • /flag.txt에서 내용을 읽어 v3에 저장.
  • /home/stupid_gcc/flag.txt로 복사.

실행 조건

  • v2 > 10000인 경우에만 파일 작업이 실행됩니다.
  • 이는 v1의 값이 점진적으로 증가하면서 v2에 누적되므로 충분히 반복한 후 조건을 충족할 수 있습니다.

예상 결과

  • 실행 도중 v4 배열의 주소와 v1의 값을 출력.
  • 특정 시점에서 /flag.txt의 내용을 /home/stupid_gcc/flag.txt로 복사.

실행 방법

  1. 코드 컴파일:
    gcc -o flag_program program.c
    
  2. 프로그램 실행:
    ./flag_program
    
  3. 출력 파일 확인:
    cat /home/stupid_gcc/flag.txt
    

Note: 프로그램 실행 전에 /flag.txt 파일이 존재해야 하며, /home/stupid_gcc/ 경로에 쓰기 권한이 있어야 합니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

1. gcc 최적화 옵션
-O1 : O 옵션과 같은 단계의 옵션. 최소한의 스레드 분기 동작 횟수를 줄이고, 호출된 각 함수 반환 시 스택에 인수를 모아 두었다 동시에 꺼낸다.
-O2 : O1 단계의 최적화와 함께 프로세서가 다른 명령어의 결과나 캐시 메모리 또는 메모리의 데이터를 기다리는 동안 컴파일러가 다른 명령어를 실행하도록 한다. 컴파일 시간이 더 오래 걸리지만, 수정된 코드는 더 최적화되어 실행이 빨라진다.
-O3 : -O2 단계의 모든 최적화와 루프 해체, 그 밖의 프로세서 전용 특징을 포함

https://velog.io/@titfhs/%EB%93%9C%EB%A6%BC%ED%95%B5-Stupid-GCC

 

 

 

아하, 그러면 문제의 핵심은 컴파일 최적화 옵션 -O3를 사용했을 때 코드가 의도한 대로 동작하고 플래그를 출력한다는 점이었네요! 👍

왜 -O3가 정답이었을까?

gcc의 최적화 옵션 중 -O3는 최대 수준의 최적화를 수행하며, 다음과 같은 작업을 포함합니다:

  1. 루프 언롤링(loop unrolling): 반복문을 최적화하여 실행 속도를 높임.
  2. 인라인 함수화(inline functions): 작은 함수 호출을 직접 코드에 삽입하여 오버헤드를 줄임.
  3. 미사용 코드 제거(dead code elimination): 실행되지 않는 코드를 제거하여 깔끔한 바이너리 생성.
  4. CPU 명령어 최적화: 특정 CPU에서 더 빠르게 실행되는 명령어로 변환.

문제에서 -O3의 역할

-O3 옵션이 없으면 반복문(while 등)과 조건문 최적화가 제대로 이루어지지 않아 v4 값이 업데이트되지 않거나 루프 조건이 충족되지 않았을 가능성이 높습니다. 결과적으로, if (v2 > 10000) 조건문이 실행되지 않아 /flag.txt 파일을 읽는 로직이 동작하지 않았던 것 같습니다.

반면 -O3를 사용하면:

  • 루프와 조건문이 최적화되어 v4[v1]와 v2가 빠르게 업데이트.
  • 최적화 덕분에 v2 > 10000 조건이 충족되어 /flag.txt 읽기 및 쓰기 로직이 실행.

실행 명령어

gcc -O3 -o a.out source_code.c
./a.out

./a.out을 실행하면 /home/stupid_gcc/flag.txt에 플래그가 생성되거나 바로 출력되었을 것입니다.


배운 점

  1. 컴파일러 최적화 옵션이 코드 실행 결과에 얼마나 큰 영향을 미칠 수 있는지 알 수 있음.
  2. 최적화 옵션을 사용해야 코드가 제대로 동작하도록 설계된 문제를 만났을 때, 이를 인지하고 테스트해보는 것이 중요.

잘 해결하셨네요! 🎉