6

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

Серия Жестокие эксперименты

Рассказываю как мы сделали самые крутые визитки на Диком Западе в отечественной ИТ-индустрии.

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

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

Все началось когда автор наткнулся на одну интересную статью где эксперт по 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

Локально запустится простейший HTTP-сервер, который отдаст текстовую страничку с нашими контактами. При наличии 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'>$t/Yo.java

Результат всех преобразований записывается в файл 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-адресов, уверен — удивит даже бывалых админов.

P.S.

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

Лига программистов

2.3K поста12K подписчиков

Правила сообщества

- Будьте взаимовежливы, аргументируйте критику

- Приветствуются любые посты по тематике программирования

- Если ваш пост содержит ссылки на внешние ресурсы - он должен быть самодостаточным. Вариации на тему "далее читайте в моей телеге" будут удаляться из сообщества

Темы

Политика

Теги

Популярные авторы

Сообщества

18+

Теги

Популярные авторы

Сообщества

Игры

Теги

Популярные авторы

Сообщества

Юмор

Теги

Популярные авторы

Сообщества

Отношения

Теги

Популярные авторы

Сообщества

Здоровье

Теги

Популярные авторы

Сообщества

Путешествия

Теги

Популярные авторы

Сообщества

Спорт

Теги

Популярные авторы

Сообщества

Хобби

Теги

Популярные авторы

Сообщества

Сервис

Теги

Популярные авторы

Сообщества

Природа

Теги

Популярные авторы

Сообщества

Бизнес

Теги

Популярные авторы

Сообщества

Транспорт

Теги

Популярные авторы

Сообщества

Общение

Теги

Популярные авторы

Сообщества

Юриспруденция

Теги

Популярные авторы

Сообщества

Наука

Теги

Популярные авторы

Сообщества

IT

Теги

Популярные авторы

Сообщества

Животные

Теги

Популярные авторы

Сообщества

Кино и сериалы

Теги

Популярные авторы

Сообщества

Экономика

Теги

Популярные авторы

Сообщества

Кулинария

Теги

Популярные авторы

Сообщества

История

Теги

Популярные авторы

Сообщества

Недвижимость и ремонт

Теги

Популярные авторы

Сообщества