专栏名称: 看雪学苑
致力于移动与安全研究的开发者社区,看雪学院(kanxue.com)官方微信公众帐号。
目录
相关文章推荐
广西电力交易中心  ·  交易中心数字化转型成果 荣获中国信息协会优秀案例 ·  2 天前  
广西电力交易中心  ·  交易中心数字化转型成果 荣获中国信息协会优秀案例 ·  2 天前  
研讯社  ·  2.0,进击的算力! ·  2 天前  
研讯社  ·  2.0,进击的算力! ·  2 天前  
Java基基  ·  IDEA+Docker 远程一键部署项目,真香! ·  4 天前  
信安之路  ·  玩儿转网站备份泄漏漏洞 ·  6 天前  
51好读  ›  专栏  ›  看雪学苑

SECCON 2024:动态二进制插桩trace 破解函数式编程混淆

看雪学苑  · 公众号  · 互联网安全  · 2024-12-20 17:59

正文

这道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"





ida 逆向初探


题目由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至少需要三层调用才能到达真实逻辑:


// level 1
*((_QWORD *)a1 + 3) = std::_Function_handler<:variant int>> ()(void),main::{lambda(void)#1}>::_M_invoke;

//level 2
std::__invoke_r<:variant int>>,main::{lambda(void)#1} &>(a1, pointer);

//level 3
std::__invoke_impl<:variant int>>,main::{lambda(void)#1} &>(a1, v2);

//level 4
main::{lambda(void)#1}::operator()(a1, v2);

//level5
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


使用pyda trace来跟踪程序的主逻辑


首先搭建环境:


FROM ubuntu:24.04 as target

FROM ghcr.io/ndrewh/pyda

COPY --from=target /usr/lib/x86_64-linux-gnu/ /target_libs/

RUN apt update && apt install -y patchelf

COPY F /F
RUN patchelf --set-interpreter /target_libs/ld-linux-x86-64.so.2 --set-rpath /target_libs/ /F
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

# TODO: memory cmp
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 -- /F
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 -- /F
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 -- /F
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 结果


//enc
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')

//sbox
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')

//input
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')

//1 sub_byte
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')

//2 mul 0xe14de95e
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')

//3 rotl(0x93af4e5e, 29) ^ rotl(0x93af4e5e, 17) ^ rotl(0xd36975c1, 7) ^ 0xe14de95e
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


//1 sub_byte
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))
#if sym == addr:
# return name
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))
#fun_name = get_symbol_name(ret_addr)
#print(fun_name)
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)))}")
#if u32(p.read(p.regs.rsi, 4)) == 0x444a414e | u32(p.read(p.regs.rsi, 4)) == 0x93af4e5e:
input("continue")

#p.hook(e.address + 0x1891b, bt_hook)

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)))}")

#if u32(p.read(p.regs.rsi, 4)) == 0x4e4e4e4e:
p.hook(e.address + 0x15a06,bt_hook)

#p.hook(e.address + 0x15a06, cons)

p.recvuntil("FLAG: ")
p.sendline(("ABCDEFGH").ljust(0x40,"A"))
#get_symbol_name(1)
p.run()


下面截取了第一次最后一组和第二次第一组的transform的backtrace:


Stack trace at 0x7f8abea24a06:
Frame #0: ret = 0x1c40b
Frame #1: ret = 0x1bb86
Frame #2: ret = 0x1a834
Frame #3: ret = 0x18efd
Frame #4: ret = 0x1684a
Frame #5: ret = 0x3bfe
Frame #6: ret = 0x8741
Frame #7: ret = 0x13a61
Frame #8: ret = 0x117c2
Frame #9: ret = 0xf18e
Frame #10: ret = 0x167d4
Frame #11: ret = 0x3ba6
Frame #12: ret = 0x8a06
Frame #13: ret = 0x4cec
Frame #14: ret = 0xc124
Frame #15: ret = 0x1430f
Frame #16: ret = 0x12afe
Frame #17: ret = 0x1007c
Frame #18: ret = 0x167d4
Frame #19: ret = 0x3ba6
Frame #20: ret = 0xc427

