[原创]2022祥云杯CTF babyparser
2022-11-25 20:44:44 Author: bbs.pediy.com(查看原文) 阅读量:27 收藏

原题附件需要glibc2.34,但我的kali版本不够高,所以我patchelf成自编译的2.34,也因此搞丢了原题附件,现在上传的是patchelf以后的版本, 如果有需要可以自己patch回去。

比赛时候打开一看啊,发现这个程序用了ollvm的deflat来进行混淆,而且仔细一看是个全混淆,所有能混淆的地方它都混淆了。还好之前有所准备,拿起了之前备好的脚本准备一把梭。 梭完一看发现失败了,原来是这个ollvm改良过,出题人修改了deflat的llvm pass ,分发逻辑增加了一步。 其中真实块的样子大概是这样的

这时候我以为出题人把打乱的函数都用上图中类似的call rax 间接调用来实现。 此时我心态平和,我以为还剩三小时放出来的题必不会很难,于是想尝试修改上面那个基于angr的deflat脚本。我低估了其中的难度,对于angr本身也不熟悉的我根本改不来。所以最后尝试动调加手动patch binary。 但是只是尝试了一会我就发现这个做不到,因为量太大了。因此最后决定这题先放弃,跑去做别的了。

赛后当然还是想把这道题deflat出来, 于是在之前的基础上学习了angr的用法,折腾了几天,发现angr作为符号执行的工具对于这种去混淆的处理其实并不是很合适。 因为deflat部分的处理本质上是在模拟执行程序,并且记录真实的控制流,而这部分并不是angr所擅长的。那么谁更合适呢? 当然是模拟执行的神器unicorn!

转头去尝试用unicorn完成deflat后,这题终于也走上了正轨。找了已有的unicorn deflat脚本,并在此基础上进行更改,写出了一个deflat脚本:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

import collections

import angr

import am_graph

from util import *

from unicorn import *

from unicorn.x86_const import *

from capstone import *

from capstone.x86 import *

obfus = [0x405DA0, 0x417610, 0x4149D0]

