May 8, 2023

miniLctf EasyPass出题记录

额……实在不知道出什么题,正好最近在看LLVM Pass,就顺手糊了一个题(雾

Usage.md里写了使用方法,实际上就是运行了一个LLVM Pass,处理的ir是main.bc。
主逻辑实际上在EasyPass.so里,main.bc里是一些有用的数据。

题目使用一个LLVM Pass处理了作为ir的main.bc,加密逻辑使用与非虚拟机,通过llvm::Value::getName()获取ir里的函数名作为opcode,一个函数名加密一位flag,加密逻辑为前半段异或后半段,后半段异或0xff。
想要知道此函数获取了什么数据作为opcode,可以查询LLVM的文档或下载相应版本的LLVM进行调试。
至于如何从main.bc中获取函数名,可以将main.bc编译后获取符号表,按以下方式操作:

1
2
3
$ llc main.bc -o main.s
$ clang main.s
$ readelf a.out -s

或者直接去看main.bc的hex,在偏移为0x1e9e的地方也可获得函数名。
或者也可以调试获取opcode。

由于笨人出题的时候没看LLVM Pass的版本直接出了,导致我出好之后才发现自己的LLVM Pass竟然是10.0.0的远古版本……
而且更诡异的是,新版LLVM Pass竟然不兼容旧版本,所以这个pass是没办法在新版LLVM Pass上跑的……
而且更更诡异的是,后来新的LLVM发行版都没有Ubuntu的,所以笨人直接下的release只有10.0.0的远古版本……………………
总而言之都是LLVM的错!

不过其实解题很简单,如果不知道pass里对ir的处理函数做了什么,安装一个10.0.0的LLVM调试一下这个pass就好

解题部分抄自dr3的wp
这可不是我偷懒,是他答应我写的!

如何解题

运行尝试

由于出题人的高超技巧,使得这个pass只能在非常稳(yuan)定(gu)的llvm 10上面运行。然而,由于我的kali最旧最旧的版本也是13,而奇怪的是这个老版本压根就没法运行

反正我是不懂得,没办法,装呗

https://packages.ubuntu.com/focal/llvm-10

直接用这个装就行,带dpkg的都可以

img_1.png

静态分析

进入load函数,可以发现注册了一个pass

最后的pass传到了sub_2660

打开发现函数很大,没法直接反编译

函数的头部全是函数调用。但是仔细看可以发现每个函数都只做了
一件事,就是return了一个1

所以其实就是干扰的花指令,全部nop掉就可以

1
2
for i in range(0x2673,0x6C9B):
patch_byte(i,0x90)

之后就可以f5

img_2.png

首先是通过getName找到了16个string放到了数组里

img_3.png
之后进入一个函数对数组进行加密

img_4.png
最后从数组的最后几位进行对比

getName

其实开始不知道得到了什么,不过好在配好了环境可以调试

虽然是llvm加载的so,但是配置好了输入的参数就可以直接进行调试
就像下面这张图一样
img_5.png
之后其实可以发现,那个循环就是获得了函数的名称。

因此,加密之后应该是固定的。只需要把加密算法抠出来,之后直接初始化flag,然后用z3跑一下就能出来了

还有一点就是提取数据的时候注意要小端序

以下是完整脚本

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
from z3 import *

str0 = [0x01, 0x61, 0x61, 0x43, 0x43, 0x7A, 0x43, 0x7A, 0x7A, 0x4D,
0x4D, 0x61, 0x4D, 0x43, 0x4D, 0x61, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x79, 0x6F, 0x75,
0x5F, 0x73, 0x68, 0x6F, 0x75, 0x6C, 0x64, 0x5F, 0x70, 0x61,
0x74, 0x63, 0x68, 0x5F, 0x74, 0x68, 0x69, 0x73, 0x5F, 0x66,
0x6C, 0x61, 0x67]

text1 = [BitVec(f"{i}", 8) for i in range(124 - 97)]
for i in range(97, 123):
str0[i] = text1[i - 97]
str0[0] = 1
names = ["aaCCzCzzMMaMCMa",
"bbMMyMyyZZbZMZb",
"ccCCxCxxJJcJCJc",
"ddQQwQwwMMdMQMd",
"eeYYvYvvKKeKYKe",
"ffHHuHuuCCfCHCf",
"ggDDtDttKKgKDKg",
"hhDDsDssOOhODOh",
"iiCCrCrrIIiICIi",
"jjOOqOqqDDjDODj",
"kkSSpSppEEkESEk",
"llXXoXooTTlTXTl",
"mmLLnLnnVVmVLVm",
"nnII_I__HHnHIHn",
"ooOO_O__CCoCOCo",
"ppFF_F__NNpNFNp",
"qqDD_D__BBqBDBq",
"rrTT_T__BBrBTBr",
"ssJJ_J__CCsCJCs",
"ttGG_G__XXtXGXt",
"uuDD_D__OOuODOu",
"vvUU_U__JJvJUJv",
"wwOO_O__SSwSOSw",
"xxRR_R__SSxSRSx",
"yyEE_E__KKyKEKy",
"zzJJ_J__TTzTJTz",
]
for str1 in names:
for i in range(1, len(str1) + 1):
str0[i] = ord(str1[i - 1])
str0[0] = 1
for i in range(0, 5):
v4 = str0[str0[0] + 1]
v3 = str0[str0[0]]
v2 = str0[str0[0] + 2]
str0[0] += 3
str0[v2] = (~(str0[v4] & str0[v3])) & 0xff
s = Solver()
cipher = [0x64, 0x04, 0x65, 0x0F, 0x2C, 0x5D, 0x39, 0x23, 0x23, 0x00,
0x16, 0x05, 0x1D, 0x8F, 0x93, 0x9A, 0xA0, 0xB3, 0x93, 0xA9,
0x92, 0xA0, 0xAF, 0xCB, 0x8C, 0xCA]
for i in range(97, 123):
s.add(cipher[i - 97] == str0[i])
print(s.check())
m = s.model()

for d in m.decls():
print(f"{d.name()} = {m[d]}")

# final = {20: 109,
# 18: 108,
# 13: 112,
# 17: 76,
# 10: 115,
# 21: 95,
# 15: 101,
# 23: 52,
# 7: 79,
# 5: 48,
# 6: 111,
# 8: 111,
# 0: 81,
# 2: 81,
# 12: 109,
# 3: 95,
# 16: 95,
# 14: 108,
# 22: 80,
# 9: 95,
# 11: 105,
# 1: 119,
# 25: 53,
# 19: 86,
# 24: 115,
# 4: 115,
# }

然后就可以了

出题部分

实际上出题人没活了,用的还是单指令虚拟机(x
本来预期解是插桩打log观察逻辑,奈何你们逆向壬全是一个z3走天下
哼哼,下次必要制裁你们z3党

这里贴个纯享版代码(x,方便以后继续整活(不是

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
#include "llvm/IR/Function.h"
#include "llvm/Pass.h"
#include "llvm/Support/raw_ostream.h"
using namespace llvm;

unsigned char unk_51B0[] = {
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,0, 0, 0, 0, 0xff, 0,
//flag
121, 111, 117, 95, 115, 104, 111, 117, 108, 100, 95, 112, 97, 116, 99, 104, 95, 116, 104, 105, 115, 95, 102, 108, 97, 103
};

//unsigned char* flag[] = {"miniLctf{QwQ_s0oOo_simple_LlVm_P4s5}"};
//miniLctf{you_should_patch_this_flag}
int l = 0;

namespace{
struct YunZh1Jun : public FunctionPass{
static char ID;
YunZh1Jun() : FunctionPass(ID){}
//void vm(); func319
//bool check(); func629

void func319();

bool func629();


bool runOnFunction(Function &F) override{

//outs() << "Function: " << F.getName() ;

for(int i=0;i<15;i++)
unk_51B0[i+1]=F.getName().str().c_str()[i];

unsigned char right[]={75, 112, 126, 113, 109, 56, 57, 93, 118, 119, 62, 109, 57, 127, 118, 107, 126, 124, 109, 57, 116, 112, 119, 112, 85, 122, 109, 127, 98, 100, 103,0};
unsigned char wrong[]={72, 110, 72, 53, 57, 105, 117, 99, 57, 109, 107, 96, 57, 120, 126, 120, 112, 119, 103, 0};

func319();
l++;
if(l == 26){
if(func629()){
for(int i=0;i<31;i++){
right[i] ^= 0x19;
outs() << *(right + i);
}
outs() << "\n";
}
else{
for(int i=0;i<19;i++){
wrong[i] ^= 0x19;
outs() << *(wrong + i);
}
outs() << "\n";
}
}

return false;
}
};
}

void YunZh1Jun::func319()
{
unsigned char *p_unk_51B0 = unk_51B0;
unsigned char PC, a, b, f, r;
unk_51B0[0] = 1;
for(int i = 0; i < 5; i++) {
PC = *p_unk_51B0;
a = p_unk_51B0[PC + 1];
b = p_unk_51B0[PC];
r = p_unk_51B0[PC + 2];
*p_unk_51B0 = PC + 3;
f = ~(p_unk_51B0[b] & p_unk_51B0[a]);
p_unk_51B0[r] = f;
}
}

bool YunZh1Jun::func629()
{
unsigned char check_num[30] = {100, 4, 101, 15, 44, 93, 57, 35, 35, 0, 22, 5, 29, 143, 147, 154, 160, 179, 147, 169, 146, 160, 175, 203, 140, 202};

for(int j = 0; j<26;j++)
if(unk_51B0[97+j]!=check_num[j])
{
//outs() << j;
return false;
}
return true;
}

char YunZh1Jun::ID = 0;
static RegisterPass<YunZh1Jun> X("YunZh1Jun", "YunZh1Jun's simple pass");

虚拟机的逻辑和预期exp等在github仓库都有,可以自己去看。
产生opcode的代码在miniLctf的github仓库
注意r0和r1作为寄存器是用random生成的,当生成的opcode达到一定数量时就会有很大的概率生成两个一样的字符,而这必然会导致错误(检查了好久问题在哪……)所以生成opcode之后自己看一下,如果一个循环内有一样的寄存器要手动改一下。
总而言之是个简单题 = =

关于本文

本文作者 云之君, 许可由 CC BY-NC 4.0.