Registers:
RIP: 0x7f8abea24a06
RSP: 0x7ffcab0cb268
RBP: 0x7ffcab0cb310
[RSI]: 0x444a414e
continue
Stack trace at 0x7f8abea24a06:
Frame #0: ret = 0x1c40b
Frame #1: ret = 0x1bb86
Frame #2: ret = 0x1a834
Frame #3: ret = 0x18efd
Frame #4: ret = 0x1684a
Frame #5: ret = 0x3bfe
Frame #6: ret = 0x8e67
Frame #7: ret = 0x13c1b
Frame #8: ret = 0x11bae
Frame #9: ret = 0xf4bc
Frame #10: ret = 0x167d4
Frame #11: ret = 0x3ba6
Frame #12: ret = 0x914a
Frame #13: ret = 0x8c54
Frame #14: ret = 0x1192b
Frame #15: ret = 0xf2e2
Frame #16: ret = 0xd73f
Frame #17: ret = 0x8dcc
Frame #18: ret = 0x13c1b
Frame #19: ret = 0x11bae
Frame #20: ret = 0xf4bc

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 #0: ret = 0x1c40b
Frame #1: ret = 0x1bb86
Frame #2: ret = 0x1a834
Frame #3: ret = 0x18efd
Frame #4: ret = 0x1684a
Frame #5: ret = 0x3bfe
Frame #6: ret = 0x8741 // first_transform
Frame #7: ret = 0x13a61
Frame #8: ret = 0x117c2
Frame #9: ret = 0xf18e
Frame #10: ret = 0x167d4
Frame #11: ret = 0x3ba6
Frame #12: ret = 0x8a06
Frame #13: ret = 0x8558
Frame #14: ret = 0x1153f
Frame #15: ret = 0xefb4
Frame #16: ret = 0xd539
Frame #17: ret = 0x86d0
Frame #18: ret = 0x13a61
Frame #19: ret = 0x117c2
Frame #20: ret = 0xf18e

Registers:
RIP: 0x7f8abea24a06
RSP: 0x7ffcab0c5688
RBP: 0x7ffcab0c5730
[RSI]: 0x4e4e4e4e


根据前面的trace结果,transform的过程如下:


0x41414141 -> 0x4e4e4e4e 


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);// 0X8741


经过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);
// do first_transform
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);//32bits mul
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; // ebx
int v7; // [rsp+24h] [rbp-1Ch] BYREF
unsigned __int64 v8; // [rsp+28h] [rbp-18h]

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 #0: ret = 0x1c40b
Frame #1: ret = 0x1bb86
Frame #2: ret = 0x1a834
Frame #3: ret = 0x18efd
Frame #4: ret = 0x1684a
Frame #5: ret = 0x3bfe
Frame #6: ret = 0x983e
Frame #7: ret = 0x13ddb
Frame #8: ret = 0x11fde
Frame #9: ret = 0xf804
Frame #10: ret = 0x167d4
Frame #11: ret = 0x3ba6
Frame #12: ret = 0x9f2e
Frame #13: ret = 0x13f95
Frame #14: ret = 0x12340
Frame #15: ret = 0xfa54
Frame #16: ret = 0x167d4
Frame #17: ret = 0x3ba6
Frame #18: ret = 0xa5a6
Frame #19: ret = 0x93cf
Frame #20: ret = 0x11d2f

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);// DO THIRD TRANSFORM
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 = []
#sub_byte
for num in oldlist:
newnum = 0
for j in range(8):
index = num >> (4*j) & 0xf
newnum |= sbox[index] << (4*j)
newlist.append(newnum)
#mul
for index,num in enumerate(newlist):
newlist[index] = ((num * 0x4E6A44B9) & 2 ** 32 - 1)
#rotl ^ rotl ^ rotl ^ num
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 = []
#rotl ^ rotl ^ rotl ^ num
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 i in oldlist:
# print(hex(i),end=',')
# print()
#mul
for index,num in enumerate(oldlist):
oldlist[index] = ((num * 0x20808189) & 2 ** 32 - 1)
#sub_byte
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

# plaintext = ("ABCDEFGH").ljust(0x40,"A")
# plainlist = []

# for i in range(0,len(plaintext),4):
# plainlist.append(u32(plaintext[i:i+4]))
# enc_list = enc(plainlist)

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)
#b'SECCON{fUnCt10n4l_pRoGr4mM1n6_1s_pR4c7iC4lLy_a_pUr3_0bfu5c4T1oN}'


写在后面

通过flag我们可以看出,这是一个函数式编程混淆的程序,我选择对抗的函数式编程混淆的方式是二进制插桩trace,来还原出程序的运行逻辑,其实函数式编程的混淆的最主要的防护就是通过函数互相调用实现某种循环的效果来进行控制流的混淆。


如果我们使用工具进行trace能还原出执行过程,那么函数式编程混淆的防护基本就土崩瓦解了,这里使用pyda也算是对症下药了,在解题过程中也考虑过使用frida,但是frida attch的粒度好像到不了汇编指令这一块,或者说支持但是效果不尽人意,在我想要输出的地方没有输出,所以最后选用了二进制插桩工具实现trace,只能说真的好用,也是我第一次使用pyda,特此写一篇记录下学习心得。





看雪ID:SleepAlone

https://bbs.kanxue.com/user-home-950548.htm

*本文为看雪论坛精华文章,由 SleepAlone 原创,转载请注明来自看雪社区


# 往期推荐

1、PWN入门-SROP拜师

2、一种apc注入型的Gamarue病毒的变种

3、野蛮fuzz:提升性能

4、关于安卓注入几种方式的讨论,开源注入模块实现

5、2024年KCTF水泊梁山-反混淆





球分享

球点赞

球在看



点击阅读原文查看更多