# [HKCERT CTF 2022] C++harming website (富榮花園) This is a part of the series (coming soon:tm:?) of writeups for HKCERT CTF 2022, where I competed with 3 [blackb6a](https://b6a.black/) members as `Black Banana`, instead of [Maple Bacon](https://maplebacon.org) as usual. ## Challenge > Seems someone encrypt their flag with [some weird online website](http://chal.hkcert22.pwnable.hk:28248/). And seems the website is written in C++... > Does anyone even use C++ to write their web server? I guess C++ is still charm but it must be easy to reverse.... right? > Attachment: [cryptor_b6ce33b8d5442db2a4fcbce6c5df9c18.zip](https://file.hkcert22.pwnable.hk/cryptor_b6ce33b8d5442db2a4fcbce6c5df9c18.zip) - Author: harrier - Solves: 4 (first blood) - Category: reverse (4*, 350 pts) <br><br> ## Investigation ``` $ ls -lah cryptor_b6ce33b8d5442db2a4fcbce6c5df9c18 total 2.2M drwxrwxrwx 1 desktop desktop 4.0K Nov 15 07:36 . drwxrwxrwx 1 desktop desktop 4.0K Nov 15 09:39 .. -rwxrwxrwx 1 desktop desktop 2.2M Jan 1 1980 cryptor-exe -rwxrwxrwx 1 desktop desktop 174 Jan 1 1980 flag.txt ``` Oh boy, don't you like looking at binaries that has it's size in the megabytes range? Welp, time to overcome our fears and open it in IDA to see what it has to offer, as always: ![](https://i.imgur.com/pnsqcOt.png) After waiting for a surprisingly short amount of time, we are greeted with also a surprisingly large amount of symbols! Both of which are really good news for us - it saves a lot of time reversing something as big as this if symbols are present. Moreso if we can figure out whether it is using an open source library, so we can reference the source code directly - binaries this large usually is using a framework of some sorts, or are statically linked with existing libraries. A quick google search on the function names brings us to [oatpp](https://github.com/oatpp/oatpp) - a C++ web framework with quite a lot of documentation. Turns out, this also matches what was given when we visit the "weird website online": ``` server=oatpp/1.3.0 code=404 description=Not Found message=No mapping for HTTP-method: 'GET', URL: '/' ``` Now that we know what we are working with (a web framework), it would be logical to see what endpoints are mapped, and their respective handlers - since that's the part that needs to interact with the world, most of the actual user written code that is unrelated to the framework will be in the handlers themselves. From the [documentation](https://oatpp.io/docs/components/api-controller/#endpoint-types), we see that there is an `ENDPOINT` macro responsible for declaring endpoints, which expands to: ```cpp #define ENDPOINT(METHOD, PATH, ...) \ OATPP_MACRO_EXPAND(OATPP_MACRO_MACRO_BINARY_SELECTOR(OATPP_MACRO_API_CONTROLLER_ENDPOINT_MACRO_, (__VA_ARGS__)) (METHOD, PATH, __VA_ARGS__)) ``` Ah, more macros - looks like we might have to chase down a rabbit hole... Luckily, the macros themselves are pretty readable, and after resolving `OATPP_MACRO_API_CONTROLLER_ENDPOINT_MACRO_` -> `OATPP_MACRO_API_CONTROLLER_ENDPOINT_MACRO_0` -> `OATPP_MACRO_API_CONTROLLER_ENDPOINT_0`, we reach something that finally gives us a function declaration, and very important hint with that: ```cpp #define OATPP_MACRO_API_CONTROLLER_ENDPOINT_0(NAME, METHOD, PATH) \ OATPP_MACRO_API_CONTROLLER_ENDPOINT_DECL_DEFAULTS(NAME, METHOD, PATH) \ OATPP_MACRO_API_CONTROLLER_ENDPOINT_DECL_0(NAME, METHOD, PATH) \ \ std::shared_ptr<oatpp::web::protocol::http::outgoing::Response> \ Z__PROXY_METHOD_##NAME(const std::shared_ptr<oatpp::web::protocol::http::incoming::Request>& __request) \ { \ (void)__request; \ return NAME(); \ } \ \ std::shared_ptr<oatpp::web::protocol::http::outgoing::Response> NAME() ``` Notice `Z__PROXY_METHOD_`? That's something we can definitely search up on IDA to identify all the endpoints at once! Let's obtain the list from the function window: ``` Function name Segment Start Length Locals Arguments R F L M S B T = MyController::Z__PROXY_METHOD_Encrypt(std::shared_ptr<oatpp::web::protocol::http::incoming::Request> const&) .text 00000000000BDBC2 0000009C 00000058 00000000 R . . . . B . . ``` Oh hey, seems like they *helpfully* didn't even rename the controller from the default in the docs (`MyController`), so we could've searched that up instead, but that doesn't matter that much anymore - we have a much stronger lead now. Anyway, we have exactly one endpoint - `encrypt`! That's very promising - it shrinks our reversing scope by a huge amount knowing this. Let's look into what it offers: ```c __int64 __fastcall MyController::Z__PROXY_METHOD_Encrypt(__int64 a1, __int64 a2, __int64 a3) { char v4[24]; // [rsp+20h] [rbp-30h] BYREF unsigned __int64 v5; // [rsp+38h] [rbp-18h] v5 = __readfsqword(0x28u); std::shared_ptr<oatpp::web::protocol::http::incoming::Request>::shared_ptr(v4, a3); oatpp::async::CoroutineWithResult<MyController::Encrypt,std::shared_ptr<oatpp::web::protocol::http::outgoing::Response> const&>::startForResult<MyController*,std::shared_ptr<oatpp::web::protocol::http::incoming::Request>>( a1, a2, v4); std::shared_ptr<oatpp::web::protocol::http::incoming::Request>::~shared_ptr(v4); return a1; } ``` Hmm, looks like we need to do some function chasing. Upon further look, it seems like this function was declared with `ENDPOINT_ASYNC` instead too, with the `startForResult` function call and the coroutines, which makes things more convoluted with callbacks and such needed for asynchronous operations. But the general gist is still the same - we can just figure out the differences in control flow ourselves. Clicking into the only one that mentions "encrypt", we finally see the actual endpoint function - `MyController::Encrypt::Encrypt`. This is still not the encryption handler yet though - we'll have to dig deeper for that. ![](https://i.imgur.com/82HVyXE.png) Oh? We stopped having mentions of "encrypt" anymore in the decompiled function. Turns out, the `off_17EBE0` pointer is actually a part of the `MyController::Encrypt` vtable - it helpfully defines everything we need to know about this handler! Continuing our function chasing, we can eliminate all the functions aside from `MyController::Encrypt::act` - they seem to be destructors and other unrelated C++ fluff. ```cpp MyController::Encrypt *__fastcall MyController::Encrypt::act(MyController::Encrypt *this, __int64 a2) { __int64 v2; // rbx __int64 v3; // rax char v5[8]; // [rsp+10h] [rbp-30h] BYREF unsigned __int64 v6; // [rsp+18h] [rbp-28h] v6 = __readfsqword(0x28u); v2 = std::__shared_ptr_access<oatpp::web::protocol::http::incoming::Request,(__gnu_cxx::_Lock_policy)2,false,false>::operator->(a2 + 72); v3 = oatpp::web::server::api::ApiController::getDefaultObjectMapper(*(oatpp::web::server::api::ApiController **)(a2 + 64)); oatpp::web::protocol::http::incoming::Request::readBodyToDtoAsync<oatpp::data::mapping::type::DTOWrapper<EncryptRequest>>( v5, v2, v3); oatpp::async::AbstractCoroutineWithResult<oatpp::data::mapping::type::DTOWrapper<EncryptRequest> const&>::StarterForResult::callbackTo<MyController::Encrypt>( this, v5, MyController::Encrypt::encrypt, 0LL); oatpp::async::AbstractCoroutineWithResult<oatpp::data::mapping::type::DTOWrapper<EncryptRequest> const&>::StarterForResult::~StarterForResult(v5); return this; } ``` With this, we finally reach the actual encryption handler function - `MyController::Encrypt::encrypt`. <br><Br> ## Oho you think we are near? Get Sidetracked! Now that we've reached the handler, which has a lot of promising details that tells us it's the actual endpoint like `oatpp::base::Environment::logFormatted` with the string `msg incoming with len=%d`, `oatpp::web::server::api::ApiController::createResponse` with `&oatpp::web::protocol::http::Status::CODE_400` etc etc, I can't help but wonder how this endpoint behaves when we interact with it - usually this saves a lot of guesswork and reversing time to be able to understand how the input and the output is formatted. But to be able to do this, we need to figure out how we can actually use this endpoint through HTTP requests. As such, it's time to embark on another journey to find how we can interact with the endpoint! Just kidding, it's actually pretty straightforward - it's pretty much the same deal as before. Going back to the `ENDPOINT` macro, we see `OATPP_MACRO_API_CONTROLLER_ENDPOINT_DECL_DEFAULTS` - which tells us where it sets the path and all that at. ```cpp #define OATPP_MACRO_API_CONTROLLER_ENDPOINT_DECL_0(NAME, METHOD, PATH) \ \ EndpointInfoBuilder Z__CREATE_ENDPOINT_INFO_##NAME = [this](){ \ auto info = Z__EDNPOINT_INFO_GET_INSTANCE_##NAME(); \ info->name = #NAME; \ info->path = ((m_routerPrefix != nullptr) ? m_routerPrefix + PATH : PATH); \ info->method = METHOD; \ if (info->path == "") { \ info->path = "/"; \ } \ return info; \ }; \ \ const std::shared_ptr<oatpp::web::server::api::Endpoint> Z__ENDPOINT_##NAME = createEndpoint(m_endpoints, \ Z__ENDPOINT_HANDLER_GET_INSTANCE_##NAME(this), \ Z__CREATE_ENDPOINT_INFO_##NAME); ``` For whatever reason, IDA failed to demangle the function names here, and there's also quite a bit of fluff functions with the same name. But that doesn't matter too much - we can see exactly all the information we needed as we go through them. ```cpp MyController *__fastcall ZNK12MyController31Z__CREATE_ENDPOINT_INFO_EncryptMUlvE_clEv(MyController *a1, __int64 *a2) { __int64 v2; // rax __int64 v3; // rax __int64 v4; // rax MyController::Z__EDNPOINT_INFO_GET_INSTANCE_Encrypt(a1, *a2); v2 = std::__shared_ptr_access<oatpp::web::server::api::Endpoint::Info,(__gnu_cxx::_Lock_policy)2,false,false>::operator->(a1); oatpp::data::mapping::type::String::operator=<char,void>(v2 + 8, "Encrypt"); v3 = std::__shared_ptr_access<oatpp::web::server::api::Endpoint::Info,(__gnu_cxx::_Lock_policy)2,false,false>::operator->(a1); oatpp::data::mapping::type::String::operator=<char,void>(v3 + 80, "/encrypt"); v4 = std::__shared_ptr_access<oatpp::web::server::api::Endpoint::Info,(__gnu_cxx::_Lock_policy)2,false,false>::operator->(a1); oatpp::data::mapping::type::String::operator=<char,void>(v4 + 104, "POST"); return a1; } ``` Now that we know the path is `/encrypt`, and the request method is `POST`, we still need to know what the payload should be. Backtracking to the [docs](https://oatpp.io/docs/components/api-controller/#endpoint-annotation-and-api-documentation), we know that there's a `ENDPOINT_INFO` macro, which after expansion gives us the following code: ```cpp #define ENDPOINT_INFO(NAME) \ \ std::shared_ptr<oatpp::web::server::api::Endpoint::Info> Z__ENDPOINT_CREATE_ADDITIONAL_INFO_##NAME() { \ auto info = Z__EDNPOINT_INFO_GET_INSTANCE_##NAME(); \ Z__ENDPOINT_ADD_INFO_##NAME(info); \ return info; \ } \ \ const std::shared_ptr<oatpp::web::server::api::Endpoint::Info> Z__ENDPOINT_ADDITIONAL_INFO_##NAME = Z__ENDPOINT_CREATE_ADDITIONAL_INFO_##NAME(); \ \ void Z__ENDPOINT_ADD_INFO_##NAME(const std::shared_ptr<oatpp::web::server::api::Endpoint::Info>& info) ``` Once again searching up on IDA and sifting through the functions, we reach `MyController::Z__ENDPOINT_ADD_INFO_Encrypt`: ```cpp unsigned __int64 __fastcall MyController::Z__ENDPOINT_ADD_INFO_Encrypt(__int64 a1, __int64 a2) { __int64 v2; // rax __int64 v3; // rbx __int64 v4; // rbx char v6[32]; // [rsp+10h] [rbp-50h] BYREF char v7[24]; // [rsp+30h] [rbp-30h] BYREF unsigned __int64 v8; // [rsp+48h] [rbp-18h] v8 = __readfsqword(0x28u); v2 = std::__shared_ptr_access<oatpp::web::server::api::Endpoint::Info,(__gnu_cxx::_Lock_policy)2,false,false>::operator->(a2); oatpp::data::mapping::type::String::operator=<char,void>(v2 + 32, "Create new User"); v3 = std::__shared_ptr_access<oatpp::web::server::api::Endpoint::Info,(__gnu_cxx::_Lock_policy)2,false,false>::operator->(a2); oatpp::data::mapping::type::String::String((oatpp::data::mapping::type::String *)v7); oatpp::data::mapping::type::String::String<char,void>(v6, "application/json"); oatpp::web::server::api::Endpoint::Info::addConsumes<oatpp::data::mapping::type::DTOWrapper<EncryptRequest>>( v3, v6, v7); oatpp::data::mapping::type::String::~String((oatpp::data::mapping::type::String *)v6); oatpp::data::mapping::type::String::~String((oatpp::data::mapping::type::String *)v7); v4 = std::__shared_ptr_access<oatpp::web::server::api::Endpoint::Info,(__gnu_cxx::_Lock_policy)2,false,false>::operator->(a2); oatpp::data::mapping::type::String::String((oatpp::data::mapping::type::String *)v7); oatpp::data::mapping::type::String::String<char,void>(v6, "application/json"); oatpp::web::server::api::Endpoint::Info::addResponse<oatpp::data::mapping::type::DTOWrapper<EncryptResponse>>( v4, &oatpp::web::protocol::http::Status::CODE_200, v6, v7); oatpp::data::mapping::type::String::~String((oatpp::data::mapping::type::String *)v6); oatpp::data::mapping::type::String::~String((oatpp::data::mapping::type::String *)v7); return v8 - __readfsqword(0x28u); } ``` Which tells us it expects a JSON object, and... "Create new User"? ```cpp ENDPOINT_INFO(createUser) { info->summary = "Create new User"; info->addConsumes<Object<UserDto>>("application/json"); info->addResponse<Object<UserDto>>(Status::CODE_200, "application/json"); } ENDPOINT("POST", "demo/api/users", createUser, BODY_DTO(Object<UserDto>, userDto)) { return createDtoResponse(Status::CODE_200, m_database->createUser(userDto)); } ``` Ah, now I think we all know what happened - this is from the example in the docs :wink: <br> We still need the JSON object's exact format though, so let's chase some more functions. Reading through the [docs for `UserDto`](https://oatpp.io/docs/components/dto/), we can understand that its serialization method is JSON, which is exactly what we want. Once again expanding macros for `DTO_FIELD` (I'll save you the repetition), we finally reach these functions: ``` Function name Segment Start Length Locals Arguments R F L M S B T = EncryptRequest::Z__PROPERTY_OFFSET_message(void) .text 00000000000BBEED 00000050 00000068 00000000 R . . . . B T . EncryptResponse::Z__PROPERTY_OFFSET_message(void) .text 00000000000BC1FC 00000050 00000078 00000000 R . . . . B T . EncryptResponse::Z__PROPERTY_OFFSET_tag(void) .text 00000000000BC3FB 00000050 00000078 00000000 R . . . . B T . EncryptResponse::Z__PROPERTY_OFFSET_iv(void) .text 00000000000BC5FA 00000050 00000078 00000000 R . . . . B T . ``` Which defines the entire JSON format for both input and output! A simple `curl` tells us if our theory is correct: ```sh $ curl http://chal.hkcert22.pwnable.hk:28248/encrypt -X POST --data '{"message":"test"}' {"message":"g79CQw==","tag":"VUFOIEyv50BY7iiT0rigyw==","iv":"muZYMECsgVtniXfMGpbnow=="} ``` With all these, we can finally communicate with the server. We can see that it does live up to the name of `cryptor-exe` - it's just encrypting a message for you. <br> Then I wondered, where might the flag be? Then I saw `flag.txt` in the challenge attachment. ```json {"message":"5Arys5Y8epqG4aSeRcXvf+SKlhNKlQjI22x8ojRv9Deu0EYBstAMMrs+tvnmUV5uFAuXW5kN2jrz4NJm7eh6vG4Mq+w=","tag":"0FtMYG2IPxj\/qFhD\/NLGrA==","iv":"4k92GNijCq+ov+7mXOkEHg=="} ``` Then I wondered again, what was the purpose for finding all these?<br> It's fine, it only sidetracked me for a good 30 minutes 🥲 <br><br> ## *Charmingly* Back on Track With my pondering now all cleared (~~forcefully~~), it's time to take a look at `MyController::Encrypt::encrypt` seriously again. ![](https://i.imgur.com/vMCFOYF.png) The first thing that caught my eyes are the `uc_*` functions - they seem to be initializing some state for the encryption algorithm, like most encryption methods do. In fact, sifting through all the C++ fluff, we can see the following slightly down the decompilation: ```c strncpy(dest, v10, n); ((void (__fastcall *)(char *, char *, size_t, char *))uc_encrypt)(v28, dest, n, v30); ``` `uc_encrypt`! If we can figure out how this encryption works, we can reverse it and decrypt it. Considering how simple the endpoint is when we interacted with it, it's likely that it has nothing much more than just this encryption operation, so we are basically there if we finish analysing this. So the logical way is to clean up the decompilation and convert it to runnable C code, then symbolically execute it to solve the state, considering it's probably a handrolled simple encryption algorithm, right?<br> W r o n g - while cleaning it up I realized it was way too complicated for me to reverse this cryptosystem. So I resorted to every CTFer's best friend - googling again, and guess what - ![](https://i.imgur.com/DLwoZNp.png) ![](https://i.imgur.com/5r7C1cP.png) "**charm**.h" :sob: As one might exclaim in this situation: "bruh". I mean, I'll take anything that leads me to the flag - and lead me to the flag it does! Looking at [`charm.c`](https://github.com/jedisct1/dsvpn/blob/master/src/charm.c), we can see that everything from the function signatures to how `endian_swap_rate` is a nop on a little endian machine matches up with what we have in the challenge binary. This just means we need to extract the key from the binary, combine it with the data in `flag.txt`, set the code up and run `uc_decrypt` to get our flag! For the key, remember how we had this line in the screenshot near the top of this section? ```c ((void (__fastcall *)(char *, void *, char *))uc_state_init)(v28, &static_key, v29); ``` The function signature from the source tells us `static_key` is exactly the key we want: ```c void uc_state_init(uint32_t st[12], const unsigned char key[32], const unsigned char iv[16]) ``` By grabbing the `static_key` bytes, and decoding the base64 data in the JSON in `flag.txt`, we can obtain all the info needed to write our solve script. ```c #include "charm.h" #include <stdio.h> const unsigned char key[32] = { 0xf2, 0x9c, 0x0b, 0xf1, 0xc5, 0x1a, 0x7e, 0x65, 0x75, 0x80, 0x23, 0x6e, 0x8b, 0x74, 0x38, 0xbf, 0x59, 0x39, 0x8a, 0x1a, 0x05, 0xc6, 0x43, 0xfa, 0x1d, 0x57, 0x82, 0x0a, 0xb9, 0xc6, 0xdc, 0x50 }; const unsigned char iv[16] = {0xe2,0x4f,0x76,0x18,0xd8,0xa3,0x0a,0xaf,0xa8,0xbf,0xee,0xe6,0x5c,0xe9,0x04,0x1e}; const unsigned char tag[16] = {0xd0,0x5b,0x4c,0x60,0x6d,0x88,0x3f,0x18,0xff,0xa8,0x58,0x43,0xfc,0xd2,0xc6,0xac}; unsigned char msg[] = {0xe4,0x0a,0xf2,0xb3,0x96,0x3c,0x7a,0x9a,0x86,0xe1,0xa4,0x9e,0x45,0xc5,0xef,0x7f,0xe4,0x8a,0x96,0x13,0x4a,0x95,0x08,0xc8,0xdb,0x6c,0x7c,0xa2,0x34,0x6f,0xf4,0x37,0xae,0xd0,0x46,0x01,0xb2,0xd0,0x0c,0x32,0xbb,0x3e,0xb6,0xf9,0xe6,0x51,0x5e,0x6e,0x14,0x0b,0x97,0x5b,0x99,0x0d,0xda,0x3a,0xf3,0xe0,0xd2,0x66,0xed,0xe8,0x7a,0xbc,0x6e,0x0c,0xab,0xec}; int main() { uint32_t st[12]; uc_state_init(st, key, iv); uc_decrypt(st, msg, sizeof(msg), tag, 16); printf("%s\n", msg); } ``` <br> (Slight digression, but I've recently been using Binary Ninja more thanks to [@Jason](https://maplebacon.org/authors/Jason/), and it's just so nice to have some QoL features like these: ![](https://i.imgur.com/DM0IEe2.png) No more regex needed to nudge IDA text copies!)<br> With this, we can compile and finally obtain the flag: ``` $ gcc cryptor-solve.c charm.c -o cryptor-solve $ ./cryptor-solve hkcert22{n3v3r_s4w_4n_c++_ap1_s3Rv3R?m3_n31th3r_bb4t_17_d0e5_eX15ts} ``` <br><br> ## Thoughts Looking through a challenge this big, it reminds me a lot on the real world Windows binaries that I used to reverse before I started CTFing - it's not too common to see something of this size in a CTF, since it's very likely that one might get disoriented and waste a lot of time even if the solution is simple. <br> The main takeaway for this challenge for me is that reversing isn't just blindly reading the code in the binary - but also to identify patterns and things that you can look up at a high level, bypassing much of the pain needed to understanding most of the logic when possible.<br> This challenge did a pretty good job at balancing this by providing symbols so we can identify the bulk of the code pretty quickly, so we can focus on the actual details that we need to reverse. Putting aside the jokes I made about being sidetracked and such, solving this challenge was actually pretty painless - I had a clear idea of what I should look for, and with that I was able to obtain first blood 3 hours after it has been released, after about an hour of work in total. Props to the author harrier for making such a fun challenge!