Сервер на визитке

от автора

Все началось еще лет 10 назад, когда я впервые наткнулся на статью где было описано как эксперт по 3D-технологиям вместил сильно обфрусцированный код рейтрейсера на C++ в размеры своей визитки.

Внимание на код - он полностью рабочий!

Внимание на код — он полностью рабочий!

Вот такой код:

 #include <stdlib.h>   // card > aek.ppm    #include <stdio.h>    #include <math.h>    typedef int i;typedef float f;struct v{    f x,y,z;v operator+(v r){return v(x+r.x    ,y+r.y,z+r.z);}v operator*(f r){ return    v(x*r,y*r,z*r);}f operator%(v r){return    x*r.x+y*r.y+z*r.z;}v(){}v operator^(v r    ){return v(y*r.z-z*r.y,z*r.x-x*r.z,x*r.    y-y*r.x);}v(f a,f b,f c){x=a;y=b;z=c;}v    operator!(){return*this*(1/sqrt(*this%*    this));}};i G[]={133022, 133266,133266,    133022, 254096, 131216, 131984, 131072,    258048,};f R(){return(f)rand()/RAND_MAX    ;}i T(v o,v d,f&t,v&n){t=1e9;i m=0;f p=    -o.z/d.z; if(.01<p)t=p, n=v(0,0,1),m=1;    for(i k=19;k--;)for(i j=9;j--;)if(G[j]&    1<<k){v p=o+v(-k,0,-j-4);f b=p%d,c=p%p-    1,q=b*b-c;if(q>0){f s=-b-sqrt(q);if(s<t    &&s>.01)t=s,n=!(p+d*t),m=2;}}return m;}    v S(v o,v d){f t;v n;i m=T(o,d,t,n);if(    !m)return v(.7,.6,1)*pow(1-d.z,4);v h=o    +d*t,l=!(v(9+R(),9+R(),16)+h*-1),r=d+n*    (n%d*-2);f b=l%n;if(b<0||T(h,l,t,n))b=0    ;f p=pow(l%r*(b>0),99);if(m&1){h=h*.2;    return((i)(ceil(h.x)+ceil(h.y))&1?v(3,1    ,1):v(3,3,3))*(b*.2+.1);}return v(p,p,p    )+S(h,r)*.5;}i main(){printf("P6 512  "    "512 255 ");v g=!v(-6,-16,0),a=!(v(0,0,    1)^g)*.002,b=!(g^a)*.002,c=(a+b)*-256+g    ;for(i y=512;y--;)for(i x=512;x--;){v p    (9,9,9);for(i r=64;r--;){v t=a*(R()-.5)    *99+b*(R()-.5)*99;p=S(v(17,16,8)+t,!(t*    -1+(a*(R()+x)+b*(y+R())+c)*16))*3.5+p;}    printf("%c%c%c",(i)p.x,(i)p.y,(i)p.z);}} 

После компиляции:

 c++ -O3 -o card card.cpp

Генерировал у автора вот такую картинку:

Я тоже захотел себе что-то такое, но поскольку занимаюсь все же больше серверами чем 3D-графикой и Java, а не C++ — решил что будет круто уместить на стороне визитки простейший HTTP-сервер на Java.

Вместе с запуском и компиляцией.

Еще при наличии графического окружения будет запущен браузер.

Плюс немного криптографии для защиты от подделки.

Весь код уместился в 18 строк, выровненных по ширине так чтобы влезть в размеры визитки:

Вбиваете код с визитки в любимый редактор, сохраняете файл как vcard.sh и запускаете:

chmod +x ./vcard.sh ./vcard.sh

Результат:

FreeBSD 14 и Java 17

FreeBSD 14 и Java 17
Solaris (OpenNexenta) и JDK 22

Solaris (OpenNexenta) и JDK 22
Ubuntu и Java 1.8

Ubuntu и Java 1.8

Локально запустится простейший вебсервер, который отдаст текстовую страничку с текстом и контактами. При наличии GUI  — запустится еще и браузер по-умолчанию, с автоматическим открытием страницы этого сервера.

И все это в 18 строк кода.

Да, еще будет нужен любой Linux/BSD/MacOS/Solaris и любая версия JDK начиная с 1.8 на машине.

Поддержку запуска на Windows делать не стал (хотя это и возможно технически), но можно спокойно запустить в WSL .

Чтобы вы не мучились с вводом кода с картинки, вот текстовая версия:

#!/bin/sh t=$(mktemp -d);e=$(realpath  $0);sed '1,4d' $0|sed -e 's/p /public /g ; s/i /import /g' \  -e 's/j\./java./g ; s/E!/Exception/g ; s/U8!/UTF-8/g ; s/RE?/ResponseHeaders/g'>$t/Yo.java cd $t;javac -XDignore.symbol.file -cp . Yo.java && java -cp . Yo $e;exit 0 i com.sun.net.httpserver.*;i j.awt.Desktop;i j.io.*;i j.util.*;i j.net.*;i j.nio.file.*; i java.security.MessageDigest;i javax.crypto.Cipher;i javax.crypto.spec.SecretKeySpec; p class Yo{p static void main(String[]args)throws E!{Cipher dcipher=Cipher.getInstance("AES"); dcipher.init(2,new SecretKeySpec(Arrays.copyOf(MessageDigest.getInstance("SHA-1").digest( new String(Files.readAllBytes(Paths.get(args[0]))).replaceFirst("ED=\"([^<]*)\";","") .trim().getBytes()),16),"AES"));String ddata=new String(dcipher.doFinal(Base64.getDecoder() .decode(ED)),"U8!");int p=8000;String h="0x7f000001";HttpServer s=HttpServer.create() .createContext("/",(HttpExchange t)->{String r=String.format(ddata,System.nanoTime());t.getRE?() .set("Content-type","text/plain;charset=U8!");t.sendRE?(200,r.getBytes("U8!").length); try(OutputStream os=t.getResponseBody()){os.write(r.getBytes("U8!"));}}).getServer(); s.bind(new InetSocketAddress(p),1);new Timer().schedule(new TimerTask(){public void run(){ if(!Desktop.isDesktopSupported()){System.out.println(ddata);return;}try{Desktop.getDesktop() .browse(new URI("http://"+h+":"+p));}catch(E! e){throw new RuntimeE!(e);}}},2000);s.start();} static String ED="TfPrCIlXEUInGJPpr4++hQfa2Whq4RFzdbFP5C4s/s8=";}

Ну разве не прелесть?

Как это работает

Тут используется связка из заголовочного shell-скрипта и слегка обфрусцированного кода на Java. Еще я не стал кодировать весь блок на Java полностью в HEX-строку, чтобы визуально оставалось ощущение исходного кода.

Начнем с заголовочного скрипта:

#!/bin/sh t=$(mktemp -d);e=$(realpath  $0);sed '1,4d' $0|sed -e 's/p /public /g ; s/i /import /g' \  -e 's/j\./java./g ; s/E!/Exception/g ; s/U8!/UTF-8/g ; s/RE?/ResponseHeaders/g'>$t/Yo.java cd $t;javac -XDignore.symbol.file -cp . Yo.java && java -cp . Yo $e;exit 0

Самая первая строчка:

#!/bin/sh

Это shebang, стандартное для Unix указание на используемый интерпретатор, про него и так все знают.

Дальше происходит создание временного каталога в /tmp и присваивание его имени переменной в скрипте:

t=$(mktemp -d);

Затем получение скриптом собственного имени с полным путем:

e=$(realpath  $0);

Чтение скриптом самого себя, с отрезанием первых 4х строк — чтобы получить блок кода на Java:

sed '1,4d' $0

Дальше начинается pipe, в котором результат предыдущей команды передается на вход следующей:

|sed -e 's/p /public /g ; s/i /import /g' \ -e 's/j./java./g ; s/E!/Exception/g ; s/U8!/UTF-8/g ; s/RE?/ResponseHeaders/g'>а результат всех преобразований записывается в файл Yo.java, в том самом временном каталоге.Вся эта длинная и сложная сопля аргументов к sed производит «деобфрускацию» Java-кода и его восстановление в полную версию, пригодную для компиляции.Переход во временный каталог:cd " class="formula inline">t;Компиляция:javac -XDignore.symbol.file -cp . Yo.java

Малоизвестная опция -XDignore.symbol.file отключает предупреждение об использовании системных классов JDK (com.sun.net.httpserver.*) в проекте — в 1.8 версии классы встроенного в JDK HTTP-сервера еще считались системными.

Запуск с передачей полного пути оригинального скрипта для последующего его чтения из Java-кода:

java -cp . Yo $e

Сам код после деобфрускации и форматирования выглядит уже вот так:

