SCTF2021 Christomas
这俩题用的是作者自写的slang语言,还是挺有意思的,所以后来复现了一下
题目分析
服务端就是接收用户输入的slang代码,然后编译运行,所以关键得看懂slang语言
大概知道架构之后,就知道两个重要的语法文件
parser.y和scanner.l,一个是词法分析的规则,一个是语法分析的规则
具体的格式可以看这两篇文章
第08章 用 flex 做词法分析 — 自己动手写编译器 (pandolia.net)
第13章 用 bison 做语法分析 — 自己动手写编译器 (pandolia.net)
scanner.l中两个百分号之间是格式定义,每行都是pattern + action的模式
parse里面的格式,可以看出程序大致由三种语句组成,call_expr,want_stmt,var_expr
call语句
BACK是brings back gift,WORD就是正常的单词,NEWLINE就是;REINDEER是reindeer(驯鹿),DELIVERING 是delivering gift
所以语句应该是 Reindeer (funcname) delivering gift (arg1) (arg2) (arg3) ,这里是猜测成这种函数调用形式
后面应该可以跟brings back gift (return_value)得到返回值
want语句
Want是this family wants gift,Endwant是ok, they should already have a gift;
list的组成
IF是if the gift,OPERATOR是 is,equal to,greater than,还有+-*/,NEXT是:
AGAIN是Brave reindeer! Fear no difficulties!
所以格式应该如下
This family wants gift (WORD)
IF THE GIFT (OPERATOR) : stmts
Ok,they should already have a gift
这里want语句应该是看起来最费劲的,实际上其就是对应的循环和判断语句,毕竟是语言基本结构么
具体含义搞不明白,就得去看编译时的代码以及运行时的代码,两块都得考虑,才能搞明白具体含义
var expr
这里就是定义一个礼物,可以看成定义一个变量
编译器具体分析
整个语言编译出来的核心组成就是这个结构体
code是字节码数组,number数字常量池,string是字符串池,word是标识符池,slang解释器加载程序时会把程序拆分到这四个向量中
具体的编译在这里,这里map是一个宏,解开后就是switch case,对不同的语句调用不同的编译函数
因为主要的疑惑在want语句,所以先看want语句,而want的核心是list
1 |
|
先分析下这个函数里面用到的几个子函数,compiler_word_push内部会调用到lambda_emit_insn,最终会case到emit_insn_load_word,其内部是通过insn向字节码数组中插入操作数,所以这里push完,code数组中的组成为OP_PUSH_WORD WORD_INDEX
1 | void compile_word_push(arg){ |
emit_insn_operator是对操作符的push,就是压入一个操作符
1 |
|
这里编译jz的函数比较细,在编译stmt时是用来配合组成循环语句的,编译stmt时调用传入的是0xffff,因为还不知道循环结束的位置,因此先插入0xFFFF占位,redirection 是获得当前code的ip,此时走的是if的分支
1 | //先插入一条if语句,此时还不知道循环结束的位置 |
再编译完循环体之后
1 | //此时已编译完循环体,获得结尾到循环开始处的偏移 |
分析完之后,就可以得出结论,want语句就是关键的循环和分支语句
解释器具体分析
解释器代码基本都在vm/vm_call.c和runtime.c中
解释器入口,sleep是暴漏给Christmas Bash的,第一题用不到,这里就是按字节码去case执行
1 | void vm_call_lambda(lambda_t *l){ |
看下jz,这里会从栈上pop出上条指令的计算结果,如果为0就跳转
1 |
|
运算操作,就是从栈上取出操作数,然后根据操作符运算,再将结果压入栈中
1 | void vm_opcode_operator(arg, int opcode){ |
这些都比较常规,比较重要的是call函数,这里提供了几个特殊的函数,orw都已经给了
1 |
|
exp
Christmas Songs
参考的SU的exp如下
给了ORW以及字符串比较,我们这里可以直接用字符串比较,逐字节爆破,这里忘了原题是怎么给的了,但是exp里利用了下Rudolph函数,因为Rudolph会打印一个error,可以用这里的error信息结合分支语句判断flag是否正确,
1 | void vm_opcode_call(arg){ |
利用脚本
Christmas Bash
这题提供了一个sleep的函数地址,可以借这个地址得到libc的基地址
1 | void vm_call_lambda(lambda_t *l){ |
exp如下
1 | gift libcbase is sleep - 935488; |
这里第一部应该是获取sleep的偏移,泄露出偏移后,对比应该是2.34的libc
1 | //打印sleep的偏移 |
exp用的方法是直接将_IO_2_1_stdout_的vtable中的_IO_file_jumps改为system
这个是全局变量,我们是可以修改的 ,其实在2.29以前,虚表是不能更改的,因此才有了一些绕过的方法,但是在2.29以后虚表又能改了
这里exp的利用其实很有技巧,首先因为C++堆环境比较复杂,这里利用到了残留数据的技巧,首先定义一个system的gift,让其等于system的地址,此时在堆中就已经这个值了,接下来我们就可以通过堆上的偏移来找到system地址所处的堆地址,
1 | gift system is libcbase + 324944; |
下面就是这样操作的,这里var = 12345678也是声明在对上的数据,因此通过var与system间的偏移,我们就能索引到system的地址,注意这里要得到的是地址,为什么不直接用gift system,如果直接用gift system,等会memcpy就是copy的system函数的开头的几个汇编指令了,所以要得到的是gift system的堆上的地址,memcpy拷贝过去的才是system的地址,这里是有一层地址上的引用关系的
1 | gift var is "12345678"; |
接下来就是将system拷贝过去
1 | reindeer Vixen delivering gift target var Size; |
/bin/sh拷贝过去就没那么多绕绕了,直接拷贝
1 | reindeer Vixen delivering gift Stdout Binsh tmp; |
最后随便打开一个不存在的文件触发exit流程
1 | */ |
小Trick,快速在内存中搜索值,两种方式都行