魔界之地黑岩角

  黑岩角位于深圳南澳镇最南端,当地人叫西尾,意思是西冲尾,一般人很少会去这里,大部分都是在西冲海滩玩。

  计划来此也是有许久了,一来是这里号称深圳“魔界”之地,其独特的岩石,表面上看呈灰黑色,常年的海风腐蚀成如刀刃般锋利,很有层次感,也使得这里更具险峻,稍不留意可能伤到皮肤,这不脚上就被岩石划伤了,还好准备了创可贴。

  早早在附近定好了民宿,和夜叔两人驱车来这里,由于两个人都不熟悉登山路线,车到酒店稍做休整后便出发先行踩点,以准备早上拍摄黑岩角日出,然而车到南西路尽头却发现是军事区,出于谨慎考虑,便没再前行。于是采取B方案,包快艇前去。

  驱车进入西冲一号沙滩,和快艇租赁的人聊了几句,开始都说到不了,在他们看来黑岩角是位于牛奶排或者更远的地方,因为那边根本没办法让船靠岸。后来给他们看了网上的样片后,才明白原来就是在西尾,很多拍婚纱的也是租他们的快艇过去的。最后以200块的价格送我们两过去,实际上行驶时间也就5分钟不到。

  上去后发现已经有不少摄影爱好者在上面了,这里可走动的面积也不多。询问得知他们都是山上爬下来的,抬头一看天啦,海拔大概也就100米样子,坡度几乎接近垂直于地面,没有任何扶手,下面还是坚硬的岩石,想想就觉得可怕了。不过看到陆续有人爬下来,何不挑战下。顺利爬到山顶观景平台,居然就是之前的军事区,和上面的人聊天得知,其实这一段是废弃了的。走出停车观景平台可以发现是两条路,一条是继续上山的军事区,而且也有自动门拦着的,而下山的路就是回到了南西路。

  就当是交了200的学费。后来想想,其实不必这么拼,200还是很划得来的,可惜人家最早也要6点半才出海,我们要在5点之前赶到,乌漆麻黑的爬这个坡更危险了,还带着这么重的器材。

  其实之前在网上也做了很多功课,却没人能说清楚究竟怎么去,这里我放上一张GPS线路图,供大伙参考。红圈是停车观景平台,车可以一直开到这上面,也可以露营。然后有石板路走到一个圆形的岗亭,之后就只能走泥地了,非常陡峭,下雨就不要尝试了,安全第一。

黑岩角GPS路径

  黑岩角只能看到日出,太阳从山后面升起,并不是海平面,并且天亮的很快,这次准备的还不够充分。本次拍摄的一些照片:https://snapast.com/albums/steve/A1EDibze1nFPDMheBKM6Gh

玩转Java字节码(下)

  接上篇文章,操作字节码的框架有很多Javassist,ASM,BCEL等,这里我用ASM来举例。在了解字节码组成后,很容易通过ASM构建一个Class出来,代码如下:

package classloader;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * Created by dorole.com on 2016/6/13.
 */
public class HelloClassGeneratorTest {
    public static void main(String[] args) throws IOException {
        ClassWriter classWriter = new ClassWriter(0);
        classWriter.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC, "Hello", null, "java/lang/Object", null);

        // 构造方法
        MethodVisitor constructorMethod = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
        constructorMethod.visitCode();
        constructorMethod.visitVarInsn(Opcodes.ALOAD, 0);
        constructorMethod.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
        constructorMethod.visitInsn(Opcodes.RETURN);
        constructorMethod.visitMaxs(1, 1);
        constructorMethod.visitEnd();

