blueprint-tsa 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +54 -0
- package/dist/commands/audit.d.ts +19 -0
- package/dist/commands/audit.js +296 -0
- package/dist/commands/bounce-check.d.ts +15 -0
- package/dist/commands/bounce-check.js +152 -0
- package/dist/commands/clean.d.ts +1 -0
- package/dist/commands/clean.js +20 -0
- package/dist/commands/drain-check.d.ts +32 -0
- package/dist/commands/drain-check.js +218 -0
- package/dist/commands/opcode-info.d.ts +23 -0
- package/dist/commands/opcode-info.js +176 -0
- package/dist/commands/owner-hijack-check.d.ts +20 -0
- package/dist/commands/owner-hijack-check.js +290 -0
- package/dist/commands/replay-attack-check.d.ts +20 -0
- package/dist/commands/replay-attack-check.js +149 -0
- package/dist/commands/reproduce.d.ts +3 -0
- package/dist/commands/reproduce.js +102 -0
- package/dist/common/analyzer-wrapper.d.ts +69 -0
- package/dist/common/analyzer-wrapper.js +198 -0
- package/dist/common/analyzer.d.ts +10 -0
- package/dist/common/analyzer.js +49 -0
- package/dist/common/build-utils.d.ts +3 -0
- package/dist/common/build-utils.js +68 -0
- package/dist/common/constants.d.ts +41 -0
- package/dist/common/constants.js +45 -0
- package/dist/common/draw.d.ts +7 -0
- package/dist/common/draw.js +33 -0
- package/dist/common/file-utils.d.ts +7 -0
- package/dist/common/file-utils.js +20 -0
- package/dist/common/format-utils.d.ts +13 -0
- package/dist/common/format-utils.js +30 -0
- package/dist/common/opcode-extractor.d.ts +7 -0
- package/dist/common/opcode-extractor.js +60 -0
- package/dist/common/paths.d.ts +19 -0
- package/dist/common/paths.js +139 -0
- package/dist/common/result-parsing.d.ts +4 -0
- package/dist/common/result-parsing.js +40 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +16 -0
- package/dist/install/architecture.d.ts +5 -0
- package/dist/install/architecture.js +51 -0
- package/dist/install/downloading.d.ts +1 -0
- package/dist/install/downloading.js +44 -0
- package/dist/install/java.d.ts +1 -0
- package/dist/install/java.js +89 -0
- package/dist/install/postinstall.d.ts +1 -0
- package/dist/install/postinstall.js +12 -0
- package/dist/install/tsa-jar.d.ts +1 -0
- package/dist/install/tsa-jar.js +23 -0
- package/dist/install/unzip.d.ts +1 -0
- package/dist/install/unzip.js +14 -0
- package/dist/reproduce/build-config.d.ts +3 -0
- package/dist/reproduce/build-config.js +24 -0
- package/dist/reproduce/concrete-analysis.d.ts +15 -0
- package/dist/reproduce/concrete-analysis.js +21 -0
- package/dist/reproduce/network.d.ts +19 -0
- package/dist/reproduce/network.js +70 -0
- package/dist/reproduce/reproduce-config.d.ts +30 -0
- package/dist/reproduce/reproduce-config.js +59 -0
- package/dist/reproduce/utils.d.ts +4 -0
- package/dist/reproduce/utils.js +34 -0
- package/dist/tsa.d.ts +2 -0
- package/dist/tsa.js +22 -0
- package/package.json +45 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ownerHijackCheckConcrete = exports.runOwnerHijackCheckAnalysis = exports.configureOwnerHijackCommand = void 0;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const core_1 = require("@ton/core");
|
|
6
|
+
const analyzer_wrapper_js_1 = require("../common/analyzer-wrapper.js");
|
|
7
|
+
const build_config_js_1 = require("../reproduce/build-config.js");
|
|
8
|
+
const constants_js_1 = require("../common/constants.js");
|
|
9
|
+
const build_utils_js_1 = require("../common/build-utils.js");
|
|
10
|
+
const utils_js_1 = require("../reproduce/utils.js");
|
|
11
|
+
const paths_js_1 = require("../common/paths.js");
|
|
12
|
+
const opcode_extractor_js_1 = require("../common/opcode-extractor.js");
|
|
13
|
+
const ONE_MINUTE_SECONDS = 60;
|
|
14
|
+
const configureOwnerHijackCommand = (context) => ({
|
|
15
|
+
command: constants_js_1.OWNER_HIJACK_CHECK_ID,
|
|
16
|
+
description: "Analyze contract for the possibility of owner hijack",
|
|
17
|
+
builder: (yargs) => yargs
|
|
18
|
+
.option("timeout", {
|
|
19
|
+
alias: "t",
|
|
20
|
+
type: "number",
|
|
21
|
+
description: "Analysis timeout in seconds",
|
|
22
|
+
})
|
|
23
|
+
.option("contract", {
|
|
24
|
+
alias: "c",
|
|
25
|
+
type: "string",
|
|
26
|
+
description: "Contract name",
|
|
27
|
+
demandOption: true,
|
|
28
|
+
})
|
|
29
|
+
.option("method-name", {
|
|
30
|
+
alias: "m",
|
|
31
|
+
type: "string",
|
|
32
|
+
description: "The method name of get_owner getter",
|
|
33
|
+
demandOption: true,
|
|
34
|
+
})
|
|
35
|
+
.option("verbose", {
|
|
36
|
+
alias: "v",
|
|
37
|
+
type: "boolean",
|
|
38
|
+
description: "Use debug output in TSA log",
|
|
39
|
+
})
|
|
40
|
+
.option("disable-opcode-extraction", {
|
|
41
|
+
type: "boolean",
|
|
42
|
+
description: "Disable opcode extraction. This affects path selection strategy and default timeout.",
|
|
43
|
+
}),
|
|
44
|
+
handler: async (argv) => await ownerHijackCommand(context, argv),
|
|
45
|
+
});
|
|
46
|
+
exports.configureOwnerHijackCommand = configureOwnerHijackCommand;
|
|
47
|
+
const extractOptions = (ui, parsedArgs) => {
|
|
48
|
+
const contract = parsedArgs.contract;
|
|
49
|
+
if (typeof contract !== "string") {
|
|
50
|
+
throw new Error("Contract name or path is required");
|
|
51
|
+
}
|
|
52
|
+
const timeout = parsedArgs.timeout ?? null;
|
|
53
|
+
const methodid = (0, core_1.getMethodId)(parsedArgs.methodName);
|
|
54
|
+
if (!Number.isInteger(methodid)) {
|
|
55
|
+
throw new Error("MethodId is not an integer");
|
|
56
|
+
}
|
|
57
|
+
const methodId = BigInt(methodid);
|
|
58
|
+
const options = {
|
|
59
|
+
contract,
|
|
60
|
+
timeout,
|
|
61
|
+
methodId,
|
|
62
|
+
};
|
|
63
|
+
const properties = [
|
|
64
|
+
{ key: "Contract", value: options.contract },
|
|
65
|
+
{ key: "Mode", value: "TON owner hijack" },
|
|
66
|
+
{
|
|
67
|
+
key: "Options",
|
|
68
|
+
separator: true,
|
|
69
|
+
children: [
|
|
70
|
+
{
|
|
71
|
+
key: "Timeout",
|
|
72
|
+
value: options.timeout !== null ? `${options.timeout} seconds` : "not set",
|
|
73
|
+
},
|
|
74
|
+
{ key: "Method id", value: options.methodId.toString() },
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
return { options, properties };
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Runs owner hijack check analysis and returns the analyzer wrapper
|
|
82
|
+
* @param contractName - Name of the contract
|
|
83
|
+
* @param contractPath - Path to the compiled contract
|
|
84
|
+
* @param ui - UI provider
|
|
85
|
+
* @param timeout - Analysis timeout in seconds
|
|
86
|
+
* @param methodId - Method ID of the owner getter
|
|
87
|
+
* @param opcodes - List of opcodes to analyze
|
|
88
|
+
* @param verbose - Enable verbose output
|
|
89
|
+
* @returns AnalyzerWrapper instance
|
|
90
|
+
*/
|
|
91
|
+
const runOwnerHijackCheckAnalysis = async (contractName, contractPath, ui, timeout, methodId, opcodes, verbose = false, completionMessage = "Analysis complete.") => {
|
|
92
|
+
const properties = [
|
|
93
|
+
{ key: "Contract", value: contractName },
|
|
94
|
+
{ key: "Mode", value: "TON owner hijack" },
|
|
95
|
+
{
|
|
96
|
+
key: "Options",
|
|
97
|
+
separator: true,
|
|
98
|
+
children: [
|
|
99
|
+
{
|
|
100
|
+
key: "Timeout",
|
|
101
|
+
value: timeout !== null ? `${timeout} seconds` : "not set",
|
|
102
|
+
},
|
|
103
|
+
{ key: "Method id", value: methodId.toString() },
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
const checkerPath = (0, paths_js_1.getCheckerPath)(constants_js_1.OWNER_HIJACK_CHECK_SYMBOLIC_FILENAME);
|
|
108
|
+
const checkerCell = (0, core_1.beginCell)().storeUint(methodId, 32).endCell();
|
|
109
|
+
const analyzer = new analyzer_wrapper_js_1.AnalyzerWrapper({
|
|
110
|
+
ui,
|
|
111
|
+
checkerPath,
|
|
112
|
+
checkerCell,
|
|
113
|
+
properties,
|
|
114
|
+
codePath: contractPath,
|
|
115
|
+
});
|
|
116
|
+
const reportDir = (0, paths_js_1.getReportDirectory)(analyzer.id);
|
|
117
|
+
const sarifPath = (0, paths_js_1.getSarifReportPath)(analyzer.id);
|
|
118
|
+
await analyzer.run(constants_js_1.OWNER_HIJACK_CHECK_SYMBOLIC_FILENAME, (wrapper) => [
|
|
119
|
+
"custom-checker-compiled",
|
|
120
|
+
"--checker",
|
|
121
|
+
wrapper.getTempBocPath(),
|
|
122
|
+
"--contract",
|
|
123
|
+
contractPath,
|
|
124
|
+
"--stop-when-exit-codes-found",
|
|
125
|
+
constants_js_1.ERROR_EXIT_CODE.toString(),
|
|
126
|
+
"--checker-data",
|
|
127
|
+
wrapper.getTempCheckerCellPath(),
|
|
128
|
+
"--output",
|
|
129
|
+
sarifPath,
|
|
130
|
+
"--exported-inputs",
|
|
131
|
+
reportDir,
|
|
132
|
+
...(verbose ? ["-v"] : []),
|
|
133
|
+
...opcodes.flatMap((opcode) => ["--opcode", opcode.toString()]),
|
|
134
|
+
"--disable-out-message-analysis",
|
|
135
|
+
...(timeout != null ? ["--timeout", timeout.toString()] : []),
|
|
136
|
+
], completionMessage);
|
|
137
|
+
// Write reproduction config if vulnerability is found
|
|
138
|
+
const vulnerability = analyzer.getVulnerability();
|
|
139
|
+
if (vulnerability) {
|
|
140
|
+
(0, build_config_js_1.writeReproduceConfig)(vulnerability, constants_js_1.OWNER_HIJACK_CHECK_ID, timeout, analyzer.id, {
|
|
141
|
+
kind: "owner-hijack-check",
|
|
142
|
+
methodId: methodId.toString(),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return analyzer;
|
|
146
|
+
};
|
|
147
|
+
exports.runOwnerHijackCheckAnalysis = runOwnerHijackCheckAnalysis;
|
|
148
|
+
const ownerHijackCommand = async (context, parsedArgs) => {
|
|
149
|
+
const { ui } = context;
|
|
150
|
+
await (0, build_utils_js_1.buildContracts)(ui);
|
|
151
|
+
const { options, properties } = extractOptions(ui, parsedArgs);
|
|
152
|
+
const contractPath = (0, paths_js_1.findCompiledContract)(options.contract);
|
|
153
|
+
if (!(0, fs_1.existsSync)(contractPath)) {
|
|
154
|
+
ui.write(`\n${constants_js_1.Sym.ERR} Contract ${options.contract} not found`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
let opcodes = [];
|
|
158
|
+
if (!parsedArgs["disable-opcode-extraction"]) {
|
|
159
|
+
opcodes = await (0, opcode_extractor_js_1.extractOpcodes)({
|
|
160
|
+
ui,
|
|
161
|
+
codePath: contractPath,
|
|
162
|
+
contractName: options.contract,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
// If timeout wasn't provided, set it to 1 minute * (number_of_opcodes + 1)
|
|
166
|
+
if (options.timeout === null && opcodes.length > 0) {
|
|
167
|
+
options.timeout = ONE_MINUTE_SECONDS * (opcodes.length + 1);
|
|
168
|
+
ui.write("");
|
|
169
|
+
ui.write("The timeout was calculated automatically based on the number of opcodes.");
|
|
170
|
+
}
|
|
171
|
+
// Update properties to reflect the calculated timeout
|
|
172
|
+
const timeoutProperty = properties[2].children?.find((p) => p.key === "Timeout");
|
|
173
|
+
if (timeoutProperty) {
|
|
174
|
+
timeoutProperty.value =
|
|
175
|
+
options.timeout !== null ? `${options.timeout} seconds` : "not set";
|
|
176
|
+
}
|
|
177
|
+
const analyzer = await (0, exports.runOwnerHijackCheckAnalysis)(options.contract, contractPath, ui, options.timeout, options.methodId, opcodes, parsedArgs.verbose);
|
|
178
|
+
const vulnerability = analyzer.getVulnerability();
|
|
179
|
+
analyzer.reportVulnerability(vulnerability, constants_js_1.OWNER_HIJACK_DESCRIPTION_URL);
|
|
180
|
+
(0, utils_js_1.printCleanupInstructions)(ui);
|
|
181
|
+
if (vulnerability != null) {
|
|
182
|
+
(0, utils_js_1.printReproductionInstructions)(ui, analyzer.id);
|
|
183
|
+
process.exit(2);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
const readNanotons = async (request, ui) => {
|
|
187
|
+
while (true) {
|
|
188
|
+
const userInput = await ui.input(request);
|
|
189
|
+
try {
|
|
190
|
+
return (0, core_1.toNano)(userInput);
|
|
191
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
192
|
+
}
|
|
193
|
+
catch (e) {
|
|
194
|
+
ui.write(`Your input (${userInput}) was of not correct nanoton format. Please try again.`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
const ownerHijackCheckConcrete = async (config, concreteCheckerOptions, completionMessage = "Analysis complete.") => {
|
|
199
|
+
const { ui } = config;
|
|
200
|
+
if (!(0, fs_1.existsSync)(config.codePath)) {
|
|
201
|
+
ui.write(`\n${constants_js_1.Sym.ERR} Code at ${config.codePath} not found`);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
const timeout = config.timeout;
|
|
205
|
+
const properties = [
|
|
206
|
+
{ key: "Contract", value: config.contractAddress.toRawString() },
|
|
207
|
+
{ key: "Mode", value: "TON owner hijack reproduction" },
|
|
208
|
+
{
|
|
209
|
+
key: "Options",
|
|
210
|
+
separator: true,
|
|
211
|
+
children: [
|
|
212
|
+
{
|
|
213
|
+
key: "Timeout",
|
|
214
|
+
value: timeout !== null ? `${timeout} seconds` : "not set",
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
key: "Method id",
|
|
218
|
+
value: timeout !== null ? `${timeout} seconds` : "not set",
|
|
219
|
+
},
|
|
220
|
+
{ key: "Sender", value: config.senderAddress.toRawString() },
|
|
221
|
+
],
|
|
222
|
+
},
|
|
223
|
+
];
|
|
224
|
+
const maxTons = await readNanotons("Enter maximum amount of TONs for reproduction message:", ui);
|
|
225
|
+
const checkerPath = (0, paths_js_1.getCheckerPath)(constants_js_1.OWNER_HIJACK_CHECK_CONCRETE_FILENAME);
|
|
226
|
+
const getMethodId = () => {
|
|
227
|
+
const stringedMethodId = concreteCheckerOptions.methodId;
|
|
228
|
+
try {
|
|
229
|
+
return BigInt(stringedMethodId);
|
|
230
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
throw new Error(`Invalid BigInt string format (${stringedMethodId}) stored as methodId`);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
const methodId = getMethodId();
|
|
237
|
+
console.log(`methodId=${methodId} maxTons=${maxTons} address=${config.senderAddress.toRawString()}`);
|
|
238
|
+
const checkerCell = (0, core_1.beginCell)()
|
|
239
|
+
.storeInt(methodId, 32)
|
|
240
|
+
.storeAddress(config.senderAddress)
|
|
241
|
+
.storeCoins(maxTons)
|
|
242
|
+
.endCell();
|
|
243
|
+
const analyzer = new analyzer_wrapper_js_1.AnalyzerWrapper({
|
|
244
|
+
ui,
|
|
245
|
+
checkerPath,
|
|
246
|
+
checkerCell,
|
|
247
|
+
properties,
|
|
248
|
+
codePath: config.codePath,
|
|
249
|
+
});
|
|
250
|
+
await analyzer.run(constants_js_1.OWNER_HIJACK_CHECK_CONCRETE_FILENAME, (wrapper) => [
|
|
251
|
+
"custom-checker-compiled",
|
|
252
|
+
"--checker",
|
|
253
|
+
wrapper.getTempBocPath(),
|
|
254
|
+
"--contract",
|
|
255
|
+
config.codePath,
|
|
256
|
+
"--data",
|
|
257
|
+
config.dataPath,
|
|
258
|
+
"--balance",
|
|
259
|
+
config.balance.toString(),
|
|
260
|
+
"--address",
|
|
261
|
+
config.contractAddress.toRawString(),
|
|
262
|
+
"--stop-when-exit-codes-found",
|
|
263
|
+
constants_js_1.ERROR_EXIT_CODE.toString(),
|
|
264
|
+
"--checker-data",
|
|
265
|
+
wrapper.getTempCheckerCellPath(),
|
|
266
|
+
"--output",
|
|
267
|
+
(0, paths_js_1.getSarifReportPath)(wrapper.id),
|
|
268
|
+
"--disable-out-message-analysis",
|
|
269
|
+
"--exported-inputs",
|
|
270
|
+
(0, paths_js_1.getReportDirectory)(wrapper.id),
|
|
271
|
+
...(config.timeout != null
|
|
272
|
+
? ["--timeout", config.timeout.toString()]
|
|
273
|
+
: []),
|
|
274
|
+
"-v",
|
|
275
|
+
], completionMessage);
|
|
276
|
+
const vulnerability = analyzer.getVulnerability();
|
|
277
|
+
if (vulnerability == null) {
|
|
278
|
+
ui.write(`${constants_js_1.Sym.WARN} Vulnerability couldn't be reproduced with concrete data.`);
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
if (vulnerability.value == null) {
|
|
282
|
+
throw new Error("Unexpected external message");
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
address: config.contractAddress,
|
|
286
|
+
msgBody: vulnerability.msgBody,
|
|
287
|
+
suggestedValue: vulnerability.value,
|
|
288
|
+
};
|
|
289
|
+
};
|
|
290
|
+
exports.ownerHijackCheckConcrete = ownerHijackCheckConcrete;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { UIProvider } from "@ton/blueprint";
|
|
2
|
+
import { CommandContext } from "../cli.js";
|
|
3
|
+
import { AnalyzerWrapper } from "../common/analyzer-wrapper.js";
|
|
4
|
+
export declare const configureReplayAttackCheckCommand: (context: CommandContext) => any;
|
|
5
|
+
interface SeqnoData {
|
|
6
|
+
getterName: string;
|
|
7
|
+
upperBound: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Runs replay attack check analysis and returns the analyzer wrapper
|
|
11
|
+
* @param contractName - Name of the contract
|
|
12
|
+
* @param contractPath - Path to the compiled contract
|
|
13
|
+
* @param ui - UI provider
|
|
14
|
+
* @param timeout - Analysis timeout in seconds
|
|
15
|
+
* @param verbose - Enable verbose output
|
|
16
|
+
* @param seqnoData - Add the seqno constraints on the checker
|
|
17
|
+
* @returns AnalyzerWrapper instance
|
|
18
|
+
*/
|
|
19
|
+
export declare const runReplayAttackCheckAnalysis: (contractName: string, contractPath: string, ui: UIProvider, timeout: number | null, verbose?: boolean, completionMessage?: string, seqnoData?: SeqnoData | null) => Promise<AnalyzerWrapper>;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runReplayAttackCheckAnalysis = exports.configureReplayAttackCheckCommand = void 0;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const core_1 = require("@ton/core");
|
|
6
|
+
const analyzer_wrapper_js_1 = require("../common/analyzer-wrapper.js");
|
|
7
|
+
const constants_js_1 = require("../common/constants.js");
|
|
8
|
+
const build_utils_js_1 = require("../common/build-utils.js");
|
|
9
|
+
const utils_js_1 = require("../reproduce/utils.js");
|
|
10
|
+
const paths_js_1 = require("../common/paths.js");
|
|
11
|
+
const configureReplayAttackCheckCommand = (context) => {
|
|
12
|
+
return {
|
|
13
|
+
command: constants_js_1.REPLAY_ATTACK_CHECK_ID,
|
|
14
|
+
description: "Analyze contract for replay attack vulnerabilities",
|
|
15
|
+
builder: (yargs) => yargs
|
|
16
|
+
.option("timeout", {
|
|
17
|
+
alias: "t",
|
|
18
|
+
type: "number",
|
|
19
|
+
description: "Analysis timeout in seconds",
|
|
20
|
+
})
|
|
21
|
+
.option("contract", {
|
|
22
|
+
alias: "c",
|
|
23
|
+
type: "string",
|
|
24
|
+
description: "Contract name",
|
|
25
|
+
demandOption: true,
|
|
26
|
+
})
|
|
27
|
+
.option("seqno-method-name", {
|
|
28
|
+
alias: "s",
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "Method name of a seqno getter method",
|
|
31
|
+
demandOption: false,
|
|
32
|
+
})
|
|
33
|
+
.option("seqno-restriction", {
|
|
34
|
+
alias: "r",
|
|
35
|
+
type: "number",
|
|
36
|
+
description: "The upper bound of a seqno. Only the seq numbers that satisfy the restrictions are considered in executions",
|
|
37
|
+
demandOption: false,
|
|
38
|
+
})
|
|
39
|
+
.option("verbose", {
|
|
40
|
+
alias: "v",
|
|
41
|
+
type: "boolean",
|
|
42
|
+
description: "Use debug output in TSA log",
|
|
43
|
+
}),
|
|
44
|
+
handler: async (argv) => {
|
|
45
|
+
await replayAttackCheckCommand(context, argv);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
exports.configureReplayAttackCheckCommand = configureReplayAttackCheckCommand;
|
|
50
|
+
/**
|
|
51
|
+
* Runs replay attack check analysis and returns the analyzer wrapper
|
|
52
|
+
* @param contractName - Name of the contract
|
|
53
|
+
* @param contractPath - Path to the compiled contract
|
|
54
|
+
* @param ui - UI provider
|
|
55
|
+
* @param timeout - Analysis timeout in seconds
|
|
56
|
+
* @param verbose - Enable verbose output
|
|
57
|
+
* @param seqnoData - Add the seqno constraints on the checker
|
|
58
|
+
* @returns AnalyzerWrapper instance
|
|
59
|
+
*/
|
|
60
|
+
const runReplayAttackCheckAnalysis = async (contractName, contractPath, ui, timeout, verbose = false, completionMessage = "Analysis complete.", seqnoData = null) => {
|
|
61
|
+
const checkerPath = (0, paths_js_1.getCheckerPath)(constants_js_1.REPLAY_ATTACK_CHECK_SYMBOLIC_FILENAME);
|
|
62
|
+
const properties = [
|
|
63
|
+
{ key: "Contract", value: contractName },
|
|
64
|
+
{ key: "Mode", value: "Replay attack check" },
|
|
65
|
+
{
|
|
66
|
+
key: "Options",
|
|
67
|
+
separator: true,
|
|
68
|
+
children: [
|
|
69
|
+
{
|
|
70
|
+
key: "Timeout",
|
|
71
|
+
value: timeout !== null ? `${timeout} seconds` : "not set",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
key: "SeqnoData",
|
|
75
|
+
value: seqnoData !== null ? JSON.stringify(seqnoData) : "not set",
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
const checkerCell = seqnoData !== null
|
|
81
|
+
? (0, core_1.beginCell)()
|
|
82
|
+
.storeUint(1, 1)
|
|
83
|
+
.storeUint((0, core_1.getMethodId)(seqnoData.getterName), 32)
|
|
84
|
+
.storeUint(seqnoData.upperBound, 256)
|
|
85
|
+
.endCell()
|
|
86
|
+
: (0, core_1.beginCell)().storeUint(0, 1).endCell();
|
|
87
|
+
const analyzer = new analyzer_wrapper_js_1.AnalyzerWrapper({
|
|
88
|
+
ui,
|
|
89
|
+
checkerPath,
|
|
90
|
+
checkerCell,
|
|
91
|
+
properties,
|
|
92
|
+
codePath: contractPath,
|
|
93
|
+
});
|
|
94
|
+
const reportDir = (0, paths_js_1.getReportDirectory)(analyzer.id);
|
|
95
|
+
const sarifPath = (0, paths_js_1.getSarifReportPath)(analyzer.id);
|
|
96
|
+
await analyzer.run(constants_js_1.REPLAY_ATTACK_CHECK_SYMBOLIC_FILENAME, (wrapper) => [
|
|
97
|
+
"custom-checker-compiled",
|
|
98
|
+
"--checker",
|
|
99
|
+
wrapper.getTempBocPath(),
|
|
100
|
+
"--contract",
|
|
101
|
+
contractPath,
|
|
102
|
+
"--stop-when-exit-codes-found",
|
|
103
|
+
constants_js_1.ERROR_EXIT_CODE.toString(),
|
|
104
|
+
"--checker-data",
|
|
105
|
+
wrapper.getTempCheckerCellPath(),
|
|
106
|
+
"--output",
|
|
107
|
+
sarifPath,
|
|
108
|
+
...(timeout != null ? ["--timeout", timeout.toString()] : []),
|
|
109
|
+
"--exported-inputs",
|
|
110
|
+
reportDir,
|
|
111
|
+
...(verbose ? ["-v"] : []),
|
|
112
|
+
"--continue-on-contract-exception",
|
|
113
|
+
"--disable-out-message-analysis",
|
|
114
|
+
], completionMessage);
|
|
115
|
+
return analyzer;
|
|
116
|
+
};
|
|
117
|
+
exports.runReplayAttackCheckAnalysis = runReplayAttackCheckAnalysis;
|
|
118
|
+
const replayAttackCheckCommand = async (context, parsedArgs) => {
|
|
119
|
+
const { ui } = context;
|
|
120
|
+
await (0, build_utils_js_1.buildContracts)(ui);
|
|
121
|
+
if (!parsedArgs.contract) {
|
|
122
|
+
throw new Error("Contract name or path is required");
|
|
123
|
+
}
|
|
124
|
+
const contract = parsedArgs.contract;
|
|
125
|
+
const contractPath = (0, paths_js_1.findCompiledContract)(contract);
|
|
126
|
+
if (!(0, fs_1.existsSync)(contractPath)) {
|
|
127
|
+
ui.write(`\n${constants_js_1.Sym.ERR} Contract ${contract} not found`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
const timeout = parsedArgs.timeout ?? null;
|
|
131
|
+
const getSeqnoMethodName = parsedArgs.seqnoMethodName;
|
|
132
|
+
const getSeqnoRestriction = parsedArgs.seqnoRestriction;
|
|
133
|
+
var seqnoData = null;
|
|
134
|
+
if (getSeqnoMethodName !== undefined && getSeqnoRestriction !== undefined) {
|
|
135
|
+
seqnoData = {
|
|
136
|
+
getterName: getSeqnoMethodName,
|
|
137
|
+
upperBound: getSeqnoRestriction,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
else if (getSeqnoRestriction !== undefined ||
|
|
141
|
+
getSeqnoMethodName !== undefined) {
|
|
142
|
+
ui.write(`\n${constants_js_1.Sym.ERR} you should specify either both the seqno getter method and seqno restriction or neither`);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
const analyzer = await (0, exports.runReplayAttackCheckAnalysis)(contract, contractPath, ui, timeout, parsedArgs.verbose, "Analysis complete.", seqnoData);
|
|
146
|
+
const vulnerability = analyzer.getVulnerability();
|
|
147
|
+
analyzer.reportVulnerability(vulnerability, constants_js_1.REPLAY_DESCRIPTION_URL);
|
|
148
|
+
(0, utils_js_1.printCleanupInstructions)(ui);
|
|
149
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.executeReproduceCommand = exports.configureReproduceCommand = void 0;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const core_1 = require("@ton/core");
|
|
6
|
+
const blueprint_1 = require("@ton/blueprint");
|
|
7
|
+
const network_js_1 = require("../reproduce/network.js");
|
|
8
|
+
const concrete_analysis_js_1 = require("../reproduce/concrete-analysis.js");
|
|
9
|
+
const constants_js_1 = require("../common/constants.js");
|
|
10
|
+
const utils_js_1 = require("../reproduce/utils.js");
|
|
11
|
+
const reproduce_config_js_1 = require("../reproduce/reproduce-config.js");
|
|
12
|
+
const format_utils_js_1 = require("../common/format-utils.js");
|
|
13
|
+
const configureReproduceCommand = (context) => ({
|
|
14
|
+
command: constants_js_1.REPRODUCE_ID,
|
|
15
|
+
description: "Reproduce found vulnerability",
|
|
16
|
+
builder: (yargs) => yargs.option("config", {
|
|
17
|
+
alias: "c",
|
|
18
|
+
type: "string",
|
|
19
|
+
description: "Path to the reproduction config",
|
|
20
|
+
demandOption: true,
|
|
21
|
+
}),
|
|
22
|
+
handler: async (argv) => await (0, exports.executeReproduceCommand)(context, argv),
|
|
23
|
+
});
|
|
24
|
+
exports.configureReproduceCommand = configureReproduceCommand;
|
|
25
|
+
async function checkAddressContainsExpectedData(network, queriedAddress, config, ui) {
|
|
26
|
+
const contractState = await network.getContractState(queriedAddress);
|
|
27
|
+
if (contractState.state.type !== "active") {
|
|
28
|
+
throw new Error(`Contract at ${queriedAddress.toString()} is not active`);
|
|
29
|
+
}
|
|
30
|
+
let dataMatches = false;
|
|
31
|
+
if (contractState.state.data) {
|
|
32
|
+
const actualData = core_1.Cell.fromBoc(contractState.state.data)[0];
|
|
33
|
+
dataMatches = actualData.equals(config.data);
|
|
34
|
+
}
|
|
35
|
+
if (dataMatches) {
|
|
36
|
+
ui.write(`${constants_js_1.Sym.OK} The data stored at contract matches the expected data.`);
|
|
37
|
+
ui.write(`${constants_js_1.Sym.OK} Balance: ${(0, format_utils_js_1.nanotonToTon)(contractState.balance)}.`);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
ui.write(`${constants_js_1.Sym.ERR} Contract data on the contract does not match data on the config`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
return contractState;
|
|
44
|
+
}
|
|
45
|
+
const executeReproduceCommand = async (context, parsedArgs) => {
|
|
46
|
+
const { ui } = context;
|
|
47
|
+
const reproduceConfigPath = parsedArgs.config;
|
|
48
|
+
if (!reproduceConfigPath) {
|
|
49
|
+
throw new Error("Please specify the reproduction config file");
|
|
50
|
+
}
|
|
51
|
+
const configJsonResult = reproduce_config_js_1.TsaVulnerabilityConfigSchema.safeParse(JSON.parse((0, fs_1.readFileSync)(reproduceConfigPath, "utf-8")));
|
|
52
|
+
if (!configJsonResult.success) {
|
|
53
|
+
ui.write("Failed to parse reproduce config file");
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
const configJson = configJsonResult.data;
|
|
57
|
+
const network = await (0, blueprint_1.createNetworkProvider)(ui, { _: [] });
|
|
58
|
+
if (configJson.mode === constants_js_1.DEPLOY_AND_REPRODUCE_COMMAND) {
|
|
59
|
+
const codeHex = JSON.parse((0, fs_1.readFileSync)(configJson.codePath, "utf-8")).hex;
|
|
60
|
+
const dataBinary = (0, fs_1.readFileSync)(configJson.dataPath);
|
|
61
|
+
const config = {
|
|
62
|
+
code: core_1.Cell.fromBoc(Buffer.from(codeHex, "hex"))[0],
|
|
63
|
+
data: core_1.Cell.fromBoc(dataBinary)[0],
|
|
64
|
+
suggestedBalance: BigInt(configJson.suggestedBalance),
|
|
65
|
+
suggestedValue: BigInt(configJson.suggestedValue),
|
|
66
|
+
};
|
|
67
|
+
const useExistingContract = await ui.prompt("Do you want to reuse an already deployed contract?");
|
|
68
|
+
const getUserInputAddress = async () => {
|
|
69
|
+
return await ui.inputAddress("Input the address to deploy contract to");
|
|
70
|
+
};
|
|
71
|
+
const deployChameleon = async () => {
|
|
72
|
+
const nonces = Array.from(Array(8), () => BigInt(Math.floor(Math.random() * (1 << 29))));
|
|
73
|
+
const deployResult = await (0, network_js_1.deployViaChameleon)(network, config, nonces);
|
|
74
|
+
return deployResult.address;
|
|
75
|
+
};
|
|
76
|
+
const address = useExistingContract
|
|
77
|
+
? await getUserInputAddress()
|
|
78
|
+
: await deployChameleon();
|
|
79
|
+
const contractState = await checkAddressContainsExpectedData(network, address, config, ui);
|
|
80
|
+
const senderAddress = network.sender().address;
|
|
81
|
+
if (!senderAddress) {
|
|
82
|
+
throw new Error("Sender address is not available");
|
|
83
|
+
}
|
|
84
|
+
const concreteAnalysisConfig = {
|
|
85
|
+
codePath: configJson.codePath,
|
|
86
|
+
dataPath: configJson.dataPath,
|
|
87
|
+
balance: contractState.balance,
|
|
88
|
+
contractAddress: address,
|
|
89
|
+
senderAddress,
|
|
90
|
+
ui,
|
|
91
|
+
timeout: configJson.timeout,
|
|
92
|
+
concreteCheckerOptions: configJson.concreteCheckerOptions,
|
|
93
|
+
};
|
|
94
|
+
const vulnerability = await (0, concrete_analysis_js_1.runConcreteAnalysis)(configJson.command, concreteAnalysisConfig);
|
|
95
|
+
if (vulnerability == null) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
(0, utils_js_1.printCleanupInstructions)(ui);
|
|
99
|
+
await (0, network_js_1.reproduce)(network, vulnerability);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
exports.executeReproduceCommand = executeReproduceCommand;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { UIProvider } from "@ton/blueprint";
|
|
2
|
+
import { Cell } from "@ton/core";
|
|
3
|
+
import { TreeProperty } from "./draw.js";
|
|
4
|
+
/**
|
|
5
|
+
* Configuration for analyzer wrapper
|
|
6
|
+
*/
|
|
7
|
+
export interface AnalyzerWrapperConfig {
|
|
8
|
+
ui: UIProvider;
|
|
9
|
+
checkerPath: string | null;
|
|
10
|
+
checkerCell: Cell;
|
|
11
|
+
properties: TreeProperty[];
|
|
12
|
+
codePath: string;
|
|
13
|
+
}
|
|
14
|
+
export interface VulnerabilityDescription {
|
|
15
|
+
value: bigint | null;
|
|
16
|
+
balance: bigint;
|
|
17
|
+
dataPath: string;
|
|
18
|
+
codePath: string;
|
|
19
|
+
executionIndex: number;
|
|
20
|
+
msgBody: Cell;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Generic wrapper for checker-based vulnerability analysis
|
|
24
|
+
* Handles common logic for compiling, running, and cleaning up checker analysis
|
|
25
|
+
*/
|
|
26
|
+
export declare class AnalyzerWrapper {
|
|
27
|
+
private config;
|
|
28
|
+
private tempBocPath;
|
|
29
|
+
private tempCheckerCellPath;
|
|
30
|
+
id: string;
|
|
31
|
+
constructor(config: AnalyzerWrapperConfig);
|
|
32
|
+
/**
|
|
33
|
+
* Prints analysis information to UI
|
|
34
|
+
*/
|
|
35
|
+
private printAnalysisInfo;
|
|
36
|
+
/**
|
|
37
|
+
* Validates that checker file exists
|
|
38
|
+
*/
|
|
39
|
+
private validateCheckerFile;
|
|
40
|
+
/**
|
|
41
|
+
* Compiles FunC checker to BoC and writes to temporary file
|
|
42
|
+
*/
|
|
43
|
+
private compileChecker;
|
|
44
|
+
/**
|
|
45
|
+
* Writes checker cell to temporary BoC file
|
|
46
|
+
*/
|
|
47
|
+
private writeCheckerCell;
|
|
48
|
+
/**
|
|
49
|
+
* Cleans up temporary files
|
|
50
|
+
*/
|
|
51
|
+
private cleanup;
|
|
52
|
+
/**
|
|
53
|
+
* Gets the temporary BoC file path (for use in analyzer arguments)
|
|
54
|
+
*/
|
|
55
|
+
getTempBocPath(): string;
|
|
56
|
+
/**
|
|
57
|
+
* Gets the temporary checker cell file path (for use in analyzer arguments)
|
|
58
|
+
*/
|
|
59
|
+
getTempCheckerCellPath(): string;
|
|
60
|
+
/**
|
|
61
|
+
* Runs the checker analysis with custom analyzer arguments
|
|
62
|
+
* @param checkerFilename - Name of the checker file to compile
|
|
63
|
+
* @param buildArgs - Callback function to build analyzer arguments after compilation
|
|
64
|
+
*/
|
|
65
|
+
run(checkerFilename: string | null, buildArgs: (wrapper: this) => string[], completionMessage?: string): Promise<void>;
|
|
66
|
+
getVulnerability(): VulnerabilityDescription | null;
|
|
67
|
+
vulnerabilityIsPresent(): boolean;
|
|
68
|
+
reportVulnerability(vulnerability: VulnerabilityDescription | null, descriptionUrl?: string): void;
|
|
69
|
+
}
|