Sant97
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
      • Invitee
    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Versions and GitHub Sync Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
Invitee
Publish Note

Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

Your note will be visible on your profile and discoverable by anyone.
Your note is now live.
This note is visible on your profile and discoverable online.
Everyone on the web can find and read all notes of this public team.
See published notes
Unpublish note
Please check the box to agree to the Community Guidelines.
View profile
Engagement control
Commenting
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
  • Everyone
Suggest edit
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
Emoji Reply
Enable
Import from Dropbox Google Drive Gist Clipboard
   owned this note    owned this note      
Published Linked with GitHub
Subscribed
  • Any changes
    Be notified of any changes
  • Mention me
    Be notified of mention me
  • Unsubscribe
Subscribe
### Codegate CTF 2024 qualifiers A couple of weeks ago, I played the preliminaries for Codegate CTF 2024 with about:blankets. We managed to qualify for the finals in Seoul! I was able to join for most of the 24 hours and it was really nice to come back to the CTF world after three/four really busy months. # crypto - Cogechan_Dating_Game *Challenge* * Author: BaaaaaaaaaaaaaaaaaaaaaaarkingDog * Solves: 15 * Points: 306 > Love Wins All The challenge consists of multiple files. The most important ones are `server.py` and `load_and_save.py`. The first one handles the main game and allows you to act in the game. The second one, instead, contains all the main checks to load and save the game. ````python #server.py def go(sock): sock.settimeout(60) # No response for 60s then connection will be closed. # trying to load a save file based on ID, PW first ID_len = int.from_bytes(sock.recv(2), 'little') ID = sock.recv(ID_len).decode() PW_len = int.from_bytes(sock.recv(2), 'little') PW = sock.recv(PW_len).decode() status, character = load(ID, PW) sock.send(status.to_bytes(1, 'little')) if status == load_and_save.LOAD_SUCCESS: sock.send(len(character.nickname).to_bytes(2, 'little') + character.nickname.encode()) sock.send(character.day.to_bytes(4, 'little')) sock.send(character.stamina.to_bytes(4, 'little')) sock.send(character.intelligence.to_bytes(4, 'little')) sock.send(character.friendship.to_bytes(4, 'little')) if status != load_and_save.LOAD_SUCCESS: nickname_len = int.from_bytes(sock.recv(2), 'little') character.nickname = sock.recv(nickname_len).decode('utf-8', 'ignore') character.stamina = 100 while True: com = int.from_bytes(sock.recv(1), 'little') if com == 0: # Meaning that connection is closed if DEBUG: print("connection closed") exit() elif com == EAT_COMMAND: rnd = random.randint(1, 4) sock.send(rnd.to_bytes(1, 'little')) character.stamina += rnd character.day += 1 elif com == PWN_COMMAND: rnd = random.randint(1, 4) sock.send(rnd.to_bytes(1, 'little')) if character.stamina >= 10: character.stamina -= 10 character.intelligence += rnd character.day += 1 elif com == SLEEP_COMMAND: rnd = random.randint(1, 4) sock.send(rnd.to_bytes(1, 'little')) character.stamina += rnd character.day += 1 pass elif com == DATE_COMMAND: rnd = random.randint(1, 4) sock.send(rnd.to_bytes(1, 'little')) if character.stamina >= 10 and character.intelligence.bit_length() >= character.friendship: character.stamina -= 10 character.friendship += 1 character.day += 1 if character.friendship == 34: flag = read_flag() sock.send(len(flag).to_bytes(2, 'little') + flag.encode()) elif com == SAVE_COMMAND: file_data_enc_len = int.from_bytes(sock.recv(2), 'little') file_data_enc = sock.recv(file_data_enc_len) tag = sock.recv(16) status = load_and_save.save_game(ID, PW, character, file_data_enc, tag) sock.send(status.to_bytes(1, 'little')) ```` ````python #load_and_save.py def decrypt_and_parse_save_data(key, nonce, save_data, tag): cipher = AES.new(key, AES.MODE_GCM, nonce) file_data = unpad(cipher.decrypt_and_verify(save_data, tag), 16) idx = 0 nickname_len = int.from_bytes(file_data[idx:idx+2], 'little') idx += 2 nickname = file_data[idx:idx+nickname_len].decode('utf-8', 'ignore') idx += nickname_len day = int.from_bytes(file_data[idx:idx+4], 'little') idx += 4 stamina = int.from_bytes(file_data[idx:idx+4], 'little') idx += 4 intelligence = int.from_bytes(file_data[idx:idx+4], 'little') idx += 4 friendship = int.from_bytes(file_data[idx:idx+2], 'little') character = Character.Character(nickname, day, stamina, intelligence, friendship) return character def id_pw_validity_check(ID, PW): if len(ID) < 20 or len(PW) < 20: return False if len(set(ID)) < 20 or len(set(PW)) < 20: return False if ID == PW: return False return True def load_game(ID, PW): if not id_pw_validity_check(ID, PW): return LOAD_FAIL, None id_hash = hashlib.sha256(ID.encode()).digest() pw_hash = hashlib.sha256(PW.encode()).digest() nonce = id_hash[:12] file_name = id_hash[16:24].hex() key = pw_hash[:16] # read save file try: with open(f'save/{file_name}', 'rb') as f: raw_data = f.read() file_data_enc = raw_data[:-16] tag = raw_data[-16:] except Exception as e: return LOAD_FAIL, None # parse it! try: character = decrypt_and_parse_save_data(key, nonce, file_data_enc, tag) except Exception as e: # error during decryption print("LOAD!!", e) return LOAD_FAIL, None return LOAD_SUCCESS, character def save_game(ID, PW, character, save_data, tag): if not id_pw_validity_check(ID, PW): return SAVE_FAIL id_hash = hashlib.sha256(ID.encode()).digest() pw_hash = hashlib.sha256(PW.encode()).digest() nonce = id_hash[:12] file_name = id_hash[16:24].hex() key = pw_hash[:16] try: character_parse = decrypt_and_parse_save_data(key, nonce, save_data, tag) if character.day != character_parse.day or \ character.stamina != character_parse.stamina or \ character.intelligence != character_parse.intelligence or \ character.friendship != character_parse.friendship: return SAVE_FAIL if character.friendship >= 20: # Please do not save almost-cleared one return SAVE_FAIL except Exception as e: # error during decryption print("SAVE!!", e) return SAVE_FAIL try: with open(f'save/{file_name}', 'wb') as f: f.write(save_data) f.write(tag) except: return SAVE_FAIL, None return SAVE_SUCCESS ```` In this challenge, our objective is to play a dating game. The win condition is to ask for a date while the attributes of our character are: friendliness$=33$, stamina$>=10$ and intellect>=$2^{33}$. Reaching these attributes while playing honestly is not feasible, so we need to look at all the options that the server/game grants us in order to find a way to artificially increase our stats. While connected to the server, playing a game, we are allowed to save and reload the game in a particular way. ### First play and Save: * When we connect, we specify a couple of $(ID,PWD)$ that the server uses to craft a $nonce$ and a $file name$, from $sha256(IV)$, and a $key$, from $sha256(PWD)$. * Then, we can either play the game (by doing $Sleep$, $Pwn$, $Eat$ or $Date$) or request to $Save$. * If we $Save$, the server asks us to send a couple ($ctx$,$tag$) to store as a save file for the ongoing game. * To check if we have sent a valid save file, the server immediately takes the $n=nonce$ and $k=key$ from the current session and checks if: 1. The couple $(ctx,tag)$ is a valid ciphertext in AES-GCM with the key $k$ and the nonce $n$. 2. The attributes of the character contained in the save file, after decryption, match with the current attributes in the ongoing game. * If all the checks are successful, it stores our $(ctx,tag)$ in the file $file name$. ### Load * When connecting another time, the server asks us $ID$ and $PWD$ and derives again $n=nonce$, $file name$ and $k=key$. * If the server already contains a file $filename$, then it checks if the couple $(ctx,tag)$ that was stored in $filename$ is a valid ciphertext in AES-GCM with the key $k$ and the nonce $n$. * If the check is successful, it reads the attributes of the character from the decryption of $ctx$ and uses them for the ongoing game. ### Base attack idea To cheat the system, we want to connect to the server with two different passwords $PWD_1$ and $PWD_2$ (and, therefore, with two different keys $k_1$ and $k_2$). In this way, the server will perform the saving checks under the key $k_1$ and the loading checks under the key $k_2$. This allows us to craft a special ciphertext $ctx$ that, when decrypted under the key $k_1$, will be seen as the weak start-game character that we are using before the save but, when decrypted under the key $k_2$, will be seen as a strong end-game character that is ready to ask for the final $Date$. ### The "decrypt_and_parse_save_data" function To understand how to craft such a ciphertext, we first need to understand how the server "reads" a save file. After decrypting it with $n$ and $k$, the plaintext gets translated to attributes following the following scheme: * $nick_{len}$: The first two bytes are the length of the nickname. * $nick$: The following $nick_{len}$ bytes are the nickname. * $days$: The following 4 bytes are the days. * $stam$: The following 4 bytes are the stamina. * $inte$: The following 4 bytes are the intellect. * $fr$: The following 2 bytes are the friendship. A fresh new character, with no nickname, would have, as a save-file plaintext, the following 16-byte (1 AES block) representation: $$P_1 = 00|0000|stam|0000|00,$$ where $stam$ is the 4-bytes representation of the decimal $100$. An end-game character, instead, needs to have as a save-file plaintext the following representation: $$P_2=nick_{len}|nick|0000|STAM|INTE|FR,$$ where $STAM$ is the 4-bytes representation of the decimal $2^{32}-1$ (any number greater than 50 would have worked, but hey let's boost these stats!), $INTE$ is the 4-bytes representation of the decimal $2^{32}-1$ (this time, we need this value because we need to reach $2^{32}$ to have a successful $Date$) and $FR$ is the 2-bytes representation of the decimal $33$ (again, we need this exact value for the $Date$). ### "Ambiguous" ciphertext: how to craft it and how to find the keys This parsing is really important, because it allows us to encrypt two different sets of attributes in two different positions of the ciphertext just by modifying the value $nick_{len}$. In particular, we want to focus on a three-blocks ciphertext. The idea is to have a first block that contains the attributes that we need to pass the saving check and a third block that contains the attributes that we want after the load. (The second block will be a magical tool that we will be using later.) First of all, we fix a $ID$. Any $ID$. We just need it for choosing the same file $file name$ during different connections. We won't need to act on the $n=nonce$ of the AES_GCM in the following attack, so we just fix an $ID$ and we always use it when communicating with the server. Now, we fix a key $k_1$ by just fixing any password $PWD_1$. Again, any $PWD_1$ is okay. We compute the first block $ctx_1$ of the ciphertext as the encryption of $P_1$ under the key $k_1$ and the nonce $n$. This allows us to pass the check (2.) during the save. Finding the key $k_2$ will require a little bit more effort. To trick the server, for the loading part, we need the same first ciphertext block $ctx_1$ to decrypt (under key $k_2$) to a plaintext that starts with the 2-bytes representation of the decimal 30. In this way, when reading the save file, the server will consider the remaining first block (14 bytes) and all the second block (16 bytes) as part of the nickname and will ignore them. After this, it will read the attributes from the decryption of the third block (14 bytes). This means that we can fix the third block of the ciphertext as $ctx_3 \leftarrow Enc(k_2,P_3)$, with $$ P_3= 0000|STAM|INTE|FR|PD, $$where $STAM$, $INTE$, $FR$ are as in $P_2$ and $PD$ is to get a valid padding for the block, therefore the 2-byte representation of the hex $02$ $02$. An additional requirement for this ciphertext $ctx=(ctx_1|ctx_2|ctx_3)$ to be valid is that $Dec(k_1,ctx)$ satisfies the padding. So we also need to specify, for example, that the last byte of this decryption needs to be the byte representation of the decimal $1$. Finding a key $k_2$ that satisfies both of these requirements can be done by just sampling values for $PWD_2$ again and again, and checking that the first two bytes of $Dec(k_2,ctx_1)==30_{10}$ and that the last byte of $Dec(k_1,ctx_3)==1_{10}$. This happens with probability around $2^{-24}$, we can brute force this. ````python aes_ctr_K1 = AES.new(key1, AES.MODE_CTR, nonce=nonce, initial_value=int(2)) ptx1=bytes.fromhex("00 00") + bytes.fromhex("00 00 00 00") + int(100).to_bytes(4, "little") + bytes.fromhex("00 00 00 00") + bytes.fromhex("00 00") # name length + days + STAMINA!!! + int + friendship ctx1=aes_ctr_K1.encrypt(ptx1) ptx3= bytes.fromhex("00 00 00 00") + bytes.fromhex("FF FF FF FF")+ bytes.fromhex("FF FF FF FF") + int(33).to_bytes(2, "little") + bytes.fromhex("02 02") #days + stamina + INT + friendship + PAD # NEED CHARACTER NAME LENGTH TO DECRYPT TO 2*AES.BLOCKLEN-2 alphabet="qu4DR0PhIWETYlKJGFSZXCVBNMmnbvcx" for PW2 in tqdm(itertools.combinations(alphabet,20)): # PW2 = "pHiquRWEKGFSXCBNMbvc" PW2 = ''.join(PW2) pw_hash = hashlib.sha256(PW2.encode()).digest() key2 = pw_hash[:16] aes_ctr_K1 = AES.new(key1, AES.MODE_CTR, nonce=nonce, initial_value=int(2+2)) aes_ctr_K2 = AES.new(key2, AES.MODE_CTR, nonce=nonce, initial_value=int(2)) ptx1_tmp = aes_ctr_K2.decrypt(ctx1) aes_ctr_K2 = AES.new(key2, AES.MODE_CTR, nonce=nonce, initial_value=int(2+2)) ctx3_tmp = aes_ctr_K2.encrypt(ptx3) if ptx1_tmp[:2]==int(30).to_bytes(2, "little") and aes_ctr_K1.decrypt(ctx3_tmp)[-1] == 1: print(f"{PW2 = }") # break print("DONE") print(f"{key2.hex() = }") ```` ### THE TAG "We have the ciphertext and we have the keys! What else do we need?". Unfortunately, the server also performs a verification check for the validity of the ciphertext by using the tag. This happens both when we save and when we load, so we need the same tag to verify on the same ciphertext in both schemes! (AES.GCM with key $k_1$ and AES.GCM with key $k_2$) The tag$(k=key,n=nonce,ctx)$ is computed in the following way: * Consider all the 16-bytes elements in the scheme (i.e. $ctx_1$,$ctx_2$,$ctx_3$) as elements of the Galois Field $GF(2^{128})$ (with representative polynimal $f = 1 + a + a^2 + a^7 + a^{128}$). * Compute $H_k$ as $AES_k(0_{B})$. [$0_B$ is the 16-byte representation of $0$] * Compute $LEFT_k$ as $AES_k(n|0001)$. [The argument is the content of the counter in GCM before the first encryption] * Compute $LEN$ as the element of $GF(2^{128})$ corresponding to the block: 8-byte representation of $0$ | 8-byte representation of $8\cdot3\cdot16$. * The tag is equal to $$(H_k\cdot(H_k\cdot(H_k\cdot ctx_1+ctx_2)+ctx_3)+LEN)+LEFT_k$$ ```python def compute_tag(ctx,key,nonce): IV = nonce + long_to_bytes(1,4) EK=AES.new(key,AES.MODE_ECB) LEFT = int_to_GF128(int.from_bytes(EK.encrypt(IV), "big")) H = int_to_GF128(int.from_bytes(EK.encrypt(bytes(16)), 'big')) l=len(ctx)//16 t=int_to_GF128(int(0)) for i in range(l): t+=int_to_GF128(int.from_bytes(ctx[16*i:16*(i+1)], "big")) t*=H LEN= ((8 * 0) << 64) | (8 * l * 16) t += int_to_GF128(LEN) t *= H t += LEFT return GF128_to_int(t).to_bytes(16,"big") ``` Since we have already fixed $ctx_1$, $ctx_3$, $k_1$ and $k_2$, the only remaining variable is $ctx_2$. This means that we can impose equality between the two tags of the two schemes. $$ tag(k_1,n,ctx_1|ctx_2|ctx_3)=tag(k_2,n,ctx_1|ctx_2|ctx_3) $$ $$ tag_{k_1}(ctx_2)=tag_{k_2}(ctx_2) $$ After expanding the computations, we obtain that $$ ctx_2 = [ctx_1 \cdot (H_{k_1}^4 + H_{k_2}^4) + ctx_3 \cdot (H_{k_1}^2 +H_{k_2}^2) + $$ $$+ LEN \cdot (H_{k_1} + H_{k_2}) + LEFT_{k_1} + LEFT_{k_2}]/(H_{k_1}^3 + H_{k_2}^3)$$ ```python R.<a> = PolynomialRing(GF(2)) f = 1 + a + a^2 + a^7 + a^128 F128.<b> = GF(2).extension(f) z = int_to_GF128(3) IV1 = nonce + long_to_bytes( 1 ,4) EK1 = AES.new(key1, AES.MODE_ECB) LEFT1 = EK1.encrypt(IV1) H1 = EK1.encrypt( bytes(16) ) EK2 = AES.new(key2, AES.MODE_ECB) LEFT2 = EK2.encrypt(IV1) H2 = EK2.encrypt( bytes(16) ) endianness = "big" H1 = int_to_GF128(int.from_bytes(H1, endianness)) LEFT1 = int_to_GF128(int.from_bytes(LEFT1, endianness)) H2 = int_to_GF128(int.from_bytes(H2, endianness)) LEFT2 = int_to_GF128(int.from_bytes(LEFT2, endianness)) LEN = int_to_GF128(int.from_bytes(bytes(8) + int(16*8*3).to_bytes(8, "big"))) ctx1_num = int_to_GF128(int.from_bytes(ctx1, endianness)) ctx3_num = int_to_GF128(int.from_bytes(ctx3, endianness)) ctx2_num = (ctx1_num*(H1**4 + H2**4) + ctx3_num*(H1**2 + H2**2) + LEN*(H1 + H2) + LEFT1 + LEFT2)/(H1**3 + H2**3) ctx2 = GF128_to_int(ctx2_num).to_bytes(16, "big") final_ctx = ctx1 + ctx2 + ctx3 ``` ### Recap The attack works as follows: * We connect to the server for a new game with credentials $(ID,PWD_1)$. * We immediately ask to save the game and send $(ctx,tag)$ to the server. * The server here performs two checks 1. Verifies $tag$ with $ctx$ for the scheme AES.GCM with key $k_1$. This check is successful because we computed the tag honestly. 2. Decrypts $ctx$ with $k_1$ and checks if the parameters are the same as the ongoing game. This is confirmed because the parsing function only reads the decryption of the first block, which is exactly the desired $P_1$. * The server saves $(ctx,tag)$. We disconnect. * We now connect to the server with credentials $(ID,PWD_2)$. * The server verifies $tag$ with $ctx$ for the scheme AES.GCM with key $k_2$. This check is successful because of how we crafted $ctx_2$. * The server reads the character attributes from $Dec(k_2,ctx)$. From this it skips the first two blocks as the nickname and considers the third block, which is exactly equal to $P_3$. * This game is almost won! We just need to perform a successful $Pwn$ and a $Date$ to capture the FLAG! We can easily do them honestly from the client. ### Post Scriptum After the end of the challenge, I discovered that this attack (in a slightly different version) was already known from a [2019 Crypto paper](https://eprint.iacr.org/2019/016). The Salamander attack on AES-GCM is the technique we used when crafting the ambiguous ciphertext.