def reg_ctou(regname:str):

    mp =  { 'INVALID' : 0'AH' : 1'AL' : 2'AX' : 3'BH' : 4'BL' : 5'BP' : 6'BPL' : 7'BX' : 8'CH' : 9'CL' : 10'CS' : 11'CX' : 12'DH' : 13'DI' : 14'DIL' : 15'DL' : 16'DS' : 17'DX' : 18'EAX' : 19'EBP' : 20'EBX' : 21'ECX' : 22'EDI' : 23'EDX' : 24'EFLAGS' : 25'EIP' : 26'ES' : 28'ESI' : 29'ESP' : 30'FPSW' : 31'FS' : 32'GS' : 33'IP' : 34'RAX' : 35'RBP' : 36'RBX' : 37'RCX' : 38'RDI' : 39'RDX' : 40'RIP' : 41'RSI' : 43'RSP' : 44'SI' : 45'SIL' : 46'SP' : 47'SPL' : 48'SS' : 49'CR0' : 50'CR1' : 51'CR2' : 52'CR3' : 53'CR4' : 54'CR8' : 58'DR0' : 66'DR1' : 67'DR2' : 68'DR3' : 69'DR4' : 70'DR5' : 71'DR6' : 72'DR7' : 73'FP0' : 82'FP1' : 83'FP2' : 84'FP3' : 85'FP4' : 86'FP5' : 87'FP6' : 88'FP7' : 89'K0' : 90'K1' : 91'K2' : 92'K3' : 93'K4' : 94'K5' : 95'K6' : 96'K7' : 97'MM0' : 98'MM1' : 99'MM2' : 100'MM3' : 101'MM4' : 102'MM5' : 103'MM6' : 104'MM7' : 105'R8' : 106'R9' : 107'R10' : 108'R11' : 109'R12' : 110'R13' : 111'R14' : 112'R15' : 113'ST0' : 114'ST1' : 115'ST2' : 116'ST3' : 117'ST4' : 118'ST5' : 119'ST6' : 120'ST7' : 121'XMM0' : 122'XMM1' : 123'XMM2' : 124'XMM3' : 125'XMM4' : 126'XMM5' : 127'XMM6' : 128'XMM7' : 129'XMM8' : 130'XMM9' : 131'XMM10' : 132'XMM11' : 133'XMM12' : 134'XMM13' : 135'XMM14' : 136'XMM15' : 137'XMM16' : 138'XMM17' : 139'XMM18' : 140'XMM19' : 141'XMM20' : 142'XMM21' : 143'XMM22' : 144'XMM23' : 145'XMM24' : 146'XMM25' : 147'XMM26' : 148'XMM27' : 149'XMM28' : 150'XMM29' : 151'XMM30' : 152'XMM31' : 153'YMM0' : 154'YMM1' : 155'YMM2' : 156'YMM3' : 157'YMM4' : 158'YMM5' : 159'YMM6' : 160'YMM7' : 161'YMM8' : 162'YMM9' : 163'YMM10' : 164'YMM11' : 165'YMM12' : 166'YMM13' : 167'YMM14' : 168'YMM15' : 169'YMM16' : 170'YMM17' : 171'YMM18' : 172'YMM19' : 173'YMM20' : 174'YMM21' : 175'YMM22' : 176'YMM23' : 177'YMM24' : 178'YMM25' : 179'YMM26' : 180'YMM27' : 181'YMM28' : 182'YMM29' : 183'YMM30' : 184'YMM31' : 185'ZMM0' : 186'ZMM1' : 187'ZMM2' : 188'ZMM3' : 189'ZMM4' : 190'ZMM5' : 191'ZMM6' : 192'ZMM7' : 193'ZMM8' : 194'ZMM9' : 195'ZMM10' : 196'ZMM11' : 197'ZMM12' : 198'ZMM13' : 199'ZMM14' : 200'ZMM15' : 201'ZMM16' : 202'ZMM17' : 203'ZMM18' : 204'ZMM19' : 205'ZMM20' : 206'ZMM21' : 207'ZMM22' : 208'ZMM23' : 209'ZMM24' : 210'ZMM25' : 211'ZMM26' : 212'ZMM27' : 213'ZMM28' : 214'ZMM29' : 215'ZMM30' : 216'ZMM31' : 217'R8B' : 218'R9B' : 219'R10B' : 220'R11B' : 221'R12B' : 222'R13B' : 223'R14B' : 224'R15B' : 225'R8D' : 226'R9D' : 227'R10D' : 228'R11D' : 229'R12D' : 230'R13D' : 231'R14D' : 232'R15D' : 233'R8W' : 234'R9W' : 235'R10W' : 236'R11W' : 237'R12W' : 238'R13W' : 239'R14W' : 240'R15W' : 241'IDTR' : 242'GDTR' : 243'LDTR' : 244'TR' : 245'FPCW' : 246'FPTAG' : 247'MSR' : 248'MXCSR' : 249'FS_BASE' : 250'GS_BASE' : 251'FLAGS' : 252'RFLAGS' : 253'FIP' : 254'FCS' : 255'FDP' : 256'FDS' : 257'FOP' : 258'ENDING' : 259,  }

    regname = regname.upper()

    return mp[regname]

def get_context():

    global mu

    return mu.context_save()

def set_context(context:bytes):

    global mu

    if context == None :

        return

    else:

        mu.context_restore(context)

    return

def hook_mem_access(uc,type,address,size,value,userdata):

    pc = uc.reg_read(UC_X86_REG_RIP)

    print ('pc:%x type:%d addr:%x size:%x' % (pc,type,address,size))

