极客大挑战YAKVM题目WP
2023-12-21 15:18:49 Author: mp.weixin.qq.com(查看原文) 阅读量:5 收藏

前言

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

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

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

逆向

该题目使用 golang 编译,需要使用一定手段恢复符号。恢复符号以后,将会看到一下内容:调用 NewCodeMarshaller 并继续调用其 Unmarshal 方法,这是反序列化 yakc 文件也就是题目中的 marshalCode 文件,之后调用 NewWithSymbolTable 创建一个新的作用域。然后不断调用 SetVar 方法,设置如下的符号到对应的函数:print, println, get, string2int, len :最后直接调用 Exec 方法,

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

VM

创建代码样例

直接去翻看这些 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

通过之前的打印,可以得到一个线性的反汇编 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{13710815911418590174681608117941186891687822912114910614710315611413398146116181}
 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官方资源 

Yak 语言官方教程:
https://yaklang.com/docs/intro/
Yakit 视频教程:
https://space.bilibili.com/437503777
Github下载地址:
https://github.com/yaklang/yakit
Yakit官网下载地址:
https://yaklang.com/
Yakit安装文档:
https://yaklang.com/products/download_and_install
Yakit使用文档:
https://yaklang.com/products/intro/
常见问题速查:
https://yaklang.com/products/FAQ

长按识别添加工作人员
开启Yakit进阶之旅


文章来源: https://mp.weixin.qq.com/s?__biz=Mzk0MTM4NzIxMQ==&mid=2247518462&idx=1&sn=579da384ae5c561035d28a265ddd8f80&chksm=c2d1f45af5a67d4c21490d7623af21743c143e4dd768fa326040e3a6926ec35d1789025dffca&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh