ChCore-lab0
Lab 0: 爆弾!!!
这是OS操作系统的前序实验。。。
(p.s. 怎么一上来就让人这么累。。。)
1. 准备实验环境
基于Ubuntu 22.04.2 ARM64.
首先,我们将我们的学号填入到student-number.txt中。
其次,在linux上安装所需要的东西。如果没有gcc,g++(编译器),gdb(调试器)则首先运行:
sudo apt-get install -y build-essential sudo apt-get install -y gcc sudo apt-get install -y gdb sudo apt-get install -y g++ sudo apt-get install -y make
其中-y是默认做出yes选择。如果出现资源占用,最简单粗暴的方法就是重启我们的linux。(doge)
安装完成后我们检查一下我们的编译器。
which
命令用于发现编译器藏在哪里,剩下的version就是展示我们的版本号。
接下来,我们在终端中输入
make bomb
这样就会收获一个独属于自己的专属炸弹(真好。。。)
介于我的ubuntu本身就是aarch64架构,因此完全不用担心gdb用不了。这里还是基于x86/64来进行使用。其实只需要再安装qemu和gdb-multiarch即可。
输入:
sudo apt-get install -y qemu sudo apt-get install -y gdb-multiarch
接下来我们需要开启两个终端,分别运行:
make qemu-gdb make gdb
出现这样的画面:
第一个终端:
第二个终端:
就可以准备开始实验了。
2. 実験が始める!
开始我们的实验前,我们需要了解一些基本的汇编级别调试。
- disassemble:显示目前正在运行的函数/模块的机器级源码。
- nexti:机器级源码层面向下运行1步。nexti 5 则代表向下运行5步。但是不会进入新的函数/模块,会继续在当前模块向下运行。
- stepi:机器级源码层面向下运行1步。stepi 5 则代表向下运行5步。遇到调用新的函数/模块是会进入该模块。
- p (char)
x3来代替p (char). - x $x3:显示x3所存地址指向的内存空间里的值。x/s 为打印字符串, x/c为打印字符。
- info register/breakpoints:显示当前所有寄存器/断点的情况。有助于我们判断如何读取寄存器和管理断点。
- continue/c:继续运行一直到遇到断点停止或程序退出。
- break main:为main函数打上断点。没有符号名时,采用 break *0x400a00来为具体的机器级源码打上断点。
2.1 phase_0
首先我们先给main函数打上断点。然后运行到main处显示机器级源码。
break main c disassemble
我们可以看到main里的机器级源码了!!!
从中我们可以发现我们有6个炸弹需要拆除。我们首先在第一个阶段上打上断点,这样在拆除炸弹时就可以跳转到入口处。
在另一个终端上随便输入一些东西。我们假设输入1,然后我们stepi进入到第一个阶段的炸弹拆除中。
前两行函数其实就是正常的入栈操作,将存有调用入口的地址和传入参数分别入栈后,再将栈顶的值存入寄存器中,有助于递归回调函数。
观察+12到+24处的函数,我们可以发现,函数从我们的输入中读入一个int型整数,并将其传给w0寄存器。w1从一个我们无法看到的内存空间(0x4a0084)中读取了一个数字,并与我们的输入进行比较。一旦有所不同,我们的炸弹将立刻被引爆。所以在读入数字后,我们需要查看w1里存了什么。
我们可以看到答案是2022. 一旦炸弹爆炸,我们需要重启make qemu-gdb 和 make gdb。但是我们可以将答案存入ans.txt中,采用以下命令输入答案。
make qemu-gdb < ans.txt
这样就不需要每次用手输入新的答案。
2.2 phase_1
很明显它已经暴露了。看到了strcmp就说明我们需要进行字符串的比较。
答案已经显而易见。
2.3 phase_2
打好断点,我们来看phase_2.
标签首先提示我们需要读入8个数。第一个数存储于栈顶sp+0x20(32)处,一个int型数占4byte。因此接下来+20到+36就是读取我们输入的第一个和第二个数,并且第一和第二个数必须为1.
接下来后面的行为就是:读取最后一个数和前一个数,新的数相当于这两个数的和再加上4.很像fibonacci数列。
0x4007b8 <+48>: add x19, sp, #0x20 // 保存第一个数的地址 0x4007bc <+52>: add x20, sp, #0x38 // 保存最后一个数的地址 0x4007c0 <+56>: b 0x4007d0 <phase_2+72> // 跳转。 0x4007c4 <+60>: add x19, x19, #0x4 // 将指针移到第二个数。 0x4007c8 <+64>: cmp x19, x20 // 检查是否到了结尾,如果结尾则返回。 0x4007cc <+68>: b.eq 0x4007f4 <phase_2+108> 0x4007d0 <+72>: ldr w0, [x19] // 读入第一个数的内容。 0x4007d4 <+76>: ldr w1, [x19, #4] // 读入第二个数的内容。 0x4007d8 <+80>: add w0, w0, w1 // 两数相加 0x4007dc <+84>: add w0, w0, #0x4 // 再加4 0x4007e0 <+88>: ldr w1, [x19, #8] 0x4007e4 <+92>: cmp w1, w0 // 比较第三个数是不是按照前面规则得到的数字。 0x4007e8 <+96>: b.eq 0x4007c4 <phase_2+60> 0x4007ec <+100>: bl 0x400af4 <explode> 0x4007f0 <+104>: b 0x4007c4 <phase_2+60> 0x4007f4 <+108>: ldp x19, x20, [sp, #16] 0x4007f8 <+112>: ldp x29, x30, [sp], #64 0x4007fc <+116>: ret
这样我们需要填入的数就是
1 1 6 11 21 36 61 101
phase_3
难度有些上升了,不过不怕,慢慢来看。
(真长啊,以前的人是怎么用汇编写游戏的。。。)
这个__isoc99_sscanf
函数用于识别参数个数并将输入的数压入栈中。我们需要清醒地认识到,入栈时,栈顶指针的地址将减小。因此先压入第一个参数,再压入第二个参数,则第二个参数的地址要小于第一个参数的地址。在这里的机器语言中,我们可以发现[
从+36行开始读入第一个数,我们可以从后面看见,读入的数可以有3种选择。2,3,4均可。否则我们就会粉身碎骨。
我们首先来看首位数字是2的情况。
0x400844 <+68>: ldp x29, x30, [sp], #32 0x400848 <+72>: ret ... 0x400884 <+132>: ldr w0, [sp, #24] //读入第二个数字,第一个数字存在sp+28处 0x400888 <+136>: eor w0, w0, w0, asr #3 //将w0进行算术右移3位后,加上w0,并与自己进行异或,存入w0。 0x40088c <+140>: and w0, w0, #0x7 //取消高位1,即将其处理为不大于7的正数。 0x400890 <+144>: ldr w1, [sp, #28] // 将得到的数与第一个输入的数字比较,相等则成功拆弹。 0x400894 <+148>: cmp w0, w1 0x400898 <+152>: b.eq 0x400844 <phase_3+68> 0x40089c <+156>: bl 0x400af4 <explode>
这样我们很明显可以发现,我们有多个答案。这里我们给出一个最简单的答案:2 2.这样在+132处得到的w0即为2,与w1相同。
接下来来看首位数字是3的情况。
0x400844 <+68>: ldp x29, x30, [sp], #32 0x400848 <+72>: ret ... 0x400854 <+84>: ldr w2, [sp, #24] 0x400858 <+88>: mov w0, #0x6667 // #26215存入w0中 0x40085c <+92>: movk w0, #0x6666, lsl #16 // w0=0x66666667. 0x400860 <+96>: smull x0, w2, w0 // w2与w0相乘,存入x0。 0x400864 <+100>: asr x0, x0, #34 // 将x0算术右移34位。 0x400868 <+104>: sub w0, w0, w2, asr #31 // w0=w0-w2算术右移31位。 0x40086c <+108>: add w1, w0, w0, lsl #2 // w1=w0+w0逻辑左移两位 0x400870 <+112>: sub w1, w2, w1, lsl #1 // w2-w1逻辑左移两位 0x400874 <+116>: add w0, w1, w0 // w0=w1+w0,并比较w0是否为3. 0x400878 <+120>: cmp w0, #0x3 0x40087c <+124>: b.eq 0x400844 <phase_3+68> 0x400880 <+128>: bl 0x400af4 <explode>
很明显最简单的答案为3 3.
最后是首位数字为4:
0x400884 <+132>: ldr w0, [sp, #24] 0x400888 <+136>: eor w0, w0, w0, asr #3 0x40088c <+140>: and w0, w0, #0x7 0x400890 <+144>: ldr w1, [sp, #28] 0x400894 <+148>: cmp w0, w1 0x400898 <+152>: b.eq 0x400844 <phase_3+68> 0x40089c <+156>: bl 0x400af4 <explode>
和之前的套路类似,我们就不在赘述了。最简单的答案为4 4.
phase_4
坏了,看到了encrypt,还进行了两段加密。我们慢慢来解开其到底是如何进行加密的。
首先我们先随便输入一串文字:ABCDEFGHIJK。
运行到+16处,我们观察一下x0(也就是w0,只是w0为x0的低32位)是什么。可以看到,在+12处我们将x0指向的字符串的地址复制给了x19。现在x19指向的是我们刚刚输入的字符串。
运行到+24处,我们观察一下存入x20的是什么,也就是x0发生了什么变化。
可以看到,x0=11,也就是我们输入的字符串的长度。那么刚刚在+16调用的函数就是统计输入字符串的长度,并且其长度不能大于10.一旦大于10将粉身碎骨。
于是我们将输入改成ABCDEFGHIJ。
x20存入的是我们的字符串长度,x19指向我们输入字符串的第一个地址。这样我们将这两个作为参数传入到encrypt_method1
当中。
step进入其中:
0x4008e8 <+32>: mov x4, x2 0x4008ec <+36>: mov x2, #0x0 //进入循环 0x4008f0 <+40>: lsl x5, x2, #1 0x4008f4 <+44>: ldrb w5, [x0, x5] 0x4008f8 <+48>: strb w5, [x4], #1 0x4008fc <+52>: add x2, x2, #0x1 0x400900 <+56>: cmp w3, w2 0x400904 <+60>: b.gt 0x4008f0 <encrypt_method1+40>
这个循环就是提取字符串中的奇数位的字符,并将其存储在一块内存中。
0x400918 <+80>: sub w4, w1, w5 0x40091c <+84>: sub w2, w5, w3 0x400920 <+88>: add x2, x0, w2, sxtw #1 0x400924 <+92>: add x2, x2, #0x1 0x400928 <+96>: mov x1, #0x0 0x40092c <+100>: add x3, sp, #0x10 0x400930 <+104>: add x5, x3, w5, sxtw 0x400934 <+108>: lsl x3, x1, #1 0x400938 <+112>: ldrb w3, [x2, x3] 0x40093c <+116>: strb w3, [x5, x1] 0x400940 <+120>: add x1, x1, #0x1 0x400944 <+124>: cmp x1, x4 0x400948 <+128>: b.ne 0x400934 <encrypt_method1+108> 0x40094c <+132>: add x1, sp, #0x10 0x400950 <+136>: bl 0x421cc0 <strcpy>
完成这一部分之后,我们的字符串将会重新变成奇数位拼接偶数位,即ACEGIBDFHJ。
在encrypt_method2打上断点,continue跑步进入encrypted_method2!
前面从+0到+40全部都是读入数据。在读入数据完成后,我们需要关注的是
我们可以看到,x19指向我们前面处理过的字符串,x20保存了字符串的长度数据。x21和x22目前我们还不知道是用来做什么的,但是别着急,我们接着往下。
我们发现这里进行了一些操作,首先将x19存有的指向字符串的首地址存于x20,然后获取首个字符,检查该字符ascii是否大于0x61(97).那么我们通过man ascii
可以看到:
必须是小写字母即之后的一些符号,炸了。😦
那我们重新再来,输入abcdefghij试一试。
通过像486一样的能力,我们再一次回到了被炸死的地方。这一次,我们成功通过,并来到了+44处。
0x400990 <+44>: ldrb w1, [x20] 0x400994 <+48>: ldr x0, [x22, #8] 0x400998 <+52>: add x0, x0, x1 0x40099c <+56>: ldurb w0, [x0, #-97] 0x4009a0 <+60>: strb w0, [x20] 0x4009a4 <+64>: add x19, x19, #0x1 0x4009a8 <+68>: cmp x19, x21
我们来观察一下这段。首先,我们从x20处载入一个字节,也就是一个字符。接着,我们载入了前面神秘的x22+8处内存的内容到x0。这到底是什么呢?
可以看见这是一个字符串!所以此时x0存储的是这个字符串的地址。那么接下来的三行就很明显了,我们建立了一个如下的映射:
新字符=原字符+x0字符串-97.用人话来讲就是:
原字符:abcdefghijklmnopqrstuvwxyz 新字符:qwertyuiopasdfghjklzxcvbnm
回到phase_4.观察到在进入strcmp之前,x1获得了某个神秘的地址:
我们可以看到加密后的密文:
经过我们刚刚发现的加密过程,逆向思考可以得到:
isggstsvke hloolelwrc helloworlc
因此我们的答案即为:helloworlc.
phase_5
终于来到了最后一个实验。老规矩,先看看这个模块里是什么:
短,好,喜欢。首先,提示我们要读入一个整数。然后我们会让x1获得某个地址(说明该地址是某个数据结构/字符串的开始,然后进入一个名为func_5的函数,最终的结果是让x0内存有3这样一个数。否则粉身碎骨。。。
接下来我们进入func_5.
从+0到+28,我们获得神秘的数据结构的第一个内容,并将其与我们的输入比较。如果相等,我们就功亏一篑。如果等于0,说明我们遍历了整个数据结构。返回。
首个存储的数是49.我们的输入为5.因此没有爆炸!
那么我们就很好奇这个数据结构里面存储着什么?我们需要查看一下这部分的汇编语言。我们先以十进制打印一下x19的第一个数据,发现其位于一个叫做search_tree的数据结构中。
采用disassemble命令。我们通过试探法得知search_tree数据结构的边缘在+168.因此我们打印:
我们可以发现有以下比较重要的部分:
0x4a0070 <search_tree+0>: udf #49 0x4a0088 <search_tree+24>: udf #20 0x4a00a0 <search_tree+48>: udf #88 0x4a00b8 <search_tree+72>: udf #3 0x4a00d0 <search_tree+96>: udf #37 0x4a00e8 <search_tree+120>: udf #55 0x4a0100 <search_tree+144>: udf #91
在arm汇编指令中,.inst
指令可以强制干涉PC的指向,使其直接指向所需要指向的地址。
接着我们对比上面func_5
中的指令:
<func_5> ... // 检查是否有相等,出现则爆炸,当出现读取到0的时候返回。 0x400a74 <+40>: cmp w0, w20 0x400a78 <+44>: b.le 0x400aa0 <func_5+84> 0x400a7c <+48>: ldr x1, [x19, #8] 0x400a80 <+52>: mov w0, w20 0x400a84 <+56>: bl 0x400a4c <func_5> 0x400a88 <+60>: lsl w0, w0, #1 ... ret ... 0x400aa0 <+84>: ldr x1, [x19, #16] 0x400aa4 <+88>: mov w0, w20 0x400aa8 <+92>: bl 0x400a4c <func_5> 0x400aac <+96>: lsl w0, w0, #1 0x400ab0 <+100>: add w0, w0, #0x1 ... ret
结合前面的指令我们可以发现:
当我们输入的数大于当前节点的数时,我们寻找的是当前节点+16的节点,进入递归。当输入的数小于当前节点的数时,我们寻找当前节点+8的节点,进入递归。当输入等于当前节点的数时,虽然进入了+16的递归但是很快就会爆炸。这很像是“遍历AVL树”。我们可以画出这样一个树:
那么接下来我们就要思考如何才能使得w0最终等于3.我们发现,在递归调用函数后,每次返回时分别有以下两种操作:
- 输入的数字大于当前节点数字,w0=w0*2,w0=w0+1.
- 输入的数字小于当前节点数字,w0=w0*2.
树只有三层,因此我们递归时也只能选择以上操作三次。我们则需要进行操作:2->1->1.由于这是函数返回时进行的操作,所以我们应该选择数字,使得他能够在递归调用时采取1->1->2的方式行动。这样一来我们有(假设输入数字为a):
- a>49.
- a>88.
- 88<a<91.
所以合理的答案就应该为:89,90这两个数中的任意一个。
拆弹结束。
Edited by mumujun12345. 20240918
勿忘国耻。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库