About

给学校 CTF 出题, 方向是逆向 + 虚拟机, 正好最近在看 Peter Kogge, The Architecture of Symbolic Computers, 那么就不妨换掉之前的那套 “汇编风格” 的虚拟机, 而是使用 Lambda 算子来实现一套自洽的 “纯函数式虚拟机”?

本篇可以看作是: [[/reading/calculi-of-lambda-conversion/][[Reading] The Calculi of Lambda Conversion]] 这篇阅读笔记的更加实际的一个实现以及具体应用的一个思考.

为了方便大家阅读源码进行一个逆向, 所以我选择使用 Python 来进行书写.

Python, 那么体验如何?

感觉一般… 虽然有类型提示, 但是可能是我的 LSP (ruff) 不够智能? 也有可能是我的数据结构并没有用类的形式 (用的 list, 可能还得手动加类型标记? 不是很清楚), 总之在多次之后就没法正确识别类型并进行补全提示了.

(所以不如不带任何类型提示地瞎写? 虽然这么想到, 但是还是写了类型提示, 可能到了后面有点懒得写了, 有一些会缺少类型标记. )

然后就是代码风格, 不是说游标卡尺的缩进, 其实实际上除了乱写的那部分代码, 感觉很少有非常长的游标卡尺, 并且就算有, 你也不是不能在编辑器里面加美化.

而是感觉不太建议使用递归? 例:

def dotimes(n : int, block : callable) -> None:
    if n > 0:
        block(n)
        dotimes(n - 1, block)

dotimes(23333, lambda x : x) # => RecursionError: maximum recursion depth exceeded

emm… 虽然这个代码写得很明显是有问题的 (因为完全可以变成循环), 但是对于更加复杂的代码, 这种递归优化感觉还是很需要诶…

什么? 你说能不能调包来实现? 我觉得是可以的吧? 比如 python-lambda-calculus (随便网上找的). 但是这样感觉就不好玩了. (还是自己作死导致的)

感觉 Python 的真谛还得是调包, 自己写复杂代码还是有点痛苦的.

详细的代码我会放到仓库里面, 这篇博客里面就不展开写了 (当然, 也不会吃饱了撑的用 Lisp 重写, 虽然如果有时间的话, 我觉得可以用来作为教学用途来用 Lisp 重写一遍. )

如果真的用 Lisp 来重写, 或者你喜欢的语言, 你应该做什么?

最偷懒的方式: 如果你用的编程语言里面支持 Lambda 匿名函数, 直接用它来实现.

稍微不那么偷懒的方式: 调用一个 parser 库, 解析 lambda 算子表达式到 ast, 然后从 ast 按照规则进行约化.

更加不偷懒的方式: 实现一个 parser 库 (我想的方式)

首先实现 BNF 语法的解析, 这个部分可以先写一个非常简单的固定语法, 然后实现 BNF 到 BNF AST 的过程. 提取所有的终结符 (terminator), (假设终结符以正则表达式的形式进行). 那么词法转换器 (tokenrizer) 应当为这些正则表达式的或. 大概的一个伪代码如下:

(let ((bnf-ast (parse-bnf *bnf-definition-for-some-language*))
      (regexp  (regexp:or-by-list (terminators bnf-ast))))
  (regexp:scan regexp input-code)) ;; => tokens

这里的 REGEXP 可以参考 A Simple Regexp for Parser 一文. 不过会不会出现简单的 or 使得正则匹配出现问题? (懒得管了, 到时候再说).

然后在得到 token-list 之后, 就可以自动根据规则构造递归解析的函数.

以上, 相当于实现了一个 CC (compiler compiler) 的工作, 任务量并不复杂, 并且可以实现自举, 适合作为编译原理前端入门. 在这之后可以考虑剪枝优化, 前端检查之类的工作? 或者可以到后端考虑实现除了递归之外的解析方法.

然后实现了这个小编译器的工作, 就可以去实现 lambda calculus 的计算规则 (正则序和应用序), 很适合作为宏和一般函数调用的入门, 并且从中也可以进一步展开正则序, 来介绍一些计算图的编译期优化, 从而实现计算程序的自动并行化.

最后, 在 lambda calculus 讲解完后, 开始构造一个小 DSL, 从这个 DSL 到 lambda calclus 表达式, 然后可以去尝试自举. 或者可以考虑在各种平台上的编译都行, 感觉到了这步可以布置那种大作业, 让学生自己整活了. 比如让大家用 lambda calculus 写点小程序, 或者给自己的 DSL 增加一些其他的功能 (比如 IO 之类的).

