Skip to content

Instantly share code, notes, and snippets.

@matthw
Last active August 26, 2023 21:58
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save matthw/bbcb94c084691ac595187b8b2569685e to your computer and use it in GitHub Desktop.
Save matthw/bbcb94c084691ac595187b8b2569685e to your computer and use it in GitHub Desktop.
ASC Wargames 2023 Quals

Amber

File is packed, drop it in unpac.me, get unpacked PE. The PE does some antidebug checks and ends up doing some RC4 decryption of the flag. If the anti debug fails, the key will be incorrect.

patch the unpacked PE with EBFE after the RC4 so it does an infinite loop:

x = bytearray(open("daa6f5823995e8ffe4fd9de4f86358ec6e0b112c38c78c36f1ae0054277e43c8", "rb").read())
x[0x162b] = 0xeb
x[0x162c] = 0xfe
open("lol.exe", "wb").write(x)

end of main will look like

    local_10 = CONCAT13((char)uVar17,(undefined3)local_10);
    uVar5 = FUN_7ffa1166();
    local_c = CONCAT11(local_c._1_1_,uVar5);
    RC4((char *)&local_1c,(int)&local_50,pbVar6);
    do {
                    /* WARNING: Do nothing block with infinite loop */
    } while( true );
}

just run the exe and dump the VM mem:

% virsh dump win10 dmp.bin    
% strings dmp.bin| grep ASCW
ASCWG{What_A_ReverseEngineering_Beast_13337_-_-}

Awaken

Modify the assembly listing a bit so NASM is happy with it, like:

section .data
    .LC0:
    db "Enter The Flag:", 0
    .LC1:
    db "%s", 0
    .LC2:
    db "Wrong Flag", 0
    .LC3:
    db "Correct Flag", 0

section .bss
    flag_input resb 50

section .text
    global flag_check
    extern printf, puts, __isoc99_scanf

flag_check:
    push    rbp
    mov     rbp, rsp
    sub     rsp, 464

    mov     QWORD [rbp-112], 0
    mov     QWORD [rbp-104], 0
   
etc...

create bla.c

int flag_check(void);

int main()
{
    flag_check();
}

compile

% nasm -f elf64 bla.asm -o your_filename.o
% gcc -c bla.c
% gcc bla.o your_filename.o -no-pie -o lol

reverse the lol and solve:

array1 = b'Z\x84\x06E\xae\xcb\xe8\xf3W\xfe\xa6=^A\x08\xd03"!\x81 \xdd\x00\xa0#\xafq\x04\x8b\xf5\x18\x1d\xe1\x0fe\t\xceBx>\xc37\xca\x8fd2\xe0\xac\xde\x91|*\xc0\x07\xf4\x95\x9f@S\xe5g\xb6zRN?\x83K\xc9\x82r.v\x1c\xf1\x1e\xcc\xb7\xd7\xc7\x8a\x10y\x1aM\x195\x16}C+\xcd\x86\xabD\x92\xd4\x0e\x98\x14\xb9\x9b\xa7$\x1b<\xe2:\xd3\xf0\xfdOw\xd1\xa3\x0cH\x80j\xda\xbd\xd8G[\xfa\x96\x0b\xec\xcfI\xd9\x11\x7f\xb1\'\xe7\xc5\xb2c\xe6(6\xb3]\xfb\xdc\xa8p%\xf6\xb0\x9c\xa5_\xb89\xe4\x85\xa9\xfc\x13\x02Q0\xf2i\xfft\xbfY\xb5F\x17\xc2Xa\x99\xeb\xa4\x9e\x89\xeel\xef\xa2\x90s\x8cT\xbcm\xdb,\xd6\xe3\xa1\x8dP\xf74\xd5\xf9\x01{\x8e\xbehkU\x9d-\xed/\x93\x15\x1f\xc4\x88\xaa\xf8\r\\\xeaV\x03\xc1\x9a8\x05obJ\x12\xdf`\x94)u~\xad\xe9\n1\xb4\xbb\xba\x87;&\xd2nf\xc8L\x97\xc6'
array2 = b"689df84315f73e8c01efe535"
array3 = b'\x01\xa3\x1e\xc9\x89\x1b\xd8\xa2\xadd\x06l\x05\xea\xbeP$y\xc3\xf9*\xc0\xb9\xef#xG\xa5\x06\x99\x05\xbb\xfcJ\x06Z\xfe{\x84\x97\xec\x0e\xbd\x17\xb3\xf3\xc8\xf3\x3a\xef'

def find(n):
    for x in range(256):
        v10 = array1[x]
        v9 = array2[n % 24]
        if (n % 24) & 1 != 0:
            v9 ^= 0xff

        v10 ^= v9
        v10 = array1[v10]
        if v10 & 1 != 0:
            v10 ^= 0x42
        v10 ^= 0xff

        if v10 == array3[n]:
            return x
    return -1

output = ""
for x in range(len(array3)):
    output += chr(find(x))
print(output)
% python slv.py
ASCWG{What_do_you_see_before_it_is_over?_998bd0d4}

if first started to bruteforce counting instructions with vagrant but it got stuck at the last 3 chars, so switched strat...

Guess The Flag

main is a decoy, the meat in a _DT_INIT constructor.

The bin forks:

  • child process will load a shellcode in memory and jump to it, the shellcode decrypts itself.
  • parent uses ptrace to control the child

classic nanomite case.

The child part looks like this:

    pid = fork();
    if (pid == 0) {
        decrypted_code = (undefined8 *)mmap(NULL,0x6d0,7,0x22,-1,0);
        *decrypted_code = _LAB_00104080;
        decrypted_code[0xd9] = DAT_00104748;
        lVar3 = (long)decrypted_code - (long)(undefined8 *)((ulong)(decrypted_code + 1) & 0xfffffffffffffff8);
        puVar5 = (undefined8 *)(&LAB_00104080 + -lVar3);
        puVar6 = (undefined8 *)((ulong)(decrypted_code + 1) & 0xfffffffffffffff8);
        for (uVar4 = (ulong)((int)lVar3 + 0x6d0U >> 3); uVar4 != 0; uVar4 = uVar4 - 1) {
            *puVar6 = *puVar5;
            puVar5 = puVar5 + (ulong)bVar1 * -2 + 1;
            puVar6 = puVar6 + (ulong)bVar1 * -2 + 1;
        }
        ptrace(PTRACE_TRACEME,0,0,0);
        local_40 = decrypted_code;
        (*(code *)decrypted_code)(&input_flag);
                    /* WARNING: Subroutine does not return */
        exit(0);
    }

with a debugger, we can skip over the fork() and make sure eax=0, then follow inside the mmap'ed region and then dump the whole region

The parent controler looks like:

    while( true ) {
        _Var2 = waitpid(pid,(int *)&wstatus,0);
        if (_Var2 == -1) break;
        status = (int)wstatus >> 8;
                    /* INT3 */
        if (status == SIGTRAP) {
            ptrace(PTRACE_GETREGS,(ulong)pid,0,&regs);
            uVar4 = ptrace(PTRACE_PEEKTEXT,(ulong)pid,regs.rip,0);
            local_30 = uVar4 & 0xffffffff00000000 | 0x90909090;
            bVar1 = int3(regs.r11 & 0xff,regs.r10 & 0xffffffff);
            regs.r11 = (ulonglong)bVar1;
            ptrace(PTRACE_SETREGS,(ulong)pid,0,&regs);
        }
        else {
                    /* UD2 */
            if (status == SIGILL) {
                ptrace(PTRACE_GETREGS,(ulong)pid,0,&regs);
                uVar4 = ptrace(PTRACE_PEEKTEXT,(ulong)pid,regs.rip,0);
                local_30 = uVar4 & 0xffffffffffff0000 | 0x9090;
                ptrace(PTRACE_POKETEXT,(ulong)pid,regs.rip,local_30);
                ptrace(PTRACE_GETREGS,(ulong)pid,0,&regs);
                bVar1 = ud2(regs.r11 & 0xff,regs.r10 & 0xffffffff);
                regs.r11 = (ulonglong)bVar1;
                ptrace(PTRACE_SETREGS,(ulong)pid,0,&regs);
            }
            else {
                    /* idiv rcx */
                if (status == SIGFPE) {
                    ptrace(PTRACE_GETREGS,(ulong)pid,0,&regs);
                    bVar1 = add(regs.r11 & 0xff,regs.r10 & 0xff);
                    regs.r11 = (ulonglong)bVar1;
                    ptrace(PTRACE_SETREGS,(ulong)pid,0,&regs);
                    uVar4 = ptrace(PTRACE_PEEKTEXT,(ulong)pid,regs.rip,0);
                    local_30 = uVar4 & 0xffffffffff000000 | 0x909090;
                    ptrace(PTRACE_POKETEXT,(ulong)pid,regs.rip,local_30);
                }
                else if (status == SIGSEGV) {
                    ptrace(PTRACE_GETREGS,(ulong)pid,0,&regs);
                    bVar1 = xor(regs.r11 & 0xff,regs.r10 & 0xff);
                    regs.r11 = (ulonglong)bVar1;
                    ptrace(PTRACE_SETREGS,(ulong)pid,0,&regs);
                    uVar4 = ptrace(PTRACE_PEEKTEXT,(ulong)pid,regs.rip,0);
                    local_30 = uVar4 & 0xffffffff00000000 | 0x90909090;
                    ptrace(PTRACE_POKETEXT,(ulong)pid,regs.rip,local_30);
                    uVar4 = ptrace(PTRACE_PEEKTEXT,(ulong)pid,regs.rip + 4,0);
                    local_30 = uVar4 & 0xffffffff00000000 | 0x90909090;
                    ptrace(PTRACE_POKETEXT,(ulong)pid,regs.rip + 4,local_30);
                }
                else if (status == 0x1f) {
                    ptrace(PTRACE_GETREGS,(ulong)pid,0,&regs);
                    uVar4 = ptrace(PTRACE_PEEKTEXT,(ulong)pid,regs.rip,0);
                    local_30 = uVar4 & 0xffffffff00000000 | 0x90909090;
                    /* r13 must be 0 */
                    check_and_exit(regs.r13 & 0xff);
                }
                else if (status == 0x12) {
                    ptrace(PTRACE_GETREGS,(ulong)pid,0,&regs);
                    bVar1 = FUN_00101276(regs.r14 & 0xff,regs.r10 & 0xff);
                    regs.r11 = (ulonglong)bVar1;
                    ptrace(PTRACE_SETREGS,(ulong)pid,0,&regs);
                }
                else {
                    /* kill */
                    if (status == 0x1c) {
                        ptrace(PTRACE_GETREGS,(ulong)pid,0,&regs);
                        bVar1 = kill(regs.r14 & 0xff,regs.r10 & 0xff);
                        regs.r11 = (ulonglong)bVar1;
                        ptrace(PTRACE_SETREGS,(ulong)pid,0,&regs);
                    }
                    else if (((wstatus & 0x7f) == 0) || (status == SIGCHLD)) {
                        win = 0xbeedf8f1cd;
                        uStack_180 = 0xceb9ea;
                        uStack_17d = 0x99fdebf0fc;
                        for (j = 0; j < 0xd; j = j + 1) {
                            *(byte *)((long)&win + (long)j) = *(byte *)((long)&win + (long)j) ^ 0x99;
                        }
                        puts((char *)&win);
                    }
                }
            }
        }
        ptrace(PTRACE_CONT,(ulong)pid,0,0);
    }

basically it waits for the child to crash, and based on the signal, it will modify the child context and resumes execution

we can disassemble the dumped shellcode:

from capstone import *
import sys

def disasm(code):
    md = Cs(CS_ARCH_X86, CS_MODE_64)
    for i in md.disasm(code, 0x0):
        op_str = i.op_str.replace("qword", "").replace("dword", "").replace("byte", "").replace("ptr ", "")
        print("0x%x:\t%s\t%s" %(i.address, i.mnemonic, op_str))


with open(sys.argv[1], "rb") as fp:
    fp.seek(0x24) # ignore decryption stub
    data = fp.read()
    disasm(data)

it spits out something like

0x0:    mov r12b, 0xc9 
0x3:    xor r11, r11
0x6:    rol r12b, 5
0xa:    xor r12b, 0x2d 
0xe:    mov r11b,  [r9]     # load flag char
0x11:   mov r10, 0x1c 
0x18:   ud2                 # trigger parent with SIGILL (0x4), r11 will be modded
0x1a:   inc r9              # next flag char
0x1d:   xor r11b, r12b 
0x20:   or  r13b, r11b      # r13 remain 0, so r11 must be 0 (easy to autosolve)
0x23:   mov r12b, 0xcf 
0x26:   xor r11, r11
0x29:   rol r12b, 5
0x2d:   xor r12b, 0x2d 
0x31:   mov r11b,  [r9] 
0x34:   mov r14b,  [r9] 
0x37:   mov r10, 6
0x3e:   mov rax, 0x3e 
0x45:   mov rdi, 0
0x4c:   mov rsi, 0x1c 
0x53:   syscall         # eax=0x3e, rsi=0x1c, rdi=0 > kill(0, 0x1c) -> trigger parents with 0x1c signal
0x55:   inc r9
0x58:   xor r11b, r12b 
0x5b:   or  r13b, r11b 
0x5e:   mov r12b, 0x2a 
0x61:   xor r11, r11
0x64:   rol r12b, 5
0x68:   xor r12b, 0x2d 
0x6c:   mov r11b,  [r9] 
0x6f:   mov r10, 0x15 
0x76:   int3            # SIGTRAP
0x77:   inc r9
.....

when reversing we find out that:

  • r9 is a pointer to the inputflag
  • r13 must remains 0 at all time if the flag is correct
  • r11 is modified by the parent when the child crash.

there's few possible signals from the child:

  • SIGILL -> UD2
  • SIGTRAP -> INT3
  • SIGFPE -> IDIV ECX with ECX == 0
  • SIGSEGV -> MOV RAX, [0]
  • signals numbers 0x12 and 0x1c, raised by syscall 0x3e (kill)

based on the signal, the parent will do "something" and resume execution.

due to the very low number of different instructions, i used a shitty emulator (tm) parsing the disassembly, plugged to a z3 solver to save me from using my brain trying to actually reverse that:

from z3 import *

rol = lambda val, r_bits, max_bits: \
    (val << r_bits%max_bits) & (2**max_bits-1) | \
    ((val & (2**max_bits-1)) >> (max_bits-(r_bits%max_bits)))
 

def fix(name):
    # r12b -> r12
    name = name.rstrip(',')
    if not name.startswith("0x"):
        return name.rstrip("b")
    return name

class Emu:
    def __init__(self):
        self.flag = []
        for x in range(40):
            c = BitVec("f%d"%x, 8)
            self.flag.append(c)

        self.st = {
                "r9": 0,
                "r11": 0,
                }

        self.found = ''

    def val(self, name):
        if name.startswith("0x"):
            return int(name[2:], 16)
        try:
            return int(name)
        except:
            return self.st[fix(name)]

    def solve(self, x):
        s = Solver()
        s.add(x)
        assert s.check() == sat
        m = s.model()
        self.found += chr(m[m[0]].as_long())
        print("flag:  " + self.found)


    def emu(self, code):
        for l in code:
            l = l.split()[1:]
            print(l)
            op = l[0]

            match op:
                case 'mov':
                    dst = fix(l[1])
                    src = fix(l[2])

                    if src.startswith("["):
                        # load flag
                        if src == '[r9]':
                            self.st[dst] = self.flag[self.st["r9"]]
                        # SIGSEGV
                        elif src == '[0]':
                            self.st["r11"] ^= self.st["r10"]

                        else:
                            raise
                    else:
                        self.st[dst] = self.val(src)

                case 'xor':
                    dst = fix(l[1])
                    src = fix(l[2])
                    self.st[dst] ^= self.val(src)

                case 'rol':
                    dst = fix(l[1])
                    src = fix(l[2])
                    self.st[dst] = rol(self.st[dst], self.val(src), 8)

                case 'inc':
                    dst = fix(l[1])
                    self.st[dst] += 1

                case 'or':
                    # always or r13, r11
                    # r13 must remains, so r11 must be 0
                    # send that to z3
                    self.solve(self.st["r11"] == 0)

                case 'idiv':
                    # SIGFPE
                    self.st["r11"] += self.st["r10"]

                case 'ud2':
                    # SIGILL
                    left  = (-(self.st["r10"] & 7) & 7)
                    right = self.st["r10"] & 7 
                    self.st["r11"] = RotateLeft(self.st["r11"], left) | RotateRight(self.st["r11"], right)

                case 'syscall':
                    sysnr = self.st["rax"]
                    #print(hex(sysnr))
                    # kill
                    if sysnr == 0x3e:
                        signum = self.st["rsi"]
                        #print(hex(signum))
                        if signum == 0x1c:
                            r10 = self.st["r10"] ^ 0x44
                            left  = (-(r10 & 7) & 7)
                            right = r10 & 7
                            self.st["r11"] = RotateLeft(self.st["r14"], left) | RotateRight(self.st["r14"], right)
                        elif signum == 0x12:
                            r10 = self.st["r10"] ^ 0x41
                            left = r10 & 7
                            right = (-(r10 & 7) & 7)
                            self.st["r11"] = RotateLeft(self.st["r11"], left) | RotateRight(self.st["r11"], right)
                        else:
                            raise

                    # the end
                    elif sysnr == 0x12c:
                        print("-----------------")
                        print(self.found)
                        import sys
                        sys.exit(1)
                    else:
                        raise

                case 'int3':
                    # SIGTRAP
                    #r11 = (int)(uint)r11 >> (-(r10 & 7) & 7) | (uint)r11 << (r10 & 7)
                    left = self.st["r10"] & 7
                    right = (-(self.st["r10"] & 7) & 7)
                    self.st["r11"] = RotateLeft(self.st["r11"], left) | RotateRight(self.st["r11"], right)

                case _:
                    raise

            print(self.st)



if __name__ == "__main__":
    e = Emu()
    e.emu(open("shellc.asm").readlines())

when running it spits the flag at the end:

 python emu.py 
['mov', 'r12b,', '0xc9']
{'r9': 0, 'r11': 0, 'r12': 201}
['xor', 'r11,', 'r11']
{'r9': 0, 'r11': 0, 'r12': 201}
['rol', 'r12b,', '5']
{'r9': 0, 'r11': 0, 'r12': 57}
['xor', 'r12b,', '0x2d']
{'r9': 0, 'r11': 0, 'r12': 20}
['mov', 'r11b,', '[r9]']
{'r9': 0, 'r11': f0, 'r12': 20}
['mov', 'r10,', '0x1c']
{'r9': 0, 'r11': f0, 'r12': 20, 'r10': 28}
['ud2']
{'r9': 0, 'r11': RotateLeft(f0, 4) | RotateRight(f0, 4), 'r12': 20, 'r10': 28}
['inc', 'r9']
{'r9': 1, 'r11': RotateLeft(f0, 4) | RotateRight(f0, 4), 'r12': 20, 'r10': 28}
['xor', 'r11b,', 'r12b']
{'r9': 1, 'r11': (RotateLeft(f0, 4) | RotateRight(f0, 4)) ^ 20, 'r12': 20, 'r10': 28}
['or', 'r13b,', 'r11b']
flag:  A

.....


['xor', 'r12b,', '0x2d']
{'r9': 37, 'r11': (RotateLeft(f36, 0) | RotateRight(f36, 0)) ^
56 ^
(RotateLeft(f36, 0) | RotateRight(f36, 0)) ^
56, 'r12': 95, 'r10': 16, 'r14': f35, 'rax': 62, 'rdi': 0, 'rsi': 28}
['mov', 'r11b,', '[r9]']
{'r9': 37, 'r11': f37, 'r12': 95, 'r10': 16, 'r14': f35, 'rax': 62, 'rdi': 0, 'rsi': 28}
['mov', 'r10,', '0xa']
{'r9': 37, 'r11': f37, 'r12': 95, 'r10': 10, 'r14': f35, 'rax': 62, 'rdi': 0, 'rsi': 28}
['ud2']
{'r9': 37, 'r11': RotateLeft(f37, 6) | RotateRight(f37, 2), 'r12': 95, 'r10': 10, 'r14': f35, 'rax': 62, 'rdi': 0, 'rsi': 28}
['inc', 'r9']
{'r9': 38, 'r11': RotateLeft(f37, 6) | RotateRight(f37, 2), 'r12': 95, 'r10': 10, 'r14': f35, 'rax': 62, 'rdi': 0, 'rsi': 28}
['xor', 'r11b,', 'r12b']
{'r9': 38, 'r11': (RotateLeft(f37, 6) | RotateRight(f37, 2)) ^ 95, 'r12': 95, 'r10': 10, 'r14': f35, 'rax': 62, 'rdi': 0, 'rsi': 28}
['or', 'r13b,', 'r11b']
flag:  ASCWG{N0w_1_4m_B3c0m3_D34th_500861fd8}
{'r9': 38, 'r11': (RotateLeft(f37, 6) | RotateRight(f37, 2)) ^ 95, 'r12': 95, 'r10': 10, 'r14': f35, 'rax': 62, 'rdi': 0, 'rsi': 28}
['mov', 'rax,', '0x12c']
{'r9': 38, 'r11': (RotateLeft(f37, 6) | RotateRight(f37, 2)) ^ 95, 'r12': 95, 'r10': 10, 'r14': f35, 'rax': 300, 'rdi': 0, 'rsi': 28}
['syscall']
-----------------
ASCWG{N0w_1_4m_B3c0m3_D34th_500861fd8}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment