[0] % cgrep -RiE "pass|api|key|secret|pico" *
picoCTF{grep_and_you_will_find_cdf2e7c2}
[0] % nc 2018shell.picoctf.com 10854
That wasn't so hard was it?
picoCTF{NEtcat_iS_a_NEcESSiTy_c97963fe}
Using Frida:
// agent.js
'use strict';
//const target_module = "be-quick-or-be-dead-1";
/*
* NOTE: this only works since the binary is not compiled with ASLR.
*
* $ rabin2 -I be-quick-or-be-dead-1 | grep pic
* pic false
*/
const target_addr = "0x400742"
var old_func = new NativeFunction(
ptr(target_addr),
'void',
['void']
);
var new_func = new NativeCallback(
function(args) {
console.log('[+] Doing nothing...');
},
'void',
['void']
);
Interceptor.replace(old_func, new_func);
Invoking with:
$ frida -f be-quick-or-be-dead-1 -l agent.js
%resume
[Local::be-quick-or-be-dead-1]-> %resume
Be Quick Or Be Dead 1
=====================
Calculating key...
[+] Doing nothing...
[Local::be-quick-or-be-dead-1]-> Done calculating key
Printing flag:
picoCTF{why_bother_doing_unnecessary_computation_402ca676}
Process terminated
[Local::be-quick-or-be-dead-1]->
Answer is:
.intel_syntax noprefix
.bits 32
.global asm1
asm1:
push ebp
mov ebp,esp
cmp DWORD PTR [ebp+0x8],0x9a
jg part_a # if arg1 > 0x9a, jmp to part_a
cmp DWORD PTR [ebp+0x8],0x8
jne part_b
mov eax,DWORD PTR [ebp+0x8]
add eax,0x3
jmp part_d
part_a:
cmp DWORD PTR [ebp+0x8],0x2c
jne part_c # if arg1 != 0x2c, jmp part_c
mov eax,DWORD PTR [ebp+0x8]
sub eax,0x3
jmp part_d
part_b:
mov eax,DWORD PTR [ebp+0x8]
sub eax,0x3
jmp part_d
cmp DWORD PTR [ebp+0x8],0xc8
jne part_c
mov eax,DWORD PTR [ebp+0x8]
sub eax,0x3
jmp part_d
part_c:
mov eax,DWORD PTR [ebp+0x8] # we end up here
add eax,0x3
part_d:
pop ebp
ret
After a bit of debugging, it turns out that the following bit is the one responsible for writing “You are Winner” string.
│ │ ; CODE XREF from sym.do_magic (0x8048711)
│ ┌──> 0x080486bd 8b45e8 mov eax, dword [local_18h] ; Jump is taken since size > 0
│ ╎│ 0x080486c0 0558880408 add eax, obj.sekrutBuffer ; 0x8048858 ; ")\x06\x16O+50\x1eQ\x1b[\x14K\b]+S\x10TQCM\T]"
│ ╎│ 0x080486c5 0fb608 movzx ecx, byte [eax]
│ ╎│ 0x080486c8 8b55e8 mov edx, dword [local_18h]
│ ╎│ 0x080486cb 8b45ec mov eax, dword [s]
│ ╎│ 0x080486ce 01d0 add eax, edx
│ ╎│ 0x080486d0 0fb600 movzx eax, byte [eax]
│ ╎│ 0x080486d3 31c8 xor eax, ecx
│ ╎│ 0x080486d5 8845e3 mov byte [local_1dh], al
│ ╎│ 0x080486d8 8b1538a00408 mov edx, dword obj.greetingMessage ; [0x804a038:4]=0x80487f0 str.You_have_now_entered_the_Duck_Web__a
│ ╎│ 0x080486de 8b45e8 mov eax, dword [local_18h]
│ ╎│ 0x080486e1 01d0 add eax, edx
│ ╎│ 0x080486e3 0fb600 movzx eax, byte [eax]
│ ╎│ 0x080486e6 3a45e3 cmp al, byte [local_1dh]
│ ┌───< 0x080486e9 7504 jne 0x80486ef ;[3] ; likely ; (if local_1ch == 0x19, we win)
│ │╎│ 0x080486eb 8345e401 add dword [local_1ch], 1 ; local_1ch increments everytime local_1dh == al
│ │╎│ ; CODE XREF from sym.do_magic (0x80486e9)
│ └───> 0x080486ef 837de419 cmp dword [local_1ch], 0x19 ; if local_1ch == 0x19, we win
│ ┌───< 0x080486f3 7512 jne 0x8048707 ;[4] ; likely
│ │╎│ 0x080486f5 83ec0c sub esp, 0xc
│ │╎│ 0x080486f8 68ab880408 push str.You_are_winner ; 0x80488ab ; "You are winner!" ; const char *s
│ │╎│ 0x080486fd e86efdffff call sym.imp.puts ;[5] ; int puts(const char *s)
│ │╎│ │ ; int puts(const char * s : (*0xffffffff)0x00177fec = .........................................................................................................
│ │╎│ 0x08048702 83c410 add esp, 0x10
│ ┌────< 0x08048705 eb0c jmp 0x8048713 ;[6]
│ ││╎│ ; CODE XREF from sym.do_magic (0x80486f3)
│ │└───> 0x08048707 8345e801 add dword [local_18h], 1
│ │ ╎│ ; CODE XREF from sym.do_magic (0x80486bb)
│ │ ╎└─> 0x0804870b 8b45e8 mov eax, dword [local_18h]
│ │ ╎ 0x0804870e 3b45f0 cmp eax, dword [size]
│ │ └──< 0x08048711 7caa jl 0x80486bd ;[7] ; unlikely ; (Jump is taken since size > 0)
│ └────> 0x08048713 c9 leave
└ 0x08048714 c3 ret
We can solve this with symbolic execution (TODO), but looking at the code with a debugger, we figured out the following:
[s]
.obj.sekrutBuffer
holds the following byte blob: )\x06\x16O+50\x1eQ\x1b[\x14K\b]+S\x10TQCM\T]
0x080486d3
, our input and obj.sekrutBuffer
get XOR-ed together.obj.greetingMessage
, which gets compared together in 0x080486e6
So, Our Input Buffer “B” gets XOR-ed with secret buffer “S”, and the result has to be the greeting message “G”:
B X S = G
We need “B”. To get it, we can just XOR “S” and “G” together. The following Python snippet does just that.
import sys
import base64
from pwn import *
def main():
str_to_encode = "You have now entered the Duck Web, and you're in for"
# Taken from gdb:
# gef➤ x/8xw 0x8048858
# 0x8048858 <sekrutBuffer>: 0x4f160629 0x1e30352b 0x145b1b51 0x2b5d084b
# 0x8048868 <sekrutBuffer+16>: 0x51541053 0x545c4d43 0x6f4e005d 0x6e696c20
xor_key = p32(0x4f160629)
xor_key += p32(0x1e30352b)
xor_key += p32(0x145b1b51)
xor_key += p32(0x2b5d084b)
xor_key += p32(0x51541053)
xor_key += p32(0x545c4d43)
xor_key += p32(0x6f4e005d)
print('Input: ' + str(str_to_encode))
print('XOR key: ' + str(xor_key))
print('--------------')
# Encode
plaintext_key_list = [hex(ord(elem)) for elem in str_to_encode]
xor_key_list = [hex(ord(elem)) for elem in xor_key]
cipher_str = ''
for idx in range(len(xor_key_list)):
a = int(plaintext_key_list[idx], 16)
b = int(xor_key_list[idx], 16)
x = a ^ b
cipher_str += chr(x)
print('Output: ' + cipher_str)
cipher_str = base64.b64encode(cipher_str.encode('utf-8')).decode('utf-8')
print('b64 Output: ' + cipher_str)
if __name__ == '__main__':
main()
Running it gives us the following:
[0] % py2 encoder.py
Input: You have now entered the Duck Web, and you're in for
XOR key: )\x06\x16O+50\x1e]+S\x10TQCM\T]\x00No
--------------
Output: picoCTF{qu4ckm3_6b15c941}D;\x0c
b64 Output: cGljb0NURntxdTRja20zXzZiMTVjOTQxfUQ7DA==
Flag is picoCTF{qu4ckm3_6b15c941}
I think the best way to solve those assembly mazes is not to trace it but to simply run/debug it.
Challenge assembly file. The header is modified a bit to accomodate for yasm syntax
SECTION .TEXT
GLOBAL asm2
asm2:
push ebp
mov ebp,esp
sub esp,0x10
mov eax,DWORD [ebp+0xc]
mov DWORD [ebp-0x4],eax
mov eax,DWORD [ebp+0x8]
mov DWORD [ebp-0x8],eax
jmp part_b
part_a:
add DWORD [ebp-0x4],0x1
add DWORD [ebp+0x8],0x64
part_b:
cmp DWORD [ebp+0x8],0x1d89
jle part_a
mov eax,DWORD [ebp-0x4]
mov esp,ebp
pop ebp
ret
C file to run the function asm2
#include <stdio.h>
extern unsigned int asm2(unsigned int, unsigned int);
int main(void) {
printf("%d\n", asm2(0x4, 0x2d));
return 0;
}
Compilation and linkage
[0] % yasm -f elf32 loop_asm_rev.S
[0] % gcc -m32 -c -o main.o main.c
[0] % gcc -m32 -o run loop_asm_rev.o main.o
[0] % ./run
121
[0] % rax2 121
9x79
Answer is 0x79
This one is really cool. Running the binary, shows the following:
[0] % ./be-quick-or-be-dead-2
Be Quick Or Be Dead 2
=====================
Calculating key...
You need a faster machine. Bye bye.
Plugging the setTimer
with Frida just like in the be-quick-or-be-dead-1
doesn’t work either:
[0] % frida -f be-quick-or-be-dead-2 -l agent.js
____
/ _ | Frida 12.2.30 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at http://www.frida.re/docs/home/
Spawned `be-quick-or-be-dead-2`. Use %resume to let the main thread start executing!
[Local::be-quick-or-be-dead-2]-> %resume
Be Quick Or Be Dead 2
=====================
Calculating key...
And it just keeps going for a very long time. It looks like the calculation method is either broken or it really does take a long time.
Taking a look with Radare yields the following:
$ r2 -A be-quick-or-be-dead-2
[0x0040074b 20% 345 be-quick-or-be-dead-2]> pd $r @ sym.calculate_key
┌ (fcn) sym.calculate_key 16
│ sym.calculate_key ();
│ ; CALL XREF from sym.get_key (0x4007e1)
│ 0x0040074b 55 push rbp
│ 0x0040074c 4889e5 mov rbp, rsp
│ 0x0040074f bf3b040000 mov edi, 0x43b
│ 0x00400754 e8adffffff call sym.fib ;[1] ; sym.fib(0x43b)
│ 0x00400759 5d pop rbp
└ 0x0040075a c3 ret ; rsp
The calculate_key
function apparently calculates the Fibonacci series of 0x43b (1083). From this link, the already calculated Fibonacci series of 1083 is:
My computer would need a lot of time to calculate that. If we take a look at the fib
function, we’ll actually see that the return value is stored in EAX
register. That means that the result of fib(0x43b)
will actually be warped into a 32-bit register:
[0x00400706 19% 345 be-quick-or-be-dead-2]> pd $r @ sym.fib
┌ (fcn) sym.fib 69
│ sym.fib (unsigned int arg1);
│ ; var unsigned int local_24h @ rbp-0x24
│ ; var unsigned int local_14h @ rbp-0x14
│ ; arg unsigned int arg1 @ rdi
│ ; CALL XREFS from sym.fib (0x400728, 0x400737)
│ ; CALL XREF from sym.calculate_key (0x400754)
│ 0x00400706 55 push rbp
...
...
...
│ 0x00400741 8b45ec mov eax, dword [local_14h]
│ 0x00400744 4883c428 add rsp, 0x28
│ 0x00400748 5b pop rbx
│ 0x00400749 5d pop rbp
└ 0x0040074a c3 ret
We would need then to just have Frida insert that warped figure as the return value of fib
function and we’re good to go.
In python:
$ python
>>> # Number shortened to save space
>>> fib(0x43b) % int(math.pow(2,32))
3228060226
The Frida agent would look like this now:
'use strict';
const fib_0x43b = 3228060226;
decrypt_flag_func_addr = ptr("0x00400696");
fib_func_addr = ptr("0x00400706");
// Replacing fib with our own implementation that just returns the number without calculation
var old_fib = new NativeFunction(
fib_func_addr,
'uint',
['uint']
);
var new_fib = new NativeCallback(
function(args) {
return fib_0x43b;
},
'uint',
['uint']
);
Interceptor.replace(old_fib, new_fib);
// We'll just print `this.context` which allows us to see the register values to confirm that
// the supplied argument, usually in the RDI register, is the correct Fibonacci number we returned
Interceptor.attach(decrypt_flag_func_addr, {
onEnter: function (args) {
console.log('Context : ' + JSON.stringify(this.context));
}
});
Running everything would yield:
[1] % frida -f be-quick-or-be-dead-2 -l agent.js
____
/ _ | Frida 12.2.30 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at http://www.frida.re/docs/home/
Spawned `be-quick-or-be-dead-2`. Use %resume to let the main thread start executing!
[Local::be-quick-or-be-dead-2]-> %resume
Be Quick Or Be Dead 2
=====================
Calculating key...
Done calculating key
Printing flag:
picoCTF{the_fibonacci_sequence_can_be_done_fast_88f31f48}
[+] Doing nothing...
Context : {"pc":"0x400696","sp":"0x7ffd678750e8","rax":"0xc0684a42","rcx":"0x7f7c76fd184f","rdx":"0x7f7c770a5720","rbx":"0x0","rsp":"0x7ffd678750e8","rbp":"0x7ffd678750f0","rsi":"0x1715ac0","rdi":"0xc0684a42","r8":"0x0","r9":"0xffffffff","r10":"0x7f7c75255530","r11":"0x0","r12":"0x4005a0","r13":"0x7ffd678751f0","r14":"0x0","r15":"0x0","rip":"0x400696"}
The flag is picoCTF{the_fibonacci_sequence_can_be_done_fast_88f31f48}
Let’s plug the setTimer
function to get rid of it.
[0x004008c4]> s 0x004008c4
[0x004008c4]> pd 10
Free fake stack
│ 0x004008c4 e8f8feffff call sym.set_timer
│ 0x004008c9 b800000000 mov eax, 0
│ 0x004008ce e842ffffff call sym.get_key
│ 0x004008d3 b800000000 mov eax, 0
│ 0x004008d8 e863ffffff call sym.print_flag
│ 0x004008dd b800000000 mov eax, 0
│ 0x004008e2 c9 leave ; rsp
└ 0x004008e3 c3 ret
0x004008e4 662e0f1f8400. nop word cs:[rax + rax]
0x004008ee 6690 nop
[0x004008c4]> wao nop
Failed to write
[0x004008c4]> oo+
[0x004008c4]> wao nop
[0x004008c4]> pd 10
Free fake stack
│ 0x004008c4 90 nop
│ 0x004008c5 90 nop
│ 0x004008c6 90 nop
│ 0x004008c7 90 nop
│ 0x004008c8 90 nop
│ 0x004008c9 b800000000 mov eax, 0
│ 0x004008ce e842ffffff call sym.get_key
│ 0x004008d3 b800000000 mov eax, 0
│ 0x004008d8 e863ffffff call sym.print_flag
│ 0x004008dd b800000000 mov eax, 0
[0x004008c4]>
wao nop
show do the job. Basically, it modifies the current opcode with the supplied operand.
sym.calculate_key
is different now:
[0x004008c4]> s sym.calculate_key
[0x00400792]> pdf
Free fake stack
┌ (fcn) sym.calculate_key 16
│ sym.calculate_key ();
│ ; CALL XREF from sym.get_key (0x400828)
│ 0x00400792 55 push rbp
│ 0x00400793 4889e5 mov rbp, rsp
│ 0x00400796 bf9f8e0100 mov edi, 0x18e9f
│ 0x0040079b e866ffffff call sym.calc; sym.calc(0x18e9f)
│ 0x004007a0 5d pop rbp
└ 0x004007a1 c3 ret ; rsp
[0x00400706]> s sym.calc
[0x00400706]> pds
Free fake stack
0x00400711 arg1
0x00400733 call sym.calc
0x00400742 call sym.calc
0x00400751 call sym.calc
0x00400761 call sym.calc
0x00400776 call sym.calc
0x0040079b call sym.calc
0x004007aa arg1
0x004007ad const char *s
0x004007ad str.You_need_a_faster_machine._Bye_bye.
0x004007b2 call sym.imp.puts
0x004007b7 int status
0x004007bc call sym.imp.exit
0x00000000 [30] ---- section size 741 named .strtab
sym.calc(0x18e9f)
is our function. pds
prints a function summary since the function was too big to print. It looks like it uses quite a bit of recursion which is slowing down the calculation.
After taking a look at the function, it looks like the algorithm itself is broken. It could use some memoization. Python is pretty good for this type of issue. First of all, we’ll have to get a general overview of what the function actually does. Visually tracing the assembly should be enough. I got stuck on a couple of instructions. The built-in pdc
and r2dec-js were fantastic in sorting those out.
Here is the resultant python code which performs technically the same implementation as sym.calc
:
import sys
import math
memo = {}
# Hack to get the recursion to work in python
sys.setrecursionlimit(100000)
def calc(edi):
if edi <= 0x4:
return edi * edi + 0x2345
else:
if edi not in memo:
memo[edi] = calc(edi - 0x5) * 0x1234 + (calc(edi - 0x1) - calc(edi - 0x2)) + (calc(edi - 0x3) - calc(edi - 0x4));
return memo[edi]
# Wrap it to max of 32-bits since the result is stored
# in an EAX register, which means it can only fit in a 32-bit register
print(calc(0x18e9f) % int(math.pow(2, 32)))
Result is 3610015907
. I could’ve replaced the function to return the but I wanted to use Frida again:
'use strict';
const calc_ret = 3610015907;
const calc_addr = ptr("0x00400706");
const decrypt_flag_addr = ptr("0x00400696");
var old_calc = new NativeFunction(
calc_addr,
'uint',
['int']
);
var new_calc = new NativeCallback(
function(args) {
return calc_ret;
},
'uint',
['int']
);
Interceptor.replace(old_calc, new_calc);
//
Interceptor.attach(decrypt_flag_addr, {
onEnter: function (args) {
console.log('Context : ' + JSON.stringify(this.context));
}
});
Running it yields:
[0] % frida -f be-quick-or-be-dead-3 -l agent.js
____
/ _ | Frida 12.2.30 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at http://www.frida.re/docs/home/
Spawned `be-quick-or-be-dead-3`. Use %resume to let the main thread start executing!
[Local::be-quick-or-be-dead-3]-> %resume
Be Quick Or Be Dead 3
=====================
Calculating key...
Done calculating key
Printing flag:
picoCTF{dynamic_pr0gramming_ftw_1ffc009d}
Context : {"pc":"0x400696","sp":"0x7ffc539a3e28","rax":"0xd72c78a3","rcx":"0x7f50bac8584f","rdx":"0x7f50bad59720","rbx":"0x0","rsp":"0x7ffc539a3e28","rbp":"0x7ffc539a3e30","rsi":"0x8b6ac0","rdi":"0xd72c78a3","r8":"0x0","r9":"0xffffffff","r10":"0x7f50b8f09530","r11":"0x0","r12":"0x4005a0","r13":"0x7ffc539a3f30","r14":"0x0","r15":"0x0","rip":"0x400696"}
Process terminated
[Local::be-quick-or-be-dead-3]->
Flag is picoCTF{dynamic_pr0gramming_ftw_1ffc009d}
Results of the “quack it” sequence after running it with different inputs:
hello: 11 80 20 E0 22 53 72 A1 01 41 55 20 A0 C0 25 E3 95 20 15 35 20 15 00 70 C1
helloworld: 11 80 20 E0 22 53 72 A1 01 41 55 20 A0 C0 25 E3 95 20 15 35 20 15 00 70 C1
helloworld2: 11 80 20 E0 22 53 72 A1 01 41 55 20 A0 C0 25 E3 95 20 15 35 20 15 00 70 C1
a: 11 80 20 E0 22 53 72 A1 01 41 55 20 A0 C0 25 E3 95 20 15 35 20 15 00 70 C1
The “quack it” sequence looks stable. I think we basically have to figure out the encryption mechanism to solve this one.
Looks like the encryption algorithm is not reversible:
Radare2 pro-tip: Use afc
for analyzing function calling convention. Use afC?
for analyzing function complexity. Very, very useful for sizing-up a critical function.
The following python snippet should perform the same encryption as the challenge:
# py2 encrypt.py "hello"
import binascii
from pwnlib.util.fiddling import *
import sys
input = sys.argv[1]
cipher_text = []
for c in input:
# Convert an ascii character to a hex number
c = binascii.hexlify(c)
c = int(c, 16)
c = rol(c, 4, 8)
c = c ^ 0x16
c = ror(c, 8, 8)
cipher_text.append(c)
cipher_text = [hex(n)[2:] for n in cipher_text]
print(cipher_text)
The decryption is actually just the reverse of this operation:
encryption: rol4 -> xor -> ror8
decryption: ror8 -> xor -> rol4
The following script outlines the solution:
import binascii
from pwnlib.util.fiddling import *
import sys
def encrypt(str):
"""
str: string
ret: list of hexadecimal strings, without the leading 0x
"""
cipher_text = []
for c in str:
# Convert an ascii character to a number of base 16
c = binascii.hexlify(c)
c = int(c, 16)
c = rol(c, 4, 8)
c = c ^ 0x16
c = ror(c, 8, 8)
cipher_text.append(c)
cipher_text = [hex(n)[2:] for n in cipher_text]
return cipher_text
def decrypt(arr):
"""
arr: list of hexadecimal strings, without the leading 0x
ret: string
"""
plaintext = []
for i in arr:
i = int(i, 16)
i = ror(i, 8, 8)
i = i ^ 0x16
i = rol(i, 4, 8)
plaintext.append(i)
plaintext = [hex(n)[2:] for n in plaintext]
plaintext = b''.join([binascii.unhexlify(n) for n in plaintext])
return plaintext
def test():
str = "hellodarknessmyoldfriend"
cipher = encrypt(str)
plain = decrypt(cipher)
assert str == plain
cipher = ["11", "80", "20", "E0", "22", "53", "72", "A1", "01", "41", "55", "20",
"A0", "C0", "25", "E3", "95", "20", "15", "35", "20", "15", "00", "70", "C1"]
print(decrypt(cipher))
Running it yields:
cheese@291233 : ~/course/CNIT124/picoctf/quackmeme-up
[0] % py2 decrypt.py
picoCTF{qu4ckm3_8c02c0af}
Solution is picoCTF{qu4ckm3_8c02c0af}