angstromCTF. Interesting moments in reverse tasks ("Patcherman", "Masochistic Sudoku", "Autorev, Assemble!")
@ Rakovsky Stanislav | Friday, Mar 20, 2020 | 7 minutes read | Update at Friday, Mar 20, 2020

Patcherman

Masochistic Sudoku

Autorev, Assemble!


reverse, 100 points > Patcherman

Oh no! We were gonna make this an easy challenge where you just had to run the binary
and it gave you the flag, but then clamcame along under the name of "The Patcherman"
and edited the binary! I think he also touched some bytes in the header to throw off
disassemblers. Can you still retrieve the flag?

Alternatively, find it on the shell server at /problems/2020/patcherman/.

Author: aplet123

File

Main function:

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  int v3; // eax
  int i; // [rsp+Ch] [rbp-4h]

  setvbuf(stdout, 0LL, 2, 0LL);
  dword_601054 = ptrace(PTRACE_TRACEME, 1337LL, 0x1337LL, 1337LL);
  if ( dword_601054 == -1 || leetbeef_dword != 0x1337BEEF )
  {
    puts("Hey you're not supposed to get the flag! Freeze!");
    signal(2, (__sighandler_t)1);
    signal(15, (__sighandler_t)1);
    signal(6, (__sighandler_t)1);
    while ( 1 )
      ;
  }
  to_leetbeef_dword = leetbeef_dword;
  for ( i = 0; i <= 6; ++i )
  {
    v3 = sub_400672();
    flag_array[i] = summator(dword_601060[i] ^ (unsigned int)dword_601054, v3);
  }
  printf("Here have a flag:\n%s\n", flag_array);
  return 0LL;
}

Okay, we need to bypass the first two checks.

For the first check we should NOP _ptrace call or “Jump to IP” standing on main+3F to main+44.

...
main+F                    mov     ecx, 0          ; n
main+14                   mov     edx, 2          ; modes
main+19                   mov     esi, 0          ; buf
main+1E                   mov     rdi, rax        ; stream
main+21                   call    _setvbuf
main+26                   mov     ecx, 539h
main+2B                   mov     edx, 1337h
main+30                   mov     esi, 539h
main+35                   mov     edi, 0          ; request
main+3A                   mov     eax, 0
main+3F                   call    _ptrace
main+44                   mov     cs:dword_601054, eax
main+4A                   mov     eax, cs:dword_601054
main+50                   cmp     eax, 0FFFFFFFFh
main+53                   jz      short loc_400775
main+55                   mov     eax, cs:leetbeef_dword
main+5B                   cmp     eax, 1337BEEFh
main+60                   jz      short loc_4007B0

For the second check we need to patch dword 601050 from F00DBABE to 1337BEEF.

So, well done, you are awesome.

Flag: actf{p4tch3rm4n_15_n0_m0r3}


reverse, 160 points > Masochistic Sudoku

Clam's tired of the ease and boredom of traditional sudoku.
Having just one solution that can be determined via a simple online sudoku solver
isn't good enough for him. So, he made masochistic sudoku! Since there are no hints,
there are around 6*10^21 possible solutions but only one is actually accepted!

Find it on the shell server at /problems/2020/masochistic_sudoku/.

Author: aplet123

File

Yes, it’s actually a sudoku with empty fields.

Okay, while analyzing this program we can notice 30 shellcodes from 40124E to 040171F

The basic structure of shellcode blocks:

.text:00000000004016D2                 mov     eax, cs:dword_603290 ; cell of sudoku matrix with our answer
.text:00000000004016D8                 mov     edx, eax
.text:00000000004016DA                 mov     esi, 4 ; param 1
.text:00000000004016DF                 mov     edi, 8 ; param 2
.text:00000000004016E4                 call    gen_value
.text:00000000004016E9                 cmp     eax, 134A8092h ; check value
.text:00000000004016EE                 setz    al
.text:00000000004016F1                 movzx   eax, al
.text:00000000004016F4                 mov     edi, eax
.text:00000000004016F6                 call    assert

Let’s look at gen_value (I’ve renamed values):

int __fastcall gen_value(int var2, int var1, int my_ans)
{
  srand(13 * ((100 * var2 + 10 * var1 + my_ans) ^ 0x2A) % 10067);
  return rand();
}

So, we need to play with glibc randomizer :) *sad Stas noises*

Okay, power of yara, I raise you!

rule sudoku_hunter
{
  strings:
    $chunk_1 = {
      8B 05 ?? ?? ?? ?? // our dword 2-5
      89 C2
      BE ?? ?? ?? ?? // param 1 9-12
      BF ?? ?? ?? ?? // param 2 14-17
      E8 ?? ?? ?? ?? // call our func
      3D ?? ?? ?? ?? // comparison value 24-27
      0F 94 C0
      0F B6 C0
      89 C7
      E8 // jump to assert
    }
  
  condition:
    any of them
}

… and the solution:

import yara
import os

rule = yara.compile("sudoku.yar")
match = rule.match("masochistic_sudoku")[0]


import struct
def parse_values(match_strings): # parsing values from yara matched string
    ms = match_strings
    ans = []
    correct = 0
    len_of_shellcode_block = 41
    for _, _, bts in ms:
        ans.append({
            "sudoku_pos":(struct.unpack("<I", bts[2:6])[0] - 0x201f04 + len_of_shellcode_block*correct)//4,
            "var1":struct.unpack("<I", bts[9:13])[0],
            "var2":struct.unpack("<I", bts[14:18])[0],
            "rand":struct.unpack("<I", bts[24:28])[0]
                   })
        correct+=1
    return ans

from ctypes import CDLL
def hack_sudoku(ans_from_parse_values): # pseudo-random magic
    ans = {}
    libc = CDLL("libc.so.6")
    i = -1
    for afpv in ans_from_parse_values:
        i+=1
        for guess in range(1, 10):
            seed = 13 * ((100 * afpv["var2"] + 10 * afpv["var1"] + guess) ^ 0x2A) % 10067
    
            libc.srand(seed)
            if libc.rand() == afpv["rand"]:
                print("hacked", i)
                ans.update({afpv["sudoku_pos"]: guess})
                break
    return ans

h = hack_sudoku(parse_values(match.strings))
for i in range(81):
    if i not in h.keys():
        h.update({i: "_"})
for i in range(9):
    print(" ".join(str(h[i]) for i in range(9*i, 9*i+9))) # printing my answer
1 _ _ _ 6 _ 8 5 _
_ _ 5 _ 8 3 1 _ _
_ _ _ _ 1 2 _ 9 _
9 _ 7 _ _ _ _ _ _
5 3 _ _ _ _ _ 8 9
_ _ _ _ _ _ 3 _ 5
_ 4 _ 6 2 _ _ _ _
_ _ 6 1 9 _ 7 _ _
_ 2 1 _ 3 _ _ _ 4

Use any online sudoku solver, put the solution in the program on the angstrom server, get the flag.

Wow you're good at sudoku!
actf{sud0ku_but_f0r_pe0ple_wh0_h4te_th3mselves}

Actually a quite mind breaking task.

reverse, 125 points > Autorev, Assemble!

Clam was trying to make a neural network to automatically do reverse engineering
for him, but he made a typo and the neural net ended up making a reverse engineering
challenge instead of solving one! Can you get the flag?

Find it on the shell server at /problems/2020/autorev_assemble/
or over tcp at nc shell.actf.co 20203.

Author: aplet123

File

main:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  puts("PROBLEM CREATION MODE: ON");
  puts("VISUAL BASIC GUI: ON");
  puts("HACKERMAN: ON");
  puts("HOTEL: TRIVAGO");
  puts("INPUT: ?");
  fgets(z, 256, stdin);
  if ( (unsigned int)f992(z, 256LL)
    && (unsigned int)f268(z)
    && (unsigned int)f723(z)
    && (unsigned int)f611(z)
    && (unsigned int)f985(z)
    && (unsigned int)f45(z)
    && (unsigned int)f189(z)
    && (unsigned int)f362(z)
    && (unsigned int)f857(z) // line 17
    ...
    && (unsigned int)f923(z) // line 205
    && (unsigned int)f372(z)
    && (unsigned int)f906(z)
    && (unsigned int)f915(z) )
  {
    puts("CHALLENGE: SOLVED");
  }
  else
  {
    puts("YOUR SKILL: INSUFFICIENT");
  }
  return 0;

Look the insides of any function:

push    rbp
mov     rbp, rsp
mov     [rbp+var_8], rdi
mov     rax, [rbp+var_8]
add     rax, 7Bh
movzx   eax, byte ptr [rax]
cmp     al, 5Fh
setz    al
movzx   eax, al
pop     rbp
retn

Yet another auto task.

Let’s parse the values using idapython:

from idautils import *
from idaapi import *


ans = {}
offset = 0
ea = BeginEA()

for funcea in Functions(SegStart(ea), SegEnd(ea)): # walk through all the functions
    functionName = GetFunctionName(funcea)
    if functionName.startswith("f") and functionName[1] in "0123456789":# looking for out funcs
        print(functionName)
        for (startea, endea) in Chunks(funcea):
            for head in Heads(startea, endea):
                if GetMnem(head) == "add" and "rax" in GetOpnd(head, 0): # looking for offset changes
                    offset = int(GetOpnd(head, 1).replace("h", ""), 16)
                    print offset
                if GetMnem(head) == "cmp" and "al" in GetOpnd(head, 0):
                    cmp_val = int(GetOpnd(head, 1).replace("h", ""), 16)
                    if offset in ans.keys():
                        print("RW", offset)
                    ans.update({offset:cmp_val})

Fmmm, where are much more functions than in main function. And we have a lot of “RW” warnings.

… Make xref check for the functions:

from idautils import *
from idaapi import *



ans = {}
offset = 0
ea = BeginEA()

for funcea in Functions(SegStart(ea), SegEnd(ea)): # walk through all the functions
    functionName = GetFunctionName(funcea)
    if not list(XrefsTo(funcea)): # check
        continue
    if functionName.startswith("f") and functionName[1] in "0123456789":# looking for out funcs
        print(functionName)
        for (startea, endea) in Chunks(funcea):
            for head in Heads(startea, endea):
                if GetMnem(head) == "add" and "rax" in GetOpnd(head, 0): # looking for offset changes
                    offset = int(GetOpnd(head, 1).replace("h", ""), 16)
                    print offset
                if GetMnem(head) == "cmp" and "al" in GetOpnd(head, 0):
                    cmp_val = int(GetOpnd(head, 1).replace("h", ""), 16)
                    if offset in ans.keys():
                        print("RW", offset)
                    ans.update({offset:cmp_val})

… doubles again: f680 - f153 and f215 - f359

Cause we don’t predict SUB instruction instead of ADD

The third version:

from idautils import *
from idaapi import *


ans = {}
offset = 0
ea = BeginEA()

for funcea in Functions(SegStart(ea), SegEnd(ea)): # walk through all the functions
    functionName = GetFunctionName(funcea)
    if not list(XrefsTo(funcea)):
        continue
    if functionName.startswith("f") and functionName[1] in "0123456789":# looking for out funcs
        print(functionName)
        for (startea, endea) in Chunks(funcea):
            for head in Heads(startea, endea):
                if GetMnem(head) == "add" and "rax" in GetOpnd(head, 0): # looking for offset changes
                    offset = int(GetOpnd(head, 1).replace("h", ""), 16)
                    print offset
                if GetMnem(head) == "sub" and "rax" in GetOpnd(head, 0): # looking for offset changes
                    offset = 0xFFFFFFFFFFFFFFFF - int(GetOpnd(head, 1).replace("h", ""), 16)+1
                    print offset
                if GetMnem(head) == "cmp" and "al" in GetOpnd(head, 0):
                    cmp_val = int(GetOpnd(head, 1).replace("h", ""), 16)
                    if offset in ans.keys():
                        print("RW", offset)
                    ans.update({offset:cmp_val})
                    offset = 0
                    break

Concat it:

Python>"".join(chr(ans[i]) for i in range(len(ans.keys())))
Blockchain big data solutions now with added machine learning. Enjoy!
I sincerely hope you actf{wr0t3_4_pr0gr4m_t0_h3lp_y0u_w1th_th1s_df93171eb49e21a3a436e186bc68a5b2d8ed} instead of doing it by hand.

Cats!

Under construction

Wow! Flipable!

Hello from another side of the Moon!

Looking for a flag? Okay, take this:
LFXXKIDBOJSSA43XMVSXIIC6LYQCAIA=

About me

Under construction. You can try to contact me and fill this field… haha… ha…