IT관련/C#

PE 의 파일일시를 읽을 수 없을때...

파란하늘999 2025. 10. 10. 14:02

C#의 .net5이상부터 Determiniistic이 true로 셋팅되어 있어서 발생.

 

.csproj에 기본 셋팅이 되어 있으니 false로 셋팅하면 문제해결.

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <Deterministic>true</Deterministic>
    ...
  </PropertyGroup>
  ...
</Project>

 

 

Deterministic 빌드의 유래와 역사

기원과 배경

1. 초기 문제점 (2000년대 초반)

전통적인 빌드 시스템의 문제:

같은 소스 코드 → 다른 바이너리

원인:

  • 빌드 시간 타임스탬프
  • 랜덤 GUID (Module Version ID)
  • 빌드 환경 차이 (경로, 환경변수 등)
  • 컴파일러 내부의 비결정적 요소

문제:

개발자 A의 빌드: MyApp.exe (10MB, Hash: ABC123...)
개발자 B의 빌드: MyApp.exe (10MB, Hash: DEF456...)

→ 같은 소스인데 파일이 다름!
→ 코드 리뷰 어려움
→ 보안 검증 불가능
→ 캐싱 효과 없음

2. 재현 가능한 빌드의 필요성 (2010년대 초)

주요 동기:

보안 및 신뢰성

문제: "이 바이너리가 정말 이 소스에서 나왔나?"
→ 백도어, 악성코드 삽입 검증 불가능
→ 공급망 공격(Supply Chain Attack) 위험

실제 사례:

  • Ken Thompson의 "Trusting Trust" 공격 (1984)
  • 컴파일러 자체에 백도어를 심을 수 있음
  • 소스 코드만 봐서는 알 수 없음

빌드 캐싱 및 효율성

일반 빌드:
src/File.cs (수정 안 됨)
→ File.dll (타임스탬프 변경됨)
→ 캐시 무효화
→ 전체 재빌드 필요

Deterministic 빌드:
src/File.cs (수정 안 됨)
→ File.dll (완전히 동일)
→ 캐시 히트
→ 빌드 건너뜀

3. 오픈소스 운동의 영향 (2013년~)

Reproducible Builds 프로젝트 시작:

https://reproducible-builds.org/

목표:

  • 누구나 소스에서 동일한 바이너리 생성 가능
  • 빌드 과정의 투명성 확보
  • 악성코드 삽입 방지

주요 참여 프로젝트:

  • Debian Linux (선구자, 2013년 시작)
  • Tor Browser
  • Bitcoin Core
  • F-Droid (Android 앱)

4. Microsoft의 도입 (2015년~)

