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
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
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
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.