TL;DR
- Go 캐시($GOCACHE)의 액션 인덱스를 조작하면 소스 코드를 바꾸지 않고도 패키지 빌드 결과를 바꿔 프로그램 동작을 뒤집을 수 있음.
- Go 캐시는 입력 해시인 액션 ID를 출력 데이터인 패키지 아카이브에 연결하며,
-a인덱스 파일과-d데이터 파일로 구성됨. - 같은 공개 인터페이스를 가진 악성 패키지 아카이브를 오버레이(overlay)로 빌드한 뒤, 대상 인덱스가 그 아카이브를 가리키도록 수정하는 방식임.
- 로컬 파일 쓰기 권한을 가진 공격자에게는 캐시 조작보다 비밀 키 탈취가 더 나은 선택이지만, 신뢰 경계를 넘는 CI 캐시에서는 더 현실적인 위험임.
- GitHub Actions 캐시는 기본 브랜치가 비기본 브랜치의 캐시 항목을 읽지 못하게 해, 신뢰할 수 없는 PR이 기본 브랜치의 권한 있는 빌드에 악성 아카이브를 주입하는 상황을 막음.
Go 캐시의 구성과 동작
- CloudX와 공동 작성한 ‘Scaling Golang CI by Replacing actions/setup-go’ 글에서 Go 캐시를 다룬 바 있으며, GitHub를 제외하고 캐시 구조만 살펴보려는 독자를 위해 이 글에서 캐시 포이즈닝을 시연함.
- Go 캐시($GOCACHE)는 빌드 같은 요청의 원자적 일부를 나타내는 액션 ID 해시와 그에 대응하는 출력을 연결하는 파일 디렉터리임.
- 예제 프로젝트는
github.com/lukasschwab/demo모듈과truthy패키지,main.go진입점으로 구성됨.truthy.Value()는true를 반환하고,main.go는 이 값을 출력함. - 캐시가 오염되지 않았다면
go run main.go의 출력은true임. 하지만 소스 코드를 바꾸지 않고도 캐시를 조작해 출력이false가 되도록 할 수 있음. - 격리된
/demo/cache를 지정해go build ./truthy를 실행하면 캐시 파일이 생성됨. 파일은 접미사에 따라 두 종류로 나뉨. -d파일은 재사용 가능한 출력 데이터이며, 이 예제에서는 실행 파일로 링크할 수 있는 이식 가능한 중간 빌드 산출물인 패키지 아카이브임.-d파일 이름은 파일 내용의 SHA-256 해시임. 따라서 여러 패키지가truthy에 의존해도 공유 캐시에는 해당 패키지 데이터가 한 번만 저장됨.-a파일은 액션 인덱스로, 소스 코드·플래그·의존성·환경 변수 등 사용자 입력의 해시인 액션 ID를 해당 패키지 아카이브에 연결함.- 각
-a파일은 공백으로 구분된 값 다섯 개를 한 줄에 담음. v1: 현재 항상v1인 형식 버전임.- 액션 ID: 해당
-a파일 이름과 일치하는 해시임. - 출력 ID: 캐시의
-d데이터 파일을 가리키는 해시임. - 데이터 파일 크기: 바이트 단위 크기임.
- 타임스탬프: 나노초 단위 유닉스 타임스탬프임.
- 액션 ID 계산은 패키지 아카이브 생성보다 비용이 적음. Go 도구 체인은 액션 ID를 계산해 캐시를 확인하고, 대응하는
-a파일이 있으면 그 파일이 가리키는-d데이터를 재사용함. 항목이 없으면 빌드 트리의 캐시를 확인하며 패키지 아카이브를 빌드한 뒤-d와-a파일 쌍을 저장함.
캐시 항목 찾기와 악성 아카이브 만들기
- 포이즈닝을 위해서는 캐시 조회 과정을 가로채야 함.
truthy패키지 빌드의 실제 액션 ID를 찾은 다음, 그-a파일이 악성-d패키지 아카이브를 가리키도록 바꾸는 방식임. go list -export로truthy패키지의 아카이브 경로를 찾고, 그 해시를 검색해 해당 아카이브를 가리키는 액션 인덱스 파일을 확인함.- 캐시의 다른 해시 파일은
go build과정의 다른 중간 산출물임. 이론상 포이즈닝 대상이 될 수 있지만, 이 시연에는 필요하지 않음. - 악성 패키지는 원래
truthy.go와 정확히 같은 공개 인터페이스를 제공하되,Value()가false를 반환하도록 정의함. 인터페이스를 동일하게 유지하면 악성 패키지 아카이브가 원래truthy패키지와 같은 바이너리에 링크될 수 있음. - Go 도구의 오버레이 기능을 사용해
/demo/truthy/truthy.go를demo/poison/truthy.go로 대체함. 이에 따라truthy를 빌드할 때 악성 소스가 사용되고, 새로운 패키지 아카이브가 캐시에 생성됨. - 마지막으로 앞에서 찾은
truthy패키지의-a인덱스에서 정상 출력 ID를 악성 아카이브의 출력 ID로 바꿈. 저장된 데이터 파일 크기도 실제 값에 맞춰야 하며, 이 예제에서는 크기가 변하지 않음. - 캐시를 조작한 뒤 악성 소스 디렉터리를 삭제해도
go run main.go는 캐시의 악성 아카이브를 사용해false를 출력함.
실제 공격과 신뢰 경계
- 이 예제는 인위적인 시연임. 실제 공격은 내부 패키지
truthy대신 Go 표준 라이브러리의 암호화 패키지처럼 널리 쓰이는 의존성을 악성 대체물로 바꾸는 형태일 수 있음. - 알려진 패키지를 공격 대상으로 삼는 경우, 까다로운
go list캐시 점검은 일반적인 운영체제 아키텍처와 Go 버전별로 미리 계산할 수 있음. 이런 매개변수를 제외하면 캐시의 나머지 요소는 장치마다 고유하지 않음. - 기기의 파일을 쓸 수 있는 공격자라면 Go 캐시를 조작하기보다 비밀 키를 훔치는 편이 더 나은 선택임.
- 캐시 포이즈닝은 신뢰 경계를 넘어 악성 로직을 전달할 때 더 중요한 문제임. GitHub Actions 캐시는 비기본 브랜치가 기본 브랜치에서 생성된 캐시 항목을 읽도록 허용하지만, 기본 브랜치는 비기본 브랜치가 만든 캐시 항목을 읽지 못하게 함.
- 이 제한은 신뢰할 수 없는 PR이 자격 증명을 탈취하는 패키지 아카이브를 Actions 캐시에 넣고, 이후 권한이 있는 기본 브랜치 컨텍스트에서 실행하는 상황을 막기 위한 것임. PR 검사에서 통과한 Go 테스트가
main에서 다시 실행되는 이유도 여기에 있음. - CI에서 캐시 포이즈닝을 발견하기는 어려울 수 있음. PR에 캐시를 오염시키는 코드를 올린 뒤 해당 커밋을 강제 푸시로 제거하면, 포이즈닝 코드는 커밋 기록에 남지 않더라도 이후 CI 실행에서 악성 캐시 기록이 계속 사용될 수 있음.
- 빌드 캐시에 접근할 수 있는 모든 대상을 신뢰할 수 없다면, 그 캐시를 사용하는 후속 빌드도 신뢰할 수 없음.
댓글 (0)
로그인하면 이 기사에 내 생각을 남길 수 있어요