본문으로 건너뛰기
AZ Tools

ELF 바이너리 검사기

ELF 바이너리를 readelf처럼 바이트 그대로, 브라우저 안에서만 읽습니다. 헤더에서 워드 크기와 바이트 순서, OS/ABI, 아키텍처, 진입점, 그리고 파일이 재배치 가능 오브젝트인지 일반 실행 파일인지 PIE인지 공유 라이브러리인지를 알려 줍니다. PIE와 .so는 둘 다 DYN이라 헤더만으로는 구분되지 않고, 인터프리터 유무나 DF_1_PIE 플래그가 갈라 줍니다. 프로그램 헤더는 로더가 매핑하는 대상을, 동적 배열은 인터프리터 경로와 DT_NEEDED의 모든 라이브러리, 라이브러리가 스스로 내세우는 SONAME, 파일에 박힌 RPATH·RUNPATH를 알려 줍니다. 마지막 항목은 직접 빌드하지 않은 파일이라면 확인할 값입니다. 로더가 라이브러리를 찾는 위치를 바꾸기 때문입니다. 하드닝 줄은 checksec이 답하는 질문에 같은 근거로 답합니다. PIE는 타입과 DF_1_PIE에서, NX는 PT_GNU_STACK이 쓰기·실행 가능한지에서, RELRO는 PT_GNU_RELRO 유무와 BIND_NOW 설정에 따라 없음·부분·전체로 나옵니다. 심볼 제거는 SHT_SYMTAB이 없다는 뜻입니다. 동적 심볼 테이블은 남으므로 제거된 라이브러리도 내보내는 이름은 그대로 보입니다. 하지 않는 것도 둘 있습니다. 디스어셈블은 하지 않고, 스택 보호나 FORTIFY_SOURCE 사용 여부도 알려 주지 않습니다. 둘 다 헤더 플래그가 아니라 심볼 이름을 뒤져야 알 수 있기 때문입니다. 32비트와 빅엔디언도 정확히 읽으므로 라우터나 임베디드 이미지에서 꺼낸 ARM·MIPS·PowerPC 바이너리도 x86-64와 똑같이 열립니다.

사용법

  1. 바이너리를 상자에 끌어다 놓으세요. 확장자 없는 실행 파일, .so, .o, 코어 파일 모두 됩니다.
  2. 헤더에서 아키텍처와 종류를 보세요. PIE 실행 파일과 공유 오브젝트는 둘 다 DYN이고, 아래 인터프리터 줄이 둘을 구분해 줍니다.
  3. 하드닝 칩을 확인하세요. 배포판 빌드라면 보통 PIE·NX·전체 RELRO이고, 주황색 칩은 배포 전에 설명이 필요한 항목입니다.
  4. 필요 라이브러리에서 로더가 찾을 공유 라이브러리를, RUNPATH에서 바이너리가 추가한 비표준 검색 경로를 확인하세요.
  5. 크기가 필요하면 섹션 목록을 펼치세요. 코드는 .text, 상수는 .rodata이고, 디버그 정보를 남겼다면 .debug_* 섹션이 보입니다.

자주 묻는 질문

PIE 실행 파일과 공유 라이브러리는 뭐가 다른가요? 둘 다 DYN이라고 나옵니다.
ELF 타입은 실제로 같습니다. 로더가 임의 주소에 매핑할 수 있는 위치 독립 코드입니다. 차이는 실행 파일에는 동적 로더를 지정하는 PT_INTERP 프로그램 헤더가 있어(보통 /lib64/ld-linux-x86-64.so.2) 커널이 실행 방법을 안다는 점이고, 라이브러리에는 그것이 없어 다른 프로그램이 불러올 때만 적재된다는 점입니다. 요즘 툴체인은 실행 파일의 동적 플래그에 DF_1_PIE도 설정하는데, readelf가 보고하는 값이자 이 도구가 먼저 보는 값이며, 오래된 링커가 플래그를 빠뜨린 경우에는 인터프리터 유무로 판단합니다.
PIE, NX, RELRO는 실제로 무엇을 막아 주나요?
PIE는 커널이 바이너리를 무작위 기준 주소에 적재하게 해서 라이브러리뿐 아니라 프로그램 자기 코드에도 ASLR이 적용되게 합니다. 없으면 가젯 주소가 고정되어 알려집니다. NX는 스택을 실행 불가로 표시해 주입된 셸코드로 바로 점프하지 못하게 합니다. RELRO는 시작 후 재배치 테이블을 읽기 전용으로 만듭니다. 부분은 main 전에 해결되는 것만 보호하고, 전체는 모든 함수를 미리 해결해 GOT까지 읽기 전용으로 만들어 고전적인 GOT 덮어쓰기를 막습니다. 셋 다 링크 시점의 성질이라 실행하지 않고 파일에서 읽을 수 있습니다.
심볼이 제거됐다는데 함수 이름이 아직 보입니다.
심볼 테이블이 둘이기 때문입니다. 여기서 제거됐다는 것은 디버거가 쓰는 SHT_SYMTAB이 없다는 뜻이고, 그것이 strip이 지우는 대상입니다. 동적 심볼 테이블 SHT_DYNSYM은 심볼을 내보내거나 가져오는 파일이라면 남아 있어야 합니다. 없으면 동적 링크가 불가능하기 때문입니다. 그래서 제거된 라이브러리도 내보내는 이름을 모두 보여 주고, 제거된 실행 파일도 가져오는 이름을 보여 줍니다. nm은 심볼이 없다고 하는데 nm -D는 목록을 출력하는 이유가 이것입니다.
다른 아키텍처나 빅엔디언 바이너리도 읽나요?
읽습니다. 헤더 앞부분의 클래스 바이트와 데이터 바이트가 32비트인지 64비트인지, 어떤 바이트 순서인지를 알려 주고 이후 모든 필드를 그 둘을 통해 읽으므로 AArch64, ARM, MIPS, RISC-V, 빅엔디언 PowerPC 바이너리도 x86-64만큼 정확히 읽습니다. 아키텍처는 흔한 기계 종류는 이름으로, 나머지는 원시 코드로 표시합니다. 다만 그 바이너리가 여러분 기기에서 실행될지는 알려 주지 못합니다. 헤더뿐 아니라 커널과 ABI, 라이브러리에 달린 문제입니다.
빌드 ID는 어디에 쓰나요?
링커가 노트 섹션에 넣는 해시로, 그 빌드를 특정합니다. 배포판은 이것으로 바이너리와 따로 배포되는 디버그 심볼을 맞춥니다. 디버그 파일이 /usr/lib/debug/.build-id/xx/yyyy.debug에 놓이고, 코어 덤프를 받은 디버거는 바이너리가 제거된 상태여도 맞는 심볼을 찾아냅니다. 컴파일 결과가 조금이라도 바뀌면 ID도 바뀌므로 두 파일이 정말 같은 빌드인지 확인하는 데도 믿을 만합니다.
바이너리가 어딘가로 업로드되나요?
아닙니다. 브라우저의 File API로 읽고 페이지 안의 자바스크립트가 해석합니다. 서버로 보내지 않고 보낼 서버 쪽 구성 요소도 없습니다. 이 도구에서는 특히 중요한 점입니다. 검사하려는 바이너리가 업로드해도 되는 파일이 아닌 경우가 많기 때문입니다. 페이지를 연 뒤 네트워크를 끊어도 그대로 동작합니다.

관련 도구