Linux 프로세스 인젝션에 대한 최종 가이드
서론
프로세스 인젝션 기법은 공격자의 툴셋에서 중요한 부분을 차지합니다. 이를 통해 공격자는 정상적인 프로세스 내에서 악성 코드를 실행해 탐지를 피하거나 원격 프로세스에 후크를 배치해 동작을 수정할 수 있습니다.
Windows 머신에서의 프로세스 인젝션에 대한 주제는 광범위하게 연구되어 왔으며 이에 대한 인식도 비교적 잘 알려져 있습니다. Linux 머신의 경우 그렇지 않습니다. 몇 가지 훌륭한 리소스 가 작성 되었고 이 주제를 담고 있지만, 특히 Windows와 비교할 때 Linux의 다양한 인젝션 기술에 대한 인식은 상대적으로 낮은 것으로 보입니다.
Akamai는 SafeBreach의 아밋 클라인(Amit Klein)과 이츠크 코틀러(Itzik Kotler)가 작성한 Windows 프로세스 인젝션 개요 에서 영감을 얻어 Linux 프로세스 인젝션에 대한 포괄적인 문서를 제공하고자 합니다. Akamai는 '트루 프로세스 인젝션', 즉 실행 중인 프로세스를 대상으로 하는 기법에 초점을 맞출 것입니다. 즉, 디스크의 바이너리를 수정하거나, 특정 환경 변수를 사용해 프로세스를 실행하거나, 프로세스 로딩 프로세스를 악용하는 방법은 제외됩니다.
Linux에서 프로세스 인젝션을 용이하게 하는 OS 기능과 이를 통해 허용되는 다양한 인젝션 기본 요소에 대해 설명합니다. 이전에 설명했던 기법을 다루고 이전에 문서화되지 않았던 인젝션 변종에 대해서도 강조할 것입니다. 마지막으로 강조 표시된 기법에 대한 탐지 및 방어 전략을 다루면서 마무리하겠습니다.
이 블로그 게시물과 더불어 이 게시물에서 설명한 다양한 인젝션 기본 요소에 대한 포괄적인 PoC(Proof-of-Concept) 코드 세트가 포함된 GitHub 리포지토리 를 공개합니다. 이러한 양성 PoC는 이러한 기법의 악의적인 구현이 어떤 모습인지 이해하는 데 도움을 주기 위한 것으로, 탐지 기능을 구축하고 테스트하는 데 도움이 될 수 있습니다. 자세한 내용은 프로젝트 README를 참조하시기 바랍니다.
Linux 인젝션과 Windows 인젝션 비교
Windows 머신에서 알려진 인젝션 기법의 수는 방대하며, 여기에는 APC 대기열 및 NTFS 트랜잭션 부터 아톰 테이블 및 스레드 풀까지 포함됩니다. Windows는 공격자가 원격 프로세스와 상호 작용하고 인젝션할 수 있는 많은 인터페이스를 노출하고 있습니다.
Linux는 상황이 많이 다릅니다. 원격 프로세스와의 상호 작용은 소수의 시스템 호출로 제한되어 있으며, Windows 머신에서 인젝션을 용이하게 하는 많은 기능을 찾을 수 없습니다. 원격 프로세스에서 메모리를 할당 하거나 원격 메모리 보호를 수정하기 위한 API는 존재하지 않으며 원격 스레드를 생성하기 위한 API는 더더욱 존재하지 않습니다.
이러한 차이가 인젝션 공격의 구조에 영향을 미칩니다. Windows에서 프로세스 인젝션은 일반적으로 할당 → 쓰기 → 실행의 세 단계로 구성됩니다. 먼저 원격 프로세스에 코드를 저장하는 데 사용할 메모리를 할당하고, 이 메모리에 코드를 작성한 다음 마지막으로 실행합니다.
Linux에서는 첫 번째 단계인 할당을 수행할 수 있는 기능이 부족합니다. 원격 프로세스에서 메모리를 직접 할당할 수 있는 방법이 없습니다. 따라서 덮어쓰기→실행→복구라는 인젝션 흐름이 약간 달라집니다. 원격 프로세스의 기존 메모리 를 페이로드로 덮어쓰고 실행한 다음 프로세스의 이전 상태를 복구해 정상적으로 계속 실행할 수 있도록 합니다.
원격 프로세스 상호 작용 방법
Linux에서 원격 프로세스의 메모리와 상호 작용하는 방법은 크게 세 가지 즉, ptrace, procfs, process_vm_writev로 제한됩니다. 다음 섹션에서는 각 방법에 대해 간략하게 설명합니다.
ptrace
ptrace 는 원격 프로세스를 디버깅하는 데 사용되는 시스템 호출입니다. 시작 프로세스는 디버깅된 프로세스 메모리와 레지스터를 검사하고 수정할 수 있습니다. GDB와 같은 디버거는 디버깅된 프로세스를 제어하기 위해 ptrace를 사용해 구축됩니다.
ptrace는 ptrace 요청 코드로 지정되는 다양한 작업을 지원하며, 몇 가지 주목할 만한 예로는 PTRACE_ATTACH(프로세스에 첨부), PTRACE_PEEKTEXT(프로세스 메모리에서 읽기) 및 PTRACE_GETREGS(프로세스 레지스터를 검색) 등이 있습니다. 스니펫 1은 ptrace의 사용 예를 보여줍니다.
// Attach to the remote process
ptrace(PTRACE_ATTACH, pid, NULL, NULL);
wait(NULL);
// Get registers state
struct user_regs_struct regs;
ptrace(PTRACE_GETREGS, pid, NULL, ®s);
스니펫 1: 원격 프로세스의 레지스터를 검색하기 위해 ptrace를 사용한 예
procfs
procfs 는 시스템에서 실행 중인 프로세스에 대한 인터페이스 역할을 하는 특수 의사 파일시스템입니다. procfs는 /proc 디렉터리를 통해 접속할 수 있습니다(그림 1).
각 프로세스는 디렉터리로 표시되며 PID에 따라 이름이 지정됩니다. 이 디렉토리 아래에서 프로세스에 대한 정보를 제공하는 파일을 찾을 수 있습니다. 예를 들어, cmdline 파일에는 프로세스 명령줄이 있고 environ 파일에는 프로세스 환경 변수 등이 있습니다.
procfs는 원격 프로세스 메모리와 상호 작용할 수 있는 기능도 제공합니다. 모든 프로세스 디렉토리 안에는 프로세스의 전체 주소 공간을 나타내는 특수 파일인 mem 파일이 있습니다. 주어진 오프셋에서 프로세스의 mem 파일에 접속하는 것은 동일한 주소에서 프로세스 메모리에 접속하는 것과 같습니다.
그림 2의 예에서는 xxd 유틸리티를 사용해, 지정된 오프셋에서 시작하고, 프로세스 mem 파일에서 100바이트를 읽었습니다.
GDB를 사용해 메모리에서 동일한 주소를 검사하면 내용이 동일하다는 것을 알 수 있습니다(그림 3).
maps 파일은 프로세스 디렉터리에서 찾을 수 있는 또 다른 흥미로운 파일입니다(그림 4). 이 파일에는 주소 범위와 메모리 권한을 포함해 프로세스 주소 공간의 여러 메모리 영역에 대한 정보가 포함되어 있습니다.
다음 섹션에서는 특정 권한이 있는 메모리 영역을 식별하는 기능이 어떻게 매우 유용한지 살펴보겠습니다.
process_vm_writev
원격 프로세스 메모리와 상호 작용하는 세 번째 방법은 process_vm_writev 시스템 호출입니다. 이 시스템 호출을 사용하면 원격 프로세스의 주소 공간에 데이터를 쓸 수 있습니다.
process_vm_writev는 로컬 버퍼에 대한 포인터를 수신하고 그 내용을 원격 프로세스 내부의 지정된 주소로 복사합니다. process_vm_writev의 사용 예는 스니펫 2에 있습니다.
// Initialize local and remote iovec structs used to perform the syscall
struct iovec local[1];
struct iovec remote[1];
// Place our data in the local iovec
local[0].iov_base = data;
local[0].iov_len = data_len;
// Point the remote iovec to the address in the remote process
remote[0].iov_base = (void *)remote_address;
remote[0].iov_len = data_len;
// Write the local data to the remote address
process_vm_writev(pid, local, 1, remote, 1, 0);
스니펫 2: process_vm_writev를 사용해 원격 프로세스에 데이터 쓰기
원격 프로세스에 코드 쓰기
이제 다른 프로세스와 상호 작용하는 다양한 방법을 이해했으니 이를 사용해 코드 인젝션을 수행하는 방법을 살펴봅시다. 인젝션 공격의 첫 번째 단계는 원격 프로세스 메모리에 셸코드를 작성하는 것입니다. 앞서 언급했듯이 Linux에서는 원격 프로세스에 새 메모리를 직접 할당할 수 있는 방법이 없습니다. 즉, 새로운 메모리 섹션을 생성할 수 없으며 표적 프로세스의 기존 메모리를 활용해야 합니다.
코드를 실행하려면 실행 권한이 있는 메모리 영역에 코드를 작성해야 합니다. 앞서 언급한 procfs maps 파일을 구문 분석하고 실행(x) 권한이 있는 메모리 영역을 식별하면 이러한 영역을 찾을 수 있습니다(그림 5).
실행 가능 영역에는 쓰기 가능 영역과 쓰기 불가능한 영역의 두 가지 종류가 있습니다. 다음 섹션에서는 각 영역을 언제 어떻게 사용할 수 있는지 설명합니다.
RX 메모리에 코드 쓰기
적용 대상: ptrace, procfs mem
이상적으로는 쓰기 및 실행 권한이 있는 메모리 영역을 식별해 코드를 작성하고 실행할 수 있도록 하는 것이 좋습니다. 실제로 대부분의 프로세스에는 이러한 권한이 있는 영역이 없는데, WX 메모리를 할당하는 것이 나쁜 관행으로 간주되기 때문입니다. 대신 일반적으로 읽기 및 실행 권한으로 제한됩니다.
흥미롭게도 방금 설명한 두 가지 방법, 즉 ptrace와 procfs mem을 사용하면 이 제한을 무력화할 수 있다는 것이 밝혀졌습니다. 이 두 가지 메커니즘은 모두 메모리 권한을 우회해쓰기 권한 없이도모든 주소에 쓸 수 있는 방식으로 구현됩니다. procfs의 이러한 동작에 대한 자세한 내용은 이 블로그 게시물에서 확인할 수 있습니다.
즉, 쓰기 권한에 관계없이 언제든지 ptrace 또는 procfs mem을 사용해 원격 실행 가능 메모리 영역에 코드를 쓸 수 있습니다.
ptrace
페이로드를 원격 프로세스에 쓰기 위해 POKETEXT 또는 POKEDATA ptrace 요청을 사용할 수 있습니다. 이러한 동일한 요청을 통해 원격 프로세스 메모리에 데이터 한 워드를 쓸 수 있습니다. 이러한 요청을 반복적으로 호출하면 전체 페이로드를 표적 프로세스 메모리에 복사할 수 있습니다. 이에 대한 예는 스니펫 3에 있습니다.
ptrace(PTRACE_ATTACH, pid, NULL, NULL);
wait(NULL);
// write payload to remote address
for (size_t i = 0; i < payload_size; i += 8, payload++)
{
ptrace(PTRACE_POKETEXT, pid, address + i, *payload);
}
스니펫 3: ptrace POKETEXT를 사용해 페이로드를 원격 프로세스 메모리에 쓰기
procfs mem
procfs를 사용해 원격 프로세스에 페이로드를 쓰려면 올바른 오프셋에 있는 mem 파일에 페이로드를 쓰기만 하면 됩니다. mem 파일에 대한 모든 변경 사항은 프로세스 메모리에 적용됩니다. 이러한 작업을 수행하기 위해 일반 파일 API(스니펫 4)를 사용할 수 있습니다.
// Open the process mem file
FILE *file = fopen("/proc/<pid>/mem", "w");
// Set the file index to our required offset, representing the memory address
fseek(file, address, SEEK_SET);
// Write our payload to the mem file
fwrite(payload, sizeof(char), payload_size, file);
스니펫 4: procfs mem 파일을 사용해 원격 프로세스 메모리에 데이터 쓰기
WX 메모리에 코드 쓰기
적용 대상: ptrace, procfs mem, process_vm_writev
앞서 설명한 것처럼 ptrace와 procfs mem은 모두 메모리 권한을 우회해 쓰기 불가능한 메모리 영역에 코드를 쓸 수 있게 해줍니다. 하지만 process_vm_writev는 그렇지 않습니다. process_vm_writev는 메모리 권한을 준수하므로 쓰기 가능한 메모리 영역에만 데이터를 쓸 수 있습니다.
따라서 쓰기 가능한 영역을 찾는 것이 유일한 옵션입니다. 모든 프로세스에 이러한 영역이 있는 것은 아니지만 그런 프로세스를 확실히 찾을 수 있습니다.
스니펫 5의 명령은 시스템의 모든 프로세스의 maps 파일을 스캔해 쓰기 및 실행 권한이 있는 영역을 식별합니다(그림 6).
find /proc -type f -wholename "*/maps" -exec grep -l "wx" {} +
스니펫 5: “find” 명령을 사용해 쓰기 및 실행 메모리 영역이 있는 프로세스 식별하기
이러한 영역을 식별한 후 process_vm_writev를 사용해 해당 영역에 코드를 작성할 수 있습니다(스니펫 6).
// Initialize local and remote iovec structs used to perform the syscall
struct iovec local[1];
struct iovec remote[1];
// Place our payload in the local iovec
local[0].iov_base = payload;
local[0].iov_len = payload_len;
// Point the remote iovec to the address of our wx memory region
remote[0].iov_base = (void *)wx_address;
remote[0].iov_len = payload_len;
// Write the local data to the remote address
process_vm_writev(pid, local, 1, remote, 1, 0);
스니펫 6: process_vm_writev를 사용해 원격 WX 영역으로 페이로드 쓰기
원격 실행 흐름 하이재킹
원격 프로세스 메모리에 코드를 작성한 후에는 이를 실행해야 합니다. 다음 섹션에서는 이를 위해 사용할 수 있는 다양한 기법에 대해 설명하겠습니다.
Akamai 연구는 amd64 머신에 초점을 맞췄습니다. 다른 아키텍처에서도 약간의 차이가 있을 수 있지만 일반적인 개념은 동일하게 유지될 것입니다.
프로세스 명령 포인터 수정하기
적용 대상: ptrace
ptrace를 사용해 프로세스에 연결하면 실행이 일시 중지되고 명령 포인터를 포함한 프로세스 레지스터를 검사하고 수정할 수 있습니다. 이 작업은 SETREGS 및 GETREGS ptrace 요청을 사용해 수행할 수 있습니다. 프로세스의 실행 흐름을 수정하려면 ptrace를 사용해 셸코드의 주소에 대한 명령 포인터를 수정할 수 있습니다.
스니펫 7의 예제에서는 다음 세 단계를 수행했습니다.
GETREGS ptrace 요청을 사용해 현재 레지스터 값을 검색합니다.
페이로드 주소를 가리키도록 명령 포인터를 수정합니다(2씩 증가하며, 나중에 설명하겠습니다).
SETREGS 요청을 사용해 변경 사항을 프로세스에 적용합니다.
// Get old register state.
struct user_regs_struct regs;
ptrace(PTRACE_GETREGS, pid, NULL, ®s);
// Modify the instruction pointer to point to our payload
regs.rip = payload_address + 2;
// Modify the registers
ptrace(PTRACE_SETREGS, pid, NULL, ®s);
스니펫 7: ptrace SETREGS를 사용해 명령 포인터를 페이로드에 가리키기
SETREGS는 프로세스 레지스터를 수정하는 '전통적인' 가장 많이 문서화된 방법이지만 다른 ptrace 요청인 POKEUSER를 사용해 이 작업을 수행할 수도 있습니다.
POKEUSER 요청은 USER 영역 , 즉 레지스터를 포함한 프로세스에 대한 정보를 포함하는 구조( sys/user.h에 정의됨)인 프로세스에 데이터를 쓸 수 있게 합니다. 올바른 오프셋으로 POKEUSER를 호출하면 명령 포인터를 코드의 주소로 덮어쓰고 이전과 동일한 결과를 얻을 수 있습니다(스니펫 8).
// calculate the offset of the RIP register, based on the USER struct definition
rip_offset = 16 * sizeof(unsigned long);
ptrace(PTRACE_POKEUSER, pid, rip_offset, payload_address + 2);
스니펫 8: ptrace POKEUSER를 사용해 명령 포인터를 페이로드에 가리키기
POKEUSER를 사용해 RIP를 수정하는 구축은 리포지토리에서 찾을 수 있습니다.
RIP+ = 2: 언제 그리고 왜?
스니펫 7과 스니펫 8에서 볼 수 있듯이 페이로드의 주소로 RIP를 수정할 때 2씩 증가합니다. 이는 흥미로운 ptrace 동작 을 수용하기 위해 수행됩니다. 가끔 ptrace를 사용하는 프로세스에서 분리된 후 RIP의 값이 2씩 감소합니다. 왜 이런 일이 발생하는지 이해해 봅시다.
ptrace를 사용해 프로세스에 연결할 때 현재 커널에서 실행 중인 syscall을 중단할 수 있습니다. syscall이 제대로 실행되는지 확인하기 위해 프로세스에서 분리하면 커널이 시스템 호출을 다시 실행합니다.
syscall이 실행되는 동안 RIP는 이미 실행될 다음 명령을 가리킵니다. syscall을 다시 실행하기 위해 커널은 RIP 값을 2만큼 감소시킵니다(amd64의 syscall 명령 크기). 이 변경 후 RIP는 다시 syscall 명령을 가리키게 되어 한 번 더 실행됩니다(그림 7).
코드 인젝션을 수행할 때 syscall 중에 프로세스를 중단하는 경우 문제가 발생할 수 있습니다. 코드를 가리키도록 RIP를 수정한 후에도 커널은 여전히 새 값을 2씩 감소시켜 셸코드 앞에 2바이트의 간격이 발생해 실패할 가능성이 있습니다(그림 8).
이 동작을 수용하기 위해 셸코드 앞에 두 개의 연산 없음(NOP) 명령을 추가하고 RIP가 셸코드 + 2의 주소를 가리키도록 하는 두 가지 조치를 취합니다. 이 두 단계는 코드가 제대로 실행되도록 합니다.
syscall 중에 프로세스를 중단한 경우 커널은 새 RIP 값을 감소시켜 실제 코드에 삽입할 두 개의 NOP가 포함된 셸코드의 시작 주소를 가리키게 됩니다.
syscall 중에 프로세스를 중단하지 않았다면 새 RIP가 감소하지 않으므로 두 개의 NOP가 건너뛰고 코드가 실행됩니다. 이 두 가지 시나리오는 그림 9에 있습니다.
현재 명령 수정
적용 대상: ptrace, procfs mem
procfs의 또 다른 흥미로운 파일은 syscall 파일에 저장됩니다. 이 파일에는 현재 프로세스에서 실행 중인 syscall에 대한 정보, 즉 syscall 번호, 전달된 인수, 스택 포인터, 그리고 (가장 흥미로운) 프로세스 명령 포인터가 들어 있습니다(그림 10). 프로세스가 현재 syscall을 실행하고 있지 않더라도 프로세스의 스택과 명령 포인터는 여전히 syscall 파일에 존재합니다.
이 정보를 통해 프로세스의 실행 흐름을 제어할 수 있으며, 다음에 실행할 명령의 주소를 알면 자신의 명령으로 덮어쓸 수 있습니다.
이를 구축하기 위해 공격자는 다음 네 단계를 수행할 수 있습니다.
SIGSTOP 신호를 전송해 프로세스 실행을 중지합니다
프로세스 syscall 파일을 읽어 실행할 다음 명령의 주소를 식별합니다
식별된 주소에 셸코드를 씁니다
SIGCONT 신호를 전송해 프로세스 실행을 재개합니다
스니펫 9는 이 프로세스에 대한 의사 코드를 제공합니다.
// Suspend the process by sending a SIGSTOP signal
kill(pid, SIGSTOP);
// Open the syscall file
FILE *syscall_file = fopen("/proc/<pid>/syscall", "r");
// Extract the instruction pointer from the syscall file
long instruction_pointer = ...
// Write our payload to the address of the current instruction pointer using
procfs mem
FILE *mem_file = fopen("/proc/<pid>/mem", "w");
fseek(mem_file, instruction_pointer, SEEK_SET);
fwrite(payload, sizeof(char), payload_size, mem_file);
// Resume execution by sending a SIGCONT signal
kill(pid, SIGCONT);
스니펫 9: 명령 포인터의 현재 주소에서 프로세스 메모리를 수정해 프로세스의 실행 흐름을 탈취하기 위해 procfs mem을 사용합니다
스니펫 9의 예는 procfs mem 파일을 사용해 이 기법을 구축하지만, 페이로드를 메모리에 쓰는 데에도 ptrace POKETEXT를 사용할 수 있다는 점에 유의해야 합니다.
앞서 언급했듯이 process_vm_writev는 메모리 권한에 의해 제한되므로 쓰기 가능한 메모리 영역만 수정할 수 있습니다. WX 메모리 영역에서 실행되는 코드를 찾을 가능성은 낮기 때문에 이 프리미티브에 대한 process_vm_writev의 신뢰성이 떨어집니다.
직접 procfs mem 파일을 사용해 이 기술을 구현한 방법을 확인해 보세요.
스택 탈취
적용 대상: ptrace, procfs mem 파일, process_vm_writev
또 다른 흥미로운 메모리 영역은 프로세스 스택으로, maps 파일을 사용해 식별할 수도 있습니다. 스택 메모리는 실행 가능하지는 않지만(그림 11) 프로세스의 실행 흐름을 탈취하는 데 사용할 수 있습니다.
함수가 호출될 때마다 호출 함수의 반환 주소가 스택으로 푸시됩니다. 함수가 실행을 완료하면 프로세서는 스택에서 이 반환 주소를 가져와서 해당 주소로 이동합니다(그림 12).
이 메커니즘을 악용하기 위해 스택에서 반환 주소를 식별하고 셸코드를 가리키는 새 주소로 덮어쓸 수 있습니다. 현재 함수가 실행을 완료하자마자 코드가 실행됩니다(그림 13).
스택의 최상단을 식별하기 위해 앞서 언급한 procfs syscall 파일을 파싱하면 스택 포인터 레지스터의 값도 포함되어 있습니다.
이 기법을 수행하기 위해 다음 6단계를 사용할 수 있습니다.
SIGSTOP 신호를 전송해 프로세스 실행을 중지합니다
procfs syscall 파일을 구문 분석해 프로세스의 스택 포인터를 식별합니다
프로세스 스택을 스캔하고 반환 주소를 식별합니다
앞서 언급한 쓰기 프리미티브 중 하나를 사용해 페이로드를 프로세스 메모리에 인젝션합니다
반환 주소를 페이로드의 주소로 덮어씁니다
SIGCONT 신호를 전송해 프로세스 실행을 재개합니다
현재 함수가 실행을 완료하면 페이로드가 실행됩니다
모든 프로세스 상호작용 방법을 통해 스택을 수정할 수 있으므로 이 기법을 구축하는 데 모두 사용할 수 있습니다 process_vm_writev syscall을 사용한 이 기법의 구축은 리포지토리에서 찾을 수 있습니다.
ROP 스택 탈취
적용 대상: ptrace, procfs mem 파일, process_vm_writev
스택 탈취 기법은 실행 메모리나 레지스터를 수정하지 않고 프로세스의 실행 흐름을 탈취할 수 있다는 점에서 흥미롭습니다. 그럼에도 불구하고 이 기법을 사용하려면 실행 가능한 메모리 영역에 있는 셸코드로 이동해야 합니다. 앞서 설명한 대로 WX 영역을 찾거나 ptrace/procfs mem을 사용해 쓰기 불가능한 메모리에 쓰기를 시도할 수 있습니다.
하지만 이러한 작업을 피하고 싶다면 어떻게 해야 할까요? ROP(Return-Oriented Programming) 라는 또 다른 트릭이 있습니다. 프로세스 스택에 쓰는 기능을 사용해 ROP 체인으로 스택을 덮어쓸 수 있습니다(그림 14). 이미 프로세스 메모리에 있는 실행 가젯을 이용하므로 새로운 실행 코드를 작성하지 않고도 페이로드를 구성할 수 있습니다.
이 기법은 다음 7단계로 구성됩니다.
SIGSTOP 신호를 전송해 프로세스 실행을 중지합니다
procfs syscall 파일을 구문 분석해 프로세스의 스택 포인터를 식별합니다
프로세스 스택을 스캔하고 반환 주소를 식별합니다
앞서 언급한 쓰기 프리미티브 중 하나를 사용해 실행 권한 없이 쓰기 가능한 메모리 영역에 페이로드를 인젝션합니다
ROP 체인을 구성해 mprotect 를 호출하고 셸코드 실행 파일의 메모리 영역을 표시합니다
식별된 반환 주소의 주소에서 시작해 스택을 ROP 체인으로 덮어씁니다
SIGCONT 신호를 전송해 프로세스 실행을 재개합니다
현재 함수가 실행을 완료하면 ROP 체인이 실행되어 셸코드가 실행 가능하게 되고 셸코드로 점프합니다.
이 개념은 AON Cyber Labs의 로리 맥나마라(Rory McNamara)가 자신의 블로그 게시물 에서 procfs mem 인젝션에 대해 설명하면서 시연했습니다.
이 기법은 쓰기 불가능한 메모리 영역을 수정할 필요가 없으므로 process_vm_writev를 포함한 모든 프로세스 상호 작용 기법을 사용해 수행할 수 있습니다.
직접 process_vm_writev를 사용해 이 기술을 구현한 방법을 확인해 보세요. Akamai가 아는 한, process_vm_writev syscall에만 의존하는 인젝션 기법의 공개 데모는 이번이 처음입니다.
GOT 탈취
적용 대상: ptrace, procfs mem 파일, process_vm_writev
일반적으로 쓰기 가능한 또 다른 흥미로운 메모리 섹션은 GOT입니다. GOT(Global Offset Table)는 동적으로 연결된 ELF 파일의 재배치 프로세스의 일부로 사용되는 메모리 섹션입니다. 여기서는 자세한 내용은 다루지 않고 우리의 목적과 관련된 부분, 즉 프로그램에서 가져온 함수의 주소를 저장하는 섹션에 초점을 맞추겠습니다. 프로그램이 원격 라이브러리에서 함수를 호출할 때마다 GOT에 접속해 해당 메모리 주소를 확인합니다(그림 15).
이 메커니즘은 공격자가 프로세스 실행 흐름을 탈취하기 위해 악용할 수 있습니다. GOT 메모리는 일반적으로 쓰기 가능하므로 공격자는 그 안에 있는 모든 주소를 페이로드의 주소로 덮어쓸 수 있습니다. 다음에 프로세스에서 함수를 호출할 때 공격자 코드가 대신 실행됩니다(그림 16).
이 기법은 다음 네 단계로 구성됩니다.
SIGSTOP 신호를 전송해 프로세스 실행을 중지합니다
maps 파일을 파싱해 GOT 메모리 영역을 식별합니다
해당 섹션의 주소를 페이로드 주소로 덮어씁니다
SIGCONT 신호를 전송해 프로세스 실행을 재개합니다
덮어쓴 함수가 호출되면 페이로드가 실행됩니다
이 공격에 영향을 줄 수 있는 메모리 보호 기능 중 하나는 풀 RELRO이며, 이 설정으로 바이너리를 컴파일하면 GOT 메모리에 읽기 전용 권한이 부여되어 덮어쓰기를 방지할 수 있습니다.
그럼에도 불구하고 RELRO는 대부분의 경우 이 공격을 막을 수 없습니다.
ptrace 및 procfs mem은 메모리 권한을 우회해 RELRO를 무의미하게 만듭니다
RELRO는 프로세스 바이너리 자체에는 영향을 주지만 로드된 라이브러리에는 영향을 주지 않습니다 프로세스가 RELRO 없이 컴파일된 라이브러리를 로드하는 경우, 해당 GOT은 쓰기 가능하므로 덮어쓸 수 있습니다
process_vm_writev syscall을 사용한 이 기법의 구축은 리포지토리에서 찾을 수 있습니다.
실행 프리미티브 요약
표에는 설명한 모든 가능한 실행 프리미티브와 이를 구축할 수 있는 방법이 요약되어 있습니다.
원격 프로세스 상호 작용에 대한 제한 사항
방금 설명한 방법을 사용해 원격 프로세스와 상호 작용할 수 있는 기능을 결정하는 여러 설정이 있습니다. 이 섹션에서는 두 가지 주요 설정에 대해 간략하게 설명하겠습니다.
ptrace_scope
ptrace_scope 는 원격 프로세스에서 ptrace를 사용할 수 있는 사용자를 결정하는 설정입니다. 다음과 같은 값을 가질 수 있습니다.
0 — 프로세스는 동일한 UID를 가지고 있는 한 시스템의 다른 프로세스에 연결할 수 있습니다.
1 — 일반 프로세스는 하위 프로세스에만 연결할 수 있습니다. 권한 있는 프로세스( CAP_SYS_PTRACE포함)는 관련 없는 프로세스에 계속 연결할 수 있습니다. 이는 많은 배포판에서 기본 설정입니다.
2 — CAP_SYS_PTRACE가 있는 프로세스만 프로세스에 연결할 수 있습니다. 이 기능은 일반적으로 루트에게만 부여됩니다.
3 — 원격 프로세스에 연결할 수 없습니다.
이름과 달리 이 설정은 원격 프로세스의 procfs mem 파일에 접속하고 프로세스에서 process_vm_writev를 사용하는 기능에도 영향을 미칩니다.
“dumpable” 속성
프로세스 복구에 대한 참고 사항
앞서 강조한 모든 인젝션 방법은 프로세스 레지스터를 수정하거나 실행 메모리, 스택의 반환 주소 또는 GOT를 덮어쓰는 등 어떤 식으로든 프로세스 상태를 수정해야 합니다. 이러한 모든 작업은 프로세스의 정상적인 실행 흐름을 변경하고 페이로드가 완료된 후 예기치 않은 동작으로 이어질 수 있습니다.
이는 표적 프로세스가 삽입된 페이로드와 함께 계속 실행되기를 원할 때 문제가 될 수 있습니다. 프로세스가 정상적으로 계속 실행되도록 하려면 원래 상태로 복원해야 합니다. 일반적인 복구 흐름은 다음 8단계로 구성됩니다.
원격 읽기 프리미티브를 사용해 덮어쓰려는 메모리 콘텐츠를 백업합니다
프로세스 레지스터의 현재 내용을 백업합니다. 이 작업은 ptrace를 사용하거나 셸코드를 통해 수행할 수 있습니다.
페이로드를 실행합니다(예: 별도의 스레드에서 코드를 실행하는 SO(Shared Object) 파일 로드)
페이로드가 완료되면, 실행이 완료되었음을 인젝션 프로세스에 알립니다. 인터럽트를 발생시켜 구축할 수 있습니다
원격 프로세스를 일시 중지합니다
프로세스 레지스터 상태를 복원합니다
덮어쓴 메모리를 복원합니다
프로세스 실행을 재개합니다
구축 세부 사항은 사용되는 인젝션 방법에 따라 약간씩 다르지만 이 일반적인 개요를 따라야 합니다. 아담 체스터(Adam Chester)의 Linux ptrace 인젝션 블로그 게시물 에서는 ptrace 기반 인젝션 후 프로세스 복구에 대한 자세한 예를 제공합니다.
이 게시물의 목표는 인젝션 기법에 대한 개요를 제공해 보안팀이 기법을 숙지한 다음 적절한 탐지를 구축하는 데 사용할 수 있도록 하는 것이었습니다. 방어에 중점을 두었기 때문에 공격자가 완전히 무기화하는 데 필요한 다양한 기법에 대한 복구 단계는 자세히 설명하지 않기로 했습니다.
탐지 및 방어
방금 설명한 것처럼 공격자가 Linux 머신에서 프로세스 인젝션을 수행할 수 있는 기법은 많습니다. 다행히도 이러한 모든 방법은 탐지 기회를 제공하는 비정상적인 동작을 수행해야 합니다. 다음 섹션에서는 Linux에서 프로세스 인젝션을 탐지하고 방어하기 위해 구축할 수 있는 다양한 전략에 대해 자세히 설명합니다.
“Injection syscalls”
이 게시물 전반에서 원격 프로세스와 상호 작용하는 데 ptrace, procfs, process_vm_writev의 세 가지 방법을 사용했습니다. 이러한 방법은 악의적으로 사용될 가능성이 있으므로 모니터링해야 합니다.
Linux 머신에 로깅 솔루션을 설치하는 것으로 시작하세요. Linux용 Sysmon 이나 Aqua Security의 Tracee (이 게시물에서 설명하는 많은 기법을 다루는 룰을 이미 구축 하고 있는 eBPF 기반 로깅 유틸리티를 사용해 모니터링할 수 있습니다.
로깅을 설정한 후에는 기업에서 환경에서 “injection syscall”의 정상적인 사용을 분석하고 알려진 유효한 사용 사례의 기준선을 구축하는 것이 좋습니다. 이러한 기준선을 만든 후에는 이를 벗어나는 모든 편차를 조사해 잠재적인 공격을 배제해야 합니다. syscall별 추가 고려 사항은 다음 섹션에 설명되어 있습니다.
가능하면 이러한 syscall의 사용을 제한하거나 완전히 방지하기 위해 ptrace_scope 를 사용하는 것이 이상적입니다.
ptrace
대부분의 프로덕션 환경에서 ptrace syscall을 사용하는 경우는 매우 드뭅니다. 유효한 ptrace 사용의 기준선을 설정한 후 비정상적인 ptrace 사용을 분석하는 것이 좋습니다.
다음 ptrace 요청은 원격 프로세스의 수정을 허용하므로 매우 의심스러운 것으로 간주해야 합니다.
POKEDATA/POKETEXT
POKEUSER
SETREGS
procfs
procfs mem 파일에 쓰는 것은 일부 정상적인 사용 사례도 있지만 이 동작은 그다지 일반적이지 않을 것입니다. 유효한 사용 사례의 기준선을 구축한 후에는 비정상적인 쓰기 작업을 분석하는 것이 좋습니다.
/proc/<pid>/task 디렉터리도 고려해야 합니다. 이 디렉터리에는 프로세스의 여러 스레드에 대한 정보가 노출되어 있습니다. 각 스레드에는 자체 procfs 디렉터리가 있으며 이 디렉터리에는 mem, maps, syscall 파일 등 앞서 다룬 모든 주요 procfs 파일이 포함됩니다.
그림 17에서 /proc/<pid> 디렉토리에서 syscall 파일을 읽는 것은 프로세스의 메인 스레드를 나타내는 /proc/<pid>/task/<pid> 디렉토리에서 읽는 것과 동일하다는 것을 알 수 있습니다.
process_vm_writev
다시 한번, 이 syscall의 정상적인 사용에 대한 기준을 구축함으로써 비정상적인 편차를 식별할 수 있습니다. 다른 프로세스의 메모리에 쓰는 알 수 없는 프로세스는 의심스러운 것으로 간주하고 분석해야 합니다.
프로세스 비정상 탐지
프로세스 인젝션을 직접 탐지하는 것 외에도 그 부작용을 탐지할 수도 있습니다. 원격 프로세스에 코드가 삽입되면 해당 프로세스의 동작 방식이 변경됩니다. 프로세스가 수행하는 정상적인 동작 외에도 페이로드의 동작도 동일한 프로세스에 의해 수행됩니다.
이러한 동작의 변화는 탐지의 기회를 제공할 수 있습니다. 정상적인 프로세스 동작의 기준선을 구축함으로써 코드 인젝션이 발생했음을 나타낼 수 있는 의심스러운 편차를 식별할 수 있습니다. 이러한 동작의 몇 가지 예로는 비정상적인 하위 프로세스 생성, 이전에는 보이지 않던 SO 파일 로딩, 비정상적인 포트를 통한 통신 등이 있습니다.
Akamai 연구원들은 이 접근 방식을 문서화 하고 네트워크 비정상을 분석해 코드 인젝션을 식별하는 방법을 시연했습니다.
요약
공격자는 Linux 머신에 인젝션 공격을 수행할 수 있는 다양한 옵션을 가지고 있습니다. 이러한 기법은 공격자에게는 매우 유용할 수 있지만 보안팀에게도 중요한 탐지 기회를 제공합니다. Linux 머신에 견고한 로깅 및 탐지 기능을 구축함으로써 기업은 보안 체계를 크게 개선할 수 있습니다.