Roslyn 컴파일러 개발 (C# 6.0 시대)

// 2015년, Roslyn에 Deterministic 옵션 추가
csc /deterministic MyApp.cs

초기 목적:

  1. NuGet 패키지 캐싱 최적화
  2. 빌드 서버 효율성 향상
  3. Git 히스토리 정리 (바이너리 변경 추적)

Visual Studio 2015 업데이트

<!-- MSBuild 14.0부터 지원 -->
<PropertyGroup>
  <Deterministic>true</Deterministic>
</PropertyGroup>

5. 기본값 변경 (2017년~)

.NET Core 출시 (.NET Core 1.0~)

 
<!-- .NET Core SDK 스타일 프로젝트 -->
<Project Sdk="Microsoft.NET.Sdk">
  <!-- Deterministic이 기본값 true -->
</Project>

변경 이유:

  • 클라우드 빌드 환경 최적화
  • Docker 컨테이너 레이어 캐싱
  • CI/CD 파이프라인 효율성

타임라인:

2015: Roslyn에 /deterministic 옵션 추가 (선택적)
2016: NuGet 패키지 빌드에 권장
2017: .NET Core 2.0, 기본값 true
2019: .NET Core 3.0, 강력히 권장
2020: .NET 5+, 완전히 기본값

기술적 구현

1. PE 헤더 TimeDateStamp

전통적 방식:

 
// 컴파일러 내부
time_t buildTime = time(NULL);  // 현재 시간
PE_Header.TimeDateStamp = buildTime;

Deterministic 방식:

 
// 컴파일러 내부
uint8_t hash[32];
SHA256(sourceCode, hash);
PE_Header.TimeDateStamp = *(uint32_t*)hash;  // 해시의 첫 4바이트

2. Module Version ID (MVID)

// 전통적 방식
module.ModuleVersionId = Guid.NewGuid();

// Deterministic 방식
var contentHash = ComputeContentHash(assemblyBytes);
module.ModuleVersionId = DeriveGuidFromHash(contentHash);

3. PDB (디버그 정보)

일반 PDB:
- Age: 증가하는 카운터
- GUID: 랜덤
- Timestamp: 실제 시간

Deterministic PDB:
- Age: 1 (고정)
- GUID: 내용 기반
- Timestamp: 내용 기반

산업 표준화

1. NIST (미국 국립표준기술연구소)

NIST SP 800-218: Secure Software Development Framework
→ Reproducible Builds 권장

2. SLSA (Supply-chain Levels for Software Artifacts)

Level 2 이상: Reproducible Builds 필요
Google, Linux Foundation 주도

3. 주요 언어별 지원

언어 Deterministic 지원 시작 연도
C# ✅ Roslyn 2015
Rust ✅ 기본값 2015
Go ✅ go build -trimpath 2018
Java ✅ javac --release 2014
C/C++ ⚠️ 부분적 (GCC -frandom-seed) 2016

실제 사례

1. NuGet 패키지 서명

문제: 같은 소스로 빌드했는데 서명이 다름
해결: Deterministic 빌드 → 동일한 바이너리 → 동일한 서명

2. Docker 이미지 최적화

 
# 일반 빌드
COPY src/ /app/src/
RUN dotnet build
→ 매번 새 레이어 (타임스탬프 변경)

# Deterministic 빌드
COPY src/ /app/src/
RUN dotnet build /p:Deterministic=true
→ 소스 안 바뀌면 레이어 재사용

3. Git LFS 절약

일반 빌드:
매 커밋마다 새 바이너리 → Git LFS 용량 증가

Deterministic 빌드:
소스 안 바뀌면 바이너리도 동일 → Git LFS 용량 절약

철학적 배경

"Trust, but Verify" (신뢰하되 검증하라)

개발자: "이 바이너리는 깨끗합니다"
사용자: "정말? 직접 빌드해서 확인하겠습니다"

Deterministic 빌드가 없으면:
→ 소스에서 빌드해도 다른 바이너리
→ 검증 불가능

Deterministic 빌드가 있으면:
→ 소스에서 빌드하면 동일한 바이너리
→ 검증 가능 ✅

현재 상황 (2024년)

<!-- .NET 8+ 기본 설정 -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <Deterministic>true</Deterministic>  <!-- 자동 -->
    <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>  <!-- CI 최적화 -->
  </PropertyGroup>
</Project>

장점:

  • ✅ 빌드 캐싱 최적화
  • ✅ 보안 검증 가능
  • ✅ CI/CD 효율성 향상
  • ✅ Docker 레이어 최적화

단점:

  • ❌ 실제 빌드 시간 알 수 없음
  • ❌ PE 헤더의 날짜가 의미 없음
  • ❌ 디버깅 시 혼란 가능

요약

Deterministic 빌드의 유래:

  1. 2000년대: 재현 불가능한 빌드 문제 인식
  2. 2013: Debian의 Reproducible Builds 프로젝트 시작
  3. 2015: Microsoft Roslyn에 /deterministic 추가
  4. 2017: .NET Core에서 기본값으로 채택
  5. 현재: 산업 표준, 보안 필수 요소

핵심 목적:

  • 🔒 보안: 공급망 공격 방지
  • 효율: 빌드 캐싱 최적화
  • 🔍 투명성: 빌드 과정 검증 가능

오늘날 Deterministic 빌드는 보안과 효율성의 표준이 되었습니다!

 

반응형