plaidCTF 2014 - rendezvous (misc250)
This challenge was about establishing a connection to a hidden tor service which is rather picky in accepting connections. We were given the following description:
rendezvous
Misc (250 pts)
--------------
The Plague has a friend called Alice who has some secrets on a tor
service (http://6c4dm56aer6xn2h2.onion/). We think if we can talk to
her, we can learn some useful things about The Plague. Unfortunately
she will only rendezvous with "chandler" when he brings a cookie with
"beef" baked into it. Can you help us find her secret?
Getting Started
The first thing we did was of course trying to connect to the service.
Whether using a tor to web gateway as for example onion.to or a local tor instance, the result was the same: no connection could be established.
Using curl -v –socks5-hostname localhost:9050 http://6c4dm56aer6xn2h2.onion/
showed that curl didn’t even send the request, confirming that the problem is at the tor layer and not at the HTTP layer.
Thus getting a tor connection to the hidden service is actually part of the challenge.
So next, we tried to make sense of the information provided:
- There needs to be a “rendezvous” with “chandler”
- There needs to be a “cookie” with “beef”
As no one on the team knew about the specifics of the tor protocol some googling was necessary. This led us to the website describing the hidden service connection setup. The setup can be summarized in 3 steps:
- We query a database node to get a record listing so called “introduction points” (IPs) for the specific hidden service
- We send a message to one of the IPs requesting a connection, supplying a one-time secret, a “cookie”, and specifying a node as “rendezvous point” (RP). This RP is a node to which we have an established circuit.
- The hidden service connects to the RP and we have established a connection.
The relay list provided by vidalia, a graphical user interface for tor, listed a node called “chandler”, so we assumed this node is supposed to be the RP. We can get more information about that node at its tor network status page.
We also found the word cookie mentioned in the context of hidden services in the tor man page:
HidServAuth onion-address auth-cookie [service-name]
Client authorization for a hidden service...
Missing, though, was any option to specify a specific rendezvous point. Also the description of the HidServAuth option didn’t indicate any relation to a one-time secret. The next points to figure out thus were:
- How to force “chandler” to be used as RP?
- Is the HidServAuth cookie related to the introduction cookie, and if not which is meant?
- If HidServAuth is unrelated to “beef” cookie, how do we set the “beef” cookie?
Beefy Cookies
At this point we started looking at the tor source code.
The code that handles the client side for hidden services is mostly contained in or/rendclient.c
.
It is written in an asynchronous style where a lot of the functions are called implicitly from other parts of the code base.
This makes it difficult to understand the exact execution flow by just skimming over the code.
Luckily the source file in question contains enough comments and expressive identifier names to find the relevant parts.
The presence of REND_COOKIE_LEN
and REND_DESC_COOKIE_LEN
confirms that there are two separate types of cookies.
The first one, called rendezvous cookie, is generated randomly in rend_client_send_establish_rendezvous
.
The other one, called descriptor cookie, is the one specified in the tor configuration and is parsed in rend_parse_service_authorization
.
The descriptor cookie is already used when querying database nodes (the very first step), which is done in rend_client_refetch_v2_renddesc
.
Running tor with Log info stdout
and SafeLogging 0
in the configuration produces the following messages:
[info] directory_get_from_hs_dir(): Sending fetch request for v2 descriptor for service '6c4dm56aer6xn2h2' with descriptor ID 'uxms6o3qbmuut2sol3o2xm67wpmpqmy7', auth type 0, and descriptor cookie '[none]' to hidden service directory $A5FA0BD3E8EAF99CD16544A7CAAD288360FD6D12=Enigma at 62.30.125.41
... 4 seconds later ...
[info] rend_client_desc_trynow(): Rend desc is usable. Launching circuits.
It seems that this can only happen when the descriptor cookie (or the lack of one) is correct.
The random rendezvous cookie can be changed to a repetition of the two byte sequence BEEF
by replacing
if (crypto_rand(circ->rend_data->rend_cookie, REND_COOKIE_LEN) < 0) {
log_warn(LD_BUG, "Internal error: Couldn't produce random cookie.");
circuit_mark_for_close(TO_CIRCUIT(circ), END_CIRC_REASON_INTERNAL);
return -1;
}
in the function rend_client_send_establish_rendezvous
with this code:
for (i = 0; i < REND_COOKIE_LEN; i += 2) {
circ->rend_data->rend_cookie[i] = 0xBE;
circ->rend_data->rend_cookie[i + 1] = 0xEF;
}
The Rocky Road to Chandler
The cookie alone doesn’t get us anywhere though; we need to get “chandler” as rendezvous point. We don’t even know whether we got the cookie part right or not, as we cannot test it yet. The way a rendezvous point is chosen seems to be quite complicated. Tor tries to reuse existing circuit endpoints, can extend existing circuits, uses node selection routines used at other places and so on. This means there are many places that can be patched to hardcode “chandler” as rendezvous point. As it happens though, the code is quite fragile in that respect. We made several attempts that resulted in tor not bootstrapping anymore or otherwise hanging. To avoid this we searched for a less fragile place to patch instead of just overwriting whatever node was chosen with chandler.
We were finally successful when we patched compute_weighted_bandwidths
in or/routerlist.c
.
This function is used to rank candidates router nodes, possibly all over the place, not only for a rendezvous.
What is nice about this function is, that it gets a list of all routers and just assigns a number for each.
All pre- or postconditions and other implicit stuff is handled elsewhere.
The patch simply compares the fingerprint of the ranked node with the one of “chandler” and in the case of match assigns a very high weight and bandwidth:
...
if (weight < 0.0)
weight = 0.0;
/* begin of patched code */
static const char *chandler = "\xD5\x98\x18\x25\xAF\x8B\x1D\xEF\xC9\xD8\xF2\x44\x57\x29\xA1\x1B\x12\x58\x13\x84";
if (memcmp(node->identity, chandler, DIGEST_LEN) == 0) {
weight = 1e30;
this_bw = 1e30;
}
/* end of patched code */
bandwidths[node_sl_idx].dbl = weight*this_bw + 0.5;
...
When starting tor with this patch, we can see several circuits with “chandler” as last hop in the log:
...
[info] internal (high-uptime) circ (length 3, last hop chandler): $F6EC46933CE8D4FAD5CCDAA8B1C5A377685FC521(open) $18BE989663CF3351F73D33C672BB1C985E0EA5D0(open) chandler(open)
...
[info] internal (high-uptime) circ (length 3, last hop chandler): $709B5765D5D79F230AEAFD189F5C012F8B2F5C03(open) $8746507370F71934A4709E91BF811EC099CF93AD(open) chandler(open)
...
If the assumptions we made are correct, it should reuse those as rendezvous point. Let’s try again:
% curl -v --socks5-hostname localhost:9050 http://6c4dm56aer6xn2h2.onion/
--- tor log ---
...
[info] directory_get_from_hs_dir(): Sending fetch request for v2 descriptor for service '6c4dm56aer6xn2h2' with descriptor ID 'uxms6o3qbmuut2sol3o2xm67wpmpqmy7', auth type 0, and descriptor cookie '[none]' to hidden service directory $A5EF1C5EBB94221CDDF413CDFB9CDCC33C0BD2A7~halleck at 85.119.83.76
...
[info] circuit_launch_by_extend_info(): Cannibalizing circ 'chandler' for purpose 9 (Hidden service client: Establishing rendezvous point)
[info] rend_client_rendcirc_has_opened(): rendcirc is open
[info] rend_client_send_establish_rendezvous(): Sending an ESTABLISH_RENDEZVOUS cell
...
[info] rend_client_rendezvous_acked(): Got rendezvous ack. This circuit is now ready for rendezvous.
...
[info] rend_client_introcirc_has_opened(): introcirc is open
...
[info] rend_client_send_introduction(): Sending an INTRODUCE1 cell
...
[info] rend_client_introduction_acked(): Received ack. Telling rend circ...
...
[info] rend_client_receive_rendezvous(): Got RENDEZVOUS2 cell from hidden service.
...
--- curl output ---
* Connected to localhost (127.0.0.1) port 9050 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.36.0
> Host: 6c4dm56aer6xn2h2.onion
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Server: BaseHTTP/0.3 Python/2.7.3
< Content-type: text/html
<
* Closing connection 0
flag{why_you_so_damn_creepy}
And we are done.