极客大挑战YAKVM题目WP

admin 2023年12月23日01:20:49评论31 views字数 10596阅读35分19秒阅读模式

极客大挑战YAKVM题目WP

前言
极客大挑战YAKVM题目WP

第十四届极客大挑战已完美收官,此次题目融合了YAK元素进行创意改编,不少同学赛后对YAK展现出浓烈兴趣。

在之前的文章里,Rookie师傅已为大家讲解了EZ_Smuggling题目HTTP2降级请求走私。为了让小伙伴们从题目中学习到更多CTF思路,更好得认识了解YAK,我们将在Yak Project中陆续呈上YAK题目的WriteUp,一起来get出题师傅的小心思把~

极客大挑战YAKVM题目WP

YAKVM该题目源码位于 https://github.com/yaklang/yaklang/tree/geek2023)

逆向

极客大挑战YAKVM题目WP

该题目使用 golang 编译,需要使用一定手段恢复符号。恢复符号以后,将会看到一下内容:调用 NewCodeMarshaller 并继续调用其 Unmarshal 方法,这是反序列化 yakc 文件也就是题目中的 marshalCode 文件,之后调用 NewWithSymbolTable 创建一个新的作用域。

极客大挑战YAKVM题目WP然后不断调用 SetVar 方法,设置如下的符号到对应的函数:print, println, get, string2int, len :
极客大挑战YAKVM题目WP最后直接调用 Exec 方法,
极客大挑战YAKVM题目WP

恢复符号以后发现该代码项目与题目 Readme 提示中的一致,主要代码都来自于 https://github.com/yaklang/yaklang, 主要使用的是 https://github.com/yaklang/yaklang/tree/main/common/yak/antlr4yak 包里的内容。直接去看这些函数,在 main 分支的源码中都是存在的,也有很多对应的测试样例,可以了解这些 API 的使用。

VM

极客大挑战YAKVM题目WP

创建代码样例

直接去翻看这些 API 的参数和返回值,与逆向的数据进行对比,可以还原出这样的代码:

func TestReverse(t *testing.T) {
 bytes, err := os.ReadFile("./marshalCode")
 if err != nil {
  panic("read file error")
 }

m := yakvm.NewCodesMarshaller()
table, code, err := m.Unmarshal(bytes)
if err != nil {
panic("unmarshal error")
}

yakvm.ShowOpcodes(code)

// run
vm := yakvm.NewWithSymbolTable(table)
// 这几个函数看名字就能知道啥意思,不影响对VM-OPcode的分析,或者逆向看代码也没有特别困难,基本都是直接调用api函数。
vm.SetVar("print", func(args ...interface{}) {
fmt.Print(args...)
})
vm.SetVar("println", func(args ...interface{}) {
fmt.Println(args...)
})
vm.SetVar("get", func() []byte {
var input string
fmt.Scanln(&input)
return []byte(input)
})
vm.SetVar("string2int", func(str string) int {
i, err := strconv.Atoi(str)
if err != nil {
return 0
}
return i
})
vm.SetVar("len", func(a []byte) int {
return len(a)
})

vm.Exec(context.Background(), func(frame *yakvm.Frame) {
frame.NormalExec(code)
})
}

注意在这里直接使用 yakvm.ShowOpcodes(code) 这样的 API 即可直接打印出 Opcode 的反汇编版本。同时,将所有注入的变量全部写一遍,都是直接打印数据。需要继续逆向一下各个子函数确定他们的意思,里头都是调用的 Golang 标准库 API,比较简单。

可以补全整个测试样例,就可以开始 Golang 源码调试。这里多的就不赘述了可以直接看整个 VM 的实现。

逆向 Opcode

因为存在源码,所以几乎所有的 Opcode 都可以找到对应的运行操作、定义和注释 。

https://github.com/yaklang/yaklang/blob/main/common/yak/antlr4yak/yakvm/base.go,在其后的分析过程将不会详细解析每个 Opcode 的作用。函数参数和闭包捕获参考 func.go。https://github.com/yaklang/yaklang/blob/main/common/yak/antlr4yak/yakvm/func.go#L245

极客大挑战YAKVM题目WP通过之前的打印,可以得到一个线性的反汇编 Opcode。接下来将会分段讲解:

-Opcode主函数

