Codegate2018 cpu

By pesante | February 16, 2018

Codegate2018 cpu 출제자 WriteUp

안녕하세요. 블랙펄시큐리티 pesante입니다.

2018년도 코드게이트에 출제했던 문제 CPU에 대한 풀이를 써보려고 합니다.
CPU 문제는 2017년 말, 큰 이슈였던 meltdown 취약점을 컨셉으로 잡았습니다.


멜트다운(Meltdown, “붕괴”)은 대부분의 인텔 CPU와 일부 ARM CPU에서 발생하는 보안 취약점이다. 멜트다운 버그는 마이크로프로세서가 컴퓨터의 메모리의 전체를 볼수있도록 프로그램의 접속을 허용하며, 이로 인해서 전체 컴퓨터의 내용에 접근할 수 있다. 멜트다운은 CVE-2017-5754로 등재되어 있다. -wikipedia


meltdown 취약점을 간략하게나마 구현해보고 싶었습니다.
개발을 하기에 앞서 github에서 찾아본 결과.. 어느정도 원하는 기능이 구현되어 있는 CPU를 찾았습니다. 참고한 링크는 다음과 같습니다.


Hersh Sanghvi - CPU


참고한 소스코드의 버그를 좀 고치고 원하는 기능들을 추가하여 멜트다운 컨셉의 문제를 낼 수 있었습니다.
멜트다운을 공부하기 위해서는 다음과 같은 YouTube 동영상을 참고하였습니다.


Meltdown and Spectre Webcast - Understanding and mitigating the threats


그 결과 나온 CPU 프로그램의 주요 기능은 다음과 같습니다.


  1. 레지스터 및 다수의 인스트럭션
  2. 물리 메모리와 가상메모리
  3. 커널 메모리와 유저 메모리, 그리고 접근 권한
  4. 페이지 및 페이지 테이블
  5. 캐시 메모리
  6. exception handler


프로그램에 접속하면 기계어를 입력으로 줄 수 있고 원하는 기능을 수행해 주는 프로그램입니다. 4바이트 씩 여러 번 입력을 받는데, 처음 한바이트는 opcode이며 나머지는 인스트럭션에 따라 용도가 다르지만 보통 sreg나 dreg, 연산에 필요한 treg, 그리고 val로 쓰입니다.


프로그램의 main에서는 다음과 같이 가상메모리를 할당합니다.


mem_desc1 = create_addr_space (0, 0x10000, 0x10000, 0x10000, 0x20000, 0x10000, 0x30000,0x10000);


각 메모리의 할당 용도는 다음과 같습니다.


  • 0~0x10000: code 영역
  • 0x10000~0x20000: data 영역
  • 0x20000~0x30000: stack 영역
  • 0x30000~0x40000: kernel 영역


프로그램의 최종 목표는 CPU 프로그램을 분석하여 각종 인스트럭션을 알아내고 이를 이용해 기계어로 프로그램을 작성하여 syscall을 통해 flag를 읽는 것입니다. 하지만 syscall을 하면 권한체크 과정을 거치기 때문에 그냥 호출할 순 없습니다.


권한 체크 방식은 다음과 같습니다. main에서 urandom을 통해 4바이트의 랜덤 변수를 읽어서 0x35000, 0x35004의 가상메모리에 넣습니다.


int urnd = open("/dev/urandom", O_RDONLY);
  if(urnd==-1)
    return -1;
  read(urnd, &rng, sizeof(int));
  read(urnd, &rng2, sizeof(int));
  close(urnd);
  if (lookup_page_table (0x35000, current->mem_descriptor->pgd, &paddr, &perms) < 0) {
    exit (-3);
  }

  if (mem_write_32 (paddr, rng) < 0) {
    exit (-4);
  }

  if (lookup_page_table (0x35004, current->mem_descriptor->pgd, &paddr, &perms) < 0) {
    exit (-3);
  }

  if (mem_write_32 (paddr, rng2) < 0) {
    exit (-4);
  }


그리고 syscall을 호출하기 전에 두 값을 알아내어 r7, r8 레지스터에 넣으면 됩니다.


...
rc = lookup_page_table (0x35000, current->mem_descriptor->pgd, p_addr, &perms);
  mem_read_32(*p_addr, &temp);
  k_privilege=temp;
  rc = lookup_page_table (0x35004, current->mem_descriptor->pgd, p_addr, &perms);
  mem_read_32(*p_addr, &temp);
  k_privilege=(k_privilege<<32)+temp;
  strbuf=(char*)malloc(sizeof(char)*1024);
  if (privilege==k_privilege)
  {
    switch(cpu_registers [0])
    {
      case sys_exit:
          exit(-1);
          break;
...


하지만 두 개의 랜덤값이 커널메모리에 들어있기 때문에 LOAD 명령어로 읽으려고 하면 권한이 없어서 Segfault를 출력하고 프로그램이 종료됩니다. 따라서 meltdown 취약점을 이용하여 커널 메모리를 읽고 레지스터에 삽입하여 시스템 콜을 호출하면 됩니다.

해당 프로그램은 캐싱을 할 수 있는 인스트럭션이 구현되어 있습니다. 이 때 sreg 4번을 전달하면 val 값을 주소로 받아 한 바이트를 읽어온 후 4096을 곱하여 dreg 레지스터에 더한 주소를 캐싱하는 기능이 구현되어 있습니다.

그리고 LOAD 인스트럭션에는 메모리에서 레지스터에 값을 가져오기 전에 캐시를 참조하는데, 만약 캐시에 LOAD하려던 주소의 페이지가 있으면 14번 레지스터를 1로 세팅하고 캐시의 값을 참조합니다. 이것을 이용하면 커널 메모리의 주소를 전달하여 한바이트를 참조하게 하여 캐싱한 후, LOAD 명령어를 4096씩 증가시키며 브루트 포싱하면 14번 레지스터가 세팅되었는지 여부를 판단하여 원하는 한 바이트를 leak하는 것이 가능합니다.


  • LOAD Address=dreg+4096*(1byte leak한 값)


아, 그리고 이 과정을 하기에 앞서 exception handler와 관련된 인스트럭션이 있는데 이것을 이용하여 segfault가 났을 때 무시하도록 세팅해야 프로그램이 종료되지 않습니다.


즉, 풀이 과정을 정리하면 다음과 같습니다.


  1. exception hanlder가 segfault를 무시하도록 인스트럭션 실행
  2. sreg를 4, val를 커널메모리(초기값 0x35000)으로 cache 인스트럭션 실행
  3. 0부터 4096씩 더하면서 LOAD 명령어 실행
  4. 14번 레지스터가 1이 세팅되었으면 1바이트를 계산하여 저장한 후 커널메모리 값을 1증가하여 2번의 과정 반복(이것을 여러번 반복하여 0x35000, 0x35004 값 leak)
  5. 7번, 8번 레지스터를 각각 0x35000, 0x35004에서 leak한 값으로 세팅
  6. syscall을 통해 flag 읽기


위의 과정을 기계어로 작성하여 입력값으로 보내면 플래그를 읽을 수 있습니다. 이에 대한 풀이는 jinmo 님의 링크를 참조합니다. 깔끔하게 잘 되어있네요!


Jinmo - Codegate2018 CPU WriteUp


모두 문제푸느라 수고하셨습니다!

comments powered by Disqus