def hook_code(uc, address, size, user_data):

    global base

    global is_success

    global list_trace

    global relevants

    global next_real_block_addr

    global block_start_addr

    global branch_control

    global instrs_size

    global memset_count

    global nop_instruc

    instrs_size = size

    if is_success:

        uc.emu_stop()

        return

    for ins in md.disasm(bin[address-base:address -base + size], address-base):

        if address in relevants:

            if address in list_trace:

                print("sssssss")

                uc.emu_stop()

            else:

                list_trace[address] = 1

        if address in relevants and address != block_start_addr:

            is_success = True

            next_real_block_addr = address

            uc.emu_stop()

            return

        flag_pass = False

        if ins.mnemonic.startswith("call") :

            if(ins.op_str  == '0x2090' and memset_count < 2): 

                memset_count += 1

                ptr = uc.reg_read(UC_X86_REG_RDI)

                val = uc.reg_read(UC_X86_REG_ESI).to_bytes(1,'little')

                siz = uc.reg_read(UC_X86_REG_EDX)

                uc.mem_write(ptr,val*siz)

                flag_pass = True

            elif(ins.op_str not in ('rax','rbx','rcx','rdx','rsi','rdi','r8','r9','r10','r11','r12','r13','r14','r15') ):

                flag_pass = True

            elif(ins.op_str in ('rax','rbx','rcx','rdx','rsi','rdi','r8','r9','r10','r11','r12','r13','r14','r15')):

                func = uc.reg_read(reg_ctou(ins.op_str))

                if func in obfus:

                    nop_instruc.add((address,size))

                else:

                    flag_pass = True

        if flag_pass:

            uc.reg_write(UC_X86_REG_RIP, address + size)

            return     

        if ins.id == X86_INS_RET :

            if relevants[block_start_addr] in retn_nodes:

                is_success = False

                print ("ret ins in {}".format(hex(address)))

                uc.emu_stop()

                return

            else:

                pass

        if ins.mnemonic.startswith('cmov'):

            regs = [reg_ctou(x) for x in ins.op_str.split(', ')]

            assert len(regs) == 2

            v1 = uc.reg_read(regs[0])

            v2 = uc.reg_read(regs[1])

            if branch_control == 1:

                uc.reg_write(regs[0], v1)

            else:

                uc.reg_write(regs[0], v2)

            uc.reg_write(UC_X86_REG_RIP, address + size)

def find_path(start_addr,branch = None, from_error = False):

    global bin

    global base

    global mu

    global list_trace

    global block_start_addr

    global next_real_block_addr

    global is_success

    global branch_control

    try:

        list_trace = {}

        if from_error == False:

            block_start_addr = start_addr

        is_success = False

        next_real_block_addr = 0

        branch_control = branch

        mu.emu_start(start_addr, start_addr+0x10000)

    except UcError as e:

        pc = mu.reg_read(UC_X86_REG_RIP)

        if pc != 0:

            print(" pc:%x  block_start_addr:%x" % (pc,block_start_addr))

            return find_path(pc + instrs_size, branch, from_error=True)

        else:

            print("ERROR: %s  pc:%x  block_start_addr:%x" % (e,pc,block_start_addr))

    if is_success:

        return next_real_block_addr

    return None

def fix(bin:bytearray,flow:dict,nop_nodes:list, patch_instrs :dict):

    ori_len = len(bin)

    for nop_node in nop_nodes:

        fill_nop(bin, nop_node.addr-base,

                 nop_node.size, project.arch)

    for a in nop_instruc:

        fill_nop(bin, a[0]-base, a[1], project.arch)

    for parent, childs in flow.items():

        parent = relevants[parent]

        if len(childs) == 1:

            parent_block = project.factory.block(parent.addr, size=parent.size)

            last_instr = parent_block.capstone.insns[-1]

            file_offset = last_instr.address - base

            if(parent.addr + parent.size == childs[0] and last_instr.op_str != hex(the_last_jmp_block)):

                continue

            if project.arch.name in ARCH_X86:

                fill_nop(bin, file_offset, last_instr.size, project.arch)

                patch_value = ins_j_jmp_hex_x86(last_instr.address, childs[0], 'jmp')

            patch_instruction(bin, file_offset, patch_value)

        else:

            instr = patch_instrs[parent]

            file_offset = instr.address - base

            fill_nop(bin, file_offset, parent.addr +

                     parent.size - base - file_offset, project.arch)

            if project.arch.name in ARCH_X86:

                patch_value = ins_j_jmp_hex_x86(instr.address, childs[0], instr.mnemonic[len('cmov'):])

                patch_instruction(bin, file_offset, patch_value)

                file_offset += 6

                patch_value = ins_j_jmp_hex_x86(instr.address+6, childs[1], 'jmp')

                patch_instruction(bin, file_offset, patch_value)

    assert(ori_len == len(bin))       

    return bin

