[NewStarCTF 2023 公开赛道]shell code revenge
特殊shellcode
这道题有点像我之前做过的mrctf的shellcode的revenge。不过范围不一样。

大概看一下,把所有的大写字母和数字都排除在外了,意思是只要输入大写字母和数字就不会被break——跳出。
所以我们要创造一个只有大写字母和数字的shellcode?
然后我也懒得checksec了,而且用的是rsp——也就是64位。开了一些保护啥的,反正jumpout到的66660000h这个地址在上面的mmap里面写入了权限’7’(第三参数),也就是可读可写可执行。
整个代码的逻辑是:出现Show me your magic之后,进入一个for循环,for循环一开始会有一个read让我们输入数据,因为是一个char类型指针buf,每次读取一个值。然后下面的strncpy()会把我们一个一个输入的buf存到src里面的src复制到我们的0x66660000地址里面,我们可以看main函数的堆栈视图:

我们要将数据存放到src的位置,而且只能输入264个字符(这264字符是可用的shellcode长度)。
不过这道题目比较难,是一个可见字符shellcode。要构造一个可见字符shellcode还必须是大写和数字就很麻烦了,目前自动生成的大部分都是大小写+数字的组合。
这边用了一个read函数,其中第一参数是0,也就是fd里面的标准输入,我输入进去的东西都会从这个里面获取到,第二参数是buf变量地址(一个指针)也就是读取的大小,第三参数是读取权限。

汇编这里会写的更清楚一点,edi的值变成了0。我们可以通过这个值进行运算,例如[edi+0x10]得到的值就是0+0x10=0x10。
现在的思路是,我可以用可见字符来构造一个最小的shellcode,目的是为了调用read函数,让read函数再一次读取值存放到66660000h这个地址上,覆盖我之前的为了调用read函数而构造的shellcode,再一次读取的shellcode不会有什么奇怪的判定条件,它可以直接执行我输入进去的任何东西。
构造第一段shellcode - read函数
构造可见ASCII shellcode是一个很麻烦的工作,我们先看一下普通的shellcode,拿64位举例:
1 | ;;nasm -f elf64 shellcode64.asm |
最麻烦的部分就是/bin/sh和syscall,一个是字符串,另外一个是命令。其中syscall的机器码是0x0f05,汇编的书籍或Intel/AMD开发者用书里面也会说到。
可是syscall不是可见字符啊,更不是A-Z,0-9之间的数值。这要怎么办?
异或加密
异或是一种逻辑运算,A⊕B得到的值是C,而C⊕B之后得到的值就是A。逻辑如下:、

如此,我就可以构造一个这样的设计:


我们假设字符’A’为密钥,将syscall作为原始数据输入计算得到加密数据:0x0f05⊕0x4141=0x4e44(都为可见字符),所以我们确实可以用它来作为密钥。
加密后的syscall需要写入内存,但是直接写入的指令mov机器码很显然不是可见字符,而xor为可见字符0x33=’3’。或者0x31=’1’。
为什么有两个?是因为xor的运算符定义是这样的:
1 | xor [a值], [b值] |
而在汇编里面,寄存器需要放在前面,其他的放在后面,也就变成了:<font style="color:rgb(17, 17, 17);">xor 寄存器, 其他值</font>,所代表的意思也就是将寄存器里面的值和其他值进行异或处理后,存储到寄存器里面。
如果我们要实现:<font style="color:rgb(17, 17, 17);">xor 其他值, 寄存器</font>,也就是将其他值和寄存器做异或处理后,将计算后的值存储到其他值里面,这个时候在机器码里面,依旧要按照:<font style="color:rgb(17, 17, 17);">xor机器码 寄存器机器码,其他值</font>的顺序。
于是就有了两个xor,一个代表的是第一种情况,一个代表第二种情况,分别为<font style="color:rgb(17, 17, 17);">0x33</font>和<font style="color:rgb(17, 17, 17);">0x31</font>
异或解密syscall
这个时候我们就可以开始构造一些东西了,首先看代码,
我们会发现在jmp到66660000h这个地址前,我们的eax被归0了,所以就可以利用这一点,配合xor来创造
并且制造出syscall的加密数据形式0x4e444e44(输入两遍是为了防止一遍不运行)
首先一开始是<font style="color:rgb(17, 17, 17);">xor eax,NUM</font>,实际上是<font style="color:rgb(17, 17, 17);">[rdx+0x38]</font>,为什么是<font style="color:rgb(17, 17, 17);">rdx+NUM</font>?在这里我们看到esi和edx都被复制为了66660000h,实际上这个就是打定了 内存区域中的基地址 ,那为什么偏偏是这个地址呢?这其实有点玄学,因为似乎rdx会作为这个偏移值的基地址,而刚好这边初始化了edx。而大概率选择了rdx作为基地址来偏移。
我们构造这么一个逻辑:

这里面有很多零碎的知识点,我们先讲一个大概,后面慢慢补充:
首先是上面的机器码部分,因为程序必须输入可见字符,其中第一部分的可见字符是0x300x39,可见字符’0’‘9’的部分。我在这里输入的是0x33 0x42 addr1,这个代码的汇编样式是:xor rax, [addr1],目的是将rax里面的值改成’A’,0x42是寄存器rax的机器码。rax的值一开始为0,A⊕0=A,而
1 | rax==0, [addr]=='A' |
通过这一步,我就可以把rax的值改成’A’了。
然后第二步,使用xor指令配合修改成’A’值的寄存器rax对下面的加密后syscall做处理。第一个加密syscall的起始地址为addr2,利用<font style="color:rgb(17, 17, 17);">0x31 0x42 addr2</font>这个命令,就可以修改addr2中的数据。逻辑为:[addr2] ⊕ rax =
1 | [addr2]==0x4e, rax=='A' |
第三步,我们要还原rax寄存器的值为0,为什么?因为我们的核心逻辑是再调用一次read函数,然后读取我们写入的第二段shellcode(可以直接进入/bin/sh的)到特定的内存里,然后让PC,也就是程序计数器读到第二段shellcode的内容执行即可。用rax里面的值随便找一个’A’做异或即可。
1 | rax=='A' [addr1]=='A' |
如此完成后,即可。
现在的问题是addr2和addr1应该是多少?
因为我们输入的addr2和addr1必须是可见字符,也就是0x30-0x39之间的数值,而二遍syscall最多占用四个地址。所以我们把第一个syscall的起始地址addr2放在0x30的位置就行了。中间用pop rcx这种方式来过度,他的机器码刚好也是一个可见字符,而rcx在程序中这个寄存器也没有用到,他里面的值是多少不重要(类似于NOP滑块指令,目的只是塞入值,并且让程序读到这些中间值又可以运行)
而addr的地址则为0x30后4个数字(因为包括0x30,所以第4个数字实际上是0x33),在第五位的时候塞入数据’A’做填充。一般而言只要两个’A’就行了。我们这边填充8个A。所以第一段的代码就被写出来了:
1 | payload = b'\x33\x42\x38' #33 42 38 xor eax, DWORD PTR [rdx+0x38] |
一些没有讲的小问题

1、为什么我只异或了0x30地址上的4e,syscall却正常运作了呢?
不知为何,寄存器不知道调用的是eax还是rax,其实不重要,他会与后面所有的字符A做异或,然后再与两个字节的syscall,也就是两个0x4e44做处理。如果我在这里后面的字符’A’只有两个的话就会与第一个0x4e44做处理,这个是gdb后的结果,原理是什么我也不清楚…所以字符’A’的长度必须大于2,不然0x44的部分就不会被处理了。
2、如果我把payload中的\x38改成\x3a(也是存有’A’的地址),为什么运行不了呢?
因为0x3a是字符’:’,不是可见字符……
第二段shellcode
第二段就很简单了,我只需要放入NOP字符滑过和覆盖之前的内容即可,然后再塞入/bin/sh的shellcode即可。
1 | p.sendline(b'\x90'*0x50+asm(shellcraft.sh())) |
全部代码:
1 | #[NewStarCTF 2023 public race]shellcode revenge |