如果还有时间, 没准可以考虑针对 lambda calculus 进行硬件上的设计. (虽然这一块我还没怎么仔细折腾).

仓库见 li-yiyang/lambda-calculus-vm.

为什么考虑 Lambda Calculus 作为虚拟机?

当我们在考虑对代码进行混淆的时候, 理论上的目标是尽可能提高逆向的难度, 同时尽量保证原本程序的运行效率.

逆向的难度

前者用下面的一个例子估计就非常容易理解了:

代码太长了, 折叠掉
(λA|(λB|(λC|(λD|(λE|(λF|(λG|(λH|(λI|(λJ|(λK|(λL|((((λw|((w(λ\
x|(λy|y)))(λx|(λy|x))))(((λa|(λb|(((λw|(λz|((wz)(λx|(λy|y)))\
))((λx|(((x(λx|(λy|y)))(λw|((w(λx|(λy|y)))(λx|(λy|x)))))(λx|\
(λy|y))))(((λx|(λy|((y(λx|(λs|(λz|(((x(λg|(λh|(h(gs)))))(λu|\
z))(λu|u))))))x)))a)b)))((λx|(((x(λx|(λy|y)))(λw|((w(λx|(λy|\
y)))(λx|(λy|x)))))(λx|(λy|y))))(((λx|(λy|((y(λx|(λs|(λz|(((x\
(λg|(λh|(h(gs)))))(λu|z))(λu|u))))))x)))b)a)))))((λb|(((λx|(\
λy|((y(λx|(λy|(λz|(y((xy)z))))))x)))(((λx|(λy|(λs|(x(ys)))))\
b)(λs|(λz|(sz)))))(λs|(λz|(s(s(sz)))))))(λs|(λz|(s(s(s(s(sz)\
))))))))K))(λx|(λy|y)))((((λw|((w(λx|(λy|y)))(λx|(λy|x))))((\
(λa|(λb|(((λw|(λz|((wz)(λx|(λy|y)))))((λx|(((x(λx|(λy|y)))(λ\
w|((w(λx|(λy|y)))(λx|(λy|x)))))(λx|(λy|y))))(((λx|(λy|((y(λx\
|(λs|(λz|(((x(λg|(λh|(h(gs)))))(λu|z))(λu|u))))))x)))a)b)))(\
(λx|(((x(λx|(λy|y)))(λw|((w(λx|(λy|y)))(λx|(λy|x)))))(λx|(λy\
|y))))(((λx|(λy|((y(λx|(λs|(λz|(((x(λg|(λh|(h(gs)))))(λu|z))\
(λu|u))))))x)))b)a)))))((λb|(((λx|(λy|((y(λx|(λy|(λz|(y((xy)\
z))))))x)))(((λx|(λy|(λs|(x(ys)))))b)(λs|(λz|(sz)))))(λs|(λz\
|(s(s(s(sz))))))))(λs|(λz|(s(s(s(s(s(s(s(s(sz)))))))))))))C)\
)(λx|(λy|y)))((((λw|((w(λx|(λy|y)))(λx|(λy|x))))(((λa|(λb|((\
(λw|(λz|((wz)(λx|(λy|y)))))((λx|(((x(λx|(λy|y)))(λw|((w(λx|(\
λy|y)))(λx|(λy|x)))))(λx|(λy|y))))(((λx|(λy|((y(λx|(λs|(λz|(\
((x(λg|(λh|(h(gs)))))(λu|z))(λu|u))))))x)))a)b)))((λx|(((x(λ\
x|(λy|y)))(λw|((w(λx|(λy|y)))(λx|(λy|x)))))(λx|(λy|y))))(((λ\
x|(λy|((y(λx|(λs|(λz|(((x(λg|(λh|(h(gs)))))(λu|z))(λu|u)))))\
)x)))b)a)))))((λb|(λs|(λz|(s(s(s(s(s(s(sz))))))))))(λs|(λz|(\
s(s(s(s(s(s(s(s(sz)))))))))))))J))(λx|(λy|y)))(((((λa|(λb|((\
(λw|(λz|((wz)(λx|(λy|y)))))((λx|(((x(λx|(λy|y)))(λw|((w(λx|(\
λy|y)))(λx|(λy|x)))))(λx|(λy|y))))(((λx|(λy|((y(λx|(λs|(λz|(\
((x(λg|(λh|(h(gs)))))(λu|z))(λu|u))))))x)))a)b)))((λx|(((x(λ\
x|(λy|y)))(λw|((w(λx|(λy|y)))(λx|(λy|x)))))(λx|(λy|y))))(((λ\
x|(λy|((y(λx|(λs|(λz|(((x(λg|(λh|(h(gs)))))(λu|z))(λu|u)))))\
)x)))b)a)))))((λb|(((λx|(λy|((y(λx|(λy|(λz|(y((xy)z))))))x))\
)(λs|(λz|(s(s(sz))))))(((λx|(λy|(λs|(x(ys)))))b)(λs|(λz|(sz)\
)))))(λs|(λz|(s(s(s(s(s(sz))))))))))I)(((((λa|(λb|(((λw|(λz|\
((wz)(λx|(λy|y)))))((λx|(((x(λx|(λy|y)))(λw|((w(λx|(λy|y)))(\
λx|(λy|x)))))(λx|(λy|y))))(((λx|(λy|((y(λx|(λs|(λz|(((x(λg|(\
λh|(h(gs)))))(λu|z))(λu|u))))))x)))a)b)))((λx|(((x(λx|(λy|y)\
))(λw|((w(λx|(λy|y)))(λx|(λy|x)))))(λx|(λy|y))))(((λx|(λy|((\
y(λx|(λs|(λz|(((x(λg|(λh|(h(gs)))))(λu|z))(λu|u))))))x)))b)a\
)))))((λb|(((λx|(λy|((y(λx|(λy|(λz|(y((xy)z))))))x)))(((λx|(\
λy|(λs|(x(ys)))))b)(((λx|(λy|((y(λx|(λy|(λz|(y((xy)z))))))x)\
))(λs|(λz|(sz))))(((λx|(λy|(λs|(x(ys)))))b)(λs|(λz|(sz))))))\
)(λs|(λz|(s(sz))))))(λs|(λz|(s(s(sz)))))))H)(((((λa|(λb|(((λ\
w|(λz|((wz)(λx|(λy|y)))))((λx|(((x(λx|(λy|y)))(λw|((w(λx|(λy\
|y)))(λx|(λy|x)))))(λx|(λy|y))))(((λx|(λy|((y(λx|(λs|(λz|(((\
x(λg|(λh|(h(gs)))))(λu|z))(λu|u))))))x)))a)b)))((λx|(((x(λx|\
(λy|y)))(λw|((w(λx|(λy|y)))(λx|(λy|x)))))(λx|(λy|y))))(((λx|\
(λy|((y(λx|(λs|(λz|(((x(λg|(λh|(h(gs)))))(λu|z))(λu|u))))))x\
)))b)a)))))((λb|(λs|(λz|(s(s(s(sz)))))))(λs|(λz|(s(s(s(s(s(s\
(sz)))))))))))E)((((λw|((w(λx|(λy|y)))(λx|(λy|x))))(((λa|(λb\
|(((λw|(λz|((wz)(λx|(λy|y)))))((λx|(((x(λx|(λy|y)))(λw|((w(λ\
x|(λy|y)))(λx|(λy|x)))))(λx|(λy|y))))(((λx|(λy|((y(λx|(λs|(λ\
z|(((x(λg|(λh|(h(gs)))))(λu|z))(λu|u))))))x)))a)b)))((λx|(((\
x(λx|(λy|y)))(λw|((w(λx|(λy|y)))(λx|(λy|x)))))(λx|(λy|y))))(\
((λx|(λy|((y(λx|(λs|(λz|(((x(λg|(λh|(h(gs)))))(λu|z))(λu|u))\
))))x)))b)a)))))((λb|(λs|(λz|(sz))))(λs|(λz|(s(s(s(sz)))))))\
)B))(λx|(λy|y)))(((((λa|(λb|(((λw|(λz|((wz)(λx|(λy|y)))))((λ\
x|(((x(λx|(λy|y)))(λw|((w(λx|(λy|y)))(λx|(λy|x)))))(λx|(λy|y\
))))(((λx|(λy|((y(λx|(λs|(λz|(((x(λg|(λh|(h(gs)))))(λu|z))(λ\
u|u))))))x)))a)b)))((λx|(((x(λx|(λy|y)))(λw|((w(λx|(λy|y)))(\
λx|(λy|x)))))(λx|(λy|y))))(((λx|(λy|((y(λx|(λs|(λz|(((x(λg|(\
λh|(h(gs)))))(λu|z))(λu|u))))))x)))b)a)))))((λb|(((λx|(λy|((\
y(λx|(λy|(λz|(y((xy)z))))))x)))(((λx|(λy|(λs|(x(ys)))))b)(λs\
|(λz|(s(s(sz)))))))(λs|(λz|(s(sz))))))(λs|(λz|(s(s(s(s(s(sz)\
)))))))))L)((((λw|((w(λx|(λy|y)))(λx|(λy|x))))(((λa|(λb|(((λ\
w|(λz|((wz)(λx|(λy|y)))))((λx|(((x(λx|(λy|y)))(λw|((w(λx|(λy\
|y)))(λx|(λy|x)))))(λx|(λy|y))))(((λx|(λy|((y(λx|(λs|(λz|(((\
x(λg|(λh|(h(gs)))))(λu|z))(λu|u))))))x)))a)b)))((λx|(((x(λx|\
(λy|y)))(λw|((w(λx|(λy|y)))(λx|(λy|x)))))(λx|(λy|y))))(((λx|\
(λy|((y(λx|(λs|(λz|(((x(λg|(λh|(h(gs)))))(λu|z))(λu|u))))))x\
)))b)a)))))((λb|(((λx|(λy|((y(λx|(λy|(λz|(y((xy)z))))))x)))(\
((λx|(λy|(λs|(x(ys)))))b)(λs|(λz|(s(sz))))))(λs|(λz|z))))(λs\
|(λz|(s(s(s(s(s(sz))))))))))A))(λx|(λy|y)))(((((λa|(λb|(((λw\
|(λz|((wz)(λx|(λy|y)))))((λx|(((x(λx|(λy|y)))(λw|((w(λx|(λy|\
y)))(λx|(λy|x)))))(λx|(λy|y))))(((λx|(λy|((y(λx|(λs|(λz|(((x\
(λg|(λh|(h(gs)))))(λu|z))(λu|u))))))x)))a)b)))((λx|(((x(λx|(\
λy|y)))(λw|((w(λx|(λy|y)))(λx|(λy|x)))))(λx|(λy|y))))(((λx|(\
λy|((y(λx|(λs|(λz|(((x(λg|(λh|(h(gs)))))(λu|z))(λu|u))))))x)\
))b)a)))))((λb|(λs|(λz|(s(sz)))))(λs|(λz|(s(s(s(s(s(s(s(s(s(\
sz))))))))))))))D)(((((λa|(λb|(((λw|(λz|((wz)(λx|(λy|y)))))(\
(λx|(((x(λx|(λy|y)))(λw|((w(λx|(λy|y)))(λx|(λy|x)))))(λx|(λy\
|y))))(((λx|(λy|((y(λx|(λs|(λz|(((x(λg|(λh|(h(gs)))))(λu|z))\
(λu|u))))))x)))a)b)))((λx|(((x(λx|(λy|y)))(λw|((w(λx|(λy|y))\
)(λx|(λy|x)))))(λx|(λy|y))))(((λx|(λy|((y(λx|(λs|(λz|(((x(λg\
|(λh|(h(gs)))))(λu|z))(λu|u))))))x)))b)a)))))((λb|(((λx|(λy|\
((y(λx|(λy|(λz|(y((xy)z))))))x)))(((λx|(λy|(λs|(x(ys)))))b)(\
λs|(λz|(sz)))))(λs|(λz|(sz)))))(λs|(λz|(s(s(s(s(s(s(s(s(s(sz\
))))))))))))))G)(((((λa|(λb|(((λw|(λz|((wz)(λx|(λy|y)))))((λ\
x|(((x(λx|(λy|y)))(λw|((w(λx|(λy|y)))(λx|(λy|x)))))(λx|(λy|y\
))))(((λx|(λy|((y(λx|(λs|(λz|(((x(λg|(λh|(h(gs)))))(λu|z))(λ\
u|u))))))x)))a)b)))((λx|(((x(λx|(λy|y)))(λw|((w(λx|(λy|y)))(\
λx|(λy|x)))))(λx|(λy|y))))(((λx|(λy|((y(λx|(λs|(λz|(((x(λg|(\
λh|(h(gs)))))(λu|z))(λu|u))))))x)))b)a)))))((λb|(λs|(λz|(sz)\
)))(λs|(λz|(s(s(s(sz))))))))F)(λx|(λy|x)))(λx|(λy|y))))(λx|(\
λy|y))))(λx|(λy|y)))))(λx|(λy|y)))))(λx|(λy|y))))(λx|(λy|y))\
))(λx|(λy|y))))))))))))))))))

对应的逻辑大概如下所示:

(lambda (a b c d ...)
  (if (eq b flag-b)
      (if (not (eq c flag-b))
          nil
          (if ...))
      nil))

原本的逻辑非常简单吧? 但是却可以被展开混淆为 Lambda 算子表示.

当然不是牢不可破的

因为这个逻辑是一个一一匹配 (虽然不是正序), 所以完全是可能逐位爆破的. 并且为了防止这个计算过程中爆了 Python 的递归限制, 输入的 flag 只有 a-z 这 26 个字母, 可以说爆破难度很小了.

(不过我在 task 的 tip 里面应该留了足够详细的提示了)

不过这种应该怪混淆器吗? 这很明显是代码逻辑层的问题吧 (笑)

程序运行效率

虽然看起来程序是解释运行的, 这样的运行效率是非常低下的. 诚然, 效率上如果没有专门设计的硬件的话, 应该是会有一定的损耗的.

Lambda Calculus 的表示

理论上来说, 我们可以用 Lambda Calculus 等效任意的程序, 但这是如何实现的? (详细请参考 manual 从 .m(5).m(7) 的内容)

如果你不想仔细阅读, 或者想要直接用自己熟悉的编程语言来进行测试, 这里有一个非常快速的介绍: .m(10).

折叠了
(defpackage #:lambda-machine
  (:use :cl))