if __name__ == "__main__":   

    md = Cs(CS_ARCH_X86,CS_MODE_64)

    md.detail = True

    filename = "babyparser_recover_main"

    new_filename = filename

    with open(filename, 'rb') as fp:

        bin = fp.read()

    project = angr.Project(filename, load_options={'auto_load_libs': False})

    base = project.loader.main_object.mapped_base >> 12 << 12 

    start = 0x40fe20

    the_last_jmp_block = 0x411448

    cfg = project.analyses.CFGFast(normalize=True,force_complete_scan=False)

    target_function = cfg.functions.get(start)

    assert target_function != None

    supergraph = am_graph.to_supergraph(target_function.transition_graph)

    prologue_nodes = []

    retn_nodes = []

    relevant_nodes = []

    nop_nodes = []

    patch_instrs = {}

    for node in supergraph.nodes():

        if supergraph.in_degree(node) == 0:

            prologue_nodes.append(node)

        elif supergraph.out_degree(node) == 0 :

            retn_nodes.append(node)

        elif supergraph.out_degree(node) == 1 and node.addr != the_last_jmp_block:

            relevant_nodes.append(node)

        elif supergraph.out_degree(node) == 2 :

            nop_nodes.append(node)

    print(prologue_nodes)

    print(retn_nodes)

    if len(prologue_nodes) != 1 or prologue_nodes[0].addr != start:

        print("Something must be wrong...")

        exit(0)

    main_dispatcher_node = list(supergraph.successors(prologue_nodes[0]))[0]

    relevant_block_addrs = [(node.addr) for node in relevant_nodes]

    relevants = {}

    for node in relevant_nodes+prologue_nodes+retn_nodes:

        relevants[node.addr] = node

    print('*******************relevant blocks************************')

    print('prologue: %#x' % start)

    print('main_dispatcher: %#x' % main_dispatcher_node.addr)

    print('retn: ' , [hex(node.addr) for node in retn_nodes])

    print('relevant_blocks:', [hex(addr) for addr in relevant_block_addrs])

    print('*******************simulated execution*********************')

    flow = collections.defaultdict(list)

    instrs_size = 0

    memset_count = 0

    nop_instruc = set()

    mu = Uc(UC_ARCH_X86, UC_MODE_64)

    mu.mem_map(0x80000000,0x10000 * 8)

    mu.mem_map(base, 16 * 1024 * 1024)

    mu.mem_write(0x401000, bin[0x2000:0x2000+0x19559]) 

    mu.mem_write(0x420dd0,bin[0x20dd0:0x20dd0+0x6270]) 

    mu.reg_write(UC_X86_REG_RSP, 0x80000000 + 0x10000 * 6)

    mu.hook_add(UC_HOOK_CODE, hook_code)

    mu.hook_add(UC_HOOK_MEM_UNMAPPED, hook_mem_access)

    mu.reg_write(UC_X86_REG_RDI, 1)

    list_trace = {}

    is_debug = False

    queue = [(start, None)]

    while len(queue) != 0:

        env = queue.pop()

        address = env[0]

        context = env[1]

        if address in flow:

            continue

        set_context(context)

        node = relevants[address]

        block = project.factory.block(address, size=node.size) 

        has_branches = False

        hook_addr = []

        for ins in block.capstone.insns:

            if ins.insn.mnemonic.startswith('cmov'):

                has_branches = True

                if node not in patch_instrs:

                    patch_instrs[node] = ins

                break

        if has_branches:

            ctx = get_context()

            p1 = find_path(address, 0)

            if p1 != None:

                queue.append((p1, get_context()))

                flow[address].append(p1)

            set_context(ctx)

            p2 = find_path(address, 1)

            if p1 == p2:

                p2 = None

            if p2 != None:

                queue.append((p2, get_context()))

                flow[address].append(p2)

        else:

            p = find_path(address)

            if p != None:

                queue.append((p, get_context()))

                flow[address].append(p)

    print('************************flow******************************')

    for k, v in flow.items():

        print('%#x: ' % k, [hex(child) for child in v])

    print('************************fix******************************')

    new_bin = fix(bytearray(bin), flow, nop_nodes, patch_instrs)

    with open(new_filename,"wb") as fp:

        fp.write(new_bin)

接下来就是逆向的问题了,先简单逆一下能知道,程序要求的输入是符号和数字,其中数字必须是0x[a-f][a-f][a-f][a-f] 符号只能是?!^| ,再加上题目说这是一个parser,我们可以猜到这是一个算式的解析。 那么肯定代码里会出现stack 这个stl 的数据结构, 而我对此并不是很熟悉。 果然我在动调的时候发现了sub_4151B0是一个parser功能的函数,其中它的第一个参数是这样的结构:

很有可能这就是栈的结构,我们都知道栈的默认底层container是deque, 因此去查了一下cpp里deque的实现:https://zhuanlan.zhihu.com/p/494261593 , 通过这篇博客,我们可以写出deque的结构体。 当然还有vector也是逆向中很重要的数据结构:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

struct deque_iter{

        obj*  cur;

        obj* begin;

        obj* end;

        obj* cur_node;

};

struct deque{

        ptr  M_map;

        size_t M_map_size;

        deque_iter  start;

        deque_iter  finish;

};

struct vector{

        obj  begin[vector.size];

        obj*  obj_end;

           obj*  capacity_end;

};

在ida里加入这些结构体以后再逆向,整个逻辑就清楚了很多。 ?! 相当于是() ,只有^ 和 | 是运算符。 又根据明显的先序后序遍历能知道最后parser出来的是一个二叉树。 因此知道了输入的结构是 ?0x|0x!^?0x|0x!

sub_411DE0("smithsmith666666", cmp1) ^ sub_411DE0("smithsmith777777", sub_40F5B(hello123,b) ^ sub_40F5B(hello456,a) ) == cmp2

由于a b c d 四个未知量,其中每个未知量都只有$6^4$种可能,而且由于sub_40F5B是ECB模式的块加密,每一个未知量又可以分为两个部分,可能性进一步退化到$6^2$。 因此可以采用爆破的方法。 虽然我们不知道sub_40F5B是什么加密,但是也可以用unicorn 来进行模拟执行。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

from unicorn import *

from unicorn.x86_const import *

from capstone import *

from capstone.x86 import *

from itertools import product

from tqdm import tqdm

base = 0x3ff000

def trans(a)->bytes:

    ret = [0]*(len(a)*4)

    for i in range(len(a)):

        ret[i*4] = ord(a[i])

    return bytes(ret)

candi = []

for c in product('abcdef',repeat=2):

    candi.append(trans(c))

a = list(0xB997FEA9D35F1491.to_bytes(8,'little') + 0xF199D36FCF4A12C7.to_bytes(8,'little'))

b = list(0x26F04376032014A3.to_bytes(8,'little') + 0x4744333708D0E9AC.to_bytes(8,'little'))

with open("hello123",'rb') as f:

    con = f.read()

h123 = {}

for i in range(0,len(con),0x20):

    h123[con[i+0x10:i+0x20]] = con[i:i+0x10]

h456 = {}

with open("hello456",'rb') as f:

    con = f.read()

for i in range(0,len(con),0x10):

    h456[con[i+0x8:i+0x10]] = con[i:i+0x8]

def  brute(cmp:list):

    for n123 in tqdm(h123):

        for n456_1 in h456:

            for n456_2 in h456:

                n456 = n456_1+n456_2

                tmp = [n123[i]^n456[i] for i in range(16)]

                if(tmp == list(cmp)):

                    print(h123[n123],h456[n456[0:8]],h456[n456[8:16]])

                    return

import sm4

k1 = sm4.SM4Key(b'smithsmith666666')

tmp = k1.encrypt(bytes(a))

print(tmp)

cmp2 = [tmp[i]^b[i] for i in range(16)]

k2 = sm4.SM4Key(b'smithsmith777777')

cmp2 = k2.decrypt(bytes(cmp2))

brute(cmp2)


文章来源: https://bbs.pediy.com/thread-275296.htm
如有侵权请联系:admin#unsafe.sh