CTFZone2018-Signature server

翻譯自:https://github.com/p4-team/ctf/tree/master/2018-07-21-ctfzone-quals/crypto_signature

給了server.py的代碼:

#!/usr/bin/python
import sys
import hashlib
import logging
import SocketServer
import base64
from flag import secret
from checksum_gen import WinternizChecksum


logger = logging.getLogger()
logger.setLevel(logging.INFO)
ch = logging.StreamHandler(sys.stdout)
ch.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
logger.addHandler(ch)


HASH_LENGTH=32
CHECKSUM_LENGTH=4
MESSAGE_LENGTH=32
CHANGED_MESSAGE_LENGTH=MESSAGE_LENGTH+CHECKSUM_LENGTH
BITS_PER_BYTE=8
show_flag_command="show flag"+(MESSAGE_LENGTH-9)*"\xff"
admin_command="su admin"+(MESSAGE_LENGTH-8)*"\x00"
PORT = 1337

def extend_signature_key(initial_key):
  full_sign_key=str(initial_key)
  for i in range(0,255):
    for j in range(0,CHANGED_MESSAGE_LENGTH):
      full_sign_key+=hashlib.sha256(full_sign_key[j*HASH_LENGTH+i*CHANGED_MESSAGE_LENGTH*HASH_LENGTH:(j+1)*HASH_LENGTH+i*CHANGED_MESSAGE_LENGTH*HASH_LENGTH]).digest()
  return full_sign_key
class Signer:
  
  def __init__(self):
    with open("/dev/urandom","rb") as f:
      self.signkey=f.read(HASH_LENGTH*CHANGED_MESSAGE_LENGTH)
    self.full_sign_key=extend_signature_key(self.signkey)
    self.wc=WinternizChecksum()
    self.user_is_admin=False

  def sign_byte(self,a,ind):
    assert(0<=a<=255)
    signature=self.full_sign_key[(CHANGED_MESSAGE_LENGTH*a+ind)*HASH_LENGTH:(CHANGED_MESSAGE_LENGTH*a+ind+1)*HASH_LENGTH]
    return signature

  def sign(self,data):
    decoded_data=base64.b64decode(data)
    if len(decoded_data)>MESSAGE_LENGTH:
      return "Error: message too large"
    if decoded_data==show_flag_command or decoded_data==admin_command:
      return "Error: nice try, punk"
    decoded_data+=(MESSAGE_LENGTH-len(decoded_data))*"\xff"
    decoded_data+=self.wc.generate(decoded_data)
    signature=""
    for i in range(0, CHANGED_MESSAGE_LENGTH):
      signature+=self.sign_byte(ord(decoded_data[i]),i)
    return base64.b64encode(decoded_data)+','+base64.b64encode(signature)
  
  def execute_command(self,data_sig):
    (data_with_checksum, signature)=map(base64.b64decode,data_sig.split(','))
    data=data_with_checksum[:MESSAGE_LENGTH]
    data_checksummed=data+self.wc.generate(data)
    if data_checksummed!=data_with_checksum:
      return "Error: wrong checksum!"
    signature_for_comparison=""
    for i in range(0, CHANGED_MESSAGE_LENGTH):
      signature_for_comparison+=self.sign_byte(ord(data_with_checksum[i]),i)
    if signature!=signature_for_comparison:
      return "Error: wrong signature!"
    if data==admin_command:
      self.user_is_admin=True
      return "Hello, admin"
    if data==show_flag_command:
      if self.user_is_admin:
        return "The flag is %s"%secret
      else:
        return "Only admin can get the flag\n"
    else:
      return "Unknown command\n"
def process(data,signer):
  [query,params]=data.split(':')
  params=params.rstrip("\n")
  if query=="hello":
    return "Hi"
  elif query=="sign":
    return signer.sign(params)
  elif query=="execute_command":
    return signer.execute_command(params)
  else:
    return "bad query"

class ThreadedTCPRequestHandler(SocketServer.BaseRequestHandler):
  
  def handle(self):
    signer=Signer()
    logger.info("%s client sconnected" % self.client_address[0])
    self.request.sendall("Welcome to the Tiny Signature Server!\nYou can sign any messages except for controlled ones\n")
    while True:
      data = self.request.recv(2048)
      try:
        ret = process(data,signer)
      except Exception:
        ret = 'Error'
      try:
        self.request.sendall(ret + '\n')
      except Exception:
        break

  def finish(self):
    logger.info("%s client disconnected" % self.client_address[0])


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

if __name__ == '__main__':
  server = ThreadedTCPServer(('0.0.0.0', PORT), ThreadedTCPRequestHandler)
  server.allow_reuse_address = True
  server.serve_forever()

根據代碼,這個服務器有三個功能

  • 發送 hi信息,沒什么用
  • 讓服務器對我們發送的數據進行簽名
  • 執行帶有簽名的指令

有兩條比較重要的命令,切換為admin以及請求flag。這兩條指令具體是:

show_flag_command = "show flag" + (MESSAGE_LENGTH - 9) * "\xff"
admin_command = "su admin" + (MESSAGE_LENGTH - 8) * "\x00"

我們可以具體看下簽名是如何生成的:

def sign(self, data):
    decoded_data = base64.b64decode(data)
    if len(decoded_data) > MESSAGE_LENGTH:
        return "Error: message too large"
    if decoded_data == show_flag_command or decoded_data == admin_command:
        return "Error: nice try, punk"
    decoded_data += (MESSAGE_LENGTH - len(decoded_data)) * "\xff"
    decoded_data += self.wc_generate(decoded_data)
    signature = ""
    for i in range(0, CHANGED_MESSAGE_LENGTH):
        signature += self.sign_byte(ord(decoded_data[i]), i)
    return base64.b64encode(decoded_data) + ',' + base64.b64encode(signature)

需要注意的是對敏感命令的檢查是在padding之前發生的,這也就意味著我們可以直接發送show flag字符串,這將會通過檢查,服務器會對其進行padding 并 簽名。我們可以比較簡單的獲得簽了名的show flag指令。
比較難獲得的是切換為admin身份的指令,因為其padding為\x00,因此沒有辦法像上面一樣繞過檢查。
因此我們只需要偽造對該條指令的簽名。
當我們深入簽名生成算法,我們可以觀察到兩點:

  • 原始的輸入會使用\xff進行pad,并加上Winternitz checksum
  • 簽名是逐字節生成的,每個字節對應的簽名和字節的位置以及值有關。

此外服務器會依次對checksum和簽名進行檢查,并告訴我們具體是哪步出錯。

第一步需要偽造checksum,可以看到checksum的長度為4字節,如果要直接爆破 的 話2^32還是有點大。但如果我們嘗試讓服務器簽名一些輸入,并觀察他的返回值,會發現checksum的后兩個字節永遠是\x00\x00,因此只需要爆破2個字節。
因此我們可以在我們想要執行的指令后加上窮舉的兩個字節,兩個\x00 以及一些隨機字節作為簽名,并發送,如果服務器返回 incorrect signature 就說明了checksum 猜對了。
代碼如下:

def find_checkum_conflict(s, wanted_msg, signature):
    print("Looking for checksum conflict")
    for a in range(256):
        for b in range(256):
            forged = wanted_msg + chr(a) + chr(b) + "\x00\x00"
            result = execute_command(s, forged, signature)
            if 'wrong signature' in result:
                print('Found checksum conflict for', a, b)
                return a, b

第二部需要偽造正確的簽名,在一開始的分析中,我們提到簽名是逐字節生成的,這意味著如果我們發送admin_command,把器最后一個字節替換掉,我們將得到前31字節的正確簽名。同時由于后兩個字節恒為\x00,因此這兩個字節的簽名也是正確的。
這樣,我們只缺中間3個字節的簽名。由于checksum只有兩個字節,而前面的message有32個字節,顯然會有很多沖突,所以我們可以爆破找到一個輸入和我們想要的命令有同樣的checksum。同時我們可以讓這些輸入的結尾都為'\x00',這樣當我們找到一個checksum沖突的時候,同時也獲得了\x00 對應的簽名。
代碼實現如下:

def get_proper_signature(checksum_we_need, s, original_signature_chunks):
    print("Looking for signature suffix for conflicting checksum")
    i = 0
    while True:
        msg = long_to_bytes(i)
        pad = 32 - len(msg)
        msg = msg + ('a' * (pad - 1)) + "\x00"
        result = sign(s, msg)
        ext_msg, signature = map(base64.b64decode, result.split(","))
        if ext_msg[32:36] == checksum_we_need:
            forged_signature_chunks = chunk(signature, 32)
            return "".join(original_signature_chunks[:-5] + forged_signature_chunks[-5:])
        i += 1

這樣我們就獲得了正確的簽名,即可執行兩條指令,獲得flag:

def main():
    url = "crypto-02.v7frkwrfyhsjtbpfcppnu.ctfz.one"
    port = 1337
    s = nc(url, port)
    receive_until_match(s, "You can sign any messages except for controlled ones")
    receive_until(s, "\n")
    msg = "show flag"
    show_flag_command = sign(s, msg)
    msg = "su admin" + (32 - 9) * "\x00"
    almost_admin_command = sign(s, msg)
    print(almost_admin_command)
    msg, signature = map(base64.b64decode, almost_admin_command.split(","))
    signature_chunks = chunk(signature, 32)
    wanted_msg = 'su admin\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
    a, b = find_checkum_conflict(s, wanted_msg, signature)
    checksum = chr(a) + chr(b) + "\x00\x00"
    forged_msg = wanted_msg + checksum
    signature = get_proper_signature(checksum, s, signature_chunks)
    print(execute_command(s, forged_msg, signature))
    send(s, 'execute_command:' + show_flag_command)
    interactive(s)


main()
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容