(in-package :lambda-machine)

;; Macro helper

(defun <- (fn arg)
  "Function application. "
  (if (functionp fn)
      (funcall fn arg)
      `(<- ,fn ,arg)))

(defmacro <-* (expr &rest exprs)
  (if (endp exprs) expr
      `(<-* (<- ,expr ,(first exprs)) ,@(rest exprs))))

(defmacro λ (lambda-list body)
  "Quick wrapper for the lambda calculus. "
  (if (endp lambda-list)
      body
      `(lambda (,(first lambda-list))
         (declare (ignorable ,(first lambda-list)))
         (λ ,(rest lambda-list) ,body))))

;; Basic Components

(defparameter +zero+ (λ (s z) z)
  "Church number zero. ")

(defparameter +successor+ (λ (x y z) (<- y (<-* x y z)))
  "Inc lambda church number. ")

(defparameter +addition+  (λ (j k) (<-* k +successor+ j))
  "Add number j and k. ")

(defparameter +multiplication+ (λ (x y) (λ (s) (<- x (<- y s))))
  "Multiply number x and y. ")

(defparameter +predecessor+ (λ (x) (λ (s z) (<-* x (λ (g h) (<- h (<- g s))) (λ (u) z) (λ (u) u))))
  "Dec number x")

(defparameter +subtraction+ (λ (x y) (<-* y +predecessor+ x))
  "x - y")

(defparameter +true+  (λ (x y) x))
(defparameter +false+ (λ (x y) y))
(defparameter +not+   (λ (w) (<-* w +false+ +true+)))
(defparameter +zerop+ (λ (x) (<-* x +false+ +not+ +false+)))

(defparameter +y-combinator+
  (λ (y) (<- (λ (x) (<- y (<- x x))) (λ (x) (<- y (<- x x))))))

(defparameter +cons+  (λ (a b) (λ (x) (<-* x a b))))
(defparameter +car+   (λ (x) (<- x (λ (a b) a))))
(defparameter +cdr+   (λ (x) (<- x (λ (a b) b))))
(defparameter +nil+   (λ (x a b) a))
(defparameter +nilp+  (λ (n) (<- n (λ (a b) +false+))))

;; DSL on Lambda Calculus

(defun ->church-number (num)
  (if (<= num 0) +zero+ (<- +successor+ (->church-number (1- num)))))

(defun church-number-> (church-number)
  (<-* church-number #'1+ 0))

(defun true-false-> (bool)
  (<-* bool t nil))

(defun ->true-false (bool)
  (if bool +true+ +false+))

从这几个基本的元素, 理论上就可以实现任意的函数与程序了.

Lambda Calculus 的性能与优化

虽然现在还是可以接受的慢, 但是如果程序更加复杂和庞大, 那么这慢一点估计就不是那么能够接受了.

那么该如何提高性能? 一个做法就是进行并行化的计算, 因为可以发现, 在分支的化简上, 如果一次只化简一部分, 但是另外一部分仍然还是可以单独化简而不需要等到下次再化简 (因为完全没有影响, 所有的函数作用域都是有限的. )

另一个做法就是按照 The Architecture of Symbolic Computers 中提到的那样用专用设计的芯片来进行加速 (这部分还没有读), 感觉会很有意思.

后记

虽然但是, 我觉得 LVM REPL 是一个比较失败的代码练习, 因为我把里面的代码基本上写死了, 没啥拓展性也不是很灵活. 并且主要是没有一个比较实际的目标, 导致写的时候很坐牢.