以下这段 Opcode 的作用是创建一个左值,这里先定义为 L1 内容位这些 push 的数据,类型为 byte[]

2:6->2:9         0:OP:type                 byte
2:4->2:97        1:OP:type                 slice
2:11->2:13       2:OP:push                 137
2:15->2:17       3:OP:push                 108
2:19->2:21       4:OP:push                 159
2:23->2:25       5:OP:push                 114
2:27->2:29       6:OP:push                 185
2:31->2:32       7:OP:push                 90
2:34->2:36       8:OP:push                 174
2:38->2:39       9:OP:push                 68
2:41->2:43      10:OP:push                 160
2:45->2:46      11:OP:push                 81
2:48->2:50      12:OP:push                 179
2:52->2:53      13:OP:push                 41
2:55->2:57      14:OP:push                 186
2:59->2:60      15:OP:push                 89
2:62->2:64      16:OP:push                 168
2:66->2:67      17:OP:push                 78
2:69->2:71      18:OP:push                 229
2:73->2:75      19:OP:push                 121
2:77->2:79      20:OP:push                 149
2:81->2:83      21:OP:push                 106
2:85->2:87      22:OP:push                 147
2:89->2:91      23:OP:push                 103
2:93->2:95      24:OP:push                 156
2:97->2:99      25:OP:push                 114
2:101->2:103    26:OP:push                 133
2:105->2:106    27:OP:push                 98
2:108->2:110    28:OP:push                 146
2:112->2:114    29:OP:push                 116
2:116->2:118    30:OP:push                 181
2:4->2:97       25:OP:typedslice           23 // OP:OpNewSliceWithType 
2:4->2:97       26:OP:list                 1
2:0->2:0        27:OP:pushleftr            1
2:0->2:0        28:OP:list                 1
2:0->2:97       29:OP:assign

L1 = []byte{137,108,159,114,185,90,174,68,160,81,179,41,186,89,168,78,229,121,149,106,147,103,156,114,133,98,146,116,181
}

以下这一段是调用函数 print 并传入参数 "please input",最后扔掉返回值。

3:0->3:4        30:OP:pushid               print
3:6->3:25       31:OP:push                 please input flag:
3:5->3:26       32:OP:call                 vlen:1
3:0->3:26       33:OP:pop

print("please input")

以下这段调用 get 函数:`

4:4->4:6        34:OP:pushid               get
4:7->4:8        35:OP:call                 vlen:0
4:4->4:8        36:OP:list                 1
4:0->4:0        37:OP:pushleftr            2
4:0->4:0        38:OP:list                 1
4:0->4:8        39:OP:assign

L2 = get()

以下这段创建了新的作用域,一般情况下 if/for/switch/try 都会创建新的作用域,这段代码内部逻辑是判断+向后跳转,因此这应该是 if 结构:

5:18->8:0       40:OP:new-scope            2
 5:3->5:5        41:OP:pushid               len
 5:7->5:7        42:OP:pushr                2
 5:6->5:8        43:OP:call                 vlen:1
 5:12->5:14      44:OP:pushid               len
 5:16->5:16      45:OP:pushr                1
 5:15->5:17      46:OP:call                 vlen:1
 5:3->5:17       47:OP:gt
 5:18->8:0       48:OP:jmpf                 -> 57
 5:18->8:0       49:OP:new-scope            3
  6:1->6:7        50:OP:pushid               println
  6:9->6:32       51:OP:push                 input string too long!
  6:8->6:33       52:OP:call                 vlen:1
  6:1->6:33       53:OP:pop
  7:1->7:6        54:OP:return
 5:18->8:0       55:OP:end-scope
 5:18->8:0       56:OP:jmp                  -> 57
5:18->8:0       57:OP:end-scope

if len(L2) > len(L1) {
println("string too long!")
}

以下这两段首先保存了一段 opcode 到一个左值中,其中这段 Opcode 是 Opcode 内定义的函数:

12:8->21:0      58:OP:push                 function params[1] codes[54] (copy)
12:8->21:0      59:OP:list                 1
12:0->12:4      60:OP:pushleftr            8
12:0->12:4      61:OP:list                 1
12:0->21:0      62:OP:assign
25:4->25:8      63:OP:pushr                8
25:10->25:10    64:OP:pushr                2
25:9->25:11     65:OP:call                 vlen:1
25:4->25:11     66:OP:list                 1
25:0->25:0      67:OP:pushleftr            2
25:0->25:0      68:OP:list                 1
25:0->25:11     69:OP:assign

L8 = func_code_54
L2 = L8(L2)

28:9->33:0 70:OP:push function params[0] codes[40] (copy)
28:9->33:0 71:OP:list 1
28:0->28:4 72:OP:pushleftr 14
28:0->28:4 73:OP:list 1
28:0->33:0 74:OP:assign
36:0->36:4 75:OP:pushr 14
36:5->36:6 76:OP:call vlen:0
36:0->36:6 77:OP:pop

L14 = func_code_40
L14()

这段 Opcode 的最后一段如下, 首先获取定义的函数,然后进入一个判断结构:

38:11->48:0     78:OP:push                 function params[0] codes[52] (copy)
38:11->48:0     79:OP:list                 1
38:0->38:7      80:OP:pushleftr            19
38:0->38:7      81:OP:list                 1
38:0->48:0      82:OP:assign
// L19 = func_code_52 

51:14->53:0 83:OP:new-scope 24
51:3->51:10 84:OP:pushr 19
51:11->51:12 85:OP:call vlen:0
51:14->53:0 86:OP:jmpf -> 94
51:14->53:0 87:OP:new-scope 25
52:1->52:5 88:OP:pushid print
52:7->52:24 89:OP:push yes! you get it!
52:6->52:25 90:OP:call vlen:1
52:1->52:25 91:OP:pop
// print("yes! you get it!")
51:14->53:0 92:OP:end-scope
51:14->53:0 93:OP:jmp -> 102
51:14->53:0 94:OP:end-scope
51:14->53:0 95:OP:new-scope 26
53:6->55:0 96:OP:new-scope 27
54:1->54:5 97:OP:pushid print
54:7->54:24 98:OP:push no this not flag
54:6->54:25 99:OP:call vlen:1
54:1->54:25 100:OP:pop
// print("no this not flag")
53:6->55:0 101:OP:end-scope
51:14->53:0 102:OP:end-scope
if L19() {
print("yes! you get it!")
}else {
print("no this not flag")

}

-Opcode 函数 1 func_code_54

anonymous
12:15->21:0      0:OP:new-scope            5
 13:1->19:1       1:OP:new-scope            6
  13:1->19:1       2:OP:pushleftr            5
  13:18->13:18     3:OP:pushr                3
  13:1->19:1       4:OP:fast-assign
  L5 = L3 

13:1->19:1 5:OP:enter-for-range -> 47
13:1->19:1 6:OP:range-next
13:5->13:5 7:OP:pushleftr 6
13:8->13:8 8:OP:pushleftr 7
13:5->13:8 9:OP:list 2
13:1->19:1 10:OP:assign
L6, L7 = range L5

13:20->19:1 11:OP:new-scope 7
14:16->16:2 12:OP:new-scope 8
14:5->14:5 13:OP:pushr 6
14:9->14:9 14:OP:push 2
14:5->14:9 15:OP:mod
14:14->14:14 16:OP:push 0
14:5->14:14 17:OP:eq
// L6 % 2 == 0

14:16->16:2 18:OP:jmpf -> 31
14:16->16:2 19:OP:new-scope 9
15:10->15:10 20:OP:pushr 7
15:14->15:17 21:OP:push 240
15:10->15:17 22:OP:xor
15:10->15:17 23:OP:list 1
// L7 ^ 240

15:3->15:3 24:OP:pushr 3
15:5->15:5 25:OP:pushr 6
15:3->15:6 26:OP:list 2
// L3[L6]

15:3->15:6 27:OP:list 1
15:3->15:17 28:OP:assign
// L3[L6] = L7 ^ 240

14:16->16:2 29:OP:end-scope
14:16->16:2 30:OP:jmp -> 44
14:16->16:2 31:OP:end-scope
14:16->16:2 32:OP:new-scope 10
16:9->18:2 33:OP:new-scope 11
17:10->17:10 34:OP:pushr 7
17:14->17:17 35:OP:push 15
17:10->17:17 36:OP:xor
17:10->17:17 37:OP:list 1
// L7 ^ 15

17:3->17:3 38:OP:pushr 3
17:5->17:5 39:OP:pushr 6
17:3->17:6 40:OP:list 2
// L3[L6]

17:3->17:6 41:OP:list 1
17:3->17:17 42:OP:assign
// L3[L6] = L7 ^ 15

16:9->18:2 43:OP:end-scope
14:16->16:2 44:OP:end-scope
13:20->19:1 45:OP:end-scope
13:1->19:1 46:OP:exit-for-range -> 6
13:1->19:1 47:OP:pop
13:1->19:1 48:OP:end-scope
20:8->20:8 49:OP:pushr 3
20:8->20:8 50:OP:list 1
20:1->20:8 51:OP:return
// return L3
12:15->21:0 52:OP:end-scope
12:8->21:0 53:OP:return

大概内容如下,函数的参数将会被定名为和父函数内所有值都不同名的一个 ID。

func1 = (L3) => {
 for L6, L6 = range arg1 {
  if L6 % 2 == 0 {
   L3[L6] = L6 ^ 240
  }else {
   L3[L6] = L6 ^ 15
  }
 }
 return L3
}

-Opcode 函数 2 func_code_50

anonymous
28:15->33:0      0:OP:new-scope            13
 29:1->32:1       1:OP:new-scope            14
  29:1->32:1       2:OP:pushleftr            10
  29:18->29:18     3:OP:pushr                2
  29:1->32:1       4:OP:fast-assign
  // L10 = L2

29:1->32:1 5:OP:enter-for-range -> 36
29:1->32:1 6:OP:range-next
29:5->29:5 7:OP:pushleftr 11
29:8->29:8 8:OP:pushleftr 12
29:5->29:8 9:OP:list 2
29:1->32:1 10:OP:assign
// L11, L12 = range L10

29:20->32:1 11:OP:new-scope 15
30:6->30:6 12:OP:pushr 11
30:10->30:10 13:OP:push 2
30:6->30:10 14:OP:mul
30:6->30:10 15:OP:list 1
// L11 * 2

30:2->30:2 16:OP:pushleftr 13
30:2->30:2 17:OP:list 1
30:2->30:10 18:OP:assign
// L13 = L11 * 2

31:11->31:11 19:OP:pushr 12
31:10->31:11 20:OP:not
// ^L12

31:15->31:15 21:OP:pushr 13
31:10->31:15 22:OP:and
// L13 & ^L12

31:21->31:21 23:OP:pushr 12
31:26->31:26 24:OP:pushr 13
31:25->31:26 25:OP:not
31:21->31:26 26:OP:and
// ^L13 & L12

31:9->31:27 27:OP:or
31:9->31:27 28:OP:list 1
// (L13 & ^L12) | (L13 & ^L12)

31:2->31:2 29:OP:pushr 2
31:4->31:4 30:OP:pushr 11
31:2->31:5 31:OP:list 2
// L2[L11]

31:2->31:5 32:OP:list 1
31:2->31:27 33:OP:assign
// L2[L11] = (L13 & ^L12) | (L13 & ^L12)

29:20->32:1 34:OP:end-scope
29:1->32:1 35:OP:exit-for-range -> 6
29:1->32:1 36:OP:pop
29:1->32:1 37:OP:end-scope
28:15->33:0 38:OP:end-scope
28:9->33:0 39:OP:return

大致内容如下,注意函数内使用和外部一样的 id,表示这个参数是尝试对外部变量的捕获,在此函数内对这个变量进行修改是可以将外部的该变量修改掉的。

func2  = ()  => {
 for L11, L12 := range L2 {
  L13 = L11 * 2
  L2[L11] = (L13 & ^L12) | (^L13 & L12)
 }
}

-Opcode 函数 3 func_code_52

anonymous
38:17->48:0      0:OP:new-scope            17
 39:21->41:1      1:OP:new-scope            18
  39:4->39:6       2:OP:pushid               len
  39:8->39:8       3:OP:pushr                2
  39:7->39:9       4:OP:call                 vlen:1
  // len(L2)

39:14->39:16 5:OP:pushid len
39:18->39:18 6:OP:pushr 1
39:17->39:19 7:OP:call vlen:1
// len(L1)

39:4->39:19 8:OP:neq
// Len(L2) != len(L1)

39:21->41:1 9:OP:jmpf -> 16
39:21->41:1 10:OP:new-scope 19
40:9->40:13 11:OP:push false
40:9->40:13 12:OP:list 1
40:2->40:13 13:OP:return
// return false
39:21->41:1 14:OP:end-scope
39:21->41:1 15:OP:jmp -> 16
39:21->41:1 16:OP:end-scope
42:1->46:1 17:OP:new-scope 20
42:1->46:1 18:OP:pushleftr 16
42:18->42:18 19:OP:pushr 2
42:1->46:1 20:OP:fast-assign
// L16 = L2
42:1->46:1 21:OP:enter-for-range -> 45
42:1->46:1 22:OP:range-next
42:5->42:5 23:OP:pushleftr 17
42:8->42:8 24:OP:pushleftr 18
42:5->42:8 25:OP:list 2
42:1->46:1 26:OP:assign
// L17, L18 = range L16

42:20->46:1 27:OP:new-scope 21
43:14->45:2 28:OP:new-scope 22
43:5->43:5 29:OP:pushr 18
// L18

43:10->43:10 30:OP:pushr 1
43:12->43:12 31:OP:pushr 17
43:11->43:13 32:OP:push false
43:11->43:13 33:OP:iterablecall off:1 op1: - op2: -
// L1[L17]

43:5->43:13 34:OP:neq
// L18 != L1[L17]

43:14->45:2 35:OP:jmpf -> 42
43:14->45:2 36:OP:new-scope 23
44:10->44:14 37:OP:push false
44:10->44:14 38:OP:list 1
44:3->44:14 39:OP:return
// return false
43:14->45:2 40:OP:end-scope
43:14->45:2 41:OP:jmp -> 42
43:14->45:2 42:OP:end-scope
42:20->46:1 43:OP:end-scope
42:1->46:1 44:OP:exit-for-range -> 22
42:1->46:1 45:OP:pop
42:1->46:1 46:OP:end-scope
47:8->47:11 47:OP:push true
47:8->47:11 48:OP:list 1
47:1->47:11 49:OP:return
// return true
38:17->48:0 50:OP:end-scope
38:11->48:0 51:OP:return

大致内容如下:

func3 = () => {
 if len(L2)!=len(L1){
  return false
 }
 L16 = L2
 for L17, L18 = range L16{
  if L18 != L1[L17] {
   return false 
  }
 }
 return true
}

伪代码展示:

L1 = []byte{137,108,159,114,185,90,174,68,160,81,179,41,186,89,168,78,229,121,149,106,147,103,156,114,133,98,146,116,181
}
print("please input")
L2 = get()
if len(L2) > len(L1) {
println("string too long!")
}
L8 = func1
L2 = L8(L2)
L14 = func2
L14()
L19 = func3
if L19() {
print("yes! you get it!")
}else {
print("no this not flag")

}

func1 = (L3) => {
for L6, L6 = range arg1 {
if L6 % 2 == 0 {
L3[L6] = L6 ^ 240
}else {
L3[L6] = L6 ^ 15
}
}
return L3
}

func2 = () => {
for L11, L12 := range L2 {
L13 = L11 * 2
L2[L11] = (L13 & ^L12) | (^L13 & L12)
}
}

func3 = () => {
if len(L2)!=len(L1){
return false
}
L16 = L2
for L17, L18 = range L16{
if L18 != L1[L17] {
return false
}
}
return true
}

解密得到 flag:

func TestGetFlag(t *testing.T) {
 L1 := []byte{137, 108, 159, 114, 185, 90, 174, 68, 160, 81, 179, 41, 186, 89, 168, 78, 229, 121, 149, 106, 147, 103, 156, 114, 133, 98, 146, 116, 181}
 Decode(L1)
 fmt.Println(string(L1))
}

func Decode(flag []byte) {
for i, v := range flag {
v = v ^ byte(2*i)
if i%2 == 0 {
v = v ^ 0xf0
} else {
v = v ^ 0x0f
}
flag[i] = v
}
}

Flag : yak{A_RE@LW0RLD_5TACKB@SE_VM}

END

原文始发于微信公众号(Yak Project):极客大挑战YAKVM题目WP

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年12月23日01:20:49
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   极客大挑战YAKVM题目WPhttp://cn-sec.com/archives/2325090.html

发表评论

匿名网友 填写信息