        // sayHello方法
        MethodVisitor helloMethod = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "sayHello", "(Ljava/lang/String;)Ljava/lang/String;", null, null);
        helloMethod.visitCode();
        helloMethod.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        helloMethod.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
        helloMethod.visitInsn(Opcodes.DUP);
        helloMethod.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
        helloMethod.visitVarInsn(Opcodes.ALOAD, 0);
        helloMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
        helloMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class", "getName", "()Ljava/lang/String;", false);
        helloMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        helloMethod.visitLdcInsn(" -> ");
        helloMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        helloMethod.visitVarInsn(Opcodes.ALOAD, 1);
        helloMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        helloMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
        helloMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        helloMethod.visitVarInsn(Opcodes.ALOAD, 1);
        helloMethod.visitInsn(Opcodes.ARETURN);
        helloMethod.visitMaxs(3, 2);
        helloMethod.visitEnd();

        classWriter.visitEnd();

        byte[] data = classWriter.toByteArray();
        File file = new File("D://Hello.class");
        FileOutputStream fileOutputStream = new FileOutputStream(file);
        fileOutputStream.write(data);
        fileOutputStream.close();
    }
}

  内容很简单,通过ClassWriter创建了一个Hello类,定义了一个构造方法和sayHello方法,接收一个String参数,返回String类型。将得到的Class字节码保存到D://Hello.class文件中,这个文件可以用jd-gui反编译回Java源代码。

  ASM设计模式很有意思,访问者模式(Visitor Pattern),我常常把它比作一个拥有多层的抽屉的模具,一层一层打开,放原材料,最后产出一个东西。MethodVisitor的用法可以参考文档,这里不详述,大致上都是一些字节码指令操作。

  以上代码也是模仿了javap反编译出来的内容,运行后无误的话可以看到在D盘有了一个Hello.class文件,接下来我们要加载这个文件,并运行其中的方法。代码如下:

package classloader;

/**
 * Created by dorole.com on 2016/6/13.
 */
public class MyLoader extends ClassLoader {
    public Class<?> defineMyClass(byte[] b, int off, int len) {
        return super.defineClass(null, b, off, len);
    }
}

  自定义一个Classloader,仅调用父类的defineClass即可。

package classloader;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * Created by dorole.com on 2016/6/13.
 */
public class MyLoaderTest {
    public static void main(String[] args) throws IOException, IllegalAccessException, InstantiationException {
        File file = new File("D://Hello.class");
        InputStream input = new FileInputStream(file);
        byte[] result = new byte[1024];

        int count = input.read(result);
        MyLoader loader = new MyLoader();
        Class clazz = loader.defineMyClass(result, 0, count);
        System.out.println(clazz.getCanonicalName());

        Object o = clazz.newInstance();
        try {
            Method method = clazz.getMethod("sayHello", String.class);
            Object returnObject = method.invoke(o, "World!");
            System.out.println("returnObject = " + returnObject);
        } catch (IllegalArgumentException | InvocationTargetException
                | NoSuchMethodException | SecurityException e) {
            e.printStackTrace();
        }
    }
}

  将class以字节数组形式读取给Classloader来加载,通过反射调用sayHello方法,专递一个World!参数进去,可以看到输出以下结果:

Hello
Hello -> World!
returnObject = World!

  这样一个从Java代码编译与反编译,自己生成字节码到加载运行,还能反编译回去。这样一个完整的循环就算完成了,虽然谈不上很有深度,但从这里能引发很多值得思考的地方。更多内容还是要参考Java虚拟机规范。

玩转Java字节码(上)

  Java字节码(Byte-code)是指Java源代码编译而成的,供JVM虚拟机执行的代码。用文本编辑器打开将是一团乱码,用十六进制编辑器打开能勉强看懂头部一些规范,例如魔数,主次版本。而用/bin/javap“反编译”之后可以得到一个人类可读的代码段,类似于用Wireshark来分析cap数据包。

  要看懂这个文件必须要知道字节码规范,主要有以下两个表格内容构成。其中表1是Class文件的组成部分,各段依次排列,排列紧密,无多余的分隔符。其中u1~u8表示占用字节数,1表示1个字节,2表示2个字节,4字节,8字节。例如魔数:仅有1个,占用4个字节,位于文件的前4个字节。紧接着4个字节是minor_version,major_version各占用2个字节。接下来2个字节是常量池数量统计,意味着一个Class不能有超过65535个常量池(maybe)。接下来是常量池,常量池的格式由表2来定义,下一段再来分析。依次类推可以将整个Class文件分析出来。

表1Class文件组成

  常量池中的每一个常量项通常有2~3个项目组成。例如:CONSTANT_Utf8_info这一项,第1个字节是表示定义(tag),值为1,紧接着2个字节表示该项将占用的长度(length),意味着接下来的几个字节长度将是字符串的实际内容。

  再来分析下CONSTANT_Integer_info项,从下表可以看出头一个字节tag值为3,接下来4个字节安高位在前编码表示int值,所以在Java中int数据类型的最大值是去掉一个符号位的0x7FFFFFFF。Utf8,Integer,Float,Long,Double这5个都是类似的,直接以值的形式存储。其余常量项都是存储着引用值(index),分别指向这5个值或其它地方,这里不一一介绍了。

  等等java不是八大基本数据类型吗,这里怎么只有提到了4个,没错,小于int的都当作int处理了。也就是boolean,byte,char,short都“拉长”至int级别来对待。例如:boolean a = “true” 对应的字节码指令是:iconst_1,putfield a,将int型常量值1进栈赋值给变量a。

  介于编译器可能会做一些性能优化,例如int值超过2字节(32768)才会加入到常量项,小于2字节由sipush指令在运行时分配。

表2Class 14个常量项

  分析这玩意实在有些无聊,既然本文是以玩转为主题,自然就说点有趣的。首先一个简单的Java代码如下:

package classloader;

/**
 * Created by dorole.com on 2016/6/13.
 */
public class Hello {
    public String sayHello(String name) {
        System.out.println(this.getClass().getName() + " -> " + name);
        return name;
    }
}

  这段代码自然再简单不过了,随便一个文本编辑器敲进去,javac编译,找个main方法调用完事。那能不能不编译直接调用?当然没问题,先不谈这个,先来看看编译后究竟是个什么样子。我们用WinHex来打开编译好的Class文件,截图如下:

Hello.class

  黑色标记的是全部的常量项tag值(一个个标出来,该是有多蛋疼~),自己可以对照表2慢慢分析。当然也可以借助javap来“反编译”下,输出更为直观的表格:

Constant pool:
   #1 = Utf8               classloader.Hello
   #2 = Class              #1             // "classloader.Hello"
   #3 = Utf8               java/lang/Object
   #4 = Class              #3             // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = NameAndType        #5:#6          // "<init>":()V
   #8 = Methodref          #4.#7          // java/lang/Object."<init>":()V
   #9 = Utf8               sayHello
  #10 = Utf8               (Ljava/lang/String;)Ljava/lang/String;
  #11 = Utf8               java/lang/System
  #12 = Class              #11            // java/lang/System
  #13 = Utf8               out
  #14 = Utf8               Ljava/io/PrintStream;
  #15 = NameAndType        #13:#14        // out:Ljava/io/PrintStream;
  #16 = Fieldref           #12.#15        // java/lang/System.out:Ljava/io/PrintStream;
  #17 = Utf8               java/lang/StringBuilder
  #18 = Class              #17            // java/lang/StringBuilder
  #19 = Methodref          #18.#7         // java/lang/StringBuilder."<init>":()V
  #20 = Utf8               getClass
  #21 = Utf8               ()Ljava/lang/Class;
  #22 = NameAndType        #20:#21        // getClass:()Ljava/lang/Class;
  #23 = Methodref          #4.#22         // java/lang/Object.getClass:()Ljava/lang/Class;
  #24 = Utf8               java/lang/Class
  #25 = Class              #24            // java/lang/Class
  #26 = Utf8               getName
  #27 = Utf8               ()Ljava/lang/String;
  #28 = NameAndType        #26:#27        // getName:()Ljava/lang/String;
  #29 = Methodref          #25.#28        // java/lang/Class.getName:()Ljava/lang/String;
  #30 = Utf8               append
  #31 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #32 = NameAndType        #30:#31        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #33 = Methodref          #18.#32        // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #34 = Utf8                ->
  #35 = String             #34            //  ->
  #36 = Utf8               toString
  #37 = NameAndType        #36:#27        // toString:()Ljava/lang/String;
  #38 = Methodref          #18.#37        // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #39 = Utf8               java/io/PrintStream
  #40 = Class              #39            // java/io/PrintStream
  #41 = Utf8               println
  #42 = Utf8               (Ljava/lang/String;)V
  #43 = NameAndType        #41:#42        // println:(Ljava/lang/String;)V
  #44 = Methodref          #40.#43        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #45 = Utf8               Code

  这里我仅放上常量池部分,这下很好对了把。Class文件就是这样一个紧凑结构,不愧是为嵌入式而打造的一门语言。回到之前的问题,有没有可能自己来生成字节码?答案是肯定的,下回分解。

