2024 红明谷 dualrsa

WriteUp 3周前 admin
37 0 0

这道赛题比赛的时候没有做出来,赛后复现的时候和 1997 的群友”集思广益“后发现其实也不是很难,主要是构造字符串利用 eval 那里限制的太死了,粗略测了一下发现只有一个值是满足的,出题人真会卡啊。另外整个流程由于在换着 RSA 的参数用,所以乍一眼看过去还是有点绕的,再加上比赛时间也不多,所以最后遗憾败北。

题面

题目源代码:

from libnum import n2s, s2n, xgcd
import socketserver
from secret import flag
from Crypto.Util.number import getPrime


hint = s2n(flag)


class RSASystem:
    def __init__(self, nbits, e, inherit=None):
        self.nbits = nbits
        self.e = e
        if inherit:
            self.__pri_key = inherit.__pri_key
        else:
            self.__pri_key = self.gen_key()
            self.n = self.__pri_key[0] * self.__pri_key[1]

    def gen_key(self):
        p = getPrime(self.nbits)
        q = getPrime(self.nbits)
        phi = (p - 1) * (q - 1)
        d = xgcd(self.e, phi)[0] % phi
        return p, q, d

    def encrypt(self, m):
        if not isinstance(m, str):
            return ""
        try:
            return pow(eval(m), self.e, self.n)

        except:
            return pow(s2n(m), self.e, self.n)


    def decrypt(self, c):
        if not isinstance(c, str):
            return None
        try:
            return pow(eval(c), self.__pri_key[2], self.n)
        except:
            return pow(s2n(c), self.__pri_key[2], self.n)

    def restore(self, sig):
        return self.encrypt(sig)


class OuterServer:
    def __init__(self, rsa: RSASystem, inner_server):
        self.rsa = rsa
        self.inner_server = inner_server

    def get_enc(self, m):
        return str(self.inner_server.rsa.encrypt(m))

    def get_dec(self, c):
        cur = self.inner_server.rsa.decrypt(c)
        return cur % 256

    def enc(self, m):
        return str(self.rsa.encrypt(m))

    def sig(self, m):
        return self.rsa.decrypt(m)

    def restore_sig(self, cur_sig):
        return n2s(eval(self.enc(str(cur_sig)))).decode('latin')


class InnerServer:
    def __init__(self, rsa):
        self.rsa = rsa

class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    pass


class EncHandler(socketserver.BaseRequestHandler):
    def handle(self):
        inner_server = InnerServer(rsa1)
        outer_server = OuterServer(rsa2, inner_server)
        flag_enc = outer_server.enc(flag)
        
        self.sendline(f"The public keys are: {outer_server.rsa.n}{inner_server.rsa.n}")
        self.sendline("menu:n1. getFlagn2. encrypt your messagen3. decrypt your message")
        for _ in range(600):
            self.send("Please choose your option![+]")
            choice = eval(self.recv(1))
            if choice == 1:
                self.sendline(flag_enc)
            elif choice == 2:
                self.send("Input your message's sig![+]")
                cur_sig = int(self.recv())
                stored_sig = outer_server.restore_sig(cur_sig)
                message_enc = outer_server.get_enc(stored_sig)
                self.sendline(message_enc)
            elif choice == 3:
                self.send("Input your ciphertext![+]")
                cur_cipher = int(self.recv())
                cur_ans = outer_server.enc(str(outer_server.get_dec(str(cur_cipher))))
                self.sendline(cur_ans)

    def sendline(self, msg):
        self.request.sendall((msg + 'n').encode())

    def send(self, msg):
        self.request.send((msg + 'n').encode())

    def recv(self, byte=2048):
        message = self.request.recv(2048).strip().decode()
        return message[0: min(len(message), byte)]


if __name__ == "__main__":
    HOST, PORT = "0.0.0.0"1213
    rsa1 = RSASystem(204865537)
    rsa2 = RSASystem(204983)
    print('Start serving...')
    server = ThreadedTCPServer((HOST, PORT), EncHandler)
    server.serve_forever()

题目首先给出两个模数 ,对应两份 RSA: InnerServer(),OuterServer(

然后提供三个功能

  1. 给出 flag 的密文
  2. 使用  OuterServer 进行加密,然后用 InnerServer 进行加密,输出
  3. 使用 InnerServer 进行解密,然后对最后一个字节用 OuterServer 进行加密,输出  

解题思路

由于 是比较小的,所以功能 3 中对一个字节的加密,我们可以直接对结果开 次根就可以得到明文,而这个结果是 InnerServer 的解密,因此,如果我们能够提供 flag 关于 InnerServer 的加密,我们就能获取 flag 的最后一个字节。那么用类似于 RSA LSB Oracle 的攻击手法应该就能获取完整的 flag。

而功能 1 是给出 flag 关于 OuterServer 的密文,但是功能  3 里面提供的是关于 InnerServer 的解密,所以应该是一个没用的功能,那么这题的切入点就在于功能 2 了。

功能 2 这个点理论上应该是没有什么问题的,毕竟只是用两个公钥在操作,我们自己也可以在本地计算。那么三个功能废了两个?其实不然,可以看到出题人在题目里用了大量的 eval,因此我甚至幻想过 rce 这道题,

2024 红明谷 dualrsa

为啥是四字节呢?注意到功能 2 中的 restore_sig 的调用链(以及数据的类型)

cur_sig = int(self.recv())
stored_sig = outer_server.restore_sig(cur_sig)

def restore_sig(self, cur_sig):
    return n2s(eval(self.enc(str(cur_sig)))).decode('latin')

def enc(self, m):
    return str(self.rsa.encrypt(m))

def encrypt(self, m):
    if not isinstance(m, str):
        return ""
    try:
        return pow(eval(m), self.e, self.n)

    except:
        return pow(s2n(m), self.e, self.n)

功能 2 将我们的输入由整型转为 字符串 然后 eval (又变回整型)再计算 83 次幂,继而再转 字符串 然后又 eval (变回整型)最后 n2s 存储为字符串。注意到这个 n2s,这里是将整型以大端的方式变成字符串存储,而不是 str 这样单纯的类型转换。

然后在功能 2 下面还有对这个数据的调用

message_enc = outer_server.get_enc(stored_sig)

def get_enc(self, m):
    return str(self.inner_server.rsa.encrypt(m))

def encrypt(self, m):
    if not isinstance(m, str):
        return ""
    try:
        return pow(eval(m), self.e, self.n)

    except:
        return pow(s2n(m), self.e, self.n)

注意到这里,直接起手一个 eval(m),如果抛异常了,才是将 进行 s2n 以大端的方式讲字符串解码成数字再计算。

因此,如果我们构造一个输入,使得其在计算 83 次幂后表示的字符串能够触发 eval 不抛异常,我们就能利用上这个 eval

于是我尝试构造,一开始的思路是直接构造出命令执行代码,然后直接开 83 次根(因为不知道 的分解,所以是没办法计算模 下的83次根的)

from Crypto.Util.number import *
from gmpy2 import *
rce = __import__('os').system("ls"
rce += 'xff' from Crypto.Util.number import *
from gmpy2 import *
rce = b"__import__('os').system('ls')"
rce += b'xff' * (512 - len(rce))
m = gmpy2.iroot(bytes_to_long(rce),83)[0]
print(m)
print(long_to_bytes(pow(m,83)))* (512 - len(rce))
gmpy2.iroot(bytes_to_long(rce),38)[0]

得到结果

708732724968359
b'__impnx9b>x8duxa17xb2x10xc5xeauxdax91x07rx8fxd3xfcxb0xfaxf07"x81-oxfex9fx88xf5x1fxf9gxb6xecQx16x89xcb{xa5x80nxccxedxdb:Yxe9xa5xa3xf7x15cxb4x1erxf9Ex91:x82x1fx90xa5-8xb2fxe1xb3xa0xbax07~x977xeax0fx02x92xc0mx97xd1Yxb6xe9Hxdfxadx8fx80yxe1bxf6xb2Fxd6x03xe1 jxddxbcx16Rxe12xf2xb4x91xebxbexcfex185x8fxddxd3xdex1bOxda_srRx00xFxe1xadx0cxd3xc478faxc8xa8cxfa<rxaeox0c5xfex1cadx02xbbx11x88xfexd7xeaxf0ixf3uxe9x9ag6itKxe3xcexb2xaexdacxb9Kxaf{xddxe9xa2x16xc8xc9xb2Yxc9xb9'x1axe4xe8xb9xfdx1cxdcxa8xb0x1bx9fMx1enKxaex89x9eNxebrPx16:%xb7zx8axb8Bmx80xeexc8x8dH81#xe8.xd9x07x85xbdx1e-xaaTnxec4xbdx85xeb|x96xb5x1a%xb1*enx06xd7}xaaOxeenx03~oxbdnx03xa3xc9Ixed]SxfexbdwQx83x12Rxbfx15Oxe8xc2Hjxa6Pxfb37x12xx0fxfax9bxe0xf3Dxcbxd8x0c9xb0x08x13xd4#Bxa7x98GCxf9Ekxb6y^xffxe1Itx05xd4lxbdDx8b?Ehx90xa1"P6xdexa4>tx96x11x1ex13x87x88xba^"tx19Qx1a7xed)Mx93"/!xd2x9d Bx07xeevx19kx82xb3xd8xea3xe3xedx18xe3xd7x86fcxfdijx0b1xa0x9bw&tOxafx9bx9aDox9ex12x1bhxe6xf8x17Saxc6_:fx19xaaxa4xe0xaexaaYx99xf9xba]x04x94xc8:x1bjx9ckxd7>F{Dx00x9cocxdcxd7xefxd1xe1xdb3xfe?x0cxf5xb8x0cx14xa1x94Gx08x90xe9Eaxe9YW;Px91x05xa1xdaX[xf8xe0xab8Jxa6xcdxc9 xb5xd33=Gxb1xa3xadv$uxee.Bxb7$xb7'

可以发现由于开根后造成的精度损失,原先的字符串只剩 5 个字符 __imp 还在,为了让 eval 不报错,第五个字符还得是注释符 # 才行,于是就有了上面的搜索:python eval 4字节getshell

显然我失败了,没找到这样的(这可是密码题)

后来放了提示 prefix = hint#

才注意到题目里有一个变量 hint = s2n(flag) ,在题面中却一直没用到,一开始还以为是出题人忘记删了,原来是特意留给我的,,愚钝了。

于是代码变为

from Crypto.Util.number import *
from gmpy2 import *
hint = 123456
rce = b"hint#"
rce += b'xff' * (512 - len(rce))
m = gmpy2.iroot(bytes_to_long(rce),83)[0]
print(m)
print(long_to_bytes(pow(m,83)))
print(eval(long_to_bytes(pow(m,83))))

果然事情并没有这么简单,运行后报错

709506354616988
b'hint#xfe"1xfbx1f*xb57x85x80xddxf2 Cx81dxc2Zxacxc4xa9x1bx03Yx070xe0xb5x95xee~x9fx13#X+#f|~x88fx85xaex96x89x19fxfex14xab'xb2lx9exd4Vxc3Hm}!xc9IQxf8xb4xe5Djxe3xe9x00pxedxabx1fxe0Yxb2xa5xa1xbfxb5xddx93xac&Txd6%xd7x9ax19xccxfdkGxffxb3xe3x93xf9x8cxcfIx15x01xb5x16x1d:x92xb0hx9exa3xcfxcdOxa4N:-xa1Axcfxd0x84ixebxffx92x1d"x0cxafx88z[x05<xf06xd1xb4Crx8axbcx9fm8x96gxfb{xfax89ux01xbdx88Axccx87xf3x9a3xe1x04x94xbcxa8x00xe6x81x0exb0xf1Cxc1xb5x9axf7xa1Ox00jx10xbbx94xfb4<x94!x9dx15x96xc2x05xb3x8bxfexa0rn'x0bZxba+?xdcx8fxc9 kxf6x1dJxc3xa0xf8xxdf5xfcx97cx1fxf5xf2xd96{x1exe8Kx0exc6xd1*K2xf1x93xa4xa0x89xf0x8fx86fx1cx88xe32xc7x08&xbb_?,xd3x89xecyx1c?xf1xe5xc8xa5@Kxdccxd77;x01x97:x8axbd/Bxa1xd8`qx1bx1977x0cxe0xebxa6amExdaxfaxd6xd2xd2xbdxedxb8xcfxa2xa7rxd1x80xe7xe7sWx93K(xd4x1f=4xf9x8exf1xf1x10rx06x89xc2b#xcbxedx96x19xb2x02x0cx01&x96J+xbfr"Axea#xde.Xx00xefxcdxbbhRx81Axc9I1xfaxb3xe6>MGB)x89x1fxc1&xb0xabx8bx1axd3)xb3x94x19Pz.Fvxe6x14xe0xe3xad>Ux0fxe8|Hxb2x85xc0-x06xcexbexa7:xc23Cxe5x85tx11xc6^xf6xdax06xd3{x03xdfKx03x8cxefxca9Txfexc80gx86_xcax94IWx82q@xd9x07xd6xeaxc8xdbx8e\xd7x9ex88x8dx01xb6xe3,Txacxb0mx1fx1axe6kexed,$x01xf1mxc0x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00'
Traceback (most recent call last):
  File "C:UsersjkangDesktoptest.py", line 42in <module>
    print(eval(long_to_bytes(pow(m,83))))
ValueError: source code string cannot contain null bytes

eval 里的字符串不能包含 x00,除了 x00,有些字符组合在一起也不行,估计以unicode解码了,但是又非法,大概这样

2024 红明谷 dualrsa

遍历了一下 709506354616988 附近的值也都没有符合要求的,然后就卡死了

赛后在 1997 的群里讨论了一下,根据套娃(Tover)给出的方案修改了一下爆破代码,核心点在于刚开始字符串的长度不用限定到刚刚好 512,小一点也行(开根再幂次也还是能有 hint# 头)

from Crypto.Util.number import *
from gmpy2 import *

hint = 1233444

for j in range(400,507):
 tmp = bytes_to_long(b"hint#"+b'x00'*j)
 tmp2 = bytes_to_long(b"hint$"+b'x00'*j)
 floor = iroot(tmp,83)[0]
 sky = iroot(tmp2,83)[0]
 #print(floor,sky)
 for i in range(floor,sky+1):

  m = long_to_bytes(pow(i,83))
  try:
   eval(m)
   print(m)
   print("[+]",j,i)
  except:
   pass

得到一个符合要求的值 475189300601805

b'hint#Bx85xa91xd8x95xedxd4_%x13x1bVxc4xfc8x832k-xc3w"gxcexa5x17nxfexacxbeP`sx17<xb6x9ex9bx87)rkxefxd4xebxaeNEx13xeffx82rQdx06x04x1dxb2Uxd6Ixc4x01xfax1dxb3xde^+-xaex9abdxf2hxe1]4txe6@x07x12xcfx98x93xddx82'xc6xe1x10xcdx87Ex1eMmx93mrxc4xc4xee0xdexbcx1ex15yxd2xdfSj1x80x82x84|xxf0Ux19xd6x84x1bxbeQn`xcdxbbxe87Yxfax1d6x82x17xc6T+xdf.yQb&xdbxb8xa7}>'x0cxefx92xeaAx8a*Qzx8elpCxa0xdfxd8x98xbeHxe1xc4x0e8UM8yuxdb|MRM0xf6xbf0E?xa3xe6xb2x03wx93FAx8d=xfcxd1x06;xa11xa1xc8xd5&xa4xccmxe0xbaoCt#xf3x07x066ct:x93hx80"xecxc8xf2x85x87x9cxe0-xb5wxe8x88x8ddAx87xacxddxe8xaaxfdxfbxfbx7fxd5xb6,t`x1fx82xa9xc9'pxf8x8exc3xc9xb1x8cc.xc2ifxc7xabvx10xb9"j1xa1`xeex98xf55xa8x94hxa7,Wxd0xb3%*x9cx9cxc5cxb3Xxe6cxdaxf7xdf@\Pxabxdf@xfbxadx16*x04x17b*xf6xf0xecxb1xf2&oxb1xf7xa4xe84Mx0cxcdxc5'iDxd5xbd1^xf7x0fxb0Sx8eqxbcKxf7&txa7xaaxe5xa0x11x2xcdx7fxe8Kx87x08x90Y[x02x90mxbdxabxf4xxf9xadxccYxdfVOixc2$xb69Hxa7xb2xc5xc0xb3xe0@xeexa7xcc'xf2]xe7xc9xe1 xa7xc4xdfxdexfbHxe3x8eA<T )>x97xcbx9cx86px82xb03xa5xccxcdG+dx87xafx11vx14xf5xb4Y`x93/x89`xc7xc0xd3_cBxbc/s7xb85"xfcnxc8x8cxcdxeaxaf\Qx07xf5xfb/xeaBxadxb8xd9x0bx8epm_sW|x80x15'
[+] 501 475189300601805

有了这个值后续问题就迎刃而解了,调用功能 2 传入 475189300601805

然后把返回值送给功能3,

再把返回值开 83 次根就能得到 flag 的最后一个字符了,不出意外是 }

2024 红明谷 dualrsa

>> print(long_to_bytes(iroot(1105429575052088912049453038438451145102848046647844017283034044181579750804790663420352960444452892999750053625107156940415372665194489176787584483463433571159839630126953125,83)[0]))
b'}'

后面部分就是一个 RSA LSB(yte) Oracle 了,相关原理和脚本可以直接参考 Automne师傅的文章(RSA LSB Oracle攻击原理分析 · Automne’s Shadow (ce-automne.github.io))的,这里就不再赘述了。

最终解题代码

from pwn import *
from gmpy2 import iroot
from Crypto.Util.number import *


def lsbOracle(c):
 sh.recvuntil("[+]n")
 sh.sendline("3")
 sh.recvuntil("[+]n")
 sh.sendline(str(c))
 m = int(sh.recvuntil("n")[:-1])
 m = (iroot(m,83)[0])
 m = int(m)
 return m

n = 342252041423688785889282765687710688255177882518369200750856155858905317457669565327799154097080309519902323704731786521119679788331163899712030592725716553131904990426795686126430473254570275940110987683808214438585625423418919468468832148182908455474456550418041742997271433240614952005578253682336437125038102326899911333463929489685779323867218442530395713429260907696832757449156018783902172677312100563325755016411027809771718818171259598563888947753292653500901534384554453984509887806078098349553817530495128573148732706790048315312568413984071196984273100823863183298941823312722426405346171773474664965706357164182619792042434685780148473772921281445129558163823564579979182803128147267240222286783559132333289318029114339721706448992027153246340364373687926229961729995351698989856745823414933965677866527270826729775261069996489981097613639888849956319227010256286532659702270390841901334861070209915452231417437313078329132875528615282343172673650907640567175830218039566101273281917952868441053517106187436348216473297765766898441932333506483172867604029033657893030890966485205568687558845875381058268702680974533364436558170483121677597702156036363994050124492877180934238685246582213926325796426144036106626768520573 # 这个值在靶机启动后就不变了,所以交互一下直接复制

sh = remote("0.0.0.0",1215)
e = 65537
sh.recvuntil("[+]n")
sh.sendline("2")
sh.recvuntil("[+]n")
sh.sendline("475189300601805")
c = int(sh.recvuntil("n")[:-1])

a = 0
plaintext = 0
i = 0
f = 0
mod = 256   # !!!!!!!!!!!!!!!!!!!! 
while True:
#for i in range(0,400):
 inv = inverse(mod,n) # pow(3,-1)
 c1 = (c * pow(inv,e*i,n)) % n
 p = lsbOracle(c1)
 print(p)
 aa = (p - (a*inv) % n) % mod
 print(aa)
 print("---------round " + str(i) + " ---------")
 if aa == 0:
  f += 1
  if f == 20:
   break
 else:
  f = 0
 a = a*inv + aa
 plaintext = mod**i*aa + plaintext
 print(long_to_bytes(plaintext))
 i += 1
 '''content = long_to_bytes(plaintext)
 if b"flag" in content:
  break'''
 
print(long_to_bytes(plaintext))

2024 红明谷 dualrsa


原文始发于微信公众号(Van1sh):2024 红明谷 dualrsa

版权声明:admin 发表于 2024年4月8日 上午11:17。
转载请注明:2024 红明谷 dualrsa | CTF导航

相关文章