Alpha version
Continuous bitcoin-inspired capture-the-flag competition.
A hackchain server was running in a cloud. Some hacker has installed the client for it:
$ npm install -g hackchain
Hacker has also requested a server info:
$ hc-client --info
{ lastBlock: '4e9c8600ca954bbfd88a1ca8715e5cf5a751a8e5945802912086e632bec85e1f',
nextBlockIn: 17,
nextCoinbaseIn: 85817,
'proof-of-work-complexity': 17 }
$ hc-client --leaderboard
===== Current top transactions =====
NOTE: It is important to use both hash and index to steal a transaction
[ { hash: '61d9faa5dc429c8eb4f00835f285b3a5f7022f8b557432a7af542d0b778bc14e',
index: 4,
value: '1500000000' },
... ]
"I wonder if I can capture some of these coins...", she thought.
The server is running bitcoin-like blockchain with the main difference that the
blocks are issued automatically every 5 minutes. One may request the latest
block hash either by using --info
argument of the hc-client
, or by running:
curl https://api.hackcha.in/
The whole point of this "continuous CTF competition" is to capture someone else's coins and protect them. It is very close to CTF competitions, but participants compete with each other instead of attacking some predefined software.
New coins are minted only once every 24 hours, so hackers are encouraged to
steal coins from each other. Current leaderboard (list of unspent coins) can be
found by using --leaderboard
argument of hc-client
, or by running:
curl https://api.hackcha.in/leaderboard
Being very similar in structure to bitcoin blockchain, hackchain provides an opportunity to learn about bitcoin internals, and most importantly have some fun!
While recommended to read in order, one may skip all sections except Capturing, where the process of capturing (stealing) coins is described in detail.
Feel free to join #hackchain IRC channel on freenode server to discuss things with other hackers.
Blocks have a link to the parent block (or a genesis block with the hash
0000000....
, all zeroes), and a list of at least one transaction (TX). First
TX in a block is called a coinbase and (in contrast to bitcoin) can be spent
by anyone, unless its output value is 0
.
Block may be inspected using --block <hash>
argument of the hc-client
:
<Block: b1e6483...
parent: 6017999...
txs: [ <Coinbase: 735c742...
v=1
inputs: [ <Input hash: 6017999...
index: -1
script: <Script len: 0
opcodes: [
]>> ]
outputs: [ <Output value: 2500000000
script: <Script len: 2
opcodes: [
irq success]>> ]> ]>
Raw hex data may be fetched by running:
curl https://api.hackcha.in/v1/block/<hash>
Spec for the binary encodings of all structures is available below.
Every transaction has at least one input and output.
Each input has a hash
of input TX, index
of output in that TX, and script
to capture it. (See "Capturing" section below).
Each output has a value
number of coins, and script
to prevent capturing
these coins.
TX cannot spend more coins than it gets from the inputs, but it can spend less.
The difference is called fee
and is added to the coinbase of the block where
TX is stored.
TX may be inspected using --tx <hash>
argument of the
hc-client
:
<TX: 10319ac...
v=1
inputs: [ <Input hash: 699eb0e...
index: 0
script: <Script len: 8
opcodes: [
lui r1 64
addi r1 r1 0
sw r0 r1 8
irq success]>> ]
outputs: [ <Output value: 2500000000
script: <Script len: 4
opcodes: [
irq yield
irq success]>> ]>
Raw hex data may be inspected by running:
curl https://api.hackcha.in/v1/tx/<hash>
In order to capture someone's coin (or coinbase), the attacker must implement an input script that will be able to defeat the output script of the TX.
Scripts are written is RiSC-16 (Ridiculously Simple Computer) instruction set and are running in a shared memory space of 0x10000 16-bit words. Yes, you read it right, the code is living in the same space, and the scripts are allowed to modify each other.
The process:
0x0
offset of the
memoryoutput
script is loaded to 0x1000
offset of the memory and executed
until irq yield
/irq success
, or until it executes more than
16 * 1024
opcodes (if so - coin is captured, and further steps are
skipped)irq success
was executed in step 2 - coin is captured and the process
ends. If irc yield
was executed - proceed to step 4input
script is loaded to 0x2000
offset of the memoryoutput
is executedirq ...
was executed - the process ends with captured coininput
is executedirq ...
was executed - steps 7-8 are replaced by no op
output
after step 4 exceeds 32 * 1024
:
process terminates, and coin is not capturedQuoting RiSC-16, there are 8 different 16-bit registers (r0
, ..., r7
),
and 10 different instructions:
add rA, rB, rC
- Add contents of regB with regC, store result in regAaddi rA, rB, imm
- Add contents of regB with imm, store result in regAnand rA, rB, rC
- Nand contents of regB with regC, store results in regAlui rA, imm
- Place the 10 bits of the 16-bit imm into the 10 bits
of regA, setting the bottom 6 bits of regA to zerosw rA, rB, imm
- Store value from regA into memory. Memory address is formed
by adding imm with contents of regBlw rA, rB, imm
- Load value from memory into regA. Memory address is formed
by adding imm with contents of regBbeq rA, rB, imm
- If the contents of regA and regB are the same, branch to
the address PC+1+imm, where PC is the address of the beq instructionjalr rA, rB
- Branch to the address in regB. Store PC+1 into regA, where PC
is the address of the jalr instruction.They are encoded just as they are described in a paper.
Additional opcodes are added in hackchain:
irq success
- terminate thread (see capturing too), encoded
as 0xe001
word in big endianirq yield
- yield execution to input script (see
step 2 above), encoded as 0xe081
word in big endianNOTE: Reading value of r0
always returns 0
for your convenience!
Additional opcode-combos are available using the assembler in this repo:
jmp label-name
- generate short (within 64 opcodes) relative jump to the
specified labelfarjmp rA, label-name
- generate far absolute jump to the specified label.
NOTE: rA
register will be overwritten to store the absolute offsetbind label-name
- bind specified label to the current opcode offsetlea rA, label-name
- load label's absolute address into the rA
registercodeOffset <16-bit offset>
- change code offset. Absolutely needed when
using farjmp
in code that doesn't start at 0x0000
memory offsetmovi rA, <16-bit immediate>
- will generate two opcodes lui
nd addi
nop
- will generate add r0, r0, r0
data 0xabcd
- put the raw 16-bit word instead of an instructionExamples:
It is possible to generate and send TX using hc-client
. In order to do this,
a yaml-formatted tx-name.tx
file must be created:
version: 1
inputs:
- hash: 'a1dd41f1efd2498dd8126684fe164d22d6188046b1c71a7d5325c2158965237b'
index: 0
script:
- irq success
outputs:
- value: '2500000000'
script:
- irq yield
- irq success
(See examples/ for more various TX examples)
NOTE: script
arrays have string values. ,
or
separators may be used
between opcode and arguments, and between arguments. Arguments are either:
rN
- register input/output, where N
is a number from 0
to 7
N
- immediate value, where N
is a decimal integer
(either positive or negative)string
- anything that does not fit into one of two bullet points above.
Usually used in irq
opcodes (irq success
, irq yield
).Afterwards, one may execute:
$ hc-client --spend tx-name.tx
It will parse file, generate TX, confirm it, and send it to server. If the server will accept the TX, the confirmation will be printed. If the server will reject the TX, a (hopefully) meaningful error message will be printed.
NOTE: While TXs are accepted immediately, they are not available for spending
until the server will mint a new block. Please check output of
hc-client --info
to get the time until the next block.
While some scripts may be easy to follow, others may definitely require more detailed investigation. This is where internal debugger may come out handy:
$ hc-debug --tx examples/tx/debug.tx
Or alternatively, if you'd like to debug output script that it is not in the chain yet:
$ hc-debug examples/debugger/prog1.yaml
NOTE: yaml
file with the contents of both scripts and TX hash, must be
supplied to debugger. See debugger examples
All values are in Big Endian.
[ 32-bit number ] version
[ 32 bytes ] parent sha256 hash
[ 32-bit number ] TX count
... TXs
[ 32-bit number ] version
[ 32-bit number ] input count
[ 32-bit number ] output count
... inputs
... outputs
[ 32 bytes ] sha256 hash of input TX
[ 32-bit number ] input index
... script
[ 8 bytes ] big endian big number
... script
[ 32-bit number ] size of binary data below
... binary opcodes for RiSC-16
$ curl https://api.hackcha.in/help | jq .
{
"/": "information about server and last block",
"/help": "this message",
"/leaderboard": "list of currently unspent transactions",
"/v1/block/(hash)": "GET block data",
"/v1/tx/(hash)": "GET/POST transaction data",
"/v1/tx/(hash)/block": "GET the hash of transaction's block",
"/v1/tx/(hash)/(output index)/spentby": "GET the hash of spending tx"
}
If any bugs, please file an issue. We will make sure to figure it out!
This software is licensed under the MIT License.
Copyright Fedor Indutny, 2016.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.