一时兴起,自己尝试学了一点Brainfuck,纯粹就只是图一乐
简单的了解
一开始是想起来之前羊城杯2023那道GIFuck,想着给学弟们上点强度出道同一个考点的题目来着,结果自己玩起来了(笑)
Brainfuck说白了就是一个数组+一个指针然后进行操作而已。什么?你问我指针是啥?算了你还是想象有一个箭头吧:
1 | 数组: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... |
总之先了解一下基础的Brainfuck操作(其实就是图灵机操作www)
- + 指针当前指向的元素+1
- - 指针当前指向的元素-1
- < 指针左移1位
- > 指针右移1位
- [] 循环,和C里面的while(*p){}类似
- , 输入,类似*p=getchar();
- . 输出,类似putchar(*p)
它的输出是会转换为ASCII的,所以你想输出个1得要搞个0x31也就是49出来
我用的在线网站有两个,一般很多人都能搜到Splitbrains的Ook/Brainfuck编解码网站,不过最近用了另一个网站,能清晰地看到内存区的数据,所以就用后者了,不过好像直接登的话网站显示很怪?反正我是先回到Github仓库再转到这个网站的
听爹地qsdz说还有IDE来着,不过咱就只是玩玩看,没必要这么大动干戈(
基础的循环逻辑
Brainfuck只有加减,那我们先从加法开始吧
假如我们有下面的内存区:
1 | 数组: 2 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... |
我们现在需要让数组第0位变成两个数相加的结果,该怎么办?
那很简单,一边+1一边-1不就好了嘛,所以就是先把指针右移,然后-1,再左移+1再回来,直到第1位为0即可
所以代码很简单:
1 | >[-<+>] |
最后我们的内存区应该是这样的结果:
1 | 数组: 5 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... |
那乘法不就是多加几次而已嘛,那假如还是原来的内存区,我们需要让第0位变成2*3该怎么办?
欸,先别急,我们先学一下怎么复制数据再回来做这题
简单的复制操作
比如我们内存区长这样:
1 | 数组: 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... |
我们想让第0位的数据复制到第2位,那我们该怎么办?
可能你会想“这不就和一开始的加法一样嘛”,那我们试着写出来代码?
1 | [->>+<<] |
但是最后跑完的结果却是这样的:
1 | 数组: 0 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 ... |
?我第0位数据呢?啊,被用作循环的参数清0了
那怎么办?这还不简单,我多复制出来1个数据再搞回去不就好了
比如我们这样打:
1 | [->+>+<<] |
我们让第1位跟着我们一起变成3,得到下面的内存区:
1 | 数组: 0 3 3 0 0 0 0 0 0 0 0 0 0 0 0 0 ... |
然后再把第1位的数据搬到第0位就好了:
1 | >[-<+>] |
最后我们得到的内存区就长这样:
1 | 数组: 3 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 ... |
一些基础运用
再回到刚刚那个2*3的例子,由于我们知道循环参数会被消耗掉,所以我们得先额外复制一个出来再搞回去
假如我们转换成C/C++代码,核心逻辑应该是这样的:
1 | int sum=0, i=2, j=3; |
我们会发现这里的3被消耗掉2次,所以我们需要复制的数据就是3(当然你也可以选择复制2,这里只是个例子而已)
那我们内层循环就又要输出一个3,又要复原作为循环参数的3,所以内层逻辑就可以是:
1 | [->+>+<<] 先让3复制到第2位和第3位 |
然后我们外层再这样操作:
1 | [->(内层逻辑...)<] 外层参数-1,执行完内层逻辑指针回归第0位以判断循环 |
所以最终代码就应该是:
1 | [->[->+>+<<]>[-<+>]<<] |
最后得到的内存区应该是这样的:
1 | 数组: 0 3 0 6 0 0 0 0 0 0 0 0 0 0 0 0 ... |
之后再把第3位的结果移到第0位就行了,这一步我就省略了,这种基础的数据移动上面也有写了
给学弟们出的(没啥深度的)题目
先说一下Splitbrains那个输出基本上是怎么样的
为了数组开的尽可能小,它一般只会在第0位和第1位进行操作,比如我想输出”ABCD”的话,基本就是先构造出”A”再在”A”的基础上输出”BCD”:
1 | ++++++++[->++++++++<]>+. 输出A(65) |
不过这样的话数据就会被覆盖掉,运行完内存区就变成这样了:
1 | 数组: 0 68 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... |
我参照GIFuck出的时候把flag每个字符都放内存区了来着,不是这种覆盖,不过非flag内容因为不重要就直接借用这种覆盖的写法了
CPU给干到满占用率了(预期难度1.5⭐)
1 | +++++ +++[- >++++ ++++< ]>+.< +++++ ++[-> +++++ ++<]> +++.- --.-- -.+++ |
这个可太明显了,一看就知道是从Splitbrains直接把flag编码了的(它甚至为了美观每5个字符就打个空格,它真的我哭死)
直接拉上去解码就行了,这题就只是为了了解有Brainfuck这种东西存在,仅此而已
CPU给干烧了(预期难度3.0⭐)
1 | ++++++++[->++++++++<]>+ |
全是自己手打的,用的就是上面提到的覆盖写法,考点就是基础的输出操作符”.”
每一行加一个”.”就行了,没什么难度(本来还想去掉换行的,但是发现这样就纯恶心人www)
3.0⭐说句实话就是纯虚高,2.6⭐倒还可能有
CPU给干炸了(预期难度3.8⭐)
1 | +++++++++[->+++++++++<]>+++.<+++++[->++++<]>.---.<++++[->+++<]>+.-<++++[->---<]>.++++.<+++[->+++<]>+.-----.+.<+++[->---<]>.++++++.<++++[->---<]>+.++++++.>>>++++++++[-<++++++++>]<+>>+++++++++++[-<+++++++++++>]<---->>++++++++++[-<+++++++++++>]<++++>>++++++++++[-<+++++++++++>]<+>>++++++++++[-<+++++++++++>]<++++>>++++++++++[-<++++++++++>]<--->>+++++++++++[-<+++++++++++>]<++>>+++++++++[-<+++++++++>]<+++>>++++++++++[-<++++++++++>]<++++>>++++++++[-<++++++++>]<>>+++++++++++[-<+++++++++++>]<----->>++++++++++[-<++++++++++>]<----->>++++++++++[-<++++++++++>]<->>++++++++++[-<++++++++++>]<--->>+++++++++[-<++++++++>]<+++>>++++++++++[-<++++++++++>]<+>>++++++++++[-<++++++++++>]<----->>++++++++++[-<++++++++++>]<+++++>>++++++[-<+++++++++>]<->>++++++++++[-<++++++++++>]<----->>++++++[-<+++++++++>]<-->>++++++++++[-<++++++++++>]<----->>++++++++++[-<+++++++++++>]<-->>+++++[-<++++++++++>]<->>++++++++++[-<++++++++++>]<+>>++++++[-<++++++>]<--->>++++++[-<++++++>]<--->>++++++[-<++++++>]<--->>+++++++++++[-<+++++++++++>]<++++[<] |
Splitbrains解出来只有”Thereisnoflag”,这下很多人估计直接就傻眼了,不过说句实话有非预期解,总之先上我自己打这串代码的源码:(输出操作符只是为了检验,题目代码中所有输出flag的输出操作符都已被删除)
1 | # 84 104 101 114 101 105 115 110 111 102 108 97 103 Thereisnoflag |
非预期其实很简单:找规律,在每个”>>”前面加个输出操作符就行了,这个也是咱在出完题目后才发现的,但是懒得重新写一遍了,所以就此作罢(一看就知道全是复制粘贴www)
讲一下整个代码的思路,前面非flag就是覆盖式写法,我们直接跳过
后面的flag内容是这样的:比如我想在第i位放字符,但是考虑到i-1位已经有字符存在,所以我在第i+1位保存循环参数然后在第i位构造出字符,一直到flag构造完成,此时所有的flag都已经在内存区
最后一行那个[<]就是指针归位到第2位(因为那一位的数据刚好是0,指针就在那里停下来了)
这一题的预期考点其实就是开头提到的GIFuck里面的考点:数组内存区未清零,flag可被读取
所以我们可以在整个代码最后加几个输出操作符和指针右移的组合,也就是”.>”,这样我们就可以读取内存区的flag了
当然也可以把最后一行的”[<]”改成”[.<]”,边输出边左移,只是flag是逆序的而已,转一下就行了
如果用Splitbrains网站,你可以在代码最后打一个”#”,这样可以看到此时内存区的所有数据,当然如果是一开头提到的第2个网站就能直接看到内存区了
CPU炸了,内存也炸了(预期难度4.0⭐)
这题是作为隐藏题发布的,前提条件是完成上面的“CPU给干炸了”
1 | ++++++++[->+++++++++<]>--.<+++++[->++++++++<]>--.+<++++[->---<]>.++++++.>++++++[->++++++<]>+++.<<<++++[->+++<]>.>+++[->--<]>-.<<-----.+.+++++.>>.<<<++++[->---<]>.---.+<++++[->+++<]>.<++++[->---<]>-.>>.<<<+++[->++<]>+.+++.---.>>[-]<<[-]<++++[->>++++[-<++++>]<<]>+[->+>+<<]>[-<+>]>[-<++>>++<]++++[-<--->>---<]>-<<->>[-<+>>+>+>+>+<<<<]<--->>>>>[-<<<<+>>>>]<<<<------>--->>>+++++[-<<---->>]<++++++<[->>+>+>+<<<<]>>>>[-<<<<+>>>>]+++++[-<<---->---------->]<<+>+<<<[->>>>+>+>+>+>+>+<<<<<<<<<]>>>>>>>>>[-<<<<<<<<<+>>>>>>>>>]+++++[-<<<<<++++>>>>>]<<<<<++>-->>>>+++[-<<<---->>>]<<-->>+++++[-<---->]<--<<<<<<<<<[->>>>>>>>>>+>+>+>+>+>+>+>+>+<<<<<<<<<<<<<<<<<<]>>>>>>>>>>>>>>>>>>[-<<<<<<<<<<<<<<<<<<+>>>>>>>>>>>>>>>>>>]<<<<<<<<---->--->++++>++++>>>>>+++++[-<<<<---->>>>]<<<<+>>>>+++++++[-<<<------->>>]<<<+>>>++++[-<---->]<->>+++++++[-<+++++++>]<[->++>+>++>++>++>+>+++>+<<<<<<<<]>>>>>>>>[-<<<<<<<<+>>>>>>>>]++++[-<<<<<<<+++>>>>>>>]+++[-<<<<<<+++++++>>>>>>]+++[-<<<<<++++++>>>>>]<<<<<+>+>>>>+++[-<<<+++>>>]++++[-<<---->>]+++++[-<---->]<--[[-]<] |
这次的代码应该找不到规律了吧www
还是先上源码:
1 | # 70 108 97 103 39 115 32 110 111 116 32 104 101 114 101 32 108 111 108 Flag's not here lol |
由于上一题一堆非预期解做出来的,所以我这题就刻意不搞出来规律。我当时就想“我已经构造好了r,为什么我之后不在已构造好的r的基础上再对它进行操作呢?”,所以就得到了这一长串代码,里面夹杂了一堆复制操作
而且毕竟是隐藏题,我可不能让他们直接看内存区就能读出来,所以最后一行我很“贴心”地把所有内存区都归零了(笑)
不仅如此,我还简单优化了一下代码,因为有些指针的左移右移可以被抵消,所以我把它们全部删掉了www,所以题目代码和源码会有差别的www
我知道可以删掉归零操作再看内存区,但是前提是你真的理解了Brainfuck才能做到,不是吗?所以能这样做出来咱也算达到目的了www
考点其实就是前置题目的预期解做法,只是我把指针归位了和内存区清零了,仅此而已
这还不简单,清零前你指针肯定得到位吧,那我清零前读取数据不就好了?
所以最后一行”[[-]<]”改成”[.[-]<]”就能得到逆序flag了,理解了之后真的会觉得没啥难的
(不过感觉4.0⭐是不是有点虚低了,可能4.2⭐会好一点?)
总结…?
有啥好总结的,我玩的很开心(摸鱼谁不开心啊www),学弟们被恶心吐了,我们都有美好的未来(笑)
~~~(p≧w≦q)~~~这里是来自2024年3月20号的分割线~~~(p≧w≦q)~~~
Quine
我也没想到我会更新这个博客,主要还是和爹地聊了一下,才知道有这种东西,感觉有意思,就搞了一下
这个quine我也不会翻译,总之就是一个可以输出自己源代码的计算机程序,就比如说一个C语言quine程序,它的代码是int main(){/* 程序的代码 */},这个程序的输出结果就是int main(){/* 程序的代码 */},完全一致
可以证明的是,任意一个图灵完备的语言都存在自己的quine,但是我这里就不展开讲了,因为这一部分的重点是如何构造一个Brainfuck的quine(其实是我完全没看懂QAQ)
其实网上有很多教你自己写Brainfuck的quine的,我写的肯定没有他们的好(或者说我就是一个翻译,而且还是水平不太高的那种),所以这里先把几个链接给出来供大佬们参考:
Welcome - BrainFuck part 7 - Quine (+ some non-BF quine theory) (codingame.com)
BrainFuck how-to: Quine: introduction and mechanisms (1) (brainfuckhowto.blogspot.com)
我是跟着第二个链接走的,不过两个都是非常好的文章,感兴趣的话可以自行查看
我本人不太会表达,不过我尽量多用例子来解释
基本组成
一个quine的组成简单拆分一下的话其实就两部分:
- 一系列数据,这些数据代表的是下面的代码
- 一串代码,它的功能是利用上面的数据输出数据以及这串代码本身
不过我们如果要生成数据,那么我们肯定需要代码来生成,比如在Brainfuck里,我们至少需要”+”和”>”两个指令来生成数据. 虽然我们可以使用循环来优化代码长度,但是之后我们想要输出表示循环的”[]”就会比较麻烦,所以这里我们就用简单的”+”和”>”就行
比如:我们代码里面有个”+”(ASCII:43),那么就是用43个”+”和一个”>”构造进内存里面,此时数据中”+”的数量就和代码的ASCII发生了绑定,换句话说,我们就可以利用这个数据同时输出构造”+”的代码和”+”本身了,怎么输出之后我们再谈
当然,我们如果需要输出构造数据的指令,也就是那一大串”+”和”>”,那我们最起码需要先把这两个字符构造好放在内存里吧
输出过程
那么我们有了数据,构造好了两个字符,我们应该怎么输出呢?
假如我们有代码ABC(ASCII:65,66,67),然后我们的内存区长这样:
1 | 字符: + > A B C |
刚刚我们说了数据和ASCII发生了绑定,所以构造数据的指令可以利用这一点进行输出,用(伪)C代码表示大致上是这样的:
1 | int arr; // arr是内存区 |
换成Brainfuck的话就是这样:
1 | >>>> 指针移到A处 |
可能还是不够清晰,那我们先走一遍内层循环
1 | 第1行执行完成后: |
(有一种利用DNA构造蛋白质的感觉有木有)
然后我们再把指针回到一开始然后直接输出ABC就好了
整体逻辑与Brainfuck代码框架
那么上面的例子大致理解了,我们只需要把ABC换成最后输出自己的Brainfuck代码就行了,至于那些数据从哪里来,等会再说
最后我们就有下面的基本逻辑:
- 留白,方便构造指令字符和之后输出时的位移
- 仅使用”+”和”>”将数据存储在内存里面
- 回到内存区[0]
- 在开头构造两个指令字符
- 上面输出构造数据的代码的逻辑的循环
- 回到内存区[1]并输出存在内存里的数据,也就是代码区里的代码
至于Brainfuck框架,我这里就直接照搬上面第2个链接的框架了:
1 | >>>> 留白,方便之后的"+"和">"的构造以及不断的位移 |
所以我们现在就需要把下面的代码转换成构造数据的代码,把下面的代码理解成上面的ABC就行,进行一样的操作就行
但是手搓的话会不会太耗时间了…所以和那些教程一样,我们也直接用Brainfuck写个“脚本”来把数据构造出来
构造数据的代码的生成
那么基本的输出逻辑其实和上面的输出过程差不多,就是我们没必要复制保存数据,我们只需要读取然后输出就行了
那么整体逻辑就像这样:
- 先把两个指令字符构造好
- 读入字符
- 输出对应数量的+和>
- 回到2,直至输入结束
那么Brainfuck脚本就是这样:(嗯,还是第2个链接直接复制粘贴的)
1 | ++++++[->++++++++++>+++++++<<]>++>+ 构造"+"和">" |
具体跑的结果是怎么样我就不演示了,很长
最后把跑出来的结果扔进上面的数据段(就是两行$的中间),你就成功得到了一个Brainfuck Quine!可喜可贺~~~
冗余数据(唉,藏逼)
当时和爹地聊天的时候他有这么说过:
冗余数据?这个quine里面还能藏东西?啊???
一开始没理解的时候确实觉得不太可能,但是后面想了想,前面的数据构造区构造的数据只是后面的代码的字符串,实际上后面的代码只要有能够执行输出逻辑的代码就一定是一个quine,其它的功能可有可无,也就是这里的“冗余数据”
所以此时我们的整体逻辑就有了这样的变化:
留白,方便构造指令字符和之后输出时的位移
仅使用”+”和”>”将数据存储在内存里面
回到内存区[0]
在开头构造两个指令字符
上面输出构造数据的代码的逻辑的循环
5.5 潜在的冗余数据执行区?
回到内存区[1]并输出存在内存里的数据,也就是代码区里的代码
根据现在的逻辑,此时看到我们上面代码框架的第21行,那里就是我们潜在的冗余数据执行区
让我们看看到第21行的时候整个内存区应该是怎么样的吧:
1 | 数组: 0 data[0] data[1] ... data[len-1] 0 43 62 0 0 0 ... |
此时如果没有冗余数据,我们下一步就是直接输出data,也就是代码,但是此时我们可以进行一系列操作,只要不影响到前面的data,那么我们进行任何操作都可以,理论上我们还可以再嵌套一个quine…?
就比如我们想在中间输出一句话,那么就可以往那里面塞输出的Brainfuck代码,然后再利用上面的脚本生成数据区的代码,这样最后的输出就是(整个构造数据的代码)(你输出的那句话)(代码区的代码)
所以最后我们的代码框架如下:
1 | >>>> 留白,方便之后的"+"和">"的构造以及不断的位移 |
其实还存在Double-Quine(另称Two-Step Quine),就是一个程序P,其输出是程序Q,而Q的输出是程序P,这个之后有缘再更新www
Brainfuck逆向
我哪知道几个月之后我还会更新这块啊…
这里就拿NSS Round#7 F*ckThisProgram来举例吧
靶机nc后直接给了一串Brainfuck,直接写入txt里面导出
这里给出Brainfuck代码,有兴趣的可以自己先试着做做(笑)
1 | +++++ ++++[ ->+++ +++++ +<]>+ +++++ .<+++ [->++ +<]>+ ++++. +++++ ++.-- |
一开始我还以为出题人人很好,会把flag放在内存里面,但是吧…用了上面给的的编译器…emmm…我程序怎么卡住了?
也不清楚咋回事,总之换了一个编译器,起码是能跑起来了,不过你这返回的…怎么还真是个fake flag啊?
大致看了一下构造,猜测可以分成3部分:
- Part 1:输出
Welcome to NSSCTF Round#7 and your flag is
这一段话 - Part 2:进行flag的比对,毕竟逆向题都得让你能验证,也是我们这题的突破口
- Part 3:根据flag的正确与错误,返回对应的文字,比如错误就返回假的flag
区分的方法也很简单,2和3我们不急着区分,1和2的区分点就在于读入的位置,也就是,
所在的地方,之后就是对flag的比对
所以我们在读入之后打上断点(新的编译器里面断点符号是#
),这里给下摘要:
1 | // row 14 to 16 |
然后开始步进,因为我们还不知道是如何进行比较的,不过应该能猜到比较是从右往左的,因为你内存槽[0]总不可能再往左走吧www,所以要注意得到的flag是逆序的
上面新用的编译器有个好处,就是它的内存槽内的数据如果溢出了,会自动mod 255,也就是说这里0-1=255而不会出错而直接停止程序的运行,也方便我们debug,而且编译器在遇到指针指向内存槽[0]但是指针左移的操作的时候,它会自动跳转到内存槽[29999],这非常有用,后面会提到
这里简单讲一下它的比较逻辑(你步进一轮应该也能发现的):加入我们此时的内存槽是这样的:
1 | 下标: ... n ... |
那么它会利用内存[n+1],[n+2]和[n+3]进行构造一个数(记为x),然后存放在[n+1]处,接下来将[n]和[n+1]的两个数同步相减,直到[n+1]处的数变为0。这样的构造和相减会进行若干次,而几次构造的数之和其实就是真正的flag的ASCII
有点懵,对吧,那就拿上面第14~16行的代码解剖一下吧:
1 | ,[>,] # // 读取flag,断点 |
大致翻一下整个程序,发现最后的清0操作是完全一致的,而且如果我们在上面分析的第11行的[-]
,也就是清0之前打上断点,再利用新编译器溢出的特性,我们就能在不完整分析代码的条件下获取到flag
由于多次相减操作其实可以看做只进行一次相减操作,所以思路如下:
1 | // 首先,我们什么都不输入 |
所以比较的第1个字符我们用256去减掉调试得到的数,之后的字符就用257减去,打断点的话一个一个看挺麻烦的,所以我直接删掉空白字符然后正则匹配了
编译器没法一行识别太多代码,把替换后的字符串后面加一个\n
就行了,应该吧…反正就是打几个回车的事,无所谓的
然后就开启Debug模式,一个一个字符看过去吧(反正我是用这种笨方法做出来的)
最后做完了,可以直接把得到的flag作为input然后运行代码,如果正确得到的输出应该是这样的:
别问我为什么光明正大的放flag,动态靶机,你能随到一样的算你牛逼
话说咋没看到有人写这题wp啊…这一趟做完也不难啊…还是说都藏着掖着不让我学QAQ
- 本文作者: 9C±Void
- 本文链接: https://cauliweak9.github.io/2023/11/30/BrainFuck/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!