Import from clipboard

Paste your markdown or webpage here...

Advanced permission required

Your current role can only read. Ask the system administrator to acquire write and comment permission.

This team is disabled

Sorry, this team is disabled. You can't edit this note.

This note is locked

Sorry, only owner can edit this note.

Reach the limit

Sorry, you've reached the max length this note can be.
Please reduce the content or divide it to more notes, thank you!

Import from Gist

Import from Snippet

or

Export to Snippet

Are you sure?

Do you really want to delete this note?
All users will lose their connection.

Create a note from template

Create a note from template

Oops...
This template has been removed or transferred.
Upgrade
All
  • All
  • Team
No template.

Create a template

Upgrade

Delete template

Do you really want to delete this template?
Turn this template into a regular note and keep its content, versions, and comments.

This page need refresh

You have an incompatible client version.
Refresh to update.
New version available!
See releases notes here
Refresh to enjoy new features.
Your user state has changed.
Refresh to load new user state.

Sign in

Forgot password

or

By clicking below, you agree to our terms of service.

Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
Wallet ( )
Connect another wallet

New to HackMD? Sign up

Help

  • English
  • 中文
  • Français
  • Deutsch
  • 日本語
  • Español
  • Català
  • Ελληνικά
  • Português
  • italiano
  • Türkçe
  • Русский
  • Nederlands
  • hrvatski jezik
  • język polski
  • Українська
  • हिन्दी
  • svenska
  • Esperanto
  • dansk

Documents

Help & Tutorial

How to use Book mode

Slide Example

API Docs

Edit in VSCode

Install browser extension

Contacts

Feedback

Discord

Send us email

Resources

Releases

Pricing

Blog

Policy

Terms

Privacy

Cheatsheet

Syntax Example Reference
# Header Header 基本排版
- Unordered List
  • Unordered List
1. Ordered List
  1. Ordered List
- [ ] Todo List
  • Todo List
> Blockquote
Blockquote
**Bold font** Bold font
*Italics font* Italics font
~~Strikethrough~~ Strikethrough
19^th^ 19th
H~2~O H2O
++Inserted text++ Inserted text
==Marked text== Marked text
[link text](https:// "title") Link
![image alt](https:// "title") Image
`Code` Code 在筆記中貼入程式碼
```javascript
var i = 0;
```
var i = 0;
:smile: :smile: Emoji list
{%youtube youtube_id %} Externals
$L^aT_eX$ LaTeX
:::info
This is a alert area.
:::

This is a alert area.

Versions and GitHub Sync
Get Full History Access

  • Edit version name
  • Delete

revision author avatar     named on  

More Less

Note content is identical to the latest version.
Compare
    Choose a version
    No search result
    Version not found
Sign in to link this note to GitHub
Learn more
This note is not linked with GitHub
 

Feedback

Submission failed, please try again

Thanks for your support.

On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

Please give us some advice and help us improve HackMD.

 

Thanks for your feedback

Remove version name

Do you want to remove this version name and description?

Transfer ownership

Transfer to
    Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

      Link with GitHub

      Please authorize HackMD on GitHub
      • Please sign in to GitHub and install the HackMD app on your GitHub repo.
      • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
      Learn more  Sign in to GitHub

      Push the note to GitHub Push to GitHub Pull a file from GitHub

        Authorize again
       

      Choose which file to push to

      Select repo
      Refresh Authorize more repos
      Select branch
      Select file
      Select branch
      Choose version(s) to push
      • Save a new version and push
      • Choose from existing versions
      Include title and tags
      Available push count

      Pull from GitHub

       
      File from GitHub
      File from HackMD

      GitHub Link Settings

      File linked

      Linked by
      File path
      Last synced branch
      Available push count

      Danger Zone

      Unlink
      You will no longer receive notification when GitHub file changes after unlink.

      Syncing

      Push failed

      Push successfully