pbctf 2020: Kokoro author's writeup
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package blue.perfect.kokoro; | |
| import org.ajwerner.voronoi.Point; | |
| import org.ajwerner.voronoi.Voronoi; | |
| import org.ajwerner.voronoi.VoronoiEdge; | |
| import org.lwjgl.glfw.*; | |
| import org.lwjgl.opengl.*; | |
| import org.lwjgl.system.*; | |
| import java.io.*; | |
| import java.nio.*; | |
| import java.security.SecureRandom; | |
| import java.util.*; | |
| import static java.lang.Math.*; | |
| import static org.lwjgl.BufferUtils.createByteBuffer; | |
| import static org.lwjgl.glfw.Callbacks.*; | |
| import static org.lwjgl.glfw.GLFW.*; | |
| import static org.lwjgl.opengl.GL11.*; | |
| import static org.lwjgl.opengl.GL12.*; | |
| import static org.lwjgl.stb.STBImage.*; | |
| import static org.lwjgl.stb.STBImageResize.*; | |
| import static org.lwjgl.system.MemoryStack.*; | |
| import static org.lwjgl.system.MemoryUtil.*; | |
| import org.lwjgl.*; | |
| import java.nio.channels.*; | |
| import java.nio.file.*; | |
| public final class Main { | |
| private ByteBuffer image; | |
| private int w; | |
| private int h; | |
| private int comp; | |
| private long window; | |
| private int ww; | |
| private int wh; | |
| private boolean ctrlDown; | |
| private int scale; | |
| private Callback debugProc; | |
| public static ByteBuffer ioResourceToByteBuffer(String resource, int bufferSize) throws IOException { | |
| ByteBuffer buffer; | |
| Path path = Paths.get(resource); | |
| if (Files.isReadable(path)) { | |
| try (SeekableByteChannel fc = Files.newByteChannel(path)) { | |
| buffer = createByteBuffer((int)fc.size() + 1); | |
| while (fc.read(buffer) != -1) { | |
| ; | |
| } | |
| } | |
| } else { | |
| try ( | |
| InputStream source = Main.class.getClassLoader().getResourceAsStream(resource); | |
| ReadableByteChannel rbc = Channels.newChannel(source) | |
| ) { | |
| buffer = createByteBuffer(bufferSize); | |
| while (true) { | |
| int bytes = rbc.read(buffer); | |
| if (bytes == -1) { | |
| break; | |
| } | |
| if (buffer.remaining() == 0) { | |
| buffer = resizeBuffer(buffer, buffer.capacity() * 3 / 2); // 50% | |
| } | |
| } | |
| } | |
| } | |
| buffer.flip(); | |
| return buffer; | |
| } | |
| private static ByteBuffer resizeBuffer(ByteBuffer buffer, int newCapacity) { | |
| ByteBuffer newBuffer = BufferUtils.createByteBuffer(newCapacity); | |
| buffer.flip(); | |
| newBuffer.put(buffer); | |
| return newBuffer; | |
| } | |
| private VoronoiPolygon getPolygonUnder(Point p) { | |
| double bestD = Double.POSITIVE_INFINITY; | |
| VoronoiPolygon best = null; | |
| for (VoronoiPolygon shard : shards.values()) { | |
| double d = shard.getTranslatedSite().distanceTo(p); | |
| if (d <= bestD) { | |
| bestD = d; | |
| best = shard; | |
| } | |
| } | |
| return best; | |
| } | |
| Voronoi v; | |
| LinkedHashMap<Point, VoronoiPolygon> shards; | |
| long rng; | |
| private String flag; | |
| private void setupGame(int difficulty, long seed) { | |
| rng = seed; | |
| shards = new LinkedHashMap<>(); | |
| List<Point> sites = new ArrayList<>(); | |
| for (int i = 0; i < difficulty; i++) { | |
| rng ^= rng << 13; | |
| rng &= 0xffffffffL; | |
| rng ^= rng >>> 17; | |
| rng &= 0xffffffffL; | |
| rng ^= rng << 5; | |
| rng &= 0xffffffffL; | |
| double x = rng / (double)0xffffffffL; | |
| rng ^= rng << 13; | |
| rng &= 0xffffffffL; | |
| rng ^= rng >>> 17; | |
| rng &= 0xffffffffL; | |
| rng ^= rng << 5; | |
| rng &= 0xffffffffL; | |
| double y = rng / (double)0xffffffffL; | |
| // System.out.println("" + x + " " + y); | |
| Point site = new Point(x, y); | |
| sites.add(site); | |
| VoronoiPolygon shard = new VoronoiPolygon(site); | |
| shards.put(site, shard); | |
| } | |
| v = new Voronoi(sites); | |
| for (VoronoiEdge e : v.edgeList) { | |
| if (e.p1 != null && e.p2 != null) { | |
| shards.get(e.site1).points.add(e.p1); | |
| shards.get(e.site1).points.add(e.p2); | |
| shards.get(e.site2).points.add(e.p1); | |
| shards.get(e.site2).points.add(e.p2); | |
| } | |
| } | |
| shards.values().forEach(VoronoiPolygon::sortPoints); | |
| // now scramble it. | |
| for (VoronoiPolygon shard : shards.values()) { | |
| // dump all the pieces at the center | |
| shard.translate(0.5-shard.site.x, 0.5-shard.site.y); | |
| // random rotation | |
| rng ^= rng << 13; | |
| rng &= 0xffffffffL; | |
| rng ^= rng >>> 17; | |
| rng &= 0xffffffffL; | |
| rng ^= rng << 5; | |
| rng &= 0xffffffffL; | |
| shard.rotation = (rng % 36) * 10.f; | |
| // random color ;) | |
| rng ^= rng << 13; | |
| rng &= 0xffffffffL; | |
| rng ^= rng >>> 17; | |
| rng &= 0xffffffffL; | |
| rng ^= rng << 5; | |
| rng &= 0xffffffffL; | |
| long rgb = rng; | |
| shard.color[0] = (byte)((rgb >> 0) & 0xff); | |
| shard.color[1] = (byte)((rgb >> 8) & 0xff); | |
| shard.color[2] = (byte)((rgb >> 16) & 0xff); | |
| // System.out.println(Arrays.toString(shard.color)); | |
| } | |
| } | |
| // private long next() { | |
| // rng ^= rng << 13; | |
| // rng &= 0xffffffffL; | |
| // rng ^= rng >>> 17; | |
| // rng &= 0xffffffffL; | |
| // rng ^= rng << 5; | |
| // rng &= 0xffffffffL; | |
| // return rng; | |
| // } | |
| // | |
| // private double nextDouble() { | |
| // return next() / (double)0xffffffffL; | |
| // } | |
| private Main(String imagePath, int difficulty) { | |
| flag = new ReadFlag().readFlag(); | |
| long seed = new SecureRandom().nextLong(); | |
| // System.out.println(seed); | |
| setupGame(difficulty, seed); | |
| ByteBuffer imageBuffer; | |
| try { | |
| imageBuffer = ioResourceToByteBuffer(imagePath, 8 * 1024); | |
| } catch (IOException e) { | |
| throw new RuntimeException(e); | |
| } | |
| try (MemoryStack stack = stackPush()) { | |
| IntBuffer w = stack.mallocInt(1); | |
| IntBuffer h = stack.mallocInt(1); | |
| IntBuffer comp = stack.mallocInt(1); | |
| // Use info to read image metadata without decoding the entire image. | |
| // We don't need this for this demo, just testing the API. | |
| if (!stbi_info_from_memory(imageBuffer, w, h, comp)) { | |
| throw new RuntimeException("Failed to read image information: " + stbi_failure_reason()); | |
| } else { | |
| System.out.println("Texture OK"); | |
| } | |
| System.out.println("Image width: " + w.get(0)); | |
| System.out.println("Image height: " + h.get(0)); | |
| System.out.println("Image components: " + comp.get(0)); | |
| System.out.println("Image HDR: " + stbi_is_hdr_from_memory(imageBuffer)); | |
| // Decode the image | |
| image = stbi_load_from_memory(imageBuffer, w, h, comp, 0); | |
| if (image == null) { | |
| throw new RuntimeException("Failed to load image: " + stbi_failure_reason()); | |
| } | |
| this.w = w.get(0); | |
| this.h = h.get(0); | |
| this.comp = comp.get(0); | |
| } | |
| } | |
| public static void main(String[] args) { | |
| String imagePath = args[0]; | |
| int difficulty = Integer.parseInt(args[1]); | |
| new Main(imagePath, difficulty).run(); | |
| } | |
| private void run() { | |
| try { | |
| init(); | |
| loop(); | |
| } finally { | |
| try { | |
| destroy(); | |
| } catch (Exception e) { | |
| e.printStackTrace(); | |
| } | |
| } | |
| } | |
| private void windowSizeChanged(long window, int width, int height) { | |
| this.ww = width; | |
| this.wh = height; | |
| glMatrixMode(GL_PROJECTION); | |
| glLoadIdentity(); | |
| glOrtho(0.0, width, height, 0.0, -1.0, 1.0); | |
| glMatrixMode(GL_MODELVIEW); | |
| } | |
| private static void framebufferSizeChanged(long window, int width, int height) { | |
| glViewport(0, 0, width, height); | |
| } | |
| double mouseX, mouseY; | |
| VoronoiPolygon selectedPolygon = null; | |
| private void checkSolved() { | |
| for (VoronoiPolygon shard : shards.values()) { | |
| if (!shard.isSolved()) | |
| return; | |
| } | |
| // System.out.println("Win?"); | |
| win(); | |
| } | |
| private void win() { | |
| System.out.println("Congratulations. Here is your flag"); | |
| System.out.println(flag); | |
| glfwSetWindowShouldClose(window, true); | |
| } | |
| private void init() { | |
| scale = 0; | |
| GLFWErrorCallback.createPrint().set(); | |
| if (!glfwInit()) { | |
| throw new IllegalStateException("Unable to initialize GLFW"); | |
| } | |
| glfwDefaultWindowHints(); | |
| glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); | |
| glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE); | |
| // glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2); | |
| // glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1); | |
| // GLFWVidMode vidmode = Objects.requireNonNull(glfwGetVideoMode(glfwGetPrimaryMonitor())); | |
| // ww = max(800, min(w, vidmode.width() - 160)); | |
| // wh = max(600, min(h, vidmode.height() - 120)); | |
| ww = 800; | |
| wh = 800; | |
| this.window = glfwCreateWindow(ww, wh, "Kokoro", NULL, NULL); | |
| if (window == NULL) { | |
| throw new RuntimeException("Failed to create the GLFW window"); | |
| } | |
| glfwSetWindowSize(this.window, ww, wh); | |
| // Center window | |
| // glfwSetWindowPos( | |
| // window, | |
| // (vidmode.width() - ww) / 2, | |
| // (vidmode.height() - wh) / 2 | |
| // ); | |
| glfwSetWindowRefreshCallback(window, window -> render()); | |
| glfwSetWindowSizeCallback(window, this::windowSizeChanged); | |
| glfwSetFramebufferSizeCallback(window, Main::framebufferSizeChanged); | |
| glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> { | |
| ctrlDown = (mods & GLFW_MOD_CONTROL) != 0; | |
| if (action == GLFW_RELEASE) { | |
| return; | |
| } | |
| switch (key) { | |
| case GLFW_KEY_ESCAPE: | |
| glfwSetWindowShouldClose(window, true); | |
| break; | |
| case GLFW_KEY_KP_ADD: | |
| case GLFW_KEY_EQUAL: | |
| // setScale(scale + 1); | |
| break; | |
| case GLFW_KEY_KP_SUBTRACT: | |
| case GLFW_KEY_MINUS: | |
| // setScale(scale - 1); | |
| break; | |
| case GLFW_KEY_0: | |
| case GLFW_KEY_KP_0: | |
| if (ctrlDown) { | |
| // setScale(0); | |
| } | |
| break; | |
| } | |
| }); | |
| glfwSetMouseButtonCallback(window, (window, button, action, mods) -> { | |
| // DoubleBuffer posX = BufferUtils.createDoubleBuffer(1); | |
| // DoubleBuffer posY = BufferUtils.createDoubleBuffer(1); | |
| // glfwGetCursorPos(window, posX, posY); | |
| // System.out.println("" + button + " " + action + " " + mouseX + " " + mouseY); | |
| if (button == 0) { | |
| if (action == GLFW_PRESS) { | |
| selectedPolygon = getPolygonUnder(new Point(mouseX, mouseY)); | |
| shards.remove(selectedPolygon.site); | |
| shards.put(selectedPolygon.site, selectedPolygon); | |
| // selectedPolygon.rotation += 2.f; | |
| } else if (action == GLFW_RELEASE) { | |
| selectedPolygon = null; | |
| checkSolved(); | |
| } | |
| } | |
| // GLFW_PRESS GLFW_RELEASE | |
| }); | |
| glfwSetCursorPosCallback(window, (window, x, y) -> { | |
| double newX = x/w; | |
| double newY = y/h; | |
| if (selectedPolygon != null) { | |
| double dx = newX - mouseX; | |
| double dy = newY - mouseY; | |
| selectedPolygon.translate(dx, dy); | |
| } | |
| mouseX = newX; | |
| mouseY = newY; | |
| }); | |
| glfwSetScrollCallback(window, (window, xoffset, yoffset) -> { | |
| if (selectedPolygon != null) { | |
| selectedPolygon.rotate((float)yoffset*10.f); | |
| checkSolved(); | |
| } | |
| }); | |
| // Create context | |
| glfwMakeContextCurrent(window); | |
| GL.createCapabilities(); | |
| debugProc = GLUtil.setupDebugMessageCallback(); | |
| glfwSwapInterval(1); | |
| glfwShowWindow(window); | |
| // glfwInvoke(window, this::windowSizeChanged, Main::framebufferSizeChanged); | |
| } | |
| private void setScale(int scale) { | |
| this.scale = max(-9, scale); | |
| } | |
| private void premultiplyAlpha() { | |
| int stride = w * 4; | |
| for (int y = 0; y < h; y++) { | |
| for (int x = 0; x < w; x++) { | |
| int i = y * stride + x * 4; | |
| float alpha = (image.get(i + 3) & 0xFF) / 255.0f; | |
| image.put(i + 0, (byte) round(((image.get(i + 0) & 0xFF) * alpha))); | |
| image.put(i + 1, (byte) round(((image.get(i + 1) & 0xFF) * alpha))); | |
| image.put(i + 2, (byte) round(((image.get(i + 2) & 0xFF) * alpha))); | |
| } | |
| } | |
| } | |
| private int createTexture() { | |
| int texID = glGenTextures(); | |
| glBindTexture(GL_TEXTURE_2D, texID); | |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); | |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); | |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); | |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); | |
| int format; | |
| if (comp == 3) { | |
| if ((w & 3) != 0) { | |
| glPixelStorei(GL_UNPACK_ALIGNMENT, 2 - (w & 1)); | |
| } | |
| format = GL_RGB; | |
| } else { | |
| premultiplyAlpha(); | |
| glEnable(GL_BLEND); | |
| glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); | |
| format = GL_RGBA; | |
| } | |
| glTexImage2D(GL_TEXTURE_2D, 0, format, w, h, 0, format, GL_UNSIGNED_BYTE, image); | |
| ByteBuffer input_pixels = image; | |
| int input_w = w; | |
| int input_h = h; | |
| int mipmapLevel = 0; | |
| while (1 < input_w || 1 < input_h) { | |
| int output_w = Math.max(1, input_w >> 1); | |
| int output_h = Math.max(1, input_h >> 1); | |
| ByteBuffer output_pixels = memAlloc(output_w * output_h * comp); | |
| stbir_resize_uint8_generic( | |
| input_pixels, input_w, input_h, input_w * comp, | |
| output_pixels, output_w, output_h, output_w * comp, | |
| comp, comp == 4 ? 3 : STBIR_ALPHA_CHANNEL_NONE, STBIR_FLAG_ALPHA_PREMULTIPLIED, | |
| STBIR_EDGE_CLAMP, | |
| STBIR_FILTER_MITCHELL, | |
| STBIR_COLORSPACE_SRGB | |
| ); | |
| if (mipmapLevel == 0) { | |
| stbi_image_free(image); | |
| } else { | |
| memFree(input_pixels); | |
| } | |
| glTexImage2D(GL_TEXTURE_2D, ++mipmapLevel, format, output_w, output_h, 0, format, GL_UNSIGNED_BYTE, output_pixels); | |
| input_pixels = output_pixels; | |
| input_w = output_w; | |
| input_h = output_h; | |
| } | |
| if (mipmapLevel == 0) { | |
| stbi_image_free(image); | |
| } else { | |
| memFree(input_pixels); | |
| } | |
| return texID; | |
| } | |
| private void loop() { | |
| int texID = createTexture(); | |
| windowSizeChanged(this.window, ww, wh); // fix the fucking viewport | |
| glClearColor(0.1f,0.1f,0.1f,0.f); | |
| glPointSize(3.f); // set the point size to 3px | |
| while (!glfwWindowShouldClose(window)) { | |
| glfwPollEvents(); | |
| render(); | |
| } | |
| glDeleteTextures(texID); | |
| } | |
| private void render() { | |
| glClear(GL_COLOR_BUFFER_BIT); | |
| float scaleFactor = 1.0f + scale * 0.1f; | |
| glPushMatrix(); | |
| // scale at center of image | |
| // glTranslatef(ww * 0.5f, wh * 0.5f, 0.0f); | |
| // glScalef(scaleFactor, scaleFactor, 1f); | |
| // glTranslatef(-ww * 0.5f, -wh * 0.5f, 0.0f); | |
| glScalef(w, h, 1f); // move to image coordinate space (0.0 to 1.0 on x and y -> w,h) | |
| for (VoronoiPolygon shard : shards.values()) { | |
| glPushMatrix(); | |
| glTranslated(shard.offX, shard.offY, 0.d); | |
| Point site = shard.site; | |
| glTranslated(site.x, site.y, 0.d); | |
| glRotatef(shard.rotation, 0.f, 0.f, 1.f); | |
| glTranslated(-site.x, -site.y, 0.d); | |
| // float[] rgb = shard.color; | |
| // glColor3f(rgb[0], rgb[1], rgb[2]); | |
| // glBegin(GL_LINES); | |
| // { | |
| // for (VoronoiEdge e : shard.edges) { | |
| // glVertex2d(e.p1.x, e.p1.y); | |
| // glVertex2d(e.p2.x, e.p2.y); | |
| // } | |
| // } | |
| // glEnd(); | |
| // float i = 1.f; | |
| glColor4f(1.f,1.f,1.f,1.f); | |
| glEnable(GL_TEXTURE_2D); | |
| glBegin(GL_TRIANGLE_FAN); | |
| { | |
| glTexCoord2d(site.x, site.y); | |
| glVertex2d(site.x, site.y); | |
| for (Point p : shard.points) { | |
| glTexCoord2d(p.x, p.y); | |
| // glColor3f(rgb[0]*i, rgb[1]*i, rgb[2]*i); | |
| glVertex2d(p.x, p.y); | |
| // i -= 0.1f; | |
| } | |
| // loop back around to the first one to complete the polygon | |
| Point p = shard.points.get(0); | |
| // glColor3f(rgb[0]*i, rgb[1]*i, rgb[2]*i); | |
| glTexCoord2d(p.x, p.y); | |
| glVertex2d(p.x, p.y); | |
| // glTexCoord2f(0.0f, 0.0f); | |
| // glVertex2f(0.0f, 0.0f); | |
| // | |
| // glTexCoord2f(1.0f, 0.0f); | |
| // glVertex2f(1.0f, 0.0f); | |
| // | |
| // glTexCoord2f(1.0f, 1.0f); | |
| // glVertex2f(1.0f, 1.0f); | |
| // | |
| // glTexCoord2f(0.0f, 1.0f); | |
| // glVertex2f(0.0f, 1.0f); | |
| } | |
| glEnd(); | |
| glDisable(GL_TEXTURE_2D); | |
| glColor3f(1.f,0.f,0.f); | |
| glBegin(GL_LINE_STRIP); | |
| { | |
| for (Point p : shard.points) { | |
| glVertex2d(p.x, p.y); | |
| } | |
| // loop back around to the first one to complete the polygon | |
| Point p = shard.points.get(0); | |
| glVertex2d(p.x, p.y); | |
| } | |
| glEnd(); | |
| glPopMatrix(); | |
| } | |
| glPushAttrib(GL_CURRENT_BIT); //GL_POINT_BIT | |
| // hax | |
| glColor3f(0.f, 1.f, 0.f); | |
| glBegin(GL_LINES); | |
| { | |
| for (VoronoiEdge e : v.edgeList) { | |
| if (e.p1 != null && e.p2 != null) { | |
| if (shards.get(e.site1).isRotated() && shards.get(e.site2).isRotated()) { | |
| glVertex2d(e.p1.x, e.p1.y); | |
| glVertex2d(e.p2.x, e.p2.y); | |
| } | |
| } | |
| } | |
| } | |
| glEnd(); | |
| glBegin(GL_POINTS); | |
| { | |
| // hax | |
| // for (Point site : v.sites) { | |
| // if (shards.get(site).isRotated()) { | |
| // glVertex2d(site.x, site.y); | |
| // } | |
| // } | |
| // Because it's a linkedhashmap, the topmost shard will always be on top | |
| // This makes the challenge much easier to solve | |
| for (VoronoiPolygon shard : shards.values()) { | |
| byte[] rgb = shard.color; | |
| glColor3ub(rgb[0],rgb[1],rgb[2]); | |
| Point p = shard.getTranslatedSite(); | |
| glVertex2d(p.x, p.y); | |
| } | |
| // glColor3f(1.f,1.f,1.f); | |
| // glVertex2d(mouseX, mouseY); | |
| } | |
| glEnd(); | |
| glPopAttrib(); | |
| glPopMatrix(); | |
| glfwSwapBuffers(window); | |
| } | |
| private void destroy() { | |
| if (debugProc != null) { | |
| debugProc.free(); | |
| } | |
| glfwFreeCallbacks(window); | |
| glfwDestroyWindow(window); | |
| glfwTerminate(); | |
| Objects.requireNonNull(glfwSetErrorCallback(null)).free(); | |
| } | |
| } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # https://pwn.cat/kokoro.mp4 (video seems to not work in firefox, try chrome) | |
| """ | |
| TL;DR | |
| It was obfuscated using a custom obfuscator that implements "data-flow flattening" with some opaque predicates. The resulting stack frames incoming to each basic block are not consistent, which is why classfile verifier needs to be turned off. Also, it segfaults newer java so that's why java 1.8 is required | |
| I think that if you only read the bytecode it is still solvable in ~8 hours which is why i was so surprised no one solved it. | |
| I also think the obfuscator is very uniform and predictable so it should be simple to resolve the opaque predicates even without doing any kind of control flow analysis. Just by doing pattern matching on instruction sequences | |
| the remainder of the challenge is basically leak the RNG (xorshift implementation copied from wikipedia) seed from the colors | |
| And you can rewind the RNG all the way and get the correct positions and locations of each of the pieces | |
| The data flow flattening was implemented as an SSA pass that just eVaLuAtEs the phi nodes. | |
| Example CFG before obfuscation | |
| https://media.discordapp.net/attachments/748680423839760494/785299383814062120/pre-destruct.png | |
| Example CFG after obfuscation | |
| https://media.discordapp.net/attachments/748680423839760494/785299410258624542/post-reaalloc.png | |
| """ | |
| from z3 import * | |
| def xorshift(x): | |
| x ^= x << 13 | |
| x ^= LShR(x, 17) | |
| x ^= x << 5 | |
| return x | |
| def undo(x): # undo xorshift() | |
| assert x & 0xffffffff == x | |
| s = Solver() | |
| seed = BitVec('seed', 32) | |
| s.add(xorshift(seed) == x) | |
| assert str(s.check()) == 'sat' | |
| return s.model().eval(seed).as_long() | |
| def initial_solve(r1,g1,b1, r2,g2,b2): | |
| r1,g1,b1,r2,g2,b2 = r1 & 0xff,g1 & 0xff,b1 & 0xff,r2 & 0xff,g2 & 0xff,b2&0xff | |
| c1 = r1 | (g1 << 8) | (b1 << 16) | |
| c2 = r2 | (g2 << 8) | (b2 << 16) | |
| print(hex(c1)) | |
| print(hex(c2)) | |
| s = Solver() | |
| seed = BitVec('seed', 32) | |
| s.add((seed & 0xffffff) == c1) | |
| # rotation 2 | |
| seed = xorshift(seed) | |
| seed = xorshift(seed) | |
| s.add((seed & 0xffffff) == c2) | |
| assert str(s.check()) == 'sat' | |
| return s.model().eval(seed).as_long() | |
| def full_solve(r1,g1,b1, r2,g2,b2, n): | |
| seed = initial_solve(r1,g1,b1, r2,g2,b2) | |
| # now we have the final seed value | |
| rotations = [None] * n | |
| for i in range(n-1, -1, -1): | |
| seed = undo(seed) # undo next() for color | |
| rotation = (seed % 36)*10 | |
| seed = undo(seed) # undo next() for rotation | |
| rotations[i] = rotation | |
| positions = [None] * n | |
| for i in range(n-1, -1, -1): # now we can get the y's and x's | |
| y = float_val(seed) | |
| seed = undo(seed) | |
| x = float_val(seed) | |
| seed = undo(seed) | |
| positions[i] = (x,y) | |
| print(x,y,rotations[i]) | |
| return positions,rotations | |
| def float_val(int32_val): | |
| return int32_val / float(0xffffffff) | |
| # print(hex(undo(0x9e299bad))) | |
| # print(hex(full_solve(15,-56,94,-118,-87,99))) | |
| # exit() | |
| import win32api, win32con, win32gui | |
| import time | |
| tolerance = 0.1 | |
| def drag_piece(hwnd,src_x,src_y, dst_x,dst_y,scroll): | |
| src_x,src_y = win32gui.ClientToScreen(hwnd, (src_x, src_y)) | |
| dst_x,dst_y = win32gui.ClientToScreen(hwnd, (dst_x, dst_y)) | |
| dx,dy = dst_x - src_x, dst_y - src_y | |
| print(src_x,src_y) | |
| print(dst_x,dst_y) | |
| win32gui.SetForegroundWindow(hwnd) | |
| time.sleep(tolerance) | |
| win32api.SetCursorPos((src_x,src_y)) | |
| time.sleep(tolerance) | |
| win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN,0,0,0,0) | |
| time.sleep(tolerance) | |
| win32api.mouse_event(win32con.MOUSEEVENTF_WHEEL,0,0,int(scroll)*win32con.WHEEL_DELTA,0) # +scroll = forwards = clockwise | |
| time.sleep(tolerance) | |
| win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP|win32con.MOUSEEVENTF_MOVE,dx,dy,0,0) | |
| time.sleep(tolerance) | |
| def get_hdc(): | |
| hwnd = win32gui.FindWindow(None, "Kokoro@kokoro") | |
| if not hwnd: | |
| print ('Window not found') | |
| exit(1) | |
| hdc = win32gui.GetDC(hwnd) | |
| assert hdc | |
| return hwnd,hdc | |
| def get_top_color(hdc): | |
| color = win32gui.GetPixel(hdc, 369,490) | |
| r,g,b = color&0xff,(color>>8)&0xff,(color>>16)&0xff | |
| return r,g,b | |
| def xy_to_client(x,y): | |
| return int(x*736),int(y*983) # image width and height | |
| hwnd,hdc = get_hdc() | |
| # record top piece color | |
| r2,g2,b2=get_top_color(hdc) | |
| print(hex(r2),hex(g2),hex(b2)) | |
| # reveal second to top piece. move the top piece off to the side | |
| drag_piece(hwnd,369,490, 30, 490,0) | |
| time.sleep(1.0) # for remote | |
| # record second-to-top piece color | |
| r1,g1,b1=get_top_color(hdc) | |
| print(hex(r1),hex(g1),hex(b1)) | |
| n=50 | |
| positions,rotations = full_solve(r1,g1,b1,r2,g2,b2,n) | |
| drag_piece(hwnd, 30, 490, *xy_to_client(*positions[n-1]), -rotations[n-1]/10.0) | |
| for i in range(n-2, -1, -1): | |
| # time.sleep(0.5) | |
| drag_piece(hwnd, 369,490, *xy_to_client(*positions[i]), -rotations[i]/10.0) | |
| exit() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment