본문으로 건너뛰기
AZ Tools

파이썬 피클 검사기

피클은 사실 데이터 형식이 아니라 작은 스택 머신용 프로그램이고, pickle.load는 그 프로그램을 실행합니다. 값이 나오기도 전에 오프코드가 아무 모듈이나 임포트하고, 아무 함수나 호출하고, 아무 클래스나 만들 수 있습니다. 그래서 이 도구는 절대 언피클링하지 않습니다. pickletools.dis가 읽는 방식 그대로 바이트 스트림을 오프코드 단위로 읽어 오프셋과 해석된 인자, 스택에 미치는 영향을 보여 주고, 파이썬을 호출하지 않고도 복원할 수 있는 부분만 재구성합니다. 딕셔너리, 리스트, 튜플, 집합, 문자열, 바이트열, 크기 제한 없는 정수, 실수, 불리언, None은 그대로 돌아오고, 임포트가 필요한 것은 추측하는 대신 자리표시자로 표시합니다. 프로토콜 버전은 두 가지로 보고합니다. 두 답이 다를 수 있기 때문입니다. 프로토콜 2 이상은 버전을 선언하는 PROTO 오프코드로 시작하지만 0과 1은 아무것도 선언하지 않으므로, 파일 안의 오프코드가 실제로 요구하는 가장 높은 프로토콜도 함께 계산하고, 실제보다 낮게 선언한 파일에는 경고를 붙입니다. 메모 표는 어떤 객체가 저장되고 다시 꺼내졌는지 보여 줍니다. 공유 참조와 순환 참조는 오직 이 방식으로만 드러납니다. 자기 자신을 담은 리스트란 그 리스트를 만드는 도중에 저장된 키를 다시 꺼낸 결과이기 때문입니다. 프로토콜 4와 5는 스트림이 프레임으로 나뉘며, 프레임 표가 각 블록의 시작 위치를 알려 줍니다. 가장 먼저 볼 곳은 보안 점검 결과입니다. 불러올 때 코드를 실행시키는 구조를 모두 이름으로 짚어 줍니다. 임포트할 이름을 지정하는 GLOBAL과 STACK_GLOBAL, 실제 호출인 REDUCE, 클래스 생성인 INST·OBJ·NEWOBJ·NEWOBJ_EX, __setstate__를 부르는 BUILD, 로더의 persistent_load 훅으로 넘기는 PERSID, 이름조차 파일에 없는 copyreg 레지스트리의 EXT1/2/4까지 나열하고, 로더가 조회할 모든 모듈과 속성을 보여 주면서 os.system, subprocess, builtins.eval, posix.system처럼 공개된 공격에 등장하는 이름에는 표시를 붙입니다. 이런 오프코드가 하나도 없으면 그 피클은 데이터이고, 있으면 프로그램입니다. 아무리 들여다봐도 신뢰할 수 없는 출처의 피클을 불러오는 일이 안전해지지는 않습니다. 망가진 파일은 어중간하게 읽는 대신 이름을 붙여 알려 줍니다. 잘린 스트림, STOP 뒤에 남은 바이트, 즉 첫 피클 뒤에 숨은 두 번째 피클, 저장된 적 없는 메모 키를 꺼내는 경우가 각각 그렇게 보고됩니다.

사용법

  1. .pkl, .pickle 등 피클로 저장된 파일을 상자에 놓으세요. 업로드도 언피클링도 하지 않고 디스어셈블만 합니다.
  2. 보안 점검 결과부터 보세요. "데이터뿐"이면 임포트하거나 호출하는 오프코드가 하나도 없다는 뜻입니다. 임포트 목록이 나오면 신뢰할 수 없는 코드로 다루고 불러오지 마세요.
  3. 선언된 프로토콜과 필요한 프로토콜을 비교하세요. 프로토콜 2를 선언해 놓고 프로토콜 4 오프코드를 쓰는 파일은 pickle.dumps가 아니라 손으로 조립한 것입니다.
  4. 오프코드 표를 훑어보세요. 들여쓰기는 MARK 중첩을 따르므로 컨테이너가 블록처럼 보이고, 스택 열은 각 단계 뒤에 살아 있는 값의 개수를 보여 줍니다.
  5. 메모 표를 열어 공유 객체를 찾으세요. 재사용 횟수가 1 이상인 키는 같은 객체가 여러 곳에 나타난다는 뜻이고, 순환 참조도 같은 방식으로 기록됩니다.

자주 묻는 질문

신뢰할 수 없는 피클을 여기서 열어도 안전한가요?
네. 여기서는 언피클링을 전혀 하지 않기 때문입니다. pickletools.dis와 똑같이 오프코드 단위로 읽고, GLOBAL이나 REDUCE 오프코드에 대해 하는 일은 모듈과 속성 이름을 출력하는 것뿐입니다. 임포트도 호출도 없고, 복원은 오프코드만으로 만들 수 있는 값에서 멈춥니다. 파일은 브라우저 밖으로 나가지 않습니다. File API로 읽어 페이지 안에서 파싱합니다. 다만 이 도구가 그 파일을 안전하게 만들어 주지는 않습니다. 결과에 임포트 목록이 나왔다면 같은 파일에 pickle.load를 실행할 때는 그 임포트가 그대로 일어납니다.
직렬화 형식인데 어떻게 코드가 실행되나요?
임의의 객체를 저장하려면 다시 만들 방법이 필요하고, 파이썬의 답이 __reduce__이기 때문입니다. 객체가 "어떤 호출 대상에 어떤 인자를 주면 나를 복원할 수 있다"를 스스로 알려 주고, 스트림에는 임포트할 이름과 호출이 저장되며, 언피클러가 둘 다 수행합니다. 바로 이 구조 덕분에 datetime 객체와 numpy 배열, 모든 사용자 정의 클래스를 피클로 저장할 수 있고, 공격자는 같은 구조에서 datetime.datetime 대신 os.system을 적을 뿐입니다. 형식 안에 둘을 구분하는 장치가 없기 때문에, 파일이 무엇을 할지 아는 방법은 오프코드를 읽는 것뿐입니다.
메모는 무엇이고 순환 참조는 어떻게 나타나나요?
메모는 피클러가 이미 기록한 객체의 표입니다. 같은 객체가 다시 나오면 두 번째부터는 메모에서 꺼내 쓰는데, 프로토콜 0에서는 PUT과 GET, 이후에는 BINPUT과 BINGET, 프로토콜 4부터는 키 번호를 적지 않고 암묵적으로 매기는 MEMOIZE가 그 역할을 합니다. 덕분에 동일성이 보존됩니다. 하나의 리스트를 가리키던 두 이름은 불러온 뒤에도 하나의 리스트를 가리킵니다. 순환 참조는 여기서 한 걸음 더 나간 것입니다. 컨테이너는 내용이 기록되기 전에 메모에 저장되므로 내용이 그 컨테이너를 다시 참조할 수 있고, 그래서 자기 자신을 담은 리스트가 표현 가능하며, 이 도구도 무한히 펼치지 않고 참조로 표시합니다.
프로토콜 여섯 가지는 실제로 무엇이 다른가요?
프로토콜 0은 아스키입니다. 정수, 실수, 문자열이 줄바꿈으로 끝나는 텍스트로 적히기 때문에 눈으로도 거의 읽힙니다. 프로토콜 1은 대부분을 이진 표기로 바꿉니다. 프로토콜 2는 NEWOBJ로 클래스 생성을 효율화하고, 1바이트 튜플과 진짜 불리언을 더합니다. 프로토콜 3은 파이썬 2가 표현할 수 없던 bytes 객체를 추가합니다. 프로토콜 4는 프레이밍을 도입해 스트림을 길이가 붙은 블록으로 자르고, 64비트 크기와 STACK_GLOBAL, 집합 지원, 암묵적 메모 키를 더합니다. 프로토콜 5는 대역 외 버퍼를 추가해 큰 배열이 스트림 안이 아니라 옆으로 흐르게 하는데, NEXT_BUFFER 뒤에 데이터가 없는 이유가 그것입니다.
STOP 뒤에 바이트가 남아 있다는데 무슨 뜻인가요?
STOP은 피클 하나를 끝내고, 그 뒤는 다음에 오는 것의 몫입니다. 한 파일에 피클을 여러 개 쓰는 것은 흔한 방식이라 — 열린 파일에 pickle.dump를 반복하고 pickle.load를 반복해 읽는 식입니다 — 남은 바이트는 그 파일이 객체 하나가 아니라 레코드 스트림이라는 뜻인 경우가 많습니다. 동시에 무해한 첫 피클 뒤에 두 번째 페이로드를 숨기는 방법이기도 합니다. load를 한 번만 호출하는 프로그램은 첫 객체만 보고 나머지는 들여다보지 않기 때문입니다. 이 도구는 첫 피클을 디스어셈블하고 남은 바이트 수를 알려 줍니다. 나머지를 보려면 STOP 오프셋에서 파일을 잘라 뒷부분을 따로 열어 보세요.
numpy 배열이나 pandas DataFrame의 내용도 볼 수 있나요?
값은 볼 수 없습니다. 그런 객체는 numpy와 pandas를 호출해야 복원되기 때문입니다. 스트림에는 numpy.core.multiarray._reconstruct 같은 이름이나 pandas 클래스가 적혀 있고 거기에 바이트열이 넘겨지며, 배열은 그 호출이 끝난 뒤에야 존재합니다. 대신 파일의 윤곽은 보입니다. 임포트할 이름, 담고 있는 원시 버퍼와 그 크기를 알 수 있고, 보통은 그 파일이 주장하는 것과 맞는지 판단하기에 충분합니다. 프로토콜 5 파일이라면 배열 데이터가 아예 파일 안에 없을 수도 있습니다. NEXT_BUFFER는 페이로드가 대역 외로 전달되어 호출자가 따로 넘겨야 한다는 뜻입니다.

관련 도구