这道F is for flag(正餐),卡的时间比较久,一开始打算硬逆奈何功力不够(我知道有其他大佬硬逆做出来的),最后选择使用trace工具碰碰运气,然后又在frida和pyda之间来回反复,frida的hook效果不尽人意,最终选择pyda。
pyda是一款动态二进制插桩工具,可以通过编写python代码的方式实现hook非常方便。
官网介绍:Pyda combines Dynamorio-based instrumentation with a CPython interpreter, allowing you to write hooks in Python that directly manipulate registers/memory in the target, without going through GDB or ptrace.
https://github.com/ndrewh/pyda
题目背景
经典的flag检查:
/f
FLAG: SECCON{fUnCt10n4l_pRoGr4mM1n6_1s_pR4c7iC4lLy_a_pUr3_0bfu5c4T1oN}
"Correct"
题目由c++编写,ida打开点开main函数,会发现main函数里面发现里面有大量的std:variant, lambda闭包调用,并且其他函数都是被mangle过的。
v84 = 0;
std::variant>::variant(
(__int64)v106,
(__int64)&v84);
v83 = 0xB7E9A2A4;
std::variant>::variant(
(__int64)v105,
(__int64)&v83);
main::{lambda(std::variant>,std::variant>)#19}::operator()(
(__int64)v107,
(__int64)&v78,
(__int64)v105,
(__int64)v106,
v3,
v4);
在lamba#19中又有std::make_shared, std::variant,std::shared_ptr
std::make_shared> &,std::variant> &>(
(__int64)v7,
a3,
a4);
std::variant>::variant<:shared_ptr>,void,void,std::shared_ptr,void>(
a1,
(__int64)v7);
std::shared_ptr::~shared_ptr(v7);
std::make_shared经过一层层调用,最终会call到Cons::Cons,将v83(0xB7E9A2A4)存到cons里,后面的逻辑以此类推。
v5 = operator new(0x50uLL, a1);
v6 = std::forward<:variant int>> &>(a3);
std::variant>::variant((__int64)v11, v6);
v7 = std::forward<:variant int>> &>(a2);
std::variant>::variant((__int64)v10, v7);
Cons::Cons(v5, (__int64)v10, (__int64)v11);
std::variant>::~variant((__int64)v10);
std::variant>::~variant((__int64)v11);
到这里分析还算顺利,知道跟到这个std::function()(lambda#1)
std::function<:variant int>> ()(void)>::function<: class="code-snippet__built_in">lambda(void)#1},void>(
(std::_Function_base *)v127,
(__int64)v95);
每一个std:function至少需要三层调用才能到达真实逻辑:
*((_QWORD *)a1 + 3) = std::_Function_handler<:variant int>> ()(void),main::{lambda(void)#1}>::_M_invoke;
std::__invoke_r<:variant int>>,main::{lambda(void)#1} &>(a1, pointer);
std::__invoke_impl<:variant int>>,main::{lambda(void)#1} &>(a1, v2);
main::{lambda(void)#1}::operator()(a1, v2);
v8 = __readfsqword(0x28u);
v2 = *(_QWORD **)a2;
v3 = *(_QWORD *)(a2 + 32);
v4 = *(_QWORD *)(a2 + 8);
ZNKR3fixIZ4mainEUlT_St7variantIJjNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEESt10shared_ptrI4ConsEEESB_E_EclIJRS7_EEEDcDpOT_(
v6,
*(_QWORD *)(a2 + 16),
*(_QWORD *)(a2 + 24));
ZNKR3fixIZ4mainEUlT_St7variantIJjNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEESt10shared_ptrI4ConsEEESB_E0_EclIJSB_EEEDcDpOT_(
v7,
v4,
v6);
ZNKR3fixIZ4mainEUlT_St7variantIJjNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEESt10shared_ptrI4ConsEEESB_SB_E3_EclIJSB_RSB_EEEDcDpOT_(
a1,
v2,
(__int64)v7,
v3);
std::variant>::~variant((__int64)v7);
std::variant>::~variant((__int64)v6);
return a1
一层层跟进这个调用,函数调用递归一层接一层把我绕晕了,而且每一层代码还不少,人工分析工程量很大,而且递归调用的地方不止一处,对人的记忆力也有很高的要求。总之,人工分析的话,工程量大,难度大,并且有的地方不用真的逆。所以我们思路,让工具辅助我们,找到主逻辑,然后我们再打开ida逆向。
使用pyda trace来跟踪程序的主逻辑
首先搭建环境:
FROM ubuntu:24.04 as target
FROM ghcr.io/ndrewh/pyda
COPY
RUN apt update && apt install -y patchelf
COPY F /F
RUN patchelf
RUN apt install -y binutils
trace cmp - 确定密文长度
首先trace cmp,使用仓库中的example/cmplog.py trace cmp
from pyda import *
from pwnlib.elf.elf import ELF
from pwnlib.util.packing import u64, u32
import string
import sys
import subprocess
from collections import defaultdict
p = process()
e = ELF(p.exe_path)
e.address = p.maps[p.exe_path].base
plt_map = { e.plt[x]: x for x in e.plt }
def get_cmp(proc):
p = subprocess.run(f"objdump -M intel -d {proc.exe_path} | grep cmp", shell=True, capture_output=True)
output = p.stdout.decode()
cmp_locs = {}
for l in output.split("\n"):
if len(l) <= 1:
continue
if "QWORD PTR" in l:
continue
if ":\t" not in l:
continue
cmp_locs[int(l.split(":")[0].strip(), 16)] = l.split()[-1]
return cmp_locs
cmp_locs_unfiltered = get_cmp(p)
cmp_locs = {}
for (a, v) in cmp_locs_unfiltered.items():
info = v.split(",")
if len(info) != 2:
continue
if "[" in info[0] or "[" in info[1]:
continue
if "0x" in info[0] or "0x" in info[1]:
continue
cmp_locs[a] = info
print(f"cmp_locs: {len(cmp_locs)}")
eq_count = 0
neq_count = 0
reg_map = {
"eax": "rax",
"ebx": "rbx",
"ecx": "rcx",
"edx": "rdx",
"esi": "rsi",
"edi": "rdi",
"ebp": "rbp",
"esp": "rsp",
"r8d": "r8",
}
counts_by_pc = defaultdict(int)
good_cmps = defaultdict(int)
def cmp_hook(p):
global eq_count, neq_count
info = cmp_locs[p.regs.pc - e.address]
counts_by_pc[p.regs.pc - e.address] += 1
reg1 = reg_map.get(info[0], info[0])
reg2 = reg_map.get(info[1], info[1])
r1 = p.regs[reg1]
r2 = p.regs[reg2]
eq = r1 == r2
if eq:
eq_count += 1
else:
neq_count += 1
print(f"cmp @ {hex(p.regs.rip - e.address)} {reg1}={hex(r1)} {reg2}={hex(r2)} {eq}")
for x in cmp_locs:
p.hook(e.address + x, cmp_hook)
p.run()
开始trace:
pyda cmplog.py
cmp_locs: 46
FLAG: AAAAAAAAAAAAAAAA
//.. TOO LONG NOT TO SHOW
cmp @ 0x182a7 rcx=0x10 rdx=0x40 False
"Wrong"
可以看到程序输入“Wrong”之前的最后一个cmp,rcx=0x10 正好是我们输入的长度,所以猜测rdx=0x40是flag的真正长度。
输入正确长度的字符串,再次trace:
pyda cmplog.py
cmp_locs: 46
FLAG: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
//.. TOO LONG NOT TO SHOW
cmp @ 0x1891b rcx=0xc3df45f3 rdx=0x11793013 False
cmp @ 0x15787 rdx=0x100000001 rax=0x100000001 True
cmp @ 0x15787 rdx=0x100000001 rax=0x100000001 True
cmp @ 0x15787 rdx=0x100000001 rax=0x100000001 True
cmp @ 0x15787 rdx=0x100000001 rax=0x100000001 True
cmp @ 0x15787 rdx=0x100000001 rax=0x100000001 True
cmp @ 0x15787 rdx=0x100000001 rax=0x100000001 True
cmp @ 0x15787 rdx=0x100000001 rax=0x100000001 True
cmp @ 0x15787 rdx=0x100000001 rax=0x100000001 True
cmp @ 0x15787 rdx=0x100000001 rax=0x100000001 True
cmp @ 0x15787 rdx=0x100000001 rax=0x100000001 True
cmp @ 0x15787 rdx=0x100000001 rax=0x100000001 True
cmp @ 0x15787 rdx=0x100000001 rax=0x100000001 True
cmp @ 0x15787 rdx=0x100000001 rax=0x100000001 True
cmp @ 0x15787 rdx=0x100000001 rax=0x100000001 True
cmp @ 0x15787 rdx=0x100000001 rax=0x100000001 True
cmp @ 0x15787 rdx=0x100000001 rax=0x100000001 True
cmp @ 0x15787 rdx=0x100000002 rax=0x100000001 False
"Wrong"
在“Wrong”往上找到一个很明显的cmp false trace
cmp @ 0x1891b rcx=0xc3df45f3 rdx=0x11793013 False
这里0x11793013已经在main函数中出现过,就在main函数开始的前16组的最后一个,所以猜测rdx=0x11793013为密文,rcx=0xc3df45f3为加密后的密文
输入不同的长度正确的字符串,再次trace:
pyda cmplog.py
cmp_locs: 46
FLAG:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
//.. TOO LONG NOT TO SHOW
cmp @ 0x1891b rcx=0x5b0608cd rdx=0x11793013 False
//.. TOO LONG NOT TO SHOW
"Wrong"
可以发现确实rcx发生了变化,可以断定这就是最后的flag checker部分
trace Cons:Cons - find transformation
现在我们确定了密文的长度,接下来我们关心的密文是怎么被加密的,经过前期的逆向工作,我们已经知道Con:Cons() 最终会被调用来存储一个unsigned int,密文存储于此,那么明文也可能存储于此。
写脚本开始trace:
from pwn import *
from pyda import *
from pwnlib.elf.elf import ELF
from pwnlib.util.packing import *
p = process(io=True)
e = ELF(p.exe_path)
e.address = p.maps[p.exe_path].base
base_address = e.address
print(hex(base_address))
def cons(p):
print(f"cons {hex(p.regs.rip-base_address),hex(u32(p.read(p.regs.rdi, 4))), hex(u32(p.read(p.regs.rsi, 4))), hex(u32(p.read(p.regs.rdx, 4)))}")
p.hook(e.address + 0x15a06, cons)
p.recvuntil("FLAG: ")
p.sendline(("ABCDEFGH").ljust(0x40,"A"))
p.run()
trace 结果
cons ('0x15a06', '0x0', '0xb7e9a2a4', '0x0')
cons ('0x15a06', '0x0', '0x1904c652', '0xdbbfdbf0')
cons ('0x15a06', '0x0', '0xbe8afe4d', '0xdbbfdc60')
cons ('0x15a06', '0x0', '0xbd18775a', '0xdbbfdcd0')
cons ('0x15a06', '0x0', '0x82841cf4', '0xdbbfdd40')
cons ('0x15a06', '0x0', '0xd2c1d5af', '0xdbbfddb0')
cons ('0x15a06', '0x0', '0xf389c4a', '0xdbbfde20')
cons ('0x15a06', '0x0', '0x451f151a', '0xdbbfde90')
cons ('0x15a06', '0x0', '0xd5689a8c', '0xdbbfdf00')
cons ('0x15a06', '0x0', '0x927b5bd9', '0xdbbfdf70')
cons ('0x15a06', '0x0', '0xf86c82d7', '0xdbbfdfe0')
cons ('0x15a06', '0x0', '0x34bc7c60', '0xdbbfe050')
cons ('0x15a06', '0x0', '0x97aef869', '0xdbbfe0c0')
cons ('0x15a06', '0x0', '0x2c0cccdd', '0xdbbfe130')
cons ('0x15a06', '0x0', '0x88d2ec9b', '0xdbbfe1a0')
cons ('0x15a06', '0x0', '0x11793013', '0xdbbfe210')
cons ('0x15a06', '0x0', '0x7', '0x0')
cons ('0x15a06', '0x0', '0x0', '0xdbbfe2f0')
cons ('0x15a06', '0x0', '0xc', '0xdbbfe360')
cons ('0x15a06', '0x0', '0xd', '0xdbbfe3d0')
cons ('0x15a06', '0x0', '0x2', '0xdbbfe440')
cons ('0x15a06', '0x0', '0xf', '0xdbbfe4b0')
cons ('0x15a06', '0x0', '0xb', '0xdbbfe520')
cons ('0x15a06', '0x0', '0x8', '0xdbbfe590')
cons ('0x15a06', '0x0', '0x6', '0xdbbfe600')
cons ('0x15a06', '0x0', '0x5', '0xdbbfe670')
cons ('0x15a06', '0x0', '0x9', '0xdbbfe6e0')
cons ('0x15a06', '0x0', '0x4', '0xdbbfe750')
cons ('0x15a06', '0x0', '0xa', '0xdbbfe7c0')
cons ('0x15a06', '0x0', '0x1', '0xdbbfe830')
cons ('0x15a06', '0x0', '0xe', '0xdbbfe8a0')
cons ('0x15a06', '0x0', '0x3', '0xdbbfe910')
cons ('0x15a06', '0x0', '0x41414141', '0x0')
cons ('0x15a06', '0x0', '0x41414141', '0xdbbffe30')
cons ('0x15a06', '0x0', '0x41414141', '0xdbbffea0')
cons ('0x15a06', '0x0', '0x41414141', '0xdbbfff10')
cons ('0x15a06', '0x0', '0x41414141', '0xdbbfff80')
cons ('0x15a06', '0x0', '0x41414141', '0xdbbffff0')
cons ('0x15a06', '0x0', '0x41414141', '0xdbc00060')
cons ('0x15a06', '0x0', '0x41414141', '0xdbc000d0')
cons ('0x15a06', '0x0', '0x41414141', '0xdbc00140')
cons ('0x15a06', '0x0', '0x41414141', '0xdbc001b0')
cons ('0x15a06', '0x0', '0x41414141', '0xdbc00220')
cons ('0x15a06', '0x0', '0x41414141', '0xdbc00290')
cons ('0x15a06', '0x0', '0x41414141', '0xdbc00300')
cons ('0x15a06', '0x0', '0x41414141', '0xdbc00370')
cons ('0x15a06', '0x0', '0x48474645', '0xdbc003e0')
cons ('0x15a06', '0x0', '0x44434241', '0xdbc00450')
cons ('0x15a06', '0x862acd7e', '0x4e4e4e4e', '0x0')
cons ('0x15a06', '0x862acd7e', '0x4e4e4e4e', '0xdbc00650')
cons ('0x15a06', '0x862acd7e', '0x4e4e4e4e', '0xdbc007a0')
cons ('0x15a06', '0x862acd7e', '0x4e4e4e4e', '0xdbc008f0')
cons ('0x15a06', '0x862acd7e', '0x4e4e4e4e', '0xdbc00960')
cons ('0x15a06', '0x862acd7e', '0x4e4e4e4e', '0xdbc009d0')
cons ('0x15a06', '0x862acd7e', '0x4e4e4e4e', '0xdbc00a40')
cons ('0x15a06', '0x862acd7e', '0x4e4e4e4e', '0xdbc00ab0')
cons ('0x15a06', '0x862acd7e', '0x4e4e4e4e', '0xdbc00b20')
cons ('0x15a06', '0x862acd7e', '0x4e4e4e4e', '0xdbc00b90')
cons ('0x15a06', '0x862acd7e', '0x4e4e4e4e', '0xdbc00c00')
cons ('0x15a06', '0x862acd7e', '0x4e4e4e4e', '0xdbc00c70')
cons ('0x15a06', '0x862acd7e', '0x4e4e4e4e', '0xdbc00ce0')
cons ('0x15a06', '0x862acd7e', '0x4e4e4e4e', '0xdbc00d50')
cons ('0x15a06', '0x862acd7e', '0x48464549', '0xdbc00dc0')
cons ('0x15a06', '0x862acd7e', '0x444a414e', '0xdbc00e30')
cons ('0x15a06', '0x862acd7e', '0x93af4e5e', '0x0')
cons ('0x15a06', '0x862acd7e', '0x93af4e5e', '0xdbc006c0')
cons ('0x15a06', '0x862acd7e', '0x93af4e5e', '0xdbc00730')
cons ('0x15a06', '0x862acd7e', '0x93af4e5e', '0xdbc00f10')
cons ('0x15a06', '0x862acd7e', '0x93af4e5e', '0xdbc00810')
cons ('0x15a06', '0x862acd7e', '0x93af4e5e', '0xdbc00880')
cons ('0x15a06', '0x862acd7e', '0x93af4e5e', '0xdbc00f80')
cons ('0x15a06', '0x862acd7e', '0x93af4e5e', '0xdbc00570')
cons ('0x15a06', '0x0', '0x93af4e5e', '0xdbc005e0')
cons ('0x15a06', '0x0', '0x93af4e5e', '0xdbc00ff0')
cons ('0x15a06', '0x0', '0x93af4e5e', '0xdbc01060')
cons ('0x15a06', '0x0', '0x93af4e5e', '0xdbc010d0')
cons ('0x15a06', '0x0', '0x93af4e5e', '0xdbc01140')
cons ('0x15a06', '0x0', '0x93af4e5e', '0xdbc011b0')
cons ('0x15a06', '0x0', '0xd36975c1', '0xdbc01220')
cons ('0x15a06', '0x0', '0xe14de95e', '0xdbc01290')
0xd 0xc
cons ('0x15a06', '0x862acd8c', '0x93af4e5e', '0x0')
cons ('0x15a06', '0x862acd8c', '0x93af4e5e', '0xdbc01370')
cons ('0x15a06', '0x862acd8c', '0x93af4e5e', '0xdbc00b20')
cons ('0x15a06', '0x862acd8c', '0xac0af82', '0xdbc00e30')
cons ('0x15a06', '0x862acd8c', '0xac0af82', '0xdbc00dc0')
cons ('0x15a06', '0x862acd8c', '0xac0af82', '0xdbc00d50')
cons ('0x15a06', '0x862acd8c', '0xac0af82', '0xdbc00ce0')
cons ('0x15a06', '0x862acd8c', '0xac0af82', '0xdbc00c70')
cons ('0x15a06', '0x862acd8c', '0xac0af82', '0xdbc00c00')
cons ('0x15a06', '0x862acd8c', '0xac0af82', '0xdbc00b90')
cons ('0x15a06', '0x862acd8c', '0xac0af82', '0xdbc00ea0')
cons ('0x15a06', '0x862acd8c', '0xac0af82', '0xdbc00650')
cons ('0x15a06', '0x862acd8c', '0xac0af82', '0xdbc007a0')
cons ('0x15a06', '0x862acd8c', '0xac0af82', '0xdbc008f0')
cons ('0x15a06', '0x862acd8c', '0x4a06941d', '0xdbc00960')
cons ('0x15a06', '0x862acd8c', '0x1b3fc722', '0xdbc009d0')
可以看到第三组就是输入的明文
下面看第一组transform
cons ('0x15a06', '0x862acd7e', '0x4e4e4e4e', '0x0')
cons ('0x15a06', '0x862acd7e', '0x4e4e4e4e', '0xdbc00650')
cons ('0x15a06', '0x862acd7e', '0x4e4e4e4e', '0xdbc007a0')
cons ('0x15a06', '0x862acd7e', '0x4e4e4e4e', '0xdbc008f0')
cons ('0x15a06', '0x862acd7e', '0x4e4e4e4e', '0xdbc00960')
cons ('0x15a06', '0x862acd7e', '0x4e4e4e4e', '0xdbc009d0')
cons ('0x15a06', '0x862acd7e', '0x4e4e4e4e', '0xdbc00a40')
cons ('0x15a06', '0x862acd7e', '0x4e4e4e4e', '0xdbc00ab0')
cons ('0x15a06', '0x862acd7e', '0x4e4e4e4e', '0xdbc00b20')
cons ('0x15a06', '0x862acd7e', '0x4e4e4e4e', '0xdbc00b90')
cons ('0x15a06', '0x862acd7e', '0x4e4e4e4e', '0xdbc00c00')
cons ('0x15a06', '0x862acd7e', '0x4e4e4e4e', '0xdbc00c70')
cons ('0x15a06', '0x862acd7e', '0x4e4e4e4e', '0xdbc00ce0')
cons ('0x15a06', '0x862acd7e', '0x4e4e4e4e', '0xdbc00d50')
cons ('0x15a06', '0x862acd7e', '0x48464549', '0xdbc00dc0')
cons ('0x15a06', '0x862acd7e', '0x444a414e', '0xdbc00e30')
可以看到明文到第一组密文做了变换,那么具体的变换是怎么做的呢?
gdb + ida + pyda backtrace 破解加密逻辑
要知道transform是怎么变换的,那就在transform完成的点backtrace,一步步往前回溯加密过程,直到找到加密逻辑。
写脚本进行backtrace:
from pwn import *
from pyda import *
from pwnlib.elf.elf import ELF
from pwnlib.util.packing import *
p = process(io=True)
e = ELF(p.exe_path)
e.address = p.maps[p.exe_path].base
base_address = e.address
print(hex(base_address))
def get_symbol_name(addr):
for name, sym in e.symbols.items():
print(name,hex(sym-base_address))
return "unknown"
def bt_hook(p):
print(f"Stack trace at {hex(p.regs.rip)}:")
current_rbp = p.regs.rbp
current_rsp = p.regs.rsp
try:
frame_count = 0
while current_rbp:
ret_addr = u64(p.read(current_rbp + 8, 8))
offset = ret_addr - base_address
print(f"Frame #{frame_count}: ret = {hex(offset)}")
current_rbp = u64(p.read(current_rbp, 8))
frame_count += 1
if frame_count > 20:
break
except Exception as e:
print(f"Error while unwinding stack: {e}")
print("\nRegisters:")
print(f"RIP: {hex(p.regs.rip)}")
print(f"RSP: {hex(p.regs.rsp)}")
print(f"RBP: {hex(p.regs.rbp)}")
print(f"[RSI]: {hex(u32(p.read(p.regs.rsi, 4)))}")
input("continue")
def cons(p):
print(f"cons {hex(p.regs.rip-base_address),hex(u32(p.read(p.regs.rdi, 4))), hex(u32(p.read(p.regs.rsi, 4))), hex(u32(p.read(p.regs.rdx, 4)))}")
p.hook(e.address + 0x15a06,bt_hook)
p.recvuntil("FLAG: ")
p.sendline(("ABCDEFGH").ljust(0x40,"A"))
p.run()
下面截取了第一次最后一组和第二次第一组的transform的backtrace:
Stack trace at 0x7f8abea24a06:
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Registers:
RIP: 0x7f8abea24a06
RSP: 0x7ffcab0cb268
RBP: 0x7ffcab0cb310
[RSI]: 0x444a414e
continue
Stack trace at 0x7f8abea24a06:
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Registers:
RIP: 0x7f8abea24a06
RSP: 0x7ffcab0c5388
RBP: 0x7ffcab0c5430
[RSI]: 0x93af4e5e
continue
可以看到backtrace有些许不同,放到diff 网站对比一下:
进入ida查看了0x8741和0x8e67处的逻辑,发现是不同的逻辑,推测这里就是不同的transform实现。
下面打开ida,动态调试验证猜想。
First Transformation
第一组transform的backtrace为:
Stack trace at 0x7f8abea24a06:
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Registers:
RIP: 0x7f8abea24a06
RSP: 0x7ffcab0c5688
RBP: 0x7ffcab0c5730
[RSI]: 0x4e4e4e4e
根据前面的trace结果,transform的过程如下:
0x8741处的代码为:
v2 = *a2;
v3 = a2[5];
v4 = a2[7];
v5 = a2[6];
v10 = 1;
std::variant>::variant(
(__int64)v14,
(__int64)&v10);
std::variant>::variant((__int64)v13, a2[4]);
ADD((__int64)v15, v5, (__int64)v13, (__int64)v14);
ZNKSt17reference_wrapperIK3fixIZ4mainEUlT_St7variantIJjNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEESt10shared_ptrI4ConsEEESC_SC_E1_EEclIJRSC_SC_SI_EEENSt15__invoke_resultIRSF_JDpT_EE4typeEDpOSL_(
(__int64)v16,
v3,
a2[3],
(__int64)v15,
v4);
v6 = (__int64 *)a2[1];
ZNKR3fixIZ4mainEUlT_St7variantIJjNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEESt10shared_ptrI4ConsEEESB_SB_E_EclIJRSB_SF_EEEDcDpOT_(
(__int64)v11,
(__int64 *)a2[2],
a2[3],
a2[4]);
ZNKR3fixIZ4mainEUlT_St7variantIJjNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEESt10shared_ptrI4ConsEEESB_SB_E0_EclIJSB_EEEDcDpOT_(
(__int64)v12,
v6,
(__int64)v11);
main::{lambda(std::variant>,std::variant>)#19}::operator()(
a1,
v2,
(__int64)v12,
(__int64)v16,
v7,
v8);
std::variant>::~variant((__int64)v12);
经过gdb调试可以分析出:
ADD
ZNKSt17reference_wrapperIK3fixIZ4mainEUlT_St7variantIJjNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEESt10shared_ptrI4ConsEEESC_SC_E1_EEclIJRSC_SC_SI_EEENSt15__invoke_resultIRSF_JDpT_EE4typeEDpOSL_
ZNKR3fixIZ4mainEUlT_St7variantIJjNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEESt10shared_ptrI4ConsEEESB_SB_E0_EclIJSB_EEEDcDpOT_(
(__int64)v12,
v6,
(__int64)v11);
main::{lambda(std::variant>,std::variant>)#19}::operator()(
a1,
v2,
(__int64)v12,
(__int64)v16,
v7,
v8);
最终一层层跟进,或者直接在diff最后一个不同的地址(0xf18e)开始跟会轻松一点:
最终跟到first transformation的主逻辑:
ADD((__int64)v15, v5, (__int64)v13, (__int64)v14);
ZNKSt17reference_wrapperIK3fixIZ4mainEUlT_St7variantIJjNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEESt10shared_ptrI4ConsEEESC_SC_E1_EEclIJRSC_SC_SI_EEENSt15__invoke_resultIRSF_JDpT_EE4typeEDpOSL_(
(__int64)v16,
v3,
a2[3],
(__int64)v15,
v4);
v6 = (__int64 *)a2[1];
ZNKR3fixIZ4mainEUlT_St7variantIJjNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEESt10shared_ptrI4ConsEEESB_SB_E_EclIJRSB_SF_EEEDcDpOT_(
(__int64)v11,
(__int64 *)a2[2],
a2[3],
a2[4]);
ZNKR3fixIZ4mainEUlT_St7variantIJjNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEESt10shared_ptrI4ConsEEESB_SB_E0_EclIJSB_EEEDcDpOT_(
(__int64)v12,
v6,
(__int64)v11);
main::{lambda(std::variant>,std::variant>)#19}::operator()(
a1,
v2,
(__int64)v12,
(__int64)v16,
v7,
v8);
gdb调试可以发现是简单的sbox代换:
In [56]: sbox = [0x7,
...: 0x0,
...: 0xc,
...: 0xd,
...: 0x2,
...: 0xf,
...: 0xb,
...: 0x8,
...: 0x6,
...: 0x5,
...: 0x9,
...: 0x4,
...: 0xa,
...: 0x1,
...: 0xe,
...: 0x3][::-1]
In [57]: hex(sbox[0x4])
Out[57]: '0x4'
In [58]: hex(sbox[0x1])
Out[58]: '0xe'
Second Transformation
如法炮制,在0x8e67 发现second transformation的主逻辑:
ADD((__int64)v17, v5, (__int64)v15, (__int64)v16);
ZNKSt17reference_wrapperIK3fixIZ4mainEUlT_St7variantIJjNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEESt10shared_ptrI4ConsEEESC_SC_E2_EEclIJRSC_SC_SI_EEENSt15__invoke_resultIRSF_JDpT_EE4typeEDpOSL_(
v18,
v3,
a2[3],
v17,
v4);
v6 = a2[1];
v10 = 0x4E6A44B9;
std::variant>::variant(
(__int64)v13,
(__int64)&v10);
ZNKR3fixIZ4mainEUlT_St7variantIJjNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEESt10shared_ptrI4ConsEEESB_SB_E_EclIJRSB_SF_EEEDcDpOT_(
(__int64)v12,
(__int64 *)a2[2],
a2[3],
a2[4]);
main::{lambda(std::variant>,std::variant>)#3}::operator()(
(__int64)v14,
v6,
(__int64)v12,
(__int64)v13);
main::{lambda(std::variant>,std::variant>)#19}::operator()(
a1,
v2,
(__int64)v14,
(__int64)v18,
v7,
v8);
可以发现是简单的32位乘法:
__int64 __fastcall main::{lambda(std::variant>,std::variant>)#3}::operator()(
__int64 a1,
__int64 a2,
__int64 a3,
__int64 a4)
{
int v4;
int v7;
unsigned __int64 v8;
v8 = __readfsqword(0x28u);
v4 = *(_DWORD *)std::get>(a3);
v7 = v4 * *(_DWORD *)std::get>(a4);
std::variant>::variant(
a1,
(__int64)&v7);
return a1;
}
验证:
In [59]: hex( 0x4E6A44B9 * 0x4e4e4e4e & 2 ** 32 -1)
Out[59]: '0x93af4e5e'
Third Transformation
如法炮制,这里需要注意的是,在第三组索引13 14 15处的字符经过third transformation没有改变,所以要追踪要看改变的index处的backtrace也就是12处:
Stack trace at 0x7f8abea24a06:
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Frame
Registers:
RIP: 0x7f8abea24a06
RSP: 0x7ffcab0c00e8
RBP: 0x7ffcab0c0190
[RSI]: 0xac0af82
continue
diff一下,可以发现第一个不同的地址为0x983e:
ADD((__int64)v16, v5, (__int64)v14, (__int64)v15);
ZNKSt17reference_wrapperIK3fixIZ4mainEUlT_St7variantIJjNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEESt10shared_ptrI4ConsEEESC_SC_SC_E_EEclIJRSC_SI_SC_SI_EEENSt15__invoke_resultIRSF_JDpT_EE4typeEDpOSL_(
(__int64)v17,
v3,
a2[2],
a2[5],
(__int64)v16,
v4);
v6 = (__int64 *)a2[1];
std::variant>::variant((__int64)v12, a2[3]);
std::variant>::variant((__int64)v11, a2[2]);
main::{lambda(std::variant>,std::variant>)#21}::operator()(
(__int64)v13,
v6,
(__int64)v11,
(__int64)v12);
main::{lambda(std::variant>,std::variant>)#19}::operator()(
a1,
v2,
(__int64)v13,
(__int64)v17,
v7,
v8);
可以发现主要逻辑在lambda#21中实现,逻辑可以总结为:
rotl(p[i+3], 29) ^ rotl(p[i+2], 17) ^ rotl(p[i+1], 7) ^ p[i]
验证:
In [62]: hex(rotl(0x93af4e5e, 29) ^ rotl(0x93af4e5e, 17) ^ rotl(0x93af4e5e, 7) ^ 0x93af4e5e)
Out[62]: '0xac0af82'
后面的transformations,就是这三组transformation的循环,需要注意的是third transformation的开始下标每次都会加1,初始为12。
编写脚本写出加密逻辑并根据加密逻辑写出解密程序:
from pwn import *
import copy
sbox = [0x7,
0x0,
0xc,
0xd,
0x2,
0xf,
0xb,
0x8,
0x6,
0x5,
0x9,
0x4,
0xa,
0x1,
0xe,
0x3][::-1]
sbox_rev = {}
for index,num in enumerate(sbox):
sbox_rev[num] = index
def rotl(num,offset):
return (num << offset | num >> (32 - offset)) & 2 ** 32 - 1
def enc(plainlist):
enclist = []
oldlist = plainlist
start = 0xc
for i in range(8):
newlist = []
for num in oldlist:
newnum = 0
for j in range(8):
index = num >> (4*j) & 0xf
newnum |= sbox[index] << (4*j)
newlist.append(newnum)
for index,num in enumerate(newlist):
newlist[index] = ((num * 0x4E6A44B9) & 2 ** 32 - 1)
cur = start + i
roxlist = copy.deepcopy(newlist)
for k in range(13):
val = rotl(roxlist[(cur+1)%16],7) ^ rotl(roxlist[(cur+2)%16],17) ^ rotl(roxlist[(cur+3)%16],29) ^ roxlist[cur%16]
newlist[cur%16] = val
cur -= 1
oldlist = newlist
enclist = newlist
for i in newlist:
print(hex(i),end=',')
print()
return enclist
def dec(enc_list):
print("dec-------------------------------------------------------------------------------------------")
plain_list =[]
oldlist = enc_list
start = 3
for i in range(8):
newlist = []
cur = start - i
roxlist = oldlist
for k in range(13):
val = rotl(roxlist[(cur+1)%16],7) ^ rotl(roxlist[(cur+2)%16],17) ^ rotl(roxlist[(cur+3)%16],29) ^ roxlist[(cur)%16]
oldlist[(cur)%16] = val
cur -= 1
for index,num in enumerate(oldlist):
oldlist[index] = ((num * 0x20808189) & 2 ** 32 - 1)
for num in oldlist:
newnum = 0
for j in range(8):
index = num >> (4*j) & 0xf
newnum |= sbox_rev[index] << (4*j)
newlist.append(newnum)
oldlist = newlist
plain_list = newlist
return plain_list
enc_list = [ 0xb7e9a2a4,
0x1904c652,
0xbe8afe4d,
0xbd18775a,
0x82841cf4,
0xd2c1d5af,
0xf389c4a,
0x451f151a,
0xd5689a8c,
0x927b5bd9,
0xf86c82d7,
0x34bc7c60,
0x97aef869,
0x2c0cccdd,
0x88d2ec9b,
0x11793013][::-1]
plain_list = dec(enc_list)
flag = b''
for num in plain_list:
flag += int.to_bytes(num,4,'little')
print(flag)
写在后面
通过flag我们可以看出,这是一个函数式编程混淆的程序,我选择对抗的函数式编程混淆的方式是二进制插桩trace,来还原出程序的运行逻辑,其实函数式编程的混淆的最主要的防护就是通过函数互相调用实现某种循环的效果来进行控制流的混淆。
如果我们使用工具进行trace能还原出执行过程,那么函数式编程混淆的防护基本就土崩瓦解了,这里使用pyda也算是对症下药了,在解题过程中也考虑过使用frida,但是frida attch的粒度好像到不了汇编指令这一块,或者说支持但是效果不尽人意,在我想要输出的地方没有输出,所以最后选用了二进制插桩工具实现trace,只能说真的好用,也是我第一次使用pyda,特此写一篇记录下学习心得。
看雪ID:SleepAlone
https://bbs.kanxue.com/user-home-950548.htm
*本文为看雪论坛精华文章,由 SleepAlone 原创,转载请注明来自看雪社区