一次DDOS攻击记录

  就在昨天儿童节的时候,大概9点半左右收到微信公众号的报警信息,发现是服务器没有响应,5分钟内6次请求没有及时返回。当时并没有过多在意,因为之前也有过1~2次失败,都是是偶然因素。过了几分钟又收到dnspod的邮件报警,内容是snapast.com主站连接超时,这才意识到可能服务器宕机了。

  立刻手动访问一次,果然是打不开了,ssh也连接不上,下意识认为可能是服务器被重启了,因为之前托管在linkcloud的时候,隔个把月总会挂掉一下,但迁移到阿里云后一直很稳定,不太会出问题。

  登录到阿里云后台,在实例监控看到CPU一直满负荷运行,入网流量达到22M/s(云盾基础防护中显示有接近30M/s,忘了PPS多少来着),出网也已经塞满2M带宽。

实例监控

  通过阿里的终端登录到主机上,发现CPU是被4个nginx进程占满,后端几个tomcat负载并不高,说明并不是一般的大规模http请求(cc攻击)。而且nginx access log也是正常,但error log有很多xxx worker_connections are not enough,说明确实连接数较高,超过设定值。通过netstat统计居然有1.6w左右的连接数,远高于平时的300左右。

root@snapast:~# netstat -an | wc -l
16089

  到这里基本能判断极有可能是SYN Flood,想到云盾有一定抵抗DDOS的能力,试着开启流量清洗看看,原来人家的默认触发条件是流量100M/s,PPS是1w,我这点攻击还没办法触发呢,于是手动设置了一个最低条件达到10M/s就开始清洗,然而事实证明清洗毫无用处,依旧连接超时。但有个好处是开始清理后可以抓包。

  下载cap到本地用Wireshark打开一看,好家伙,果然是SYN握手攻击,大量的不同IP(美国为主)往443端口发SYN请求,极少数有回应RST,我猜应该是上层路由回的。云盾的抓包并不是你主机网卡包,而是在接入口的设备上。这下明了了,对于这种TCP层的攻击也没啥好办法抵御的,考虑到目前仅有相册用了https,所以先停掉nginx监听443端口,将机器负载降了下来,但也没办法彻底解决。有意思的是当时阿里云解析也出了故障没办法修改域名解析,屋漏偏逢连夜雨。

抓包

  就这么瞎折腾了2个小时,攻击才停止,一切也都恢复正常。今天来看,在微信接口调用统计中失败次数最为“壮观”的一天。

  现在想想其实也有一些办法可以减小宕机损失:比如将受攻击的域名临时解析到别处。再如将多个业务分离开来,用不同的域名,不同的IP接入,iptables将多次密集连接请求的IP drop掉。