import com.sun.net.httpserver.*; import java.awt.Desktop; import java.io.*; import java.util.*; import java.net.*; import java.nio.file.*; import java.security.MessageDigest; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; public class Yo {     public static void main(String[] args) throws Exception {         Cipher dcipher = Cipher.getInstance("AES");         dcipher.init(2,          new SecretKeySpec(Arrays.copyOf(         MessageDigest.getInstance("SHA-1").digest(                 new String(Files.readAllBytes(Paths.get(args[0])))                 .replaceFirst("ED=\"([^<]*)\";", "")                         .trim().getBytes()), 16), "AES"));         String ddata = new String(dcipher.doFinal(Base64.getDecoder()                 .decode(ED)), "UTF-8");         int p = 8000;         String h = "0x7f000001";         HttpServer s = HttpServer.create()                 .createContext("/", (HttpExchange t) -> {                     String r = String.format(ddata, System.nanoTime());                     t.getResponseHeaders()                             .set("Content-type", "text/plain;charset=UTF-8");                     t.sendResponseHeaders(200, r.getBytes("UTF-8").length);                     try (OutputStream os = t.getResponseBody()) {                         os.write(r.getBytes("UTF-8"));                     }                 }).getServer();         s.bind(new InetSocketAddress(p), 1);         new Timer().schedule(new TimerTask() {             public void run() {                 if (!Desktop.isDesktopSupported()) {                     System.out.println(ddata);                     return;                 }                 try {                     Desktop.getDesktop()                             .browse(new URI("http://" + h + ":" + p));                 } catch (Exception e) {                     throw new RuntimeException(e);                 }             }         }, 2000);         s.start();     }     static String ED = "1AtzGU0uq7J7DHPdjdJJ5JJDiwQi8mElIDOjuRK0DEU="; }

Тут уже большая часть логики вполне очевидна, поэтому раскрою лишь два самых сложных фрагмента.

Криптография

Когда я только начинал думать над реализацией этой штуки, уже было ясно что нужен какой-то неочевидный контроль целостности:

исходный код очевидно будут пересылать через сообщения, в виде постов или по почте, что легко его сломает.

Поэтому хотелось хоть какую-то защиту от подделки содержимого, чтобы компьютерные дети не добавили патч Брамина в самое интересное место, а индийский паренек не подменил авторство и мои контакты на свои, ради строчки в резюме.

Задачу усложнял факт передачи открытых исходников и ограничение по размерам, но видимо получилось:

 static String ED = "1AtzGU0uq7J7DHPdjdJJ5JJDiwQi8mElIDOjuRK0DEU=";

Именно тут находится текст:

We write software. @alex0x08

На каждую попытку как-то подменить содержимое (включая заголовок) будет выдаваться вот такая ошибка:

Exception in thread "main" javax.crypto.BadPaddingException: Given final block not properly padded. Such issues can arise if a bad key is used during decryption. at java.base/com.sun.crypto.provider.CipherCore.unpad(CipherCore.java:981) at java.base/com.sun.crypto.provider.CipherCore.fillOutputBuffer(CipherCore.java:1062) at java.base/com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:853) at java.base/com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:446) at java.base/javax.crypto.Cipher.doFinal(Cipher.java:2202) at Yo.main(Yo.java:6)

Получается код сам себя защищает от подделки.

0x7f000001

Вторым неочевидным моментом является вот такой странный адрес хоста:

 String h = "0x7f000001";

который используется при формировании ссылки для открытия браузером:

Desktop.getDesktop().browse(new URI("http://" + h + ":" + p));

Такое применение однозначно говорит о том что адрес очень даже стандартный, поскольку проходит как стадию валидации на стороне Java при формировании объекта URI, так и валидацию на стороне запускаемого браузера.

Вообщем это просто нотация, вариант написания IP-адреса 127.0.0.1, обозначающего loopback (петлю) — внутренний интерфейс, к которому можно подключиться локально, а не из сети.

Вот тут больше примеров различных вариантов написания IP-адресов, уверен — удивит даже бывалых админов.

Более фривольный оригинал статьи находится в нашем блоге, где мы подробно рассказываем об ужасах разработки, вгоняя в краску даже опытных и бывалых.

0x08 Software

Мы небольшая команда ветеранов ИТ-индустрии, создаем и дорабатываем самое разнообразное программное обеспечение, наш софт автоматизирует бизнес-процессы на трех континентах, в самых разных отраслях и условиях.

Оживляем давно умершее, чиним никогда не работавшее и создаем невозможное — затем рассказываем об этом в своих статьях.


ссылка на оригинал статьи https://habr.com/ru/articles/821959/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *