r3ctf2024-nSMC

moshui Lv1

R3CTF 2024 nSMC 单题 Writeup

这次被大佬带飞了,感谢各位大佬带我玩,此次逆向LilRan

和我各1解
此题最终9解

正片开始

第一步

题目描述看不懂一点

反正就是个容器题,打开网页看一下。

直接下载没什么好说的

ida反编译分析

快速找到这个有用的函数看一下,好家伙原来nSMC的意思是有N个SMC啊!

那么很明显我们要动调一下才能搞(当然我是懒得分析解密)

动调给我的信息

随着调试的深入,我发现他是有4层这个nSMC,每层有6个SMC
这里随便找一支走下去就是能到一个输出Wrong!的地方

简单思考

基于上述内容,我们可以发现应该一共是 $6^{4}$ 种可能性,且肯定有一个分支输出的不是Wrong!
那么这个工作量是很大的,手动我肯定不干(我是懒狗)

脚本编写

SMC嘛,所以我想到的是动态运行,然后去看每个分支是否会输出Wrong!
动态执行,我想到了qiling框架 ,那么确定好了之后就开始编写吧!

第一个坑

我这里使用 Ubuntu22.04 系统来运行qiling框架,会发生 CPU ISA level is lower than required 这个报错,经查询发现是ld-linux-x86-64.so.2在捣鬼。
解决办法如下(代码来自GitHub哈):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def try_patch_isa(ql: Qiling):
pre = bytes.fromhex("8b8a2803000089cf4421c739f9")
ins = bytes.fromhex("0f85000b0000")
skip = bytes.fromhex("488d50f84839f075c3")

def bypass_isa_check(ql: Qiling) -> None:
print("Bypassing ISA Check...")
ql.arch.regs.rip += len(ins) + len(skip)

for start, end, perm, label, img in ql.mem.get_mapinfo():
if label != "ld-linux-x86-64.so.2":
continue
if "x" not in perm:
continue

adrs = ql.mem.search(pre + ins + skip, begin=start, end=end)
for adr in adrs:
ql.hook_address(bypass_isa_check, adr + len(pre))
# 用法就是在Qiling之后调用就行
ql = Qiling([proc_name], "/", verbose=QL_VERBOSE.DISABLED)
try_patch_isa(ql)

第二个坑

跑一下就会报这个错误'NoneType' object has no attribute 'cur_thread' error
翻阅文档 发现需要加一句话。

1
ql._multithread = True 

脚本思路

因为我不太会用,所以只能搞蠢思路,分析代码发现,strncmp的结果决定了程序的分支流程

所以通过改变strncmp返回值即可控制程序流。然后一直让他走,直到遇到puts,此时读出puts的值就可以判断是否正确
简单hook就行了,此处给出部分伪代码:

1
2
3
4
5
6
7
8
9
10
def hook_strncmp(self, ql: Qiling):
print("Hook strncmp")
ql.arch.regs.rax = get_Strncmp_rax()

def hook_puts(self, ql: Qiling):
text = ql.mem.string(ql.arch.regs.rdi)
print(text)

ql.os.set_api('strncmp',hook_strncmp)
ql.os.set_api('puts', hook_puts)

那么现在问题就变成了,如何通过控制strncmp的返回值来遍历每一个分支
便于理解我先举个例子:
首先我们考虑4组全是第一个的情况,那么返回值则为 0 0 0 0

那么第二个就是返回 0 0 0 1 0

我们继续考虑一下,那么这个问题也就可以用一个矩阵描述
$$\begin{pmatrix}
0 & 1 & 1 & 1 & 1 & 1 \
1 & 0 & 1 & 1 & 1 & 1 \
1 & 1 & 0 & 1 & 1 & 1 \
1 & 1 & 1 & 0 & 1 & 1 \
1 & 1 & 1 & 1 & 0 & 1 \
1 & 1 & 1 & 1 & 1 & 0 \
\end{pmatrix}$$
当枚举每一层时用这个矩阵即可描述,于是编写出一共矩阵生成算法,用来给每个结果生成一个4维数组,每1维表示一层的strncmp状态

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
def generate_matrix():
matrix = [
0, 1, 1, 1, 1, 1,
1, 0, 1, 1, 1, 1,
1, 1, 0, 1, 1, 1,
1, 1, 1, 0, 1, 1,
1, 1, 1, 1, 0, 1,
1, 1, 1, 1, 1, 0,
]

def generate_sequence(i):
base = 6
digits = []
for _ in range(4):
digits.append(i % base)
i //= base
digits.reverse()
sequence = []
for d in digits:
start = d * base
sequence.append(matrix[start:start + base])
return sequence

mat = []
for i in range(1, 6**4):
a = generate_sequence(i)
mat.append(a)
return mat
#当然我这个算法很狗屎,我生成了个巨麻烦的矩阵
#我按6^4中的每一次遍历来生成的,也就是一次遍历需要4个矩阵比如第一次的就是
# [[0,1,1,1,1,1],[0,1,1,1,1,1],[0,1,1,1,1,1],[0,1,1,1,1,1]]
#第二次是
#[[0,1,1,1,1,1],[0,1,1,1,1,1],[0,1,1,1,1,1],[1,0,1,1,1,1]]
#...
#最后一次是
#[[1,1,1,1,1,0],[1,1,1,1,1,0],[1,1,1,1,1,0],[1,1,1,1,1,0]]

此时我们完成了此脚本最难想的部分,剩下的按逻辑写出来就行了。
这里我发现当程序结束时,会发生一个异常,不过我也不知道怎么处理,我索性就try掉了反正不影响运行当然要是知道怎么修的师傅请教教我plz!
脚本如下(我随便写了个并行,不过感觉效果还是不好,爆破的速度很慢)

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
from typing import Optional

from qiling import *
from qiling.const import QL_VERBOSE, QL_INTERCEPT
from qiling.extensions import pipe
from qiling import *
from qiling.os.mapper import *

import threading

class QilingEmulator:
def __init__(self, start, end):
self.idx = start
self.end = end
self.idx_oneLoop = 0
self.idx_1 = 0
self.mat = self.generate_matrix()
self.proc_name = './challenge'

def generate_matrix(self):
matrix = [
0, 1, 1, 1, 1, 1,
1, 0, 1, 1, 1, 1,
1, 1, 0, 1, 1, 1,
1, 1, 1, 0, 1, 1,
1, 1, 1, 1, 0, 1,
1, 1, 1, 1, 1, 0,
]

def generate_sequence(i):
base = 6
digits = []
for _ in range(4):
digits.append(i % base)
i //= base
digits.reverse()
sequence = []
for d in digits:
start = d * base
sequence.append(matrix[start:start + base])
return sequence

mat = []
for i in range(1, 6**4):
a = generate_sequence(i)
mat.append(a)
return mat

def get_Strncmp_rax(self):
matx = self.mat[self.idx]
ret_v = matx[self.idx_1][self.idx_oneLoop]
if ret_v == 0:
self.idx_1 += 1
self.idx_oneLoop = -1
return ret_v

def hook_isoc99_scanf(self, ql: Qiling):
print("Hit ___isoc99_scanf")
ql.os.stdin = pipe.SimpleInStream(0)
ql.os.stdin.write(b"1")

def hook_strncmp(self, ql: Qiling, *args):
print("Hook strncmp")
ql.arch.regs.rax = self.get_Strncmp_rax()
self.idx_oneLoop += 1

def try_patch_isa(self, ql: Qiling):
pre = bytes.fromhex("8b8a2803000089cf4421c739f9")
ins = bytes.fromhex("0f85000b0000")
skip = bytes.fromhex("488d50f84839f075c3")

def bypass_isa_check(ql: Qiling) -> None:
print("Bypassing ISA Check...")
ql.arch.regs.rip += len(ins) + len(skip)

for start, end, perm, label, img in ql.mem.get_mapinfo():
if label != "ld-linux-x86-64.so.2":
continue
if "x" not in perm:
continue

adrs = ql.mem.search(pre + ins + skip, begin=start, end=end)
for adr in adrs:
ql.hook_address(bypass_isa_check, adr + len(pre))

def hook_puts(self, ql: Qiling):
text = ql.mem.string(ql.arch.regs.rdi)
print(text)
if text == 'Wrong!':
self.idx += 1
self.idx_1 = 0
else:
raise Exception('Correct found!')

def run_emulator(self):
while self.idx < self.end:
self.idx_oneLoop = 0
ql = Qiling([self.proc_name], "/", verbose=QL_VERBOSE.DISABLED)
base = ql.loader.images[0].base
ql._multithread = True
self.try_patch_isa(ql)
ql.os.set_api('strncmp', self.hook_strncmp)
ql.os.set_api('puts', self.hook_puts)
ql.hook_address(self.hook_isoc99_scanf, base + 0x10C0)
try:
ql.run()
except Exception as e:
print(e)
pass

def find(start, end):
emulator = QilingEmulator(start, end)
emulator.run_emulator()

if __name__ == "__main__":
threading.Thread(target=find, args=(0, 100)).start()
threading.Thread(target=find, args=(100, 200)).start()
threading.Thread(target=find, args=(200, 300)).start()
threading.Thread(target=find, args=(300, 500)).start()
threading.Thread(target=find, args=(500, 600)).start()
threading.Thread(target=find, args=(600, 700)).start()
threading.Thread(target=find, args=(700, 800)).start()
threading.Thread(target=find, args=(800, 900)).start()
threading.Thread(target=find, args=(900, 1000)).start()
threading.Thread(target=find, args=(1000, 1100)).start()
threading.Thread(target=find, args=(1100, 1200)).start()
threading.Thread(target=find, args=(1200, 1296)).start()

最后解题

我努力半天也进不了2分,我对着网页瞎看了看,发现刷新竟然是用网页,那么我下个断点手动提交就好啦

  • 标题: r3ctf2024-nSMC
  • 作者: moshui
  • 创建于 : 2024-06-10 12:48:49
  • 更新于 : 2024-06-10 20:14:23
  • 链接: https://www.moshui.eu.org/2024/06/10/r3ctf2024-nSMC/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论