diff --git a/creator/internal-efiensctf-2020/Offsets b/creator/internal-efiensctf-2020/Offsets new file mode 100644 index 0000000..e575946 Binary files /dev/null and b/creator/internal-efiensctf-2020/Offsets differ diff --git a/creator/internal-efiensctf-2020/README.md b/creator/internal-efiensctf-2020/README.md new file mode 100644 index 0000000..f2a61ce --- /dev/null +++ b/creator/internal-efiensctf-2020/README.md @@ -0,0 +1,17 @@ +I created 2 challenges for the Internal Efiens CTF 2020 + +- Nodejs +- Offsets + +For the nodejs challenge, I modified the code of nodejs to add 2 functions, one at js layer and one at C++ layer. Both can be accessed from the global environment of Node by `require('flag')`. + +The js code is a simple state machine, by reading the source code it is trivial to find the flag. + +The C++ code is harder to read though. I give pointers by printing out debug information, thus it is trivial to find the function. Then a simple xor operation is done on the input array, we can easily read the key and encrypted value to decrypt the flag. + +There is a mistake when I made this challenge, I built the binary with a debug statement to print the expected character after the xor. + + +Offsets challenge are made using LLVM and O-LLVM. I added another transform operation. This transform converts `array[i]` in assembly into `array[f()]` where `f() = i`. In this challenge I apply simple math and can be read through easily. One can use angr to solve this challenge. For more information, read `StructOffset.cpp`. + +The source code for the challenge is lost, try to solve with only the binary. diff --git a/creator/internal-efiensctf-2020/StructOffset.cpp b/creator/internal-efiensctf-2020/StructOffset.cpp new file mode 100644 index 0000000..0120de1 --- /dev/null +++ b/creator/internal-efiensctf-2020/StructOffset.cpp @@ -0,0 +1,255 @@ +#include +#include + +#include "llvm/ADT/StringRef.h" +#include "llvm/IR/Function.h" +#include "llvm/IR/Instructions.h" +#include "llvm/IR/Constants.h" +#include "llvm/IR/Value.h" +#include "llvm/IR/InstrTypes.h" +#include "llvm/IR/DataLayout.h" +#include "llvm/Pass.h" +#include "llvm/Support/raw_ostream.h" + +#include "CryptoUtils.h" + +using namespace llvm; + +#define DEBUG_TYPE "structoffset" + +namespace { +struct StructOffsetJni : public FunctionPass { + static char ID; + StructOffsetJni() : FunctionPass(ID) {} + + bool runOnFunction(Function& F) override { + int var_count = 15; // a random number for debugging in IR + llvm::cryptoutils->prng_seed(); + std::stack old_gets; + DataLayout* dtl = new DataLayout(F.getParent()); + unsigned int ptr_size = + dtl->getPointerTypeSizeInBits(Type::getInt32PtrTy(F.getContext())); + + for (auto i = F.begin(); i != F.end(); i++) { + BasicBlock* blk = &*i; + for (auto ii = blk->begin(); ii != blk->end(); ii++) { + Instruction* insn = &*ii; + if (insn->getOpcode() != Instruction::GetElementPtr) continue; + GetElementPtrInst* ele = (GetElementPtrInst*)insn; + Type* source = ele->getSourceElementType(); + if (!source->isStructTy() || + source->getStructName().str() != "struct.JNINativeInterface_") + continue; + + old_gets.push(insn); + insn = insn->getNextNonDebugInstruction(); + + // We are at the `getelementptr` instruction of the code + // Because `JNINativeInterface_` is a struct with only pointers + // we can consider it as an array of pointers + // + // It could be similar to the conversion from + // ``` + // struct JNINativeInterface_ env; + // (*env)->doSomething(args); + // ```` + // to + // ``` + // struct JNINativeInterface_ env; + // typedef rettype (*doSomething)(args...); + // (doSomething)((int*)(*env)[doSomethingOffset])(args...); + // ``` + // We first cast the `struct*` to an `int*` + // Calculate the new offset + // Add the base address to the offset to get the address to member + // Cast the address to a function pointer and call with the same + // arguments + // + // In LLVM, when `(*env)->doSomething` is accessed the LLVM IR + // generates a `getelementptr` instruction like this: + // ``` + // getelementptr inbounds + // %struct.JNINativeInterface_, + // %struct.JNINativeInterface_* %R, + // i32 0, + // i32 memberIndex + // ```` + // Where: + // %R is the pointer to the struct + // memberIndex is the index of the member in struct + // the third argument is 0 if %R is a pointer to the struct + // this is actually the same as how many dereference is needed + // *%R -> 0 + // **%R -> 1 + // + // We replace this instruction with + // ``` + // %10 = bitcast %struct.JNINativeInterface_* %R to i64* + // %19 = getelementptr inbounds i64, i64* %10, i64 %OFFSET + // %20 = bitcast i64* %19 to FUNC_TYPE* + // %21 = load FUNC_TYPE, FUNC_TYPE* %20, align 8 + // + // where FUNC_TYPE is + // i8* (%struct.JNINativeInterface_**, %struct._jobject*, i8*)* + // ``` + // After that, we generate the equation to OFFSET (see below) + // which makes it harder to reverse + + // generates a temp variable + Twine var_name = Twine(var_count); + AllocaInst* var = + new AllocaInst(Type::getInt32Ty(F.getContext()), 0, var_name, insn); + ConstantInt* y = + ConstantInt::getSigned(Type::getInt32Ty(F.getContext()), + llvm::cryptoutils->get_range(50, 100)); + new StoreInst(y, var, insn); + + Value* as_array = new BitCastInst( + ele->getPointerOperand(), + Type::getIntNPtrTy(F.getContext(), ptr_size), "", insn); + + ConstantInt* index = (ConstantInt*)ele->getOperand(2); + BinaryOperator* new_index = nullptr; + if (index->isZero()) { + // generates the equation: + // a * y + b * y - c * y which is equal to 0 + // where + // a + b = c + // y is a random number + // + // beware of bit size, we are using int32 + // we have 31 bits to use, make sure not to overflow + // + // d is roughly 1000 to 2000 + // a will have about 21 bits + // b = c - a = -(a - c) + + int a_max = 1 << 21; + int a_min = 1 << 20; + // c is negative to mul easier + ConstantInt* c = + ConstantInt::getSigned(Type::getInt32Ty(F.getContext()), + llvm::cryptoutils->get_range(1000, 2000)); + ConstantInt* a = ConstantInt::getSigned( + Type::getInt32Ty(F.getContext()), + llvm::cryptoutils->get_range(a_min, a_max)); + // calculate b = -(a + (-c)) + int b_value = -((int)a->getZExtValue() + (int)c->getZExtValue()); + ConstantInt* b = + ConstantInt::getSigned(Type::getInt32Ty(F.getContext()), b_value); + + // a * y + b * y + LoadInst* l1 = new LoadInst(Type::getInt32Ty(F.getContext()), var, + var_name, insn); + BinaryOperator* a_m = + BinaryOperator::CreateNSW(BinaryOperator::Mul, a, l1, "", insn); + LoadInst* l2 = new LoadInst(Type::getInt32Ty(F.getContext()), var, + var_name, insn); + BinaryOperator* b_m = + BinaryOperator::CreateNSW(BinaryOperator::Mul, b, l2, "", insn); + BinaryOperator* left = BinaryOperator::CreateNSW(BinaryOperator::Add, + a_m, b_m, "", insn); + // - c * y + LoadInst* l3 = new LoadInst(Type::getInt32Ty(F.getContext()), var, + var_name, insn); + BinaryOperator* right = + BinaryOperator::CreateNSW(BinaryOperator::Mul, c, l3, "", insn); + + new_index = BinaryOperator::CreateNSW(BinaryOperator::Add, left, + right, "", insn); + } else { + // generates the equation: + // a * y + b * y + index * (1 - d * y) which is equal to index + // where + // index != 0 + // a + b = d * index + // y is a random number + // + // beware of bit size, we are using int32 + // we have 31 bits to use, make sure not to overflow + // + // d is roughly 1000 to 2000 + // a will have about 21 bits + // b = (index * d - a) = -(a - index * d) + + int a_max = 1 << 21; + int a_min = 1 << 20; + // d is negative to mul easier + ConstantInt* d = + ConstantInt::getSigned(Type::getInt32Ty(F.getContext()), + llvm::cryptoutils->get_range(1000, 2000)); + ConstantInt* a = ConstantInt::getSigned( + Type::getInt32Ty(F.getContext()), + llvm::cryptoutils->get_range(a_min, a_max)); + // calculate b = -(a + (index * -d))))) + int b_value = -((int)a->getZExtValue() + + (int)index->getZExtValue() * (int)d->getZExtValue()); + ConstantInt* b = + ConstantInt::getSigned(Type::getInt32Ty(F.getContext()), b_value); + ConstantInt* one = + ConstantInt::getSigned(Type::getInt32Ty(F.getContext()), 1); + + // a * y + b * y + LoadInst* l1 = new LoadInst(Type::getInt32Ty(F.getContext()), var, + var_name, insn); + BinaryOperator* a_m = + BinaryOperator::CreateNSW(BinaryOperator::Mul, a, l1, "", insn); + LoadInst* l2 = new LoadInst(Type::getInt32Ty(F.getContext()), var, + var_name, insn); + BinaryOperator* b_m = + BinaryOperator::CreateNSW(BinaryOperator::Mul, b, l2, "", insn); + BinaryOperator* left = BinaryOperator::CreateNSW(BinaryOperator::Add, + a_m, b_m, "", insn); + // index * (1 - d * y) + LoadInst* l3 = new LoadInst(Type::getInt32Ty(F.getContext()), var, + var_name, insn); + BinaryOperator* d_m = + BinaryOperator::CreateNSW(BinaryOperator::Mul, d, l3, "", insn); + BinaryOperator* add_one = BinaryOperator::CreateNSW( + BinaryOperator::Add, d_m, one, "", insn); + BinaryOperator* right = BinaryOperator::CreateNSW( + BinaryOperator::Mul, index, add_one, "", insn); + + new_index = BinaryOperator::CreateNSW(BinaryOperator::Add, left, + right, "", insn); + } + GetElementPtrInst* new_get = + GetElementPtrInst::CreateInBounds(as_array, { + // getelementptr reuires the index to be int64 + new SExtInst(new_index, Type::getInt64Ty(F.getContext()), "", + insn); + }, "", insn); + + // store + if (StoreInst::classof(insn)) { + // may not need, no one set on JNIEnv + insn->setOperand(1, new_get); + } + // load + else if (LoadInst::classof(insn)) { + // cast the int* to function pointer + // the type of the function pointer is load type + Value* func_ptr = new BitCastInst( + new_get, ((LoadInst*)insn)->getPointerOperandType(), "", insn); + insn->setOperand(0, func_ptr); + } else { + errs() << "Unknown instruction after getptr:\n\t" << *insn << "\n"; + } + + var_count += 1; + } + } + // erase old gets + while (!old_gets.empty()) { + old_gets.top()->eraseFromParent(); + old_gets.pop(); + } + return false; + } +}; +} + +char StructOffsetJni::ID = 1; +static RegisterPass JNI("jnienv", + "Randomly offset JNIEnv's members"); + diff --git a/creator/internal-efiensctf-2020/flag.js b/creator/internal-efiensctf-2020/flag.js new file mode 100644 index 0000000..c54859a --- /dev/null +++ b/creator/internal-efiensctf-2020/flag.js @@ -0,0 +1,70 @@ +const { check_password } = internalBinding('flag'); + +function easy(input_flag) { + let flag_components = input_flag.split('-'); + let message = ''; + let x = 0x9bc19a11; + while (x !== 0xdeadbeef) { + switch(x) { + case 0x54f5d109: { + message = 'You are dead wrong'; + x = 0xdeadbeef; + break; + } + case 0x22f1dede: { + message = 'The flag is efiensctf{' + input_flag + '}'; + x = 0xdeadbeef; + break; + } + case 0x9bc19a11: { + x = flag_components.length === 4 + ? 0xab9a16fd + : 0x54f5d109; + break; + } + case 0x589f0521: { + x = flag_components[0] === 'do' + ? 0x27c501f3 + : 0x54f5d109; + flag_components.shift(); + break; + } + case 0x0e2c6464: { + x = flag_components[0] === 'know' + ? 0x22f1dede + : 0x54f5d109; + flag_components.shift(); + break; + } + case 0x27c501f3: { + x = flag_components[0] === 'you' + ? 0x0e2c6464 + : 0x54f5d109; + flag_components.shift(); + break; + } + case 0xab9a16fd: { + x = flag_components[0] === 'nodejs' + ? 0x589f0521 + : 0x54f5d109; + flag_components.shift(); + break; + } + } + } + return message; +} + +function medium(pass) { + if (check_password(pass)) { + console.log("Submit the password as flag"); + } + else { + console.log("What did you miss?"); + } +} + +module.exports = flag = { + easy, + medium, +}; diff --git a/creator/internal-efiensctf-2020/node b/creator/internal-efiensctf-2020/node new file mode 100644 index 0000000..fae7bde Binary files /dev/null and b/creator/internal-efiensctf-2020/node differ diff --git a/creator/internal-efiensctf-2020/node_flag.cc b/creator/internal-efiensctf-2020/node_flag.cc new file mode 100644 index 0000000..7e9e0a2 --- /dev/null +++ b/creator/internal-efiensctf-2020/node_flag.cc @@ -0,0 +1,137 @@ +// Copyright luibo. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +#include "node.h" +#include "env-inl.h" +#include "util-inl.h" + +#include +#include +#include +#include +#include +#include +#include + +#if defined(__MINGW32__) || defined(_MSC_VER) +# include +#endif + +#include + +namespace node { + +namespace flag { + +using v8::Array; +using v8::Context; +using v8::EscapableHandleScope; +using v8::Function; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::HandleScope; +using v8::Int32; +using v8::Integer; +using v8::Isolate; +using v8::Local; +using v8::MaybeLocal; +using v8::Number; +using v8::Object; +using v8::ObjectTemplate; +using v8::Promise; +using v8::String; +using v8::Symbol; +using v8::Uint32; +using v8::Undefined; +using v8::Value; + +char encrypted[42] = { + 0x0b, 0x09, 0x0d, 0x00, 0x00, 0x1c, 0x07, + 0x11, 0x08, 0x14, 0x0a, 0x0a, 0x0a, 0x0a, + 0x0e, 0x16, 0x43, 0x06, 0x17, 0x48, 0x03, + 0x0e, 0x0d, 0x0b, 0x02, 0x16, 0x49, 0x12, + 0x1c, 0x06, 0x10, 0x11, 0x0b, 0x01, 0x49, + 0x0c, 0x00, 0x42, 0x27, 0x35, 0x3e, 0x12 +}; + +char key[4] = {0x6e, 0x6f, 0x64, 0x65}; + +void CheckPassword(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Isolate* isolate = env->isolate(); + + if (args.Length() != 1) { + std::cout << "give me one and only one argument" << std::endl; + args.GetReturnValue().Set(false); + return; + } + if (!args[0]->IsString()) { + std::cout << "we only accept string" << std::endl; + args.GetReturnValue().Set(false); + return; + } + + String* password = String::Cast(*args[0]); + if (!password->ContainsOnlyOneByte()) { + std::cout << "provide a utf8 string pleaze" << std::endl; + args.GetReturnValue().Set(false); + return; + } + + const int length = password->Utf8Length(isolate); + if (length != 7 * 6) { + std::cout << "password length is not correct" << std::endl; + args.GetReturnValue().Set(false); + return; + } + + char* buffer = (char*)malloc(sizeof(char) * length); + password->WriteUtf8(isolate, buffer, length); + + for (int i = 0; i < length; i++) { + // std::cout << (char)(encrypted[i] ^ key[i % 4]) << std::endl; + if (buffer[i] != (encrypted[i] ^ key[i % 4])) { + free(buffer); + args.GetReturnValue().Set(false); + return; + } + } + + free(buffer); + args.GetReturnValue().Set(true); +} + +void Initialize(Local target, + Local unused, + Local context, + void* priv) { + Environment* env = Environment::GetCurrent(context); + // Isolate* isolate = env->isolate(); + + env->SetMethod(target, "check_password", CheckPassword); +} + +} // namespace flag + +} // end namespace node + +NODE_MODULE_CONTEXT_AWARE_INTERNAL(flag, node::